collin/browser-terminal-extension · 70c84263
Offer Ctrl+N, Ctrl+T and Ctrl+W on Ctrl+Alt
Collin Richards · 2026-08-18 07:54 UTC · 70c842632d07d4d70bab99227471661396da8d00 · parent 09d8336c · browse files
modifiedREADME.md+38 −0
| ⋯ 588 unchanged lines | |||
| 589 | 589 | connection carries three commands (open, focus, close) and nothing else — no | |
| 590 | 590 | terminal traffic goes through it, for the reason in the header of `sidebar.js`. | |
| 591 | 591 | ||
| 592 | + | ## Keys the browser takes first | |
| 593 | + | ||
| 594 | + | Most browser shortcuts are cancelable: the page sees the keydown, calls | |
| 595 | + | `preventDefault()`, and the browser drops it. That is why Ctrl+P, Ctrl+F, | |
| 596 | + | Ctrl+S and friends reach the pty like any other key. | |
| 597 | + | ||
| 598 | + | Three do not. **Ctrl+N**, **Ctrl+T** and **Ctrl+W** are *reserved*: the browser | |
| 599 | + | process acts on them before the keystroke is handed to the renderer, so the | |
| 600 | + | panel never receives an event and has nothing to cancel. Extension commands | |
| 601 | + | cannot claim them either. `chrome://extensions/shortcuts` refuses reserved | |
| 602 | + | combinations for the same reason. Nothing inside the extension can win them | |
| 603 | + | back. | |
| 604 | + | ||
| 605 | + | So they are offered one modifier over, and only while the terminal has focus: | |
| 606 | + | ||
| 607 | + | | Press | Sent to the pty | | |
| 608 | + | |---|---| | |
| 609 | + | | **Ctrl+Alt+N** | `^N` | | |
| 610 | + | | **Ctrl+Alt+T** | `^T` | | |
| 611 | + | | **Ctrl+Alt+W** | `^W` | | |
| 612 | + | ||
| 613 | + | The bytes are written straight to the socket, so tmux and readline see exactly | |
| 614 | + | what a real Ctrl+N would have produced. | |
| 615 | + | ||
| 616 | + | **If you want the real keycap.** The browser cannot be talked out of Ctrl+N, | |
| 617 | + | but a remapper below it can make sure the browser never sees the key. Bind | |
| 618 | + | Ctrl+N to Ctrl+Alt+N and the panel picks it up as above: | |
| 619 | + | ||
| 620 | + | - [`xremap`](https://github.com/xremap/xremap) can do it per-application, so | |
| 621 | + | Ctrl+N still opens a window everywhere else. On GNOME/Wayland it needs the | |
| 622 | + | companion shell extension to know which window is focused; on X11 it does | |
| 623 | + | not. | |
| 624 | + | - [`keyd`](https://github.com/rvaiya/keyd) sits at evdev and is simpler to run, | |
| 625 | + | but it is system-wide: Ctrl+N stops meaning "new" in every other app too. | |
| 626 | + | ||
| 627 | + | Both are outside this repo, and neither is required. Ctrl+Alt+N works on its | |
| 628 | + | own. | |
| 629 | + | ||
| 592 | 630 | ## Picking elements off the page | |
| 593 | 631 | ||
| 594 | 632 | Two ways to start it, and the difference matters: | |
| ⋯ 249 unchanged lines | |||
modifiedextension/sidebar.js+36 −1
| ⋯ 248 unchanged lines | |||
| 249 | 249 | return true; | |
| 250 | 250 | } | |
| 251 | 251 | ||
| 252 | + | /** Letters the browser reserves for itself, offered on Ctrl+Alt. */ | |
| 253 | + | const STOLEN_KEYS = "ntw"; | |
| 254 | + | ||
| 255 | + | /** | |
| 256 | + | * The keys the browser keeps for itself, one modifier over. | |
| 257 | + | * | |
| 258 | + | * Ctrl+N, Ctrl+T and Ctrl+W never arrive here at all. They are *reserved* | |
| 259 | + | * accelerators: the browser process acts on them before the keystroke is | |
| 260 | + | * handed to the renderer, so unlike Ctrl+P or Ctrl+F there is no event in this | |
| 261 | + | * document to cancel, and no extension command can claim them either: the | |
| 262 | + | * shortcuts page refuses them for the same reason. A panel that is a terminal | |
| 263 | + | * wants all three (^N is next-history and tmux's next-window, ^T transposes, | |
| 264 | + | * ^W kills a word), so they are offered on Ctrl+Alt instead: Ctrl+Alt+N puts a | |
| 265 | + | * literal ^N in the pty, Ctrl+Alt+W a ^W, and so on. | |
| 266 | + | * | |
| 267 | + | * This is also where a system-level remapper should aim. Nothing inside the | |
| 268 | + | * browser can win Ctrl+N back, but a remapper below it can. Bind Ctrl+N to | |
| 269 | + | * Ctrl+Alt+N while the browser is focused (xremap can do it per-application on | |
| 270 | + | * GNOME; keyd system-wide) and the keycap you press is the key you meant. | |
| 271 | + | * | |
| 272 | + | * Returns true if the event was one of ours and has been handled. | |
| 273 | + | * | |
| 274 | + | * @param {KeyboardEvent} e | |
| 275 | + | */ | |
| 276 | + | function handleStolenKey(e) { | |
| 277 | + | if (!e.ctrlKey || !e.altKey || e.metaKey) return false; | |
| 278 | + | const letter = e.key.length === 1 ? e.key.toLowerCase() : ""; | |
| 279 | + | if (!STOLEN_KEYS.includes(letter)) return false; | |
| 280 | + | e.preventDefault(); | |
| 281 | + | // Silent when there is nothing to type into: the alternative is a log line | |
| 282 | + | // per keystroke while the panel is offline. | |
| 283 | + | if (connected && ws) ws.send(enc.encode(String.fromCharCode(letter.charCodeAt(0) - 96))); | |
| 284 | + | return true; | |
| 285 | + | } | |
| 286 | + | ||
| 252 | 287 | /** @param {string} pref */ | |
| 253 | 288 | function setTheme(pref) { | |
| 254 | 289 | themePref = Themes.PREFERENCES.includes(pref) ? pref : "auto"; | |
| ⋯ 59 unchanged lines | |||
| 314 | 349 | term.attachCustomKeyEventHandler((e) => { | |
| 315 | 350 | if (e.type !== "keydown") return true; | |
| 316 | 351 | if (staleRepeat(e)) return false; | |
| 317 | - | return !handlePanelKey(e); | |
| 352 | + | return !(handlePanelKey(e) || handleStolenKey(e)); | |
| 318 | 353 | }); | |
| 319 | 354 | ||
| 320 | 355 | $("term").addEventListener( | |
| ⋯ 5345 unchanged lines | |||