4chan Vim Navigation

Navigate 4chan threads with vim-style keybindings

// ==UserScript==
// @name         4chan Vim Navigation
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Navigate 4chan threads with vim-style keybindings
// @match        *://boards.4chan.org/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const threads = Array.from(document.querySelectorAll('.thread'));
    let index = 0;
    let buffer = ''; // For capturing gg

    if (threads.length === 0) return;

    function highlightThread(i) {
        threads.forEach(t => t.style.outline = '');
        const thread = threads[i];
        if (thread) {
            thread.scrollIntoView({behavior: "smooth", block: "center"});
            thread.style.outline = '3px solid #4cafef';
        }
    }

    highlightThread(index);

    document.addEventListener('keydown', (e) => {
        // Ignore when typing in inputs/textareas
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
            return;
        }

        if (e.key === 'j') {
            index = Math.min(index + 1, threads.length - 1);
            highlightThread(index);
            buffer = '';
        } else if (e.key === 'k') {
            index = Math.max(index - 1, 0);
            highlightThread(index);
            buffer = '';
        } else if (e.key === 'o') {
            const link = threads[index].querySelector('a[href*="/thread/"]');
            if (link) window.location.href = link.href;
            buffer = '';
        } else if (e.key === 'O') {
            const link = threads[index].querySelector('a[href*="/thread/"]');
            if (link) window.open(link.href, '_blank');
            buffer = '';
        } else if (e.key === 'g') {
            if (buffer === 'g') {
                index = 0;
                highlightThread(index);
                buffer = '';
            } else {
                buffer = 'g';
                setTimeout(() => buffer = '', 500); // reset if not pressed quickly
            }
        } else if (e.key === 'G') {
            index = threads.length - 1;
            highlightThread(index);
            buffer = '';
        }
    });
})();