Assign custom keyboard shortcuts per site – chalkboard theme, bilingual UI, settings menu
// ==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);
})();