From 7bf1a259a25f6ad2dae67ceabb0f14ce0ff900c7 Mon Sep 17 00:00:00 2001 From: Cole Lashley Date: Tue, 5 Mar 2024 18:19:47 -0800 Subject: [PATCH] Keybindings PR first section - Updating keyutil (#384) * updated keyutil * added keybindings file * export checkKeyPressed --- assets/keybindings.json | 258 ++++++++++++++++++++++++++++++++ src/util/keyutil.ts | 324 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 564 insertions(+), 18 deletions(-) create mode 100644 assets/keybindings.json diff --git a/assets/keybindings.json b/assets/keybindings.json new file mode 100644 index 00000000..9507bc7c --- /dev/null +++ b/assets/keybindings.json @@ -0,0 +1,258 @@ +[ + { + "command": "system:toggleDeveloperTools", + "keys": ["Cmd:Option:i"] + }, + { + "command": "generic:cancel", + "keys": ["Escape"] + }, + { + "command": "generic:confirm", + "keys": ["Enter"] + }, + { + "command": "generic:deleteItem", + "keys": ["Backspace", "Delete"] + }, + { + "command": "generic:selectAbove", + "keys": ["ArrowUp"] + }, + { + "command": "generic:selectBelow", + "keys": ["ArrowDown"] + }, + { + "command": "generic:selectPageAbove", + "keys": ["PageUp"] + }, + { + "command": "generic:selectPageBelow", + "keys": ["PageDown"] + }, + { + "command": "app:openHistory", + "keys": ["Cmd:h"] + }, + { + "command": "app:openTabSearchModal", + "keys": ["Cmd:p"] + }, + { + "command": "app:newTab", + "keys": ["Cmd:t"] + }, + { + "command": "app:focusCmdInput", + "keys": ["Cmd:i"] + }, + { + "command": "app:focusSelectedLine", + "keys": ["Cmd:l"] + }, + { + "command": "app:restartCommand", + "keys": ["Cmd:r"] + }, + { + "command": "app:restartLastCommand", + "keys": ["Cmd:Shift:r"] + }, + { + "command": "app:closeCurrentTab", + "keys": ["Cmd:w"] + }, + { + "command": "app:selectLineAbove", + "keys": ["Cmd:ArrowUp", "Cmd:PageUp"] + }, + { + "command": "app:selectLineBelow", + "keys": ["Cmd:ArrowDown", "Cmd:PageDown"] + }, + { + "command": "app:selectTab-1", + "keys": ["Cmd:1"] + }, + { + "command": "app:selectTab-2", + "keys": ["Cmd:2"] + }, + { + "command": "app:selectTab-3", + "keys": ["Cmd:3"] + }, + { + "command": "app:selectTab-4", + "keys": ["Cmd:4"] + }, + { + "command": "app:selectTab-5", + "keys": ["Cmd:5"] + }, + { + "command": "app:selectTab-6", + "keys": ["Cmd:6"] + }, + { + "command": "app:selectTab-7", + "keys": ["Cmd:7"] + }, + { + "command": "app:selectTab-8", + "keys": ["Cmd:8"] + }, + { + "command": "app:selectTab-9", + "keys": ["Cmd:9"] + }, + { + "command": "app:selectTabLeft", + "keys": ["Cmd:["] + }, + { + "command": "app:selectTabRight", + "keys": ["Cmd:]"] + }, + { + "command": "app:selectWorkspace-1", + "keys": ["Cmd:Ctrl:1"] + }, + { + "command": "app:selectWorkspace-2", + "keys": ["Cmd:Ctrl:2"] + }, + { + "command": "app:selectWorkspace-3", + "keys": ["Cmd:Ctrl:3"] + }, + { + "command": "app:selectWorkspace-4", + "keys": ["Cmd:Ctrl:4"] + }, + { + "command": "app:selectWorkspace-5", + "keys": ["Cmd:Ctrl:5"] + }, + { + "command": "app:selectWorkspace-6", + "keys": ["Cmd:Ctrl:6"] + }, + { + "command": "app:selectWorkspace-7", + "keys": ["Cmd:Ctrl:7"] + }, + { + "command": "app:selectWorkspace-8", + "keys": ["Cmd:Ctrl:8"] + }, + { + "command": "app:selectWorkspace-9", + "keys": ["Cmd:Ctrl:9"] + }, + { + "command": "app:toggleSidebar", + "keys": ["Cmd:Ctrl:s"] + }, + { + "command": "app:deleteActiveLine", + "keys": ["Cmd:d"] + }, + { + "command": "app:bookmarkActiveLine", + "keys": ["Cmd:b"] + }, + { + "command": "bookmarks:edit", + "keys": ["e"] + }, + { + "command": "bookmarks:copy", + "keys": ["c"] + }, + { + "command": "cmdinput:autocomplete", + "keys": ["Tab"] + }, + { + "command": "cmdinput:expandInput", + "keys": ["Cmd:e"] + }, + { + "command": "cmdinput:clearInput", + "keys": ["Ctrl:c"] + }, + { + "command": "cmdinput:cutLineLeftOfCursor", + "keys": ["Ctrl:u"] + }, + { + "command": "cmdinput:previousHistoryItem", + "keys": ["Ctrl:p"] + }, + { + "command": "cmdinput:nextHistoryItem", + "keys": ["Ctrl:n"] + }, + { + "command": "cmdinput:cutWordLeftOfCursor", + "keys": ["Ctrl:w"] + }, + { + "command": "cmdinput:paste", + "keys": ["Ctrl:y"] + }, + { + "command": "cmdinput:openHistory", + "keys": ["Ctrl:r"] + }, + { + "command": "cmdinput:openAIChat", + "keys": ["Ctrl:Space"] + }, + { + "command": "history:closeHistory", + "keys": ["Ctrl:g", "Ctrl:c"] + }, + { + "command": "history:toggleShowRemotes", + "keys": ["Cmd:r", "Ctrl:r"] + }, + { + "command": "history:changeScope", + "keys": ["Ctrl:s", "Cmd:s"] + }, + { + "command": "history:selectNextItem", + "keys": ["Ctrl:n"] + }, + { + "command": "history:selectPreviousItem", + "keys": ["Ctrl:p"] + }, + { + "command": "aichat:clearHistory", + "keys": ["Ctrl:l"] + }, + { + "command": "terminal:copy", + "keys": ["Ctrl:Shift:c"] + }, + { + "command": "terminal:paste", + "keys": ["Ctrl:Shift:v"] + }, + { + "command": "codeedit:save", + "keys": ["Cmd:s"] + }, + { + "command": "codeedit:close", + "keys": ["Cmd:d"] + }, + { + "command": "codeedit:togglePreview", + "keys": ["Cmd:p"] + } + ] \ No newline at end of file diff --git a/src/util/keyutil.ts b/src/util/keyutil.ts index 347640af..1d394d65 100644 --- a/src/util/keyutil.ts +++ b/src/util/keyutil.ts @@ -1,5 +1,8 @@ import * as React from "react"; import * as electron from "electron"; +import { parse } from "node:path"; +import { v4 as uuidv4 } from "uuid"; +import keybindings from "../../assets/keybindings.json"; type KeyPressDecl = { mods: { @@ -11,8 +14,264 @@ type KeyPressDecl = { Meta?: boolean; }; key: string; + keyType: string; }; +const KeyTypeCodeRegex = /:c{(.*)}/; +const KeyTypeKey = "key"; +const KeyTypeCode = "code"; + +type KeybindCallback = (event: WaveKeyboardEvent) => boolean; + +type Keybind = { + domain: string; + keybinding: string; + callback: KeybindCallback; +}; + +const KeybindLevels = ["system", "modal", "app", "pane", "plugin"]; + +class KeybindManager { + domainCallbacks: Map; + levelMap: Map>; + levelArray: Array; + keyDescriptionsMap: Map>; + + processLevel(nativeEvent: any, event: WaveKeyboardEvent, keybindsArray: Array): boolean { + // iterate through keybinds in backwards order + for (let index = keybindsArray.length - 1; index >= 0; index--) { + let curKeybind = keybindsArray[index]; + if (this.checkKeyPressed(event, curKeybind.keybinding)) { + let shouldReturn = false; + if (curKeybind.callback != null) { + console.log("Calling callback", curKeybind.domain); + shouldReturn = curKeybind.callback(event); + console.log("callback return value", shouldReturn); + } + if (!shouldReturn && this.domainCallbacks.has(curKeybind.domain)) { + let curDomainCallback = this.domainCallbacks.get(curKeybind.domain); + if (curDomainCallback != null) { + shouldReturn = curDomainCallback(event); + } else { + console.log("domain callback for ", curKeybind.domain, " is null. This should never happen"); + } + } + if (shouldReturn) { + nativeEvent.preventDefault(); + nativeEvent.stopPropagation(); + return true; + } + } + } + return false; + } + + processKeyEvent(nativeEvent: any, event: WaveKeyboardEvent) { + let modalLevel = this.levelMap.get("modal"); + if (modalLevel.length != 0) { + console.log("processing modal"); + // special case when modal keybindings are present + let shouldReturn = this.processLevel(nativeEvent, event, modalLevel); + if (shouldReturn) { + return; + } + let systemLevel = this.levelMap.get("system"); + this.processLevel(nativeEvent, event, systemLevel); + return; + } + for (let index = this.levelArray.length - 1; index >= 0; index--) { + let curLevel = this.levelArray[index]; + let curKeybindsArray; + if (this.levelMap.has(curLevel)) { + curKeybindsArray = this.levelMap.get(curLevel); + } else { + console.error("error processing key event: couldn't find level: ", curLevel); + continue; + } + let shouldReturn = this.processLevel(nativeEvent, event, curKeybindsArray); + if (shouldReturn) { + return; + } + } + } + + keybindingAlreadyAdded(level: string, domain: string, keybinding: string) { + if (!this.levelMap.has(level)) { + return false; + } + let keybindsArray = this.levelMap.get(level); + for (let index = 0; index < keybindsArray.length; index++) { + let curKeybind = keybindsArray[index]; + if (curKeybind.domain == domain && keybindingIsEqual(curKeybind.keybinding, keybinding)) { + console.log("keybinding is equal: ", curKeybind.keybinding, keybinding, curKeybind.domain, domain); + return true; + } + } + return false; + } + + registerKeybinding(level: string, domain: string, keybinding: string, callback: KeybindCallback): boolean { + if (domain == "" || this.keybindingAlreadyAdded(level, domain, keybinding)) { + return false; + } + // TODO: check if keybinding is valid + let newKeybind = { domain: domain, keybinding: keybinding, callback: callback } as Keybind; + if (!this.levelMap.has(level)) { + return false; + } + let curKeybindArray = this.levelMap.get(level); + curKeybindArray.push(newKeybind); + this.levelMap.set(level, curKeybindArray); + return true; + } + + registerAndCheckKeyPressed( + nativeEvent: any, + waveEvent: WaveKeyboardEvent, + level: string, + keybinding: string + ): boolean { + let rtn = false; + let curDomain = String(uuidv4()); + this.registerKeybinding(level, curDomain, keybinding, (waveEvent) => { + rtn = true; + return true; + }); + this.processKeyEvent(nativeEvent, waveEvent); + let didUnregister = this.unregisterKeybinding(level, curDomain, keybinding); + return rtn; + } + + registerAndProcessKeyEvent( + nativeEvent: any, + waveEvent: WaveKeyboardEvent, + level: string, + domain: string, + keybinding: string, + callback: KeybindCallback + ) { + this.registerKeybinding(level, domain, keybinding, callback); + this.processKeyEvent(nativeEvent, waveEvent); + this.unregisterKeybinding(level, domain, keybinding); + } + + unregisterKeybinding(level: string, domain: string, keybinding: string): boolean { + if (!this.levelMap.has(level)) { + return false; + } + let keybindsArray = this.levelMap.get(level); + for (let index = 0; index < keybindsArray.length; index++) { + let curKeybind = keybindsArray[index]; + if (curKeybind.domain == domain && keybindingIsEqual(curKeybind.keybinding, keybinding)) { + console.log("unregistering keybinding"); + keybindsArray.splice(index, 1); + index--; + this.levelMap.set(level, keybindsArray); + } + return true; + } + return false; + } + + unregisterDomain(domain: string) { + let foundKeybind = false; + for (let levelIndex = 0; levelIndex < this.levelArray.length; levelIndex++) { + let curLevel = this.levelArray[levelIndex]; + let curKeybindArray = this.levelMap.get(curLevel); + for (let curArrayIndex = 0; curArrayIndex < curKeybindArray.length; curArrayIndex++) { + let curKeybind = curKeybindArray[curArrayIndex]; + if (curKeybind.domain == domain) { + curKeybindArray.splice(curArrayIndex, 1); + curArrayIndex--; + foundKeybind = true; + } + } + } + this.domainCallbacks.delete(domain); + return foundKeybind; + } + + getKeyPressEventForDomain(domain: string, callback: KeybindCallback) { + if (callback == null) { + console.log("domain callback can't be null"); + } + this.domainCallbacks.set(domain, callback); + } + + constructor() { + this.levelMap = new Map(); + this.domainCallbacks = new Map(); + this.levelArray = KeybindLevels; + for (let index = 0; index < this.levelArray.length; index++) { + let curLevel = this.levelArray[index]; + this.levelMap.set(curLevel, new Array()); + } + this.initKeyDescriptionsMap(); + } + + initKeyDescriptionsMap() { + this.keyDescriptionsMap = new Map(); + for (let index = 0; index < keybindings.length; index++) { + let curKeybind = keybindings[index]; + this.keyDescriptionsMap.set(curKeybind.command, curKeybind.keys); + } + let error = false; + let numberedTabKeybinds = []; + for (let index = 1; index <= 9; index++) { + let curKeybind = this.keyDescriptionsMap.get("app:selectTab-" + index); + if (curKeybind == null) { + error = true; + break; + } + numberedTabKeybinds = numberedTabKeybinds.concat(curKeybind); + } + if (!error) { + this.keyDescriptionsMap.set("app:selectNumberedTab", numberedTabKeybinds); + } + let numberedWorkspaceKeybinds = []; + for (let index = 1; index <= 9; index++) { + let curKeybind = this.keyDescriptionsMap.get("app:selectTab-" + index); + if (curKeybind == null) { + error = true; + break; + } + numberedWorkspaceKeybinds = numberedWorkspaceKeybinds.concat(curKeybind); + } + if (!error) { + this.keyDescriptionsMap.set("app:selectNumberedTab", numberedWorkspaceKeybinds); + } + } + + checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean { + if (keyDescription == "any") { + return true; + } + if (!this.keyDescriptionsMap.has(keyDescription)) { + return false; + } + let keyPressArray = this.keyDescriptionsMap.get(keyDescription); + for (let index = 0; index < keyPressArray.length; index++) { + let curKeyPress = keyPressArray[index]; + let pressed = checkKeyPressed(event, curKeyPress); + if (pressed) { + return true; + } + } + return false; + } + + checkKeysPressed(event: WaveKeyboardEvent, keyDescriptions: Array) { + for (let index = 0; index < keyDescriptions.length; index++) { + let curKeyDesc = keyDescriptions[index]; + let pressed = this.checkKeyPressed(event, curKeyDesc); + if (pressed) { + return true; + } + } + return false; + } +} + var PLATFORM: string; const PlatformMacOS: string = "darwin"; @@ -20,6 +279,10 @@ function setKeyUtilPlatform(platform: string) { PLATFORM = platform; } +function keybindingIsEqual(bind1: string, bind2: string) { + return bind1 == bind2; +} + function parseKeyDescription(keyDescription: string): KeyPressDecl { let rtn = { key: "", mods: {} } as KeyPressDecl; let keys = keyDescription.replace(/[()]/g, "").split(":"); @@ -37,8 +300,10 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl { } else if (key == "Meta") { rtn.mods.Meta = true; } else { - rtn.key = key; - if (key.length == 1) { + let { key: parsedKey, type: keyType } = parseKey(key); + rtn.key = parsedKey; + rtn.keyType = keyType; + if (rtn.keyType == KeyTypeKey && key.length == 1) { // check for if key is upper case if (/[A-Z]/.test(key.charAt(0))) { // this key is an upper case A - Z - we should apply the shift key, even if it wasn't specified @@ -53,8 +318,19 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl { return rtn; } -function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean { - let keyPress = parseKeyDescription(description); +function parseKey(key: string): { key: string; type: string } { + let regexMatch = key.match(KeyTypeCodeRegex); + if (regexMatch != null && regexMatch.length > 1) { + let code = regexMatch[1]; + return { key: code, type: KeyTypeCode }; + } else if (regexMatch != null) { + console.log("error: regexMatch is not null yet there is no captured group: ", regexMatch, key); + } + return { key: key, type: KeyTypeKey }; +} + +function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean { + let keyPress = parseKeyDescription(keyDescription); if (keyPress.mods.Option && !event.option) { return false; } @@ -73,16 +349,22 @@ function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean if (keyPress.mods.Meta && !event.meta) { return false; } - let eventKey = event.key; + let eventKey = ""; let descKey = keyPress.key; - if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) { - // key is upper case A-Z, this means shift is applied, we want to allow - // "Shift:e" as well as "Shift:E" or "E" - eventKey = eventKey.toLocaleLowerCase(); - descKey = descKey.toLocaleLowerCase(); - } else if (eventKey == " ") { - eventKey = "Space"; - // a space key is shown as " ", we want users to be able to set space key as "Space" or " ", whichever they prefer + if (keyPress.keyType == KeyTypeCode) { + eventKey = event.code; + } + if (keyPress.keyType == KeyTypeKey) { + eventKey = event.key; + if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) { + // key is upper case A-Z, this means shift is applied, we want to allow + // "Shift:e" as well as "Shift:E" or "E" + eventKey = eventKey.toLocaleLowerCase(); + descKey = descKey.toLocaleLowerCase(); + } else if (eventKey == " ") { + eventKey = "Space"; + // a space key is shown as " ", we want users to be able to set space key as "Space" or " ", whichever they prefer + } } if (descKey != eventKey) { return false; @@ -139,8 +421,8 @@ function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEve let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent; rtn.control = event.ctrlKey; rtn.shift = event.shiftKey; - rtn.cmd = (PLATFORM == PlatformMacOS ? event.metaKey : event.altKey); - rtn.option = (PLATFORM == PlatformMacOS ? event.altKey : event.metaKey); + rtn.cmd = PLATFORM == PlatformMacOS ? event.metaKey : event.altKey; + rtn.option = PLATFORM == PlatformMacOS ? event.altKey : event.metaKey; rtn.meta = event.metaKey; rtn.alt = event.altKey; rtn.code = event.code; @@ -155,8 +437,8 @@ function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent { let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent; rtn.type = event.type; rtn.control = event.control; - rtn.cmd = (PLATFORM == PlatformMacOS ? event.meta : event.alt) - rtn.option = (PLATFORM == PlatformMacOS ? event.alt : event.meta); + rtn.cmd = PLATFORM == PlatformMacOS ? event.meta : event.alt; + rtn.option = PLATFORM == PlatformMacOS ? event.alt : event.meta; rtn.meta = event.meta; rtn.alt = event.alt; rtn.shift = event.shift; @@ -167,5 +449,11 @@ function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent { return rtn; } -export { adaptFromElectronKeyEvent, adaptFromReactOrNativeKeyEvent, checkKeyPressed, setKeyUtilPlatform }; +export { + KeybindManager, + adaptFromElectronKeyEvent, + adaptFromReactOrNativeKeyEvent, + setKeyUtilPlatform, + checkKeyPressed, +}; export type { WaveKeyboardEvent };