GitHub - Enhanced Shortcuts & Header Toolbar

Extends GitHub navigation: adds a header toolbar and fixes native shortcuts to work on any keyboard layout

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         GitHub - Enhanced Shortcuts & Header Toolbar
// @namespace    github-header-shortcuts
// @version      1.3.1
// @description  Extends GitHub navigation: adds a header toolbar and fixes native shortcuts to work on any keyboard layout
// @author       Vikindor (https://vikindor.github.io/)
// @homepageURL  https://github.com/Vikindor/github-header-shortcuts/
// @supportURL   https://github.com/Vikindor/github-header-shortcuts/issues
// @license      GPL-3.0
// @match        https://github.com/*
// @match        https://gist.github.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const CONFIG = {
    repositories: true,
    projects: true,
    packages: true,
    stars: true,
    gists: true,
    organizations: true,
    enterprises: true,
    issues: true,
    pulls: true,
    order: ['repositories', 'projects', 'packages', 'stars', 'gists', 'organizations', 'enterprises', 'issues', 'pulls']
  };

  const ID_CONTAINER = 'gh-shortcuts-between-start-end';

  const injectCSS = () => {
    if (document.getElementById('gh-shortcuts-style')) return;
    const style = document.createElement('style');
    style.id = 'gh-shortcuts-style';
    style.textContent = `
      #${ID_CONTAINER}::after{
        content:""; display:block; width:1px; height:20px;
        background-color:var(--borderColor-default,#30363d); opacity:.6; align-self:center;
      }
      #${ID_CONTAINER}{
        display:flex; align-items:center; gap:8px; flex-wrap:nowrap; margin-right:5px;
        font-size:var(--text-body-size-medium,14px);
      }
      #${ID_CONTAINER} .gh-shortcut-link{
        display:inline-flex; align-items:center; gap:9px; white-space:nowrap;
      }
      #${ID_CONTAINER} .gh-shortcut-link span{
        white-space:nowrap;
      }
      #${ID_CONTAINER}[data-mode="icons"] .gh-shortcut-link span{
        display:none;
      }
      #${ID_CONTAINER} svg{ flex:0 0 auto; }`;
    document.head.appendChild(style);
  };

  const getUserLogin = () =>
    document.querySelector('meta[name="user-login"]')?.getAttribute('content')?.trim() || '';

  const createContainer = () => {
    const container = document.createElement('div');
    container.id = ID_CONTAINER;
	container.style.marginLeft = 'auto';
    container.className = 'gh-shortcuts-container';
    return container;
  };

  const resolveMountPoint = () => {
    // Gist layout
    const gistNav = document.querySelector('nav[aria-label="Global"]');
    if (gistNav) {
      return {
        parent: gistNav.parentElement,
        beforeNode: gistNav.nextSibling
      };
    }

    // GitHub layout
    const nav = document.querySelector('nav[aria-label="Breadcrumbs"]');
    if (!nav) return { parent: null, beforeNode: null };
  
    const ol = nav.querySelector('ol');
    if (!ol) return { parent: null, beforeNode: null };

    return {
      parent: nav,
      beforeNode: ol.nextSibling
    };
  };

  const resolveCollisionAnchor = () =>
    document.querySelector('nav[aria-label="Breadcrumbs"] ol')
    || document.querySelector('nav[aria-label="Global"]');

  const createButton = (info) => {
    const a = document.createElement('a');
    a.href = info.href(getUserLogin());
    a.className = 'gh-shortcut-link no-underline color-fg-muted hover-color-fg-default';
    a.style.margin = '0 5px';
    a.title = info.tooltip || info.title;
    a.dataset.fullTitle = info.title;
    a.dataset.shortTitle = info.shortTitle || info.title;
    a.innerHTML = `
      <svg aria-hidden="true" height="16" width="16" viewBox="0 0 16 16" class="octicon octicon-${info.icon}">
        <path d="${info.path}"></path>
      </svg>
      <span>${info.title}</span>`;
    return a;
  };

  const BUTTONS = {
    repositories: {
      title: 'Repositories',
      shortTitle: 'Repos',
      tooltip: 'Repositories (G + R)',
      icon: 'repo',
      href: (user) => `https://github.com/${user}?tab=repositories`,
      path: 'M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z',
    },
    projects: {
      title: 'Projects',
      shortTitle: 'Proj',
      tooltip: 'Projects (G + T)',
      icon: 'table',
      href: (user) => `https://github.com/${user}?tab=projects`,
      path: 'M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25ZM6.5 6.5v8h7.75a.25.25 0 0 0 .25-.25V6.5Zm8-1.5V1.75a.25.25 0 0 0-.25-.25H6.5V5Zm-13 1.5v7.75c0 .138.112.25.25.25H5v-8ZM5 5V1.5H1.75a.25.25 0 0 0-.25.25V5Z',
    },
    packages: {
      title: 'Packages',
      shortTitle: 'Pkg',
      tooltip: 'Packages (G + K)',
      icon: 'package',
      href: (user) => `https://github.com/${user}?tab=packages`,
      path: 'm8.878.392 5.25 3.045c.54.314.872.89.872 1.514v6.098a1.75 1.75 0 0 1-.872 1.514l-5.25 3.045a1.75 1.75 0 0 1-1.756 0l-5.25-3.045A1.75 1.75 0 0 1 1 11.049V4.951c0-.624.332-1.201.872-1.514L7.122.392a1.75 1.75 0 0 1 1.756 0ZM7.875 1.69l-4.63 2.685L8 7.133l4.755-2.758-4.63-2.685a.248.248 0 0 0-.25 0ZM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432Zm6.25 8.271 4.625-2.683a.25.25 0 0 0 .125-.216V5.677L8.75 8.432Z',
    },
    stars: {
      title: 'Stars',
      shortTitle: 'Stars',
      tooltip: 'Stars (G + S)',
      icon: 'star',
      href: (user) => `https://github.com/${user}?tab=stars`,
      path: 'M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z',
    },
    gists: {
      title: 'Gists',
      shortTitle: 'Gists',
      tooltip: 'Gists (G + J)',
      icon: 'gist',
      href: (user) => `https://gist.github.com/${user}`,
      path: 'M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z'
    },
    organizations: {
      title: 'Organizations',
      shortTitle: 'Orgs',
      tooltip: 'Organizations (G + O)',
      icon: 'organization',
      href: () => 'https://github.com/settings/organizations',
      path: 'M1.75 16A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 0 0 .25-.25V8.285a.25.25 0 0 0-.111-.208l-1.055-.703a.749.749 0 1 1 .832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0 1 14.25 16h-3.5a.766.766 0 0 1-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 0 1-.75-.75V14h-1v1.25a.75.75 0 0 1-.75.75Zm-.25-1.75c0 .138.112.25.25.25H4v-1.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 .75.75v1.25h2.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25ZM3.75 6h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 3.75A.75.75 0 0 1 3.75 3h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 3.75Zm4 3A.75.75 0 0 1 7.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 7 6.75ZM7.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 9.75A.75.75 0 0 1 3.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 9.75ZM7.75 9h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z',
    },
    enterprises: {
      title: 'Enterprises',
      shortTitle: 'Ent',
      tooltip: 'Enterprises (G + E)',
      icon: 'globe',
      href: () => 'https://github.com/settings/enterprises',
      path: 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM5.78 8.75a9.64 9.64 0 0 0 1.363 4.177c.255.426.542.832.857 1.215.245-.296.551-.705.857-1.215A9.64 9.64 0 0 0 10.22 8.75Zm4.44-1.5a9.64 9.64 0 0 0-1.363-4.177c-.307-.51-.612-.919-.857-1.215a9.927 9.927 0 0 0-.857 1.215A9.64 9.64 0 0 0 5.78 7.25Zm-5.944 1.5H1.543a6.507 6.507 0 0 0 4.666 5.5c-.123-.181-.24-.365-.352-.552-.715-1.192-1.437-2.874-1.581-4.948Zm-2.733-1.5h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.507 6.507 0 0 0-4.666 5.5Zm10.181 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.507 6.507 0 0 0 4.666-5.5Zm2.733-1.5a6.507 6.507 0 0 0-4.666-5.5c.123.181.24.365.353.552.714 1.192 1.436 2.874 1.58 4.948Z',
    },
    issues: {
      title: 'Issues',
      shortTitle: 'Issues',
      tooltip: 'Issues (G + I)',
      icon: 'issue-opened',
      href: () => 'https://github.com/issues',
      path: 'M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z',
    },
    pulls: {
      title: 'Pull requests',
      shortTitle: 'PRs',
      tooltip: 'Pull requests (G + P)',
      icon: 'git-pull-request',
      href: () => 'https://github.com/pulls',
      path: 'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z',
    }
  };

  const NON_REPO_FIRST = new Set([
    'settings','orgs','organizations','notifications','issues','pulls','marketplace',
    'explore','topics','collections','sponsors','search','apps','features','pricing',
    'about','codespaces','gist','login','join'
  ]);

  function getRepoBase() {
    const path = location.pathname.replace(/\/+$/, '');
    const parts = path.split('/').filter(Boolean);
    if (parts.length < 2) return null;
    const [seg1, seg2] = parts;
    if (NON_REPO_FIRST.has(seg1)) return null;
    if (!seg2 || seg2.includes('#') || seg2.includes('?')) return null;
    return `/${seg1}/${seg2}`;
  }

  function goRepoTab(tab) {
    const base = getRepoBase();
    if (!base) return false;
    const url = tab ? `https://github.com${base}/${tab}` : `https://github.com${base}`;
    location.href = url;
    return true;
  }

  function goRepoOr(tab, globalUrl) {
    const base = getRepoBase();
    if (base) {
      location.href = tab ? `https://github.com${base}/${tab}` : `https://github.com${base}`;
    } else {
      location.href = globalUrl;
    }
    return true;
  }

  const GITHUB_SHORTCUTS = {
    'KeyG KeyD': () => { location.href = 'https://github.com/'; return true; },
    'KeyG KeyN': () => { location.href = 'https://github.com/notifications'; return true; },
    'KeyG KeyC': () => goRepoTab(''),
    'KeyG KeyA': () => goRepoTab('actions'),
    'KeyG KeyB': () => goRepoTab('projects'),
    'KeyG KeyW': () => goRepoTab('wiki'),
    'KeyG KeyG': () => goRepoTab('discussions'),
    'KeyG KeyI': () => goRepoOr('issues', 'https://github.com/issues'),
    'KeyG KeyP': () => goRepoOr('pulls', 'https://github.com/pulls'),
    'KeyG KeyR': () => { const u = getUserLogin(); if (!u) return false; location.href = `https://github.com/${u}?tab=repositories`; return true; },
    'KeyG KeyT': () => { const u = getUserLogin(); if (!u) return false; location.href = `https://github.com/${u}?tab=projects`; return true; },
    'KeyG KeyK': () => { const u = getUserLogin(); if (!u) return false; location.href = `https://github.com/${u}?tab=packages`; return true; },
    'KeyG KeyS': () => { const u = getUserLogin(); if (!u) return false; location.href = `https://github.com/${u}?tab=stars`; return true; },
    'KeyG KeyJ': () => { const u = getUserLogin(); if (!u) return false; location.href = `https://gist.github.com/${u}`; return true; },
    'KeyG KeyO': () => { location.href = 'https://github.com/settings/organizations'; return true; },
    'KeyG KeyE': () => { location.href = 'https://github.com/settings/enterprises'; return true; }
  };

  const HOTKEY_MAP = new Map(Object.entries(GITHUB_SHORTCUTS));

  let mountObserver = null;
  let resizeObserver = null;
  let scheduledPlacement = false;
  let scheduledCompactCheck = false;

  const placeShortcuts = () => {
	if (!getUserLogin()) return;
    if (document.getElementById(ID_CONTAINER)) return;
    const { parent, beforeNode } = resolveMountPoint();
    if (!parent) return;
    const container = createContainer();
    (CONFIG.order || Object.keys(BUTTONS)).forEach((key) => {
      const info = BUTTONS[key];
      if (!info) return;
      if (CONFIG[key]) container.appendChild(createButton(info));
    });
    injectCSS();
    parent.insertBefore(container, beforeNode || null);
  };

  const setButtonLabels = (mode) => {
    const container = document.getElementById(ID_CONTAINER);
    if (!container) return;

    container.dataset.mode = mode;
    container.querySelectorAll('.gh-shortcut-link').forEach((link) => {
      const label = link.querySelector('span');
      if (!label) return;
      label.textContent = mode === 'short'
        ? (link.dataset.shortTitle || link.dataset.fullTitle || '')
        : (link.dataset.fullTitle || '');
    });
  };

  const hasCollision = () => {
    const container = document.getElementById(ID_CONTAINER);
    const anchor = resolveCollisionAnchor();
    if (!container || !anchor) return false;

    const anchorRect = anchor.getBoundingClientRect();
    const containerRect = container.getBoundingClientRect();
    return containerRect.left - anchorRect.right < 1;
  };

  const scheduleCompactCheck = () => {
    if (scheduledCompactCheck) return;
    scheduledCompactCheck = true;
    requestAnimationFrame(() => {
      scheduledCompactCheck = false;

      const container = document.getElementById(ID_CONTAINER);
      if (!container) return;

      if (!resolveCollisionAnchor()) {
        setButtonLabels('full');
        return;
      }

      let nextMode = 'icons';
      for (const mode of ['full', 'short', 'icons']) {
        setButtonLabels(mode);
        if (!hasCollision()) {
          nextMode = mode;
          break;
        }
      }

      setButtonLabels(nextMode);
    });
  };

  const watchLayout = () => {
    if (!resizeObserver && typeof ResizeObserver === 'function') {
      resizeObserver = new ResizeObserver(() => scheduleCompactCheck());
    }
    if (!resizeObserver) return;

    resizeObserver.disconnect();

    const container = document.getElementById(ID_CONTAINER);
    const anchor = resolveCollisionAnchor();

    if (container) resizeObserver.observe(container);
    if (anchor) resizeObserver.observe(anchor);
  };

  const schedulePlacement = () => {
    if (scheduledPlacement) return;
    scheduledPlacement = true;
    requestAnimationFrame(() => {
      scheduledPlacement = false;
      placeShortcuts();
      watchLayout();
      scheduleCompactCheck();
    });
  };

  const startMountObserver = () => {
    if (mountObserver) return;
    mountObserver = new MutationObserver(() => {
      if (!document.getElementById(ID_CONTAINER)) schedulePlacement();
    });

    try {
      mountObserver.observe(document.documentElement, { childList: true, subtree: true });
    } catch {}
  };

  const installNavigationHooks = () => {
    const { pushState, replaceState } = history;
    const wrapHistoryMethod = (method) => function (...args) {
      const result = method.apply(this, args);
      schedulePlacement();
      return result;
    };

    history.pushState = wrapHistoryMethod(pushState);
    history.replaceState = wrapHistoryMethod(replaceState);

    window.addEventListener('popstate', schedulePlacement);
    window.addEventListener('pageshow', schedulePlacement);
    window.addEventListener('resize', scheduleCompactCheck);
    document.addEventListener('turbo:render', schedulePlacement);
    document.addEventListener('turbo:load', schedulePlacement);
    document.addEventListener('pjax:end', schedulePlacement);
  };

  (() => {
    let buf = [], timer = null;
    const reset = () => { buf = []; if (timer) { clearTimeout(timer); timer = null; } };
    const wait = () => { if (timer) clearTimeout(timer); timer = setTimeout(reset, 800); };
    const hasPrefix = seq => { for (const k of HOTKEY_MAP.keys()) if (k.startsWith(seq)) return true; return false; };
    const isTyping = el => el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable || el.closest?.('[contenteditable="true"]'));

    window.addEventListener('keydown', e => {
      if (!e.isTrusted || e.repeat || e.ctrlKey || e.altKey || e.metaKey) return;
      if (isTyping(document.activeElement)) return;

      if (e.shiftKey && e.code === 'Slash') {
        e.preventDefault();
        document.dispatchEvent(new KeyboardEvent('keydown', { key: '?', code: 'Slash', shiftKey: true, bubbles: true }));
        reset(); return;
      }

      buf.push(e.code);
      const seq = buf.join(' ');
      if (HOTKEY_MAP.has(seq)) { const handled = HOTKEY_MAP.get(seq)(); if (handled !== false) reset(); else wait(); return; }
      hasPrefix(seq) ? wait() : reset();
    });
  })();

  startMountObserver();
  installNavigationHooks();
  schedulePlacement();
})();