11/**
22 * Registers `APP_CHORDS` on `@opentui/keymap`. Lookup belongs to the package.
33 * There is no second switch from key name to handler.
4+ *
5+ * The default host keeps its keypress listener until the destroy event, but
6+ * `isDestroyed` flips first and leftover stdin then throws inside
7+ * `getLiveHost`. This host unsubscribes inside `destroy()` and ignores any
8+ * keypress already queued for this turn.
49 */
510import type { CliRenderer , KeyEvent , Renderable } from "@opentui/core" ;
6- import type { Keymap } from "@opentui/keymap" ;
7- import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" ;
11+ import { Keymap , type KeymapHost } from "@opentui/keymap" ;
12+ import {
13+ registerDefaultKeys ,
14+ registerEnabledFields ,
15+ registerMetadataFields ,
16+ } from "@opentui/keymap/addons" ;
17+ import { createOpenTuiKeymapHost } from "@opentui/keymap/opentui" ;
818
919import { bindAppHelp , setAppShortcutRows } from "../keybindings.js" ;
1020import type { AppShell } from "./internals.js" ;
@@ -21,13 +31,78 @@ bindAppHelp(appHelpLines);
2131type ShellKeymap = Keymap < Renderable , KeyEvent > ;
2232
2333const keymaps = new WeakMap < CliRenderer , ShellKeymap > ( ) ;
34+ const detached = new WeakSet < CliRenderer > ( ) ;
35+ const pressListeners = new WeakMap <
36+ CliRenderer ,
37+ ( ( event : KeyEvent ) => void ) [ ]
38+ > ( ) ;
39+
40+ function guardHost ( renderer : CliRenderer ) : KeymapHost < Renderable , KeyEvent > {
41+ const host = createOpenTuiKeymapHost ( renderer ) ;
42+ const onKeyPress = host . onKeyPress . bind ( host ) ;
43+ const onKeyRelease = host . onKeyRelease . bind ( host ) ;
44+ const offs : ( ( ) => void ) [ ] = [ ] ;
45+ const ignore = ( ) : boolean => detached . has ( renderer ) || renderer . isDestroyed ;
46+
47+ const wrap = (
48+ subscribe : ( listener : ( event : KeyEvent ) => void ) => ( ) => void ,
49+ listener : ( event : KeyEvent ) => void ,
50+ record : boolean ,
51+ ) : ( ( ) => void ) => {
52+ const wrapped = ( event : KeyEvent ) : void => {
53+ if ( ignore ( ) ) return ;
54+ listener ( event ) ;
55+ } ;
56+ if ( record ) {
57+ const list = pressListeners . get ( renderer ) ?? [ ] ;
58+ list . push ( wrapped ) ;
59+ pressListeners . set ( renderer , list ) ;
60+ }
61+ const off = subscribe ( wrapped ) ;
62+ offs . push ( off ) ;
63+ return off ;
64+ } ;
65+
66+ host . onKeyPress = ( listener ) => wrap ( onKeyPress , listener , true ) ;
67+ host . onKeyRelease = ( listener ) => wrap ( onKeyRelease , listener , false ) ;
68+
69+ const originalDestroy = renderer . destroy . bind ( renderer ) ;
70+ renderer . destroy = ( ) => {
71+ if ( ! detached . has ( renderer ) ) {
72+ detached . add ( renderer ) ;
73+ for ( const off of offs ) off ( ) ;
74+ offs . length = 0 ;
75+ }
76+ originalDestroy ( ) ;
77+ } ;
78+
79+ return host ;
80+ }
2481
2582function keymapFor ( renderer : CliRenderer ) : ShellKeymap {
2683 const existing = keymaps . get ( renderer ) ;
2784 if ( existing !== undefined ) return existing ;
28- const created = createDefaultOpenTuiKeymap ( renderer ) ;
29- keymaps . set ( renderer , created ) ;
30- return created ;
85+ const keymap = new Keymap ( guardHost ( renderer ) ) ;
86+ registerDefaultKeys ( keymap ) ;
87+ registerEnabledFields ( keymap ) ;
88+ registerMetadataFields ( keymap ) ;
89+ keymaps . set ( renderer , keymap ) ;
90+ return keymap ;
91+ }
92+
93+ /** True while this renderer's keymap is still subscribed to keypress. */
94+ export function shellKeymapListening ( renderer : CliRenderer ) : boolean {
95+ return keymaps . has ( renderer ) && ! detached . has ( renderer ) ;
96+ }
97+
98+ /**
99+ * Re-enter the keymap press listeners. After destroy they must return
100+ * without calling into a destroyed host.
101+ */
102+ export function pokeShellKeymapPress ( renderer : CliRenderer ) : void {
103+ for ( const listener of pressListeners . get ( renderer ) ?? [ ] ) {
104+ listener ( { name : "c" } as KeyEvent ) ;
105+ }
31106}
32107
33108function helpFromKeymap ( keymap : ShellKeymap ) : readonly string [ ] {
0 commit comments