@@ -51,9 +51,12 @@ type ChordActionDef = {
5151 handler : KeyHandler ;
5252} ;
5353
54+ type KeyMapEntry < T = KeyHandler > = { key : string ; handler : T } ;
55+ type ChordEntry = { key : string ; subKeys : KeyMapEntry [ ] } ;
56+
5457const simpleControlShiftAtom = jotai . atom ( false ) ;
55- const globalKeyMap = new Map < string , ( waveEvent : WaveKeyboardEvent ) => boolean > ( ) ;
56- const globalChordMap = new Map < string , Map < string , KeyHandler > > ( ) ;
58+ let globalKeyBindings : KeyMapEntry [ ] = [ ] ;
59+ let globalChordBindings : ChordEntry [ ] = [ ] ;
5760let globalKeybindingsDisabled = false ;
5861
5962// track current chord state and timeout (for resetting)
@@ -420,12 +423,12 @@ async function handleSplitVertical(position: "before" | "after") {
420423
421424let lastHandledEvent : KeyboardEvent | null = null ;
422425
423- // returns [keymatch, T]
424- function checkKeyMap < T > ( waveEvent : WaveKeyboardEvent , keyMap : Map < string , T > ) : [ string , T ] {
425- for ( const key of keyMap . keys ( ) ) {
426- if ( keyutil . checkKeyPressed ( waveEvent , key ) ) {
427- const val = keyMap . get ( key ) ;
428- return [ key , val ] ;
426+ // returns [keymatch, T] — iterates in reverse so later entries (user overrides) win
427+ function checkKeyArray < T > ( waveEvent : WaveKeyboardEvent , entries : KeyMapEntry < T > [ ] ) : [ string , T ] {
428+ for ( let i = entries . length - 1 ; i >= 0 ; i -- ) {
429+ const entry = entries [ i ] ;
430+ if ( keyutil . checkKeyPressed ( waveEvent , entry . key ) ) {
431+ return [ entry . key , entry . handler ] ;
429432 }
430433 }
431434 return [ null , null ] ;
@@ -442,25 +445,28 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
442445 lastHandledEvent = nativeEvent ;
443446 if ( activeChord ) {
444447 console . log ( "handle activeChord" , activeChord ) ;
445- // If we're in chord mode, look for the second key.
446- const chordBindings = globalChordMap . get ( activeChord ) ;
447- const [ , handler ] = checkKeyMap ( waveEvent , chordBindings ) ;
448- if ( handler ) {
449- resetChord ( ) ;
450- return handler ( waveEvent ) ;
451- } else {
452- // invalid chord; reset state and consume key
453- resetChord ( ) ;
454- return true ;
448+ // If we're in chord mode, look for the second key in the matching chord's sub-keys.
449+ const chordEntry = globalChordBindings . find ( ( c ) => c . key === activeChord ) ;
450+ if ( chordEntry ) {
451+ const [ , handler ] = checkKeyArray ( waveEvent , chordEntry . subKeys ) ;
452+ if ( handler ) {
453+ resetChord ( ) ;
454+ return handler ( waveEvent ) ;
455+ }
455456 }
456- }
457- const [ chordKeyMatch ] = checkKeyMap ( waveEvent , globalChordMap ) ;
458- if ( chordKeyMatch ) {
459- setActiveChord ( chordKeyMatch ) ;
457+ // invalid chord; reset state and consume key
458+ resetChord ( ) ;
460459 return true ;
461460 }
461+ // Check if this key initiates a chord
462+ for ( const chord of globalChordBindings ) {
463+ if ( keyutil . checkKeyPressed ( waveEvent , chord . key ) ) {
464+ setActiveChord ( chord . key ) ;
465+ return true ;
466+ }
467+ }
462468
463- const [ , globalHandler ] = checkKeyMap ( waveEvent , globalKeyMap ) ;
469+ const [ , globalHandler ] = checkKeyArray ( waveEvent , globalKeyBindings ) ;
464470 if ( globalHandler ) {
465471 const handled = globalHandler ( waveEvent ) ;
466472 if ( handled ) {
@@ -832,13 +838,35 @@ const defaultChordActions: ChordActionDef[] = [
832838] ;
833839
834840function buildKeyMaps ( userOverrides : KeybindingEntry [ ] ) : void {
835- // 1. Build resolved map: actionId -> { keys, handler }
836- const resolvedActions = new Map < string , { keys : string [ ] ; handler : KeyHandler } > ( ) ;
841+ // 1. Start with default bindings as array entries (key -> handler)
842+ const bindings : KeyMapEntry [ ] = [ ] ;
843+ const chordBindings : ChordEntry [ ] = [ ] ;
844+
845+ // Track which action IDs map to which handler (for user overrides)
846+ const actionHandlers = new Map < string , KeyHandler > ( ) ;
837847 for ( const action of defaultActions ) {
838- resolvedActions . set ( action . id , { keys : [ ...action . defaultKeys ] , handler : action . handler } ) ;
848+ actionHandlers . set ( action . id , action . handler ) ;
849+ for ( const key of action . defaultKeys ) {
850+ bindings . push ( { key, handler : action . handler } ) ;
851+ }
839852 }
840853
841- // 2. Apply user overrides in order (last wins)
854+ // 2. Build chord bindings from defaults
855+ const chordInitiatorAction = defaultActions . find ( ( a ) => a . id === "block:splitChord" ) ;
856+ if ( chordInitiatorAction ) {
857+ const subKeys : KeyMapEntry [ ] = [ ] ;
858+ for ( const chordDef of defaultChordActions ) {
859+ if ( chordDef . parentId === "block:splitChord" ) {
860+ actionHandlers . set ( chordDef . id , chordDef . handler ) ;
861+ subKeys . push ( { key : chordDef . defaultKey , handler : chordDef . handler } ) ;
862+ }
863+ }
864+ for ( const key of chordInitiatorAction . defaultKeys ) {
865+ chordBindings . push ( { key, subKeys : [ ...subKeys ] } ) ;
866+ }
867+ }
868+
869+ // 3. Apply user overrides — append to array (last wins via reverse iteration)
842870 for ( const override of userOverrides ) {
843871 if ( ! override . command || typeof override . command !== "string" ) {
844872 console . warn ( "Skipping keybinding entry with missing/invalid command" ) ;
@@ -848,75 +876,26 @@ function buildKeyMaps(userOverrides: KeybindingEntry[]): void {
848876 console . warn ( `Skipping keybinding entry with invalid key type for command: ${ override . command } ` ) ;
849877 continue ;
850878 }
851- const commandId = override . command . startsWith ( "-" ) ? override . command . substring ( 1 ) : override . command ;
852- if ( override . command . startsWith ( "-" ) || override . key == null ) {
853- // Unbind: remove the action
854- resolvedActions . delete ( commandId ) ;
855- } else {
856- // Override: replace that action's keys
857- const existing = resolvedActions . get ( commandId ) ;
858- if ( existing ) {
859- existing . keys = [ override . key ] ;
860- } else {
861- console . warn ( `Unknown keybinding action: ${ commandId } ` ) ;
862- }
863- }
864- }
865-
866- // 3. Clear and rebuild maps
867- globalKeyMap . clear ( ) ;
868- globalChordMap . clear ( ) ;
869-
870- // 4. Find chord initiator keys
871- const chordInitiatorKeys = new Map < string , string [ ] > ( ) ; // parentId -> keys
872- const chordAction = resolvedActions . get ( "block:splitChord" ) ;
873- if ( chordAction ) {
874- chordInitiatorKeys . set ( "block:splitChord" , chordAction . keys ) ;
875- resolvedActions . delete ( "block:splitChord" ) ; // don't add to globalKeyMap
876- }
877-
878- // 5. Build chord sub-key maps
879- for ( const [ parentId , initiatorKeys ] of chordInitiatorKeys ) {
880- const subKeyMap = new Map < string , KeyHandler > ( ) ;
881- for ( const chordDef of defaultChordActions ) {
882- if ( chordDef . parentId === parentId ) {
883- // Check if user overrode this chord sub-action
884- let subKey : string | null = chordDef . defaultKey ;
885- for ( const override of userOverrides ) {
886- const cmdId = override . command . startsWith ( "-" )
887- ? override . command . substring ( 1 )
888- : override . command ;
889- if ( cmdId === chordDef . id ) {
890- if ( override . command . startsWith ( "-" ) || override . key == null ) {
891- subKey = null ; // unbind
892- } else {
893- subKey = override . key ;
894- }
895- }
896- }
897- if ( subKey != null ) {
898- subKeyMap . set ( subKey , chordDef . handler ) ;
899- }
900- }
879+ const commandId = override . command ;
880+ if ( override . key == null ) {
881+ continue ; // null key = no binding, handled by reverse iteration skipping
901882 }
902- if ( subKeyMap . size > 0 ) {
903- for ( const key of initiatorKeys ) {
904- globalChordMap . set ( key , subKeyMap ) ;
905- }
883+ const handler = actionHandlers . get ( commandId ) ;
884+ if ( handler ) {
885+ bindings . push ( { key : override . key , handler } ) ;
886+ } else {
887+ console . warn ( `Unknown keybinding action: ${ commandId } ` ) ;
906888 }
907889 }
908890
909- // 6. Populate globalKeyMap from resolved simple actions
910- for ( const [ , actionData ] of resolvedActions ) {
911- for ( const key of actionData . keys ) {
912- globalKeyMap . set ( key , actionData . handler ) ;
913- }
914- }
891+ // 4. Assign to globals
892+ globalKeyBindings = bindings ;
893+ globalChordBindings = chordBindings ;
915894
916- // 7 . Re-register with Electron
917- const allKeys = Array . from ( globalKeyMap . keys ( ) ) ;
918- for ( const keys of chordInitiatorKeys . values ( ) ) {
919- allKeys . push ( ... keys ) ;
895+ // 5 . Re-register with Electron
896+ const allKeys = globalKeyBindings . map ( ( e ) => e . key ) ;
897+ for ( const chord of globalChordBindings ) {
898+ allKeys . push ( chord . key ) ;
920899 }
921900 // Special web view keys
922901 allKeys . push ( "Cmd:l" , "Cmd:r" , "Cmd:ArrowRight" , "Cmd:ArrowLeft" , "Cmd:o" ) ;
@@ -934,17 +913,19 @@ function initKeybindingsWatcher() {
934913}
935914
936915function registerBuilderGlobalKeys ( ) {
937- globalKeyMap . set ( "Cmd:w" , ( ) => {
938- getApi ( ) . closeBuilderWindow ( ) ;
939- return true ;
916+ globalKeyBindings . push ( {
917+ key : "Cmd:w" ,
918+ handler : ( ) => {
919+ getApi ( ) . closeBuilderWindow ( ) ;
920+ return true ;
921+ } ,
940922 } ) ;
941- const allKeys = Array . from ( globalKeyMap . keys ( ) ) ;
923+ const allKeys = globalKeyBindings . map ( ( e ) => e . key ) ;
942924 getApi ( ) . registerGlobalWebviewKeys ( allKeys ) ;
943925}
944926
945927function getAllGlobalKeyBindings ( ) : string [ ] {
946- const allKeys = Array . from ( globalKeyMap . keys ( ) ) ;
947- return allKeys ;
928+ return globalKeyBindings . map ( ( e ) => e . key ) ;
948929}
949930
950931export {
0 commit comments