website/src/scripts/emacs/keys.js (5661B)
// The keys: SPC opens which-key on the leader map, M-x (Alt+x) and : open the // minibuffer, ? opens *Help* and C-g quits. evil's motions scroll (motions.js), // and / searches the page, then n and N step through the matches (search.js). // Keys typed into a field are left alone. import { $, isTyping } from "../util.js"; import { message, idle } from "./echo.js"; import { call, hasCommand } from "./commands.js"; import { mx, ex } from "./mx.js"; import { toggleMenu, menuOpen } from "./menu.js"; import { motion } from "./motions.js"; import { search, searchNext, searchActive, clearSearch } from "./search.js"; const wk = $(".which-key"); const help = $(".help"); const mini = $(".mini"); const KEYMAP = [ { key: "SPC", desc: "find file", cmd: "find-file" }, { key: ":", desc: "M-x", cmd: "execute-extended-command" }, { key: "c", desc: "+computer", map: [ { key: "c", desc: "run or pause", cmd: "computer-run" }, { key: "n", desc: "step one cycle", cmd: "computer-step" }, { key: "r", desc: "reset", cmd: "computer-reset" }, { key: "l", desc: "load program", cmd: "computer-load-program" }, ] }, { key: "a", desc: "about", cmd: "goto-about" }, { key: "p", desc: "projects", cmd: "goto-projects" }, { key: "n", desc: "posts", cmd: "goto-posts" }, { key: "m", desc: "mail me", cmd: "goto-contact" }, { key: "i", desc: "imenu", cmd: "imenu" }, { key: "r", desc: "refill coffee", cmd: "coffee-refill" }, { key: "b", desc: "buy me a coffee", cmd: "buy-me-a-coffee" }, { key: "h", desc: "+help", map: [ { key: "t", desc: "load-theme", cmd: "load-theme" }, { key: "h", desc: "keys and commands", cmd: "help" }, ] }, { key: "s", desc: "+security", map: [ { key: "f", desc: "submit-flag", cmd: "submit-flag" }, { key: "g", desc: "copy GPG fingerprint", cmd: "copy-gpg-fingerprint" }, ] }, { key: "q", desc: "+quit", map: [ { key: "q", desc: "quit", cmd: "save-buffers-kill-terminal" }, { key: "r", desc: "restart", cmd: "restart-emacs" }, ] }, ]; let leader = null; // { map, prefix } let wkTimer = 0; // Only the bindings whose command this page defines (posts have no computer). const bound = (map) => map.filter((b) => (b.map ? bound(b.map).length : hasCommand(b.cmd))); function startLeader() { leader = { map: bound(KEYMAP), prefix: "SPC" }; message("SPC-", "", 0); clearTimeout(wkTimer); wkTimer = setTimeout(showWhichKey, 300); } function showWhichKey() { if (!leader) return; const title = document.createElement("p"); title.className = "wk-title"; title.textContent = `${leader.prefix}-`; wk.replaceChildren(title, ...leader.map.map((b) => { const item = document.createElement("button"); item.type = "button"; item.className = "wk-item"; item.style.cssText = "border:0;background:none;padding:0;text-align:left;font:inherit;color:inherit"; item.innerHTML = `<span class="wk-key"></span><span class="wk-arrow">→</span><span class="wk-desc${b.map ? " group" : ""}"></span>`; item.children[0].textContent = b.key; item.children[2].textContent = b.desc; item.addEventListener("mousedown", (e) => e.preventDefault()); item.addEventListener("click", () => press(b.key)); return item; })); wk.hidden = false; } function endLeader(text = null) { leader = null; clearTimeout(wkTimer); wk.hidden = true; if (text !== null) message(text, text.endsWith("undefined") ? "error" : "", 3000); else idle(); } function press(key) { const b = leader.map.find((x) => x.key === key); if (!b) return endLeader(`${leader.prefix} ${key} is undefined`); if (b.map) { leader = { map: bound(b.map), prefix: `${leader.prefix} ${key}` }; message(`${leader.prefix}-`, "", 0); showWhichKey(); return; } endLeader(); call(b.cmd); } // ── keys ────────────────────────────────────────────────────────────── addEventListener("keydown", (e) => { if (help.open) return; if (leader) { if (["Shift", "Control", "Alt", "Meta"].includes(e.key)) return; e.preventDefault(); e.stopPropagation(); if (e.key === "Escape" || (e.ctrlKey && e.key === "g")) endLeader("Quit"); else press(e.key === " " ? "SPC" : e.key); return; } if (!mini.hidden || isTyping(e)) return; // The posts list has its own j and k, and a dialog or butterfly has the keys to itself. const free = !e.target.closest?.("[role='tree']") && !document.querySelector("dialog[open], .butterfly"); if (e.altKey && e.code === "KeyX" && !e.ctrlKey && !e.metaKey) { e.preventDefault(); mx(); } else if (e.ctrlKey && e.key === "g") { e.preventDefault(); clearSearch(); call("keyboard-quit"); } else if (free && motion(e)) { // scrolled } else if (e.metaKey || e.ctrlKey || e.altKey) { // leave browser shortcuts alone } else if (e.key === " " && !e.target.closest?.("button, a, select, summary, [tabindex]")) { e.preventDefault(); startLeader(); } else if (e.key === ":") { e.preventDefault(); ex(); } else if (e.key === "?") { e.preventDefault(); call("help"); } else if (free && e.key === "/") { e.preventDefault(); search(); } else if (free && (e.key === "n" || e.key === "N") && searchActive()) { // only while there are matches; otherwise n is the computer's step e.preventDefault(); searchNext(e.key === "n" ? 1 : -1); } else if (e.key === "Escape" && menuOpen()) { toggleMenu(false); } else if (e.key === "Escape" && searchActive()) { clearSearch(); idle(); } }, true);