KeyNavGoogleResults

Keyboard shortcuts for Google search: numbers (1-0) open results in background tabs (Shift for foreground tab, Alt for current tab), Left/Right arrows to navigate pages

// ==UserScript==
// @license MIT
// @name         KeyNavGoogleResults
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Keyboard shortcuts for Google search: numbers (1-0) open results in background tabs (Shift for foreground tab, Alt for current tab), Left/Right arrows to navigate pages
// @author       aceitw
// @match        https://www.google.com/search*
// @match        https://google.com/search*
// @grant        GM_openInTab
// @grant        window.focus
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  const STYLES = {
    indicatorBg: '#f1f3f4',
    indicatorText: '#202124',
    indicatorSize: '20px',
    fontSize: '12px',
    borderColor: '#5f6368',
    shadow: '0 1px 2px rgba(0, 0, 0, 0.2)',
  };

  let isProcessing = false;

  function getNumberKey(e) {
    const code = e.which || e.keyCode;
    if (code >= 48 && code <= 57) return code === 48 ? 0 : code - 48;
    if (code >= 96 && code <= 105) return code - 96;
    return null;
  }

  function addNumberIndicators() {
    if (isProcessing) return;
    isProcessing = true;

    document.querySelectorAll('.search-result-indicator').forEach(el => el.remove());

    const results = Array.from(document.querySelectorAll('div.MjjYud')).filter(el =>
      el.querySelector('a > h3')
    );

    results.forEach((result, index) => {
      if (index < 10) {
        const title = result.querySelector('a > h3');
        if (title && !title.querySelector('.search-result-indicator')) {
          const indicator = document.createElement('span');
          indicator.textContent = (index + 1) % 10;
          indicator.className = 'search-result-indicator';
          indicator.style.cssText = `
            display: inline-block;
            width: ${STYLES.indicatorSize};
            height: ${STYLES.indicatorSize};
            background-color: ${STYLES.indicatorBg};
            color: ${STYLES.indicatorText};
            border: 2px solid ${STYLES.borderColor};
            border-radius: 50%;
            text-align: center;
            line-height: ${STYLES.indicatorSize};
            margin-right: 8px;
            font-size: ${STYLES.fontSize};
            font-weight: bold;
            position: absolute;
            left: -32px;
            box-shadow: ${STYLES.shadow};
          `;
          title.style.position = 'relative';
          title.prepend(indicator);
        }
      }
    });

    isProcessing = false;
  }

  function openUrl(url, { active = false, currentTab = false } = {}) {
    try {
      if (currentTab) {
        window.location.href = url;
      } else if (typeof GM_openInTab !== 'undefined') {
        GM_openInTab(url, { active });
      } else {
        window.open(url, '_blank');
        if (!active) setTimeout(() => window.focus(), 0);
      }
    } catch (err) {
      console.error('openUrl error:', err);
    }
  }

  function updatePageIndicator() {
    let indicator = document.getElementById('google-results-page-indicator');
    if (!indicator) {
      indicator = document.createElement('div');
      indicator.id = 'google-results-page-indicator';
      indicator.style.cssText = `
        position: fixed;
        bottom: 20px;
        left: 20px;
        z-index: 1000;
        display: flex;
        align-items: center;
        gap: 8px;
        background: ${STYLES.indicatorBg};
        padding: 4px 12px;
        border-radius: 12px;
        font-size: ${STYLES.fontSize};
        font-weight: bold;
        color: ${STYLES.indicatorText};
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      `;
      document.body.appendChild(indicator);
    }
    const start = new URLSearchParams(location.search).get('start') || '0';
    const currentPage = Math.floor(parseInt(start, 10) / 10) + 1;
    indicator.textContent = `Page ${currentPage}`;
  }

  function navigatePage(direction) {
    const next = document.querySelector('#pnnext');
    const prev = document.querySelector('#pnprev');
    if (direction === 'next' && next) next.click();
    if (direction === 'previous' && prev) prev.click();
    setTimeout(updatePageIndicator, 200);
  }

  function handleKeyPress(e) {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;

    const numberKey = getNumberKey(e);
    if (numberKey !== null && !e.ctrlKey && !e.metaKey) {
      e.preventDefault();
      const results = Array.from(document.querySelectorAll('div.MjjYud')).filter(el =>
        el.querySelector('a > h3')
      );
      const keyNum = numberKey === 0 ? 9 : numberKey - 1;
      const result = results[keyNum];
      if (result) {
        const link = result.querySelector('a');
        if (link && link.href) {
          openUrl(link.href, {
            active: e.shiftKey,
            currentTab: e.altKey,
          });
        }
      }
      return;
    }

    if (e.code === 'ArrowRight' || e.code === 'ArrowLeft') {
      e.preventDefault();
      navigatePage(e.code === 'ArrowRight' ? 'next' : 'previous');
    }
  }

  function debounce(func, wait) {
    let timeout;
    return function (...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }

  function init() {
    addNumberIndicators();
    updatePageIndicator();
    document.addEventListener('keydown', handleKeyPress);

    const observer = new MutationObserver(() => {
      debounce(addNumberIndicators, 250)();
    });

    const container = document.querySelector('#search');
    if (container) {
      observer.observe(container, { childList: true, subtree: true });
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();