Key Binder – Chalkboard

Assign custom keyboard shortcuts per site – chalkboard theme, bilingual UI, settings menu

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Key Binder – Chalkboard
// @namespace    http://tampermonkey.net/
// @version      8.1
// @description  Assign custom keyboard shortcuts per site – chalkboard theme, bilingual UI, settings menu
// @author       Mustafa Hakan
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @icon         data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect x='20' y='15' width='60' height='70' rx='10' fill='%232d4a3e' stroke='%23f5f5f5' stroke-width='3'/%3E%3Crect x='30' y='25' width='40' height='15' rx='4' fill='none' stroke='%23f5f5f5' stroke-width='2.5'/%3E%3Cline x1='35' y1='35' x2='65' y2='35' stroke='%23f5f5f5' stroke-width='2'/%3E%3Ccircle cx='50' cy='60' r='14' fill='none' stroke='%23f5f5f5' stroke-width='3'/%3E%3Cline x1='42' y1='60' x2='58' y2='60' stroke='%23f5f5f5' stroke-width='3' stroke-linecap='round'/%3E%3Cline x1='50' y1='52' x2='50' y2='68' stroke='%23f5f5f5' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const HOST = location.hostname.replace(/^www\./, '');

    const ACTIONS = {
        scrollDown: () => window.scrollBy({ top: 400, behavior: 'smooth' }),
        scrollUp: () => window.scrollBy({ top: -400, behavior: 'smooth' }),
        scrollTop: () => window.scrollTo({ top: 0, behavior: 'smooth' }),
        scrollBottom: () => window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }),
        refresh: () => location.reload(),
        back: () => history.back(),
        forward: () => history.forward(),
        zoomIn: () => document.body.style.zoom = (parseFloat(document.body.style.zoom || 1) + 0.1).toFixed(1),
        zoomOut: () => {
            const z = parseFloat(document.body.style.zoom || 1);
            if (z > 0.3) document.body.style.zoom = (z - 0.1).toFixed(1);
        },
        zoomReset: () => document.body.style.zoom = '1',
        darkMode: () => {
            if (document.documentElement.style.filter) {
                document.documentElement.style.filter = '';
                document.querySelectorAll('img, video, canvas').forEach(el => el.style.filter = '');
            } else {
                document.documentElement.style.filter = 'invert(1) hue-rotate(180deg)';
                document.querySelectorAll('img, video, canvas').forEach(el => el.style.filter = 'invert(1) hue-rotate(180deg)');
            }
        },
        copyUrl: () => {
            navigator.clipboard.writeText(location.href);
            toast(LANG.copyUrlDone);
        },
        copyTitle: () => {
            navigator.clipboard.writeText(document.title);
            toast(LANG.copyTitleDone);
        },
        copySelection: () => {
            const s = window.getSelection().toString();
            if (s) {
                navigator.clipboard.writeText(s);
                toast(LANG.copySelDone);
            }
        },
        findOnPage: () => {
            const t = prompt(LANG.searchPrompt);
            if (t) window.find(t);
        },
        fullscreen: () => {
            if (!document.fullscreenElement) document.documentElement.requestFullscreen();
            else document.exitFullscreen();
        },
        muteTab: () => {
            document.querySelectorAll('video, audio').forEach(el => el.muted = !el.muted);
            toast(LANG.muteToggled);
        },
        readAloud: () => {
            const t = window.getSelection().toString() || document.body.innerText.substring(0, 500);
            if (t) {
                const u = new SpeechSynthesisUtterance(t);
                u.lang = 'en-US';
                speechSynthesis.speak(u);
            }
        },
        stopRead: () => speechSynthesis.cancel(),
        print: () => window.print()
    };

    const LANG_EN = {
        title: 'Key Binder',
        noShortcuts: 'No shortcuts yet',
        addNew: '+ Add Shortcut',
        save: 'Save',
        cancel: 'Cancel',
        recordPrompt: 'Press your shortcut...',
        recordHint: 'e.g. Ctrl+Shift+A',
        actionSelect: 'Select Action',
        actions: {
            scrollDown: '⬇ Scroll Down',
            scrollUp: '⬆ Scroll Up',
            scrollTop: '⏫ Scroll to Top',
            scrollBottom: '⏬ Scroll to Bottom',
            refresh: '🔄 Refresh Page',
            back: '⬅ Go Back',
            forward: '➡ Go Forward',
            zoomIn: '🔍 Zoom In',
            zoomOut: '🔎 Zoom Out',
            zoomReset: '🔄 Reset Zoom',
            darkMode: '🌙 Dark Mode',
            copyUrl: '🔗 Copy Link',
            copyTitle: '📝 Copy Title',
            copySelection: '📋 Copy Selection',
            findOnPage: '🔍 Find on Page',
            fullscreen: '🖥️ Fullscreen',
            muteTab: '🔇 Mute/Unmute',
            readAloud: '🔊 Read Aloud',
            stopRead: '🔇 Stop Reading',
            print: '🖨️ Print'
        },
        savedToast: name => `✅ ${name} saved`,
        searchPrompt: 'Search:',
        copyUrlDone: 'Link copied',
        copyTitleDone: 'Title copied',
        copySelDone: 'Copied',
        muteToggled: 'Sound toggled',
        langBtn: 'TR',
        settings: 'Settings',
        resetShortcuts: 'Reset shortcuts for this site',
        resetConfirm: 'Are you sure?',
        resetDone: 'Shortcuts cleared',
        exportBtn: 'Export all profiles',
        importBtn: 'Import profiles',
        importDone: 'Profiles imported'
    };

    const LANG_TR = {
        title: 'Tuş Atama Paneli',
        noShortcuts: 'Henüz kısayol yok',
        addNew: '+ Yeni Kısayol Ekle',
        save: 'Kaydet',
        cancel: 'İptal',
        recordPrompt: 'Yeni kısayol tuşuna bas...',
        recordHint: 'Örn: Ctrl+Shift+A',
        actionSelect: 'Eylem Seç',
        actions: {
            scrollDown: '⬇ Aşağı Kaydır',
            scrollUp: '⬆ Yukarı Kaydır',
            scrollTop: '⏫ Başa Dön',
            scrollBottom: '⏬ Sona Git',
            refresh: '🔄 Sayfayı Yenile',
            back: '⬅ Geri Git',
            forward: '➡ İleri Git',
            zoomIn: '🔍 Yakınlaştır',
            zoomOut: '🔎 Uzaklaştır',
            zoomReset: '🔄 Zoom Sıfırla',
            darkMode: '🌙 Karanlık Mod',
            copyUrl: '🔗 Link Kopyala',
            copyTitle: '📝 Başlık Kopyala',
            copySelection: '📋 Seçimi Kopyala',
            findOnPage: '🔍 Sayfada Ara',
            fullscreen: '🖥️ Tam Ekran',
            muteTab: '🔇 Sesi Aç/Kapat',
            readAloud: '🔊 Sesli Oku',
            stopRead: '🔇 Okumayı Durdur',
            print: '🖨️ Yazdır'
        },
        savedToast: name => `✅ ${name} kaydedildi`,
        searchPrompt: 'Ara:',
        copyUrlDone: 'Link kopyalandı',
        copyTitleDone: 'Başlık kopyalandı',
        copySelDone: 'Kopyalandı',
        muteToggled: 'Ses değiştirildi',
        langBtn: 'EN',
        settings: 'Ayarlar',
        resetShortcuts: 'Bu site için kısayolları sıfırla',
        resetConfirm: 'Emin misiniz?',
        resetDone: 'Kısayollar temizlendi',
        exportBtn: 'Tüm profilleri dışa aktar',
        importBtn: 'Profil içe aktar',
        importDone: 'Profiller içe aktarıldı'
    };

    let LANG = GM_getValue('keybinder_lang', 'en') === 'tr' ? Object.assign({}, LANG_TR) : Object.assign({}, LANG_EN);

    const profiles = JSON.parse(GM_getValue('key_profiles', '{}'));
    if (!profiles[HOST]) {
        profiles[HOST] = { host: HOST, shortcuts: [] };
        GM_setValue('key_profiles', JSON.stringify(profiles));
    }
    let shortcuts = profiles[HOST].shortcuts;

    function toast(msg) {
        const t = document.createElement('div');
        t.textContent = msg;
        t.style.cssText = `
            position:fixed; bottom:24px; left:50%; transform:translateX(-50%);
            background: rgba(30,30,30,0.9); color: #f0f0f0; padding: 10px 22px;
            border-radius: 30px; z-index: 2147483648;
            font: 13px 'Chalkboard SE', 'Comic Sans MS', cursive;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            pointer-events: none;
        `;
        document.body.appendChild(t);
        setTimeout(() => t.remove(), 2200);
    }

    function save() {
        profiles[HOST].shortcuts = shortcuts;
        GM_setValue('key_profiles', JSON.stringify(profiles));
    }

    function recordShortcut(callback) {
        const modal = document.createElement('div');
        modal.style.cssText = `
            position:fixed; top:0; left:0; width:100%; height:100%;
            background: rgba(0,0,0,0.8); z-index: 2147483647;
            display:flex; align-items:center; justify-content:center;
            font: 16px 'Chalkboard SE', 'Comic Sans MS', cursive; color: #fff;
        `;
        modal.innerHTML = `
            <div style="background:#2d4a3e; padding:30px; border-radius: 24px 8px 24px 8px; text-align:center; border: 2px solid rgba(255,255,255,0.2); box-shadow: 0 10px 30px rgba(0,0,0,0.6);">
                <div style="font-size:40px; margin-bottom:10px;">⌨️</div>
                <div>${LANG.recordPrompt}</div>
                <div style="color:#ccc; font-size:13px; margin-top:5px;">${LANG.recordHint}</div>
                <button id="key-cancel" style="margin-top:18px; padding:8px 20px; border-radius: 16px 4px 16px 4px; border:2px solid rgba(255,255,255,0.4); background:rgba(0,0,0,0.2); color:#ddd; cursor:pointer; font-family:inherit;">${LANG.cancel}</button>
            </div>
        `;
        document.body.appendChild(modal);

        document.getElementById('key-cancel').onclick = () => {
            modal.remove();
            document.removeEventListener('keydown', handler, true);
        };

        function handler(e) {
            e.preventDefault();
            e.stopPropagation();
            const keys = [];
            if (e.ctrlKey) keys.push('Ctrl');
            if (e.shiftKey) keys.push('Shift');
            if (e.altKey) keys.push('Alt');
            if (e.metaKey) keys.push('Meta');
            if (!['Control', 'Shift', 'Alt', 'Meta'].includes(e.key)) keys.push(e.key.toUpperCase());
            if (keys.length === 0) return;
            modal.remove();
            document.removeEventListener('keydown', handler, true);
            callback(keys.join('+'));
        }
        document.addEventListener('keydown', handler, true);
    }

    function showPanel() {
        const existing = document.getElementById('key-panel');
        if (existing) { existing.remove(); return; }

        const panel = document.createElement('div');
        panel.id = 'key-panel';
        panel.style.cssText = `
            position:fixed; top:20px; right:20px;
            background: #2d4a3e;
            background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.06'/%3E%3C/svg%3E");
            border: 2px solid rgba(255,255,255,0.15);
            border-radius: 32px 8px 32px 8px / 8px 32px 8px 32px;
            padding: 20px; z-index: 2147483646;
            font: 14px 'Chalkboard SE', 'Comic Sans MS', cursive; color: #f5f5f5;
            min-width: 360px; max-height: 85vh; overflow-y: auto;
            box-shadow: 0 20px 50px rgba(0,0,0,0.5);
        `;

        let listHtml = '';
        if (shortcuts.length === 0) {
            listHtml = `<div style="color:#ccc; text-align:center; padding:20px; opacity:0.8;">${LANG.noShortcuts}</div>`;
        } else {
            shortcuts.forEach((s, i) => {
                listHtml += `
                    <div style="display:flex; justify-content:space-between; align-items:center; padding:10px; background:rgba(255,255,255,0.05); border-radius:12px; margin:4px 0;">
                        <span>${s.name}</span>
                        <div style="display:flex; gap:8px; align-items:center;">
                            <kbd style="background:rgba(0,0,0,0.3); padding:4px 10px; border-radius:8px; font-size:11px; border:1px solid rgba(255,255,255,0.3);">${s.key}</kbd>
                            <button data-idx="${i}" class="key-del" style="background:none; border:none; color:#ffb3b3; cursor:pointer; font-size:16px; padding:2px 6px;">🗑️</button>
                        </div>
                    </div>`;
            });
        }

        panel.innerHTML = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; padding-bottom:10px; border-bottom:1px dashed rgba(255,255,255,0.3);">
                <div>
                    <div style="font-weight:700; font-size:16px;">⌨️ ${LANG.title}</div>
                    <div style="opacity:0.7; font-size:11px;">${HOST}</div>
                </div>
                <div style="display:flex; gap:6px; align-items:center;">
                    <span style="font-size:12px; opacity:0.8;">${shortcuts.length}</span>
                    <button id="key-settings-btn" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.3); border-radius:12px; padding:4px 8px; color:inherit; font-family:inherit; cursor:pointer; font-size:16px;" title="${LANG.settings}">⚙️</button>
                    <button id="key-lang-btn" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.3); border-radius:12px; padding:4px 10px; color:inherit; font-family:inherit; cursor:pointer;">${LANG.langBtn}</button>
                </div>
            </div>
            <div id="key-list" style="max-height:320px; overflow-y:auto; margin-bottom:10px;">${listHtml}</div>
            <button id="key-add" style="width:100%; padding:12px; border-radius: 20px 6px 20px 6px; border:2px dashed rgba(255,255,255,0.4); background:rgba(255,255,255,0.05); color:#ddd; cursor:pointer; font-family:inherit; font-size:14px; transition:0.2s;">${LANG.addNew}</button>
        `;

        document.body.appendChild(panel);

        document.getElementById('key-lang-btn').onclick = () => {
            const next = LANG === LANG_TR ? 'en' : 'tr';
            GM_setValue('keybinder_lang', next);
            LANG = next === 'tr' ? Object.assign({}, LANG_TR) : Object.assign({}, LANG_EN);
            panel.remove();
            showPanel();
        };

        document.getElementById('key-settings-btn').onclick = (e) => {
            e.stopPropagation();
            showSettingsMenu(panel);
        };

        document.querySelectorAll('.key-del').forEach(btn => {
            btn.onclick = function() {
                const i = parseInt(this.dataset.idx);
                shortcuts.splice(i, 1);
                save();
                panel.remove();
                showPanel();
            };
        });

        document.getElementById('key-add').onclick = () => {
            const ap = document.createElement('div');
            ap.style.cssText = `
                position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
                background:#2d4a3e; border:2px solid rgba(255,255,255,0.2); border-radius: 24px 8px 24px 8px;
                padding:20px; z-index:2147483648; font:14px 'Chalkboard SE', cursive; color:#f5f5f5;
                min-width:360px; max-height:420px; overflow-y:auto;
                box-shadow:0 20px 60px rgba(0,0,0,0.6);
            `;

            let actionsHtml = '';
            Object.keys(ACTIONS).forEach(id => {
                actionsHtml += `<div class="key-action" data-id="${id}" style="padding:12px; border-radius:12px; cursor:pointer; margin:3px 0; transition:0.15s;">${LANG.actions[id] || id}</div>`;
            });

            ap.innerHTML = `
                <div style="font-weight:700; margin-bottom:12px;">📋 ${LANG.actionSelect}</div>
                ${actionsHtml}
                <button id="key-ap-close" style="width:100%; margin-top:12px; padding:10px; border-radius:16px; border:2px solid rgba(255,255,255,0.3); background:rgba(0,0,0,0.2); color:#ccc; cursor:pointer; font-family:inherit;">${LANG.cancel}</button>
            `;
            document.body.appendChild(ap);

            document.getElementById('key-ap-close').onclick = () => ap.remove();

            ap.querySelectorAll('.key-action').forEach(item => {
                item.onmouseover = () => item.style.background = 'rgba(255,255,255,0.1)';
                item.onmouseout = () => item.style.background = '';
                item.onclick = () => {
                    const id = item.dataset.id;
                    const name = LANG.actions[id] || id;
                    ap.remove();
                    recordShortcut(key => {
                        shortcuts.push({ name, key, action: id });
                        save();
                        panel.remove();
                        showPanel();
                        toast(LANG.savedToast(name) + ' → ' + key);
                    });
                };
            });
        };
    }

    function showSettingsMenu(panel) {
        const existing = document.getElementById('key-settings-popup');
        if (existing) { existing.remove(); return; }

        const popup = document.createElement('div');
        popup.id = 'key-settings-popup';
        popup.style.cssText = `
            position:fixed; top:60px; right:60px;
            background:#2d4a3e; border:2px solid rgba(255,255,255,0.2);
            border-radius: 20px 6px 20px 6px; padding: 12px 0;
            z-index: 2147483649; min-width: 200px;
            box-shadow: 0 12px 24px rgba(0,0,0,0.4);
            font: 14px 'Chalkboard SE', cursive; color:#f5f5f5;
        `;
        popup.innerHTML = `
            <div class="key-menu-item" data-action="reset">${LANG.resetShortcuts}</div>
            <div class="key-menu-item" data-action="export">${LANG.exportBtn}</div>
            <div class="key-menu-item" data-action="import">${LANG.importBtn}</div>
        `;
        popup.querySelectorAll('.key-menu-item').forEach(item => {
            item.style.cssText = 'padding:10px 18px; cursor:pointer; transition:0.15s;';
            item.onmouseover = () => item.style.background = 'rgba(255,255,255,0.1)';
            item.onmouseout = () => item.style.background = '';
            item.onclick = (e) => {
                e.stopPropagation();
                const action = item.dataset.action;
                if (action === 'reset') {
                    if (confirm(LANG.resetConfirm)) {
                        shortcuts = [];
                        save();
                        panel.remove();
                        showPanel();
                        toast(LANG.resetDone);
                    }
                } else if (action === 'export') {
                    const blob = new Blob([GM_getValue('key_profiles', '{}')], {type: 'application/json'});
                    const a = document.createElement('a');
                    a.href = URL.createObjectURL(blob);
                    a.download = 'keybinder_backup.json';
                    a.click();
                } else if (action === 'import') {
                    const input = document.createElement('input');
                    input.type = 'file';
                    input.accept = '.json';
                    input.onchange = () => {
                        const file = input.files[0];
                        const reader = new FileReader();
                        reader.onload = () => {
                            try {
                                const imported = JSON.parse(reader.result);
                                GM_setValue('key_profiles', JSON.stringify(imported));
                                // reload current shortcuts
                                const allProfiles = JSON.parse(GM_getValue('key_profiles', '{}'));
                                shortcuts = allProfiles[HOST] ? allProfiles[HOST].shortcuts : [];
                                save(); // just to update profiles obj in memory
                                panel.remove();
                                showPanel();
                                toast(LANG.importDone);
                            } catch (e) {
                                toast('Invalid file');
                            }
                        };
                        reader.readAsText(file);
                    };
                    input.click();
                }
                popup.remove();
            };
        });

        document.body.appendChild(popup);
        setTimeout(() => document.addEventListener('click', function close(e) {
            if (!popup.contains(e.target) && e.target.id !== 'key-settings-btn') {
                popup.remove();
                document.removeEventListener('click', close);
            }
        }), 50);
    }

    document.addEventListener('keydown', e => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
        for (const s of shortcuts) {
            const parts = s.key.split('+');
            let match = true;
            if (parts.includes('Ctrl') !== e.ctrlKey) match = false;
            if (parts.includes('Shift') !== e.shiftKey) match = false;
            if (parts.includes('Alt') !== e.altKey) match = false;
            if (parts.includes('Meta') !== e.metaKey) match = false;
            const keyPart = parts.filter(p => !['Ctrl', 'Shift', 'Alt', 'Meta'].includes(p))[0];
            if (keyPart && keyPart.toUpperCase() !== e.key.toUpperCase()) match = false;
            if (match) {
                e.preventDefault();
                if (ACTIONS[s.action]) ACTIONS[s.action]();
                break;
            }
        }
    });

    function createTrigger() {
        if (document.getElementById('key-trigger')) return;
        const btn = document.createElement('div');
        btn.id = 'key-trigger';
        btn.title = 'Key Binder Panel';
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="28" height="28">
            <rect x="20" y="15" width="60" height="70" rx="10" fill="#2d4a3e" stroke="#f5f5f5" stroke-width="3"/>
            <rect x="30" y="25" width="40" height="15" rx="4" fill="none" stroke="#f5f5f5" stroke-width="2.5"/>
            <line x1="35" y1="35" x2="65" y2="35" stroke="#f5f5f5" stroke-width="2"/>
            <circle cx="50" cy="60" r="14" fill="none" stroke="#f5f5f5" stroke-width="3"/>
            <line x1="42" y1="60" x2="58" y2="60" stroke="#f5f5f5" stroke-width="3" stroke-linecap="round"/>
            <line x1="50" y1="52" x2="50" y2="68" stroke="#f5f5f5" stroke-width="3" stroke-linecap="round"/>
        </svg>`;
        btn.style.cssText = `
            position:fixed; bottom:24px; right:24px; width:48px; height:48px;
            background:#2d4a3e; border:2px solid rgba(255,255,255,0.4); border-radius:14px;
            display:flex; align-items:center; justify-content:center;
            cursor:pointer; z-index:2147483644;
            box-shadow:4px 4px 0 rgba(0,0,0,0.3), 0 8px 20px rgba(0,0,0,0.4);
            transition:0.2s ease;
        `;
        btn.onmouseenter = () => btn.style.transform = 'rotate(-2deg) scale(1.08)';
        btn.onmouseleave = () => btn.style.transform = 'rotate(0deg) scale(1)';
        btn.onclick = e => {
            e.stopPropagation();
            if (document.getElementById('key-panel')) {
                document.getElementById('key-panel').remove();
            } else {
                showPanel();
            }
        };
        document.body.appendChild(btn);
    }

    setTimeout(createTrigger, 1200);
})();