From ec00768eb07d1ba5ea4e21b1f3f1fc541c26153f Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 12 Feb 2025 15:38:26 -0800 Subject: [PATCH 01/11] capture-screenshot api --- emain/emain.ts | 10 ++++++++++ emain/preload.ts | 3 ++- frontend/types/custom.d.ts | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/emain/emain.ts b/emain/emain.ts index c2037b70c2..7ac390c9cf 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -259,6 +259,16 @@ electron.ipcMain.on("get-cursor-point", (event) => { event.returnValue = retVal; }); +electron.ipcMain.handle("capture-screenshot", async (event, rect) => { + const tabView = getWaveTabViewByWebContentsId(event.sender.id); + if (!tabView) { + throw new Error("No tab view found for the given webContents id"); + } + const image = await tabView.webContents.capturePage(rect); + const base64String = image.toPNG().toString("base64"); + return `data:image/png;base64,${base64String}`; +}); + electron.ipcMain.on("get-env", (event, varName) => { event.returnValue = process.env[varName] ?? null; }); diff --git a/emain/preload.ts b/emain/preload.ts index ff5b25851c..2b1c1387f7 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { contextBridge, ipcRenderer, WebviewTag } from "electron"; +import { contextBridge, ipcRenderer, Rectangle, WebviewTag } from "electron"; contextBridge.exposeInMainWorld("api", { getAuthKey: () => ipcRenderer.sendSync("get-auth-key"), @@ -51,6 +51,7 @@ contextBridge.exposeInMainWorld("api", { sendLog: (log) => ipcRenderer.send("fe-log", log), onQuicklook: (filePath: string) => ipcRenderer.send("quicklook", filePath), openNativePath: (filePath: string) => ipcRenderer.send("open-native-path", filePath), + captureScreenshot: (rect: Rectangle) => ipcRenderer.invoke("capture-screenshot", rect), }); // Custom event for "new-window" diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 70a7777618..e6f5cc5c7e 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -101,6 +101,7 @@ declare global { sendLog: (log: string) => void; onQuicklook: (filePath: string) => void; openNativePath(filePath: string): void; + captureScreenshot(rect: Electron.Rectangle): Promise; }; type ElectronContextMenuItem = { From f123d9cc222e43bbcc00b7231ceb045026a3b6cf Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 12 Feb 2025 16:12:57 -0800 Subject: [PATCH 02/11] ctrl-shift-k to kill a block without removing it from the layout --- docs/docs/keybindings.mdx | 1 + frontend/app/block/blockframe.tsx | 4 +--- frontend/app/store/global.ts | 1 + frontend/app/store/keymodel.ts | 14 ++++++++++++++ frontend/util/keyutil.ts | 30 ++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/docs/docs/keybindings.mdx b/docs/docs/keybindings.mdx index ee3fac1075..2eb4725505 100644 --- a/docs/docs/keybindings.mdx +++ b/docs/docs/keybindings.mdx @@ -35,6 +35,7 @@ replace "Cmd" with "Alt" (note that "Ctrl" is "Ctrl" on both Mac, Windows, and L | | Show block numbers | | | Switch to block number | | | Move left, right, up, down between blocks | +| | Replace the current block with a launcher block | | | Switch to tab number | | | Switch tab left | | | Switch tab right | diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 7d359993fc..8a0b822a20 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -643,13 +643,11 @@ const BlockFrame = React.memo((props: BlockFrameProps) => { const blockId = props.nodeModel.blockId; const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); const tabData = jotai.useAtomValue(atoms.tabAtom); - if (!blockId || !blockData) { return null; } - const FrameElem = BlockFrame_Default; const numBlocks = tabData?.blockids?.length ?? 0; - return ; + return ; }); export { BlockFrame, NumActiveConnColors }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 0ee5ce87e8..0cf54433b1 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -763,6 +763,7 @@ export { getBlockComponentModel, getBlockMetaKeyAtom, getConnStatusAtom, + getFocusedBlockId, getHostName, getObjectId, getOverrideConfigAtom, diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 6aa6401474..89373d2997 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -10,9 +10,11 @@ import { getAllBlockComponentModels, getApi, getBlockComponentModel, + getFocusedBlockId, getSettingsKeyAtom, globalStore, refocusNode, + replaceBlock, WOS, } from "@/app/store/global"; import { @@ -380,6 +382,18 @@ function registerGlobalKeys() { switchBlockInDirection(tabId, NavigateDirection.Right); return true; }); + globalKeyMap.set("Ctrl:Shift:k", () => { + const blockId = getFocusedBlockId(); + if (blockId == null) { + return true; + } + replaceBlock(blockId, { + meta: { + view: "launcher", + }, + }); + return true; + }); globalKeyMap.set("Cmd:g", () => { const bcm = getBlockComponentModel(getFocusedBlockInStaticTab()); if (bcm.openSwitchConnection != null) { diff --git a/frontend/util/keyutil.ts b/frontend/util/keyutil.ts index f6558e3cca..782001e108 100644 --- a/frontend/util/keyutil.ts +++ b/frontend/util/keyutil.ts @@ -31,6 +31,35 @@ function keydownWrapper( }; } +function waveEventToKeyDesc(waveEvent: WaveKeyboardEvent): string { + let keyDesc: string[] = []; + if (waveEvent.cmd) { + keyDesc.push("Cmd"); + } + if (waveEvent.option) { + keyDesc.push("Option"); + } + if (waveEvent.meta) { + keyDesc.push("Meta"); + } + if (waveEvent.control) { + keyDesc.push("Ctrl"); + } + if (waveEvent.shift) { + keyDesc.push("Shift"); + } + if (waveEvent.key != null && waveEvent.key != "") { + if (waveEvent.key == " ") { + keyDesc.push("Space"); + } else { + keyDesc.push(waveEvent.key); + } + } else { + keyDesc.push("c{" + waveEvent.code + "}"); + } + return keyDesc.join(":"); +} + function parseKey(key: string): { key: string; type: string } { let regexMatch = key.match(KeyTypeCodeRegex); if (regexMatch != null && regexMatch.length > 1) { @@ -303,4 +332,5 @@ export { keydownWrapper, parseKeyDescription, setKeyUtilPlatform, + waveEventToKeyDesc, }; From e5b267e87861cc902d2e1cfbc8c939b3a9802ffa Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Feb 2025 13:55:11 -0800 Subject: [PATCH 03/11] add nullcheck --- frontend/util/keyutil.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/util/keyutil.ts b/frontend/util/keyutil.ts index 782001e108..867dfcb4e2 100644 --- a/frontend/util/keyutil.ts +++ b/frontend/util/keyutil.ts @@ -212,7 +212,7 @@ function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): bool } if (keyPress.keyType == KeyTypeKey) { eventKey = event.key; - if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) { + if (eventKey != null && 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(); From 9e62cd8eecc8581cb6b50fa1470fc6b1b2a5e658 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Feb 2025 14:06:49 -0800 Subject: [PATCH 04/11] basic keyboard chord handling --- emain/preload.ts | 1 + frontend/app/store/keymodel.ts | 95 ++++++++++++++++++++++--- frontend/app/view/launcher/launcher.tsx | 2 +- frontend/types/custom.d.ts | 1 + 4 files changed, 89 insertions(+), 10 deletions(-) diff --git a/emain/preload.ts b/emain/preload.ts index 2b1c1387f7..0c0633fdfe 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -52,6 +52,7 @@ contextBridge.exposeInMainWorld("api", { onQuicklook: (filePath: string) => ipcRenderer.send("quicklook", filePath), openNativePath: (filePath: string) => ipcRenderer.send("open-native-path", filePath), captureScreenshot: (rect: Rectangle) => ipcRenderer.invoke("capture-screenshot", rect), + setKeyboardChordMode: () => ipcRenderer.send("set-keyboard-chord-mode"), }); // Custom event for "new-window" diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 89373d2997..34145a61d5 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -29,8 +29,31 @@ import { fireAndForget } from "@/util/util"; import * as jotai from "jotai"; import { modalsModel } from "./modalmodel"; +type KeyHandler = (event: WaveKeyboardEvent) => boolean; + const simpleControlShiftAtom = jotai.atom(false); const globalKeyMap = new Map boolean>(); +const globalChordMap = new Map>(); + +// track current chord state and timeout (for resetting) +let activeChord: string | null = null; +let chordTimeout: NodeJS.Timeout = null; + +function resetChord() { + activeChord = null; + if (chordTimeout) { + clearTimeout(chordTimeout); + chordTimeout = null; + } +} + +function setActiveChord(activeChordArg: string) { + if (chordTimeout) { + clearTimeout(chordTimeout); + } + activeChord = activeChordArg; + chordTimeout = setTimeout(() => resetChord(), 2000); +} export function keyboardMouseDownHandler(e: MouseEvent) { if (!e.ctrlKey || !e.shiftKey) { @@ -71,7 +94,7 @@ function shouldDispatchToBlock(e: WaveKeyboardEvent): boolean { const activeElem = document.activeElement; if (activeElem != null && activeElem instanceof HTMLElement) { if (activeElem.tagName == "INPUT" || activeElem.tagName == "TEXTAREA" || activeElem.contentEditable == "true") { - if (activeElem.classList.contains("dummy-focus")) { + if (activeElem.classList.contains("dummy-focus") || activeElem.classList.contains("dummy")) { return true; } if (keyutil.isInputEvent(e)) { @@ -218,38 +241,73 @@ async function handleCmdN() { await createBlock(blockDef); } -async function handleSplitHorizontal() { +async function handleSplitHorizontal(position: "before" | "after") { const layoutModel = getLayoutModelForStaticTab(); const focusedNode = globalStore.get(layoutModel.focusedNode); if (focusedNode == null) { return; } const blockDef = getDefaultNewBlockDef(); - await createBlockSplitHorizontally(blockDef, focusedNode.data.blockId, "after"); + await createBlockSplitHorizontally(blockDef, focusedNode.data.blockId, position); } -async function handleSplitVertical() { +async function handleSplitVertical(position: "before" | "after") { const layoutModel = getLayoutModelForStaticTab(); const focusedNode = globalStore.get(layoutModel.focusedNode); if (focusedNode == null) { return; } const blockDef = getDefaultNewBlockDef(); - await createBlockSplitVertically(blockDef, focusedNode.data.blockId, "after"); + await createBlockSplitVertically(blockDef, focusedNode.data.blockId, position); } let lastHandledEvent: KeyboardEvent | null = null; +// returns [keymatch, T] +function checkKeyMap(waveEvent: WaveKeyboardEvent, keyMap: Map): [string, T] { + for (const key of keyMap.keys()) { + if (keyutil.checkKeyPressed(waveEvent, key)) { + const val = keyMap.get(key); + return [key, val]; + } + } + return [null, null]; +} + function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean { const nativeEvent = (waveEvent as any).nativeEvent; if (lastHandledEvent != null && nativeEvent != null && lastHandledEvent === nativeEvent) { + console.log("lastHandledEvent return false"); return false; } lastHandledEvent = nativeEvent; - const handled = handleGlobalWaveKeyboardEvents(waveEvent); - if (handled) { + if (activeChord) { + console.log("handle activeChord", activeChord); + // If we're in chord mode, look for the second key. + const chordBindings = globalChordMap.get(activeChord); + const [, handler] = checkKeyMap(waveEvent, chordBindings); + if (handler) { + resetChord(); + return handler(waveEvent); + } else { + // invalid chord; reset state and consume key + resetChord(); + return true; + } + } + const [chordKeyMatch] = checkKeyMap(waveEvent, globalChordMap); + if (chordKeyMatch) { + setActiveChord(chordKeyMatch); return true; } + + const [, globalHandler] = checkKeyMap(waveEvent, globalKeyMap); + if (globalHandler) { + const handled = globalHandler(waveEvent); + if (handled) { + return true; + } + } const layoutModel = getLayoutModelForStaticTab(); const focusedNode = globalStore.get(layoutModel.focusedNode); const blockId = focusedNode?.data?.blockId; @@ -321,11 +379,11 @@ function registerGlobalKeys() { return true; }); globalKeyMap.set("Cmd:d", () => { - handleSplitHorizontal(); + handleSplitHorizontal("after"); return true; }); globalKeyMap.set("Shift:Cmd:d", () => { - handleSplitVertical(); + handleSplitVertical("after"); return true; }); globalKeyMap.set("Cmd:i", () => { @@ -459,6 +517,25 @@ function registerGlobalKeys() { // special case keys, handled by web view allKeys.push("Cmd:l", "Cmd:r", "Cmd:ArrowRight", "Cmd:ArrowLeft", "Cmd:o"); getApi().registerGlobalWebviewKeys(allKeys); + + const splitBlockKeys = new Map(); + splitBlockKeys.set("ArrowUp", () => { + handleSplitVertical("before"); + return true; + }); + splitBlockKeys.set("ArrowDown", () => { + handleSplitVertical("after"); + return true; + }); + splitBlockKeys.set("ArrowLeft", () => { + handleSplitHorizontal("before"); + return true; + }); + splitBlockKeys.set("ArrowRight", () => { + handleSplitHorizontal("after"); + return true; + }); + globalChordMap.set("Ctrl:Shift:s", splitBlockKeys); } function getAllGlobalKeyBindings(): string[] { diff --git a/frontend/app/view/launcher/launcher.tsx b/frontend/app/view/launcher/launcher.tsx index b0c311f949..ceb87b32c1 100644 --- a/frontend/app/view/launcher/launcher.tsx +++ b/frontend/app/view/launcher/launcher.tsx @@ -212,7 +212,7 @@ const LauncherView: React.FC> = ({ blockId value={searchTerm} onKeyDown={keydownWrapper(model.keyDownHandler.bind(model))} onChange={(e) => setSearchTerm(e.target.value)} - className="sr-only" + className="sr-only dummy" aria-label="Search widgets" /> diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index e6f5cc5c7e..4f2960b3c5 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -102,6 +102,7 @@ declare global { onQuicklook: (filePath: string) => void; openNativePath(filePath: string): void; captureScreenshot(rect: Electron.Rectangle): Promise; + setKeyboardChordMode: () => void; }; type ElectronContextMenuItem = { From 0f8357d1dfb21adbb8a39aa172333d33a2310529 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Feb 2025 14:07:17 -0800 Subject: [PATCH 05/11] emain part of keyboard chord handling --- emain/emain-tabview.ts | 23 +++++++++++++++++++++++ emain/emain.ts | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts index 7a93269881..c978e7c6a0 100644 --- a/emain/emain-tabview.ts +++ b/emain/emain-tabview.ts @@ -45,6 +45,8 @@ export class WaveTabView extends WebContentsView { isInitialized: boolean = false; isWaveReady: boolean = false; isDestroyed: boolean = false; + keyboardChordMode: boolean = false; + resetChordModeTimeout: NodeJS.Timeout = null; constructor(fullConfig: FullConfigType) { console.log("createBareTabView"); @@ -91,6 +93,22 @@ export class WaveTabView extends WebContentsView { this._waveTabId = waveTabId; } + setKeyboardChordMode(mode: boolean) { + this.keyboardChordMode = mode; + if (mode) { + if (this.resetChordModeTimeout) { + clearTimeout(this.resetChordModeTimeout); + } + this.resetChordModeTimeout = setTimeout(() => { + this.keyboardChordMode = false; + }, 2000); + } else { + if (this.resetChordModeTimeout) { + clearTimeout(this.resetChordModeTimeout); + } + } + } + positionTabOnScreen(winBounds: Rectangle) { const curBounds = this.getBounds(); if ( @@ -220,6 +238,11 @@ export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: stri // console.log("WIN bie", tabView.waveTabId.substring(0, 8), waveEvent.type, waveEvent.code); handleCtrlShiftState(tabView.webContents, waveEvent); setWasActive(true); + if (input.type == "keyDown" && tabView.keyboardChordMode) { + e.preventDefault(); + tabView.setKeyboardChordMode(false); + tabView.webContents.send("reinject-key", waveEvent); + } }); tabView.webContents.on("zoom-changed", (e) => { tabView.webContents.send("zoom-changed"); diff --git a/emain/emain.ts b/emain/emain.ts index 7ac390c9cf..80f6e1703f 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -322,6 +322,12 @@ electron.ipcMain.on("register-global-webview-keys", (event, keys: string[]) => { webviewKeys = keys ?? []; }); +electron.ipcMain.on("set-keyboard-chord-mode", (event) => { + event.returnValue = null; + const tabView = getWaveTabViewByWebContentsId(event.sender.id); + tabView?.setKeyboardChordMode(true); +}); + if (unamePlatform !== "darwin") { const fac = new FastAverageColor(); From 3cdd08d091f9d941e365a48b8e28ac266bff2209 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Feb 2025 14:22:25 -0800 Subject: [PATCH 06/11] add chords for splitting --- docs/docs/keybindings.mdx | 52 +++++++++++++++++++++---------------- docs/src/components/kbd.tsx | 12 +++++++++ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/docs/docs/keybindings.mdx b/docs/docs/keybindings.mdx index 2eb4725505..4f2125ebed 100644 --- a/docs/docs/keybindings.mdx +++ b/docs/docs/keybindings.mdx @@ -4,7 +4,7 @@ id: "keybindings" title: "Key Bindings" --- -import { Kbd } from "@site/src/components/kbd.tsx"; +import { Kbd, KbdChord } from "@site/src/components/kbd.tsx"; import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx"; @@ -15,33 +15,39 @@ Some keybindings are always active. Others are only active for certain types of Note that these are the MacOS keybindings (they use "Cmd"). For Windows and Linux, replace "Cmd" with "Alt" (note that "Ctrl" is "Ctrl" on both Mac, Windows, and Linux). +Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd chord key after typing the first key. Hitting Escape after an initial chord key will always be a no-op. + ## Global Keybindings
-| Key | Function | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| | Open a new tab | -| | Open a new block (defaults to a terminal block with the same connection and working directory). Switch to launcher using `app:defaultnewblock` setting | -| | Split horizontally, open a new block to the right | -| | Split vertically, open a new block below | -| | Open a new window | -| | Close the current block | -| | Close the current tab | -| | Magnify / Un-Magnify the current block | -| | Open the "connection" switcher | -| | Refocus the current block (useful if the block has lost input focus) | -| | Show block numbers | -| | Switch to block number | -| | Move left, right, up, down between blocks | -| | Replace the current block with a launcher block | -| | Switch to tab number | -| | Switch tab left | -| | Switch tab right | -| | Switch to workspace number | -| | Refresh the UI | -| | Toggle terminal multi-input mode | +| Key | Function | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| | Open a new tab | +| | Open a new block (defaults to a terminal block with the same connection and working directory). Switch to launcher using `app:defaultnewblock` setting | +| | Split horizontally, open a new block to the right | +| | Split vertically, open a new block below | +| | Split vertically, open a new block above | +| | Split vertically, open a new block below | +| | Split horizontally, open a new block to the left | +| | Split horizontally, open a new block to the right | +| | Open a new window | +| | Close the current block | +| | Close the current tab | +| | Magnify / Un-Magnify the current block | +| | Open the "connection" switcher | +| | Refocus the current block (useful if the block has lost input focus) | +| | Show block numbers | +| | Switch to block number | +| | Move left, right, up, down between blocks | +| | Replace the current block with a launcher block | +| | Switch to tab number | +| | Switch tab left | +| | Switch tab right | +| | Switch to workspace number | +| | Refresh the UI | +| | Toggle terminal multi-input mode | ## File Preview Keybindings diff --git a/docs/src/components/kbd.tsx b/docs/src/components/kbd.tsx index d8550521af..21eb0b3c68 100644 --- a/docs/src/components/kbd.tsx +++ b/docs/src/components/kbd.tsx @@ -61,3 +61,15 @@ const KbdInternal = ({ k }: { k: string }) => { export const Kbd = ({ k }: { k: string }) => { return {k}}>{() => }; }; + +export const KbdChord = ({ karr }: { karr: string[] }) => { + const elems: React.ReactNode[] = []; + for (let i = 0; i < karr.length; i++) { + if (i > 0) { + elems.push(+); + } + elems.push(); + } + const fullElem = {elems}; + return {() => fullElem}; +}; From 2eab37f35f289e595e9deb3b577b36959767a4f5 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Feb 2025 14:32:30 -0800 Subject: [PATCH 07/11] call emain setKeyboardChordMode --- frontend/app/store/keymodel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 34145a61d5..64590861ef 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -48,6 +48,7 @@ function resetChord() { } function setActiveChord(activeChordArg: string) { + getApi().setKeyboardChordMode(); if (chordTimeout) { clearTimeout(chordTimeout); } From 6c9e7776150e9fef60e49ef5bc2e23e65b637787 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 14 Feb 2025 11:37:43 -0800 Subject: [PATCH 08/11] quick debug gettab wsh command --- cmd/wsh/cmd/wshcmd-debug.go | 21 +++++++++++++++++++++ frontend/app/store/wshclientapi.ts | 5 +++++ pkg/wshrpc/wshclient/wshclient.go | 6 ++++++ pkg/wshrpc/wshrpctypes.go | 1 + pkg/wshrpc/wshserver/wshserver.go | 8 ++++++++ 5 files changed, 41 insertions(+) diff --git a/cmd/wsh/cmd/wshcmd-debug.go b/cmd/wsh/cmd/wshcmd-debug.go index 9efac0ff87..e28f5df177 100644 --- a/cmd/wsh/cmd/wshcmd-debug.go +++ b/cmd/wsh/cmd/wshcmd-debug.go @@ -31,12 +31,33 @@ var debugSendTelemetryCmd = &cobra.Command{ Hidden: true, } +var debugGetTabCmd = &cobra.Command{ + Use: "gettab", + Short: "get tab", + RunE: debugGetTabRun, + Hidden: true, +} + func init() { debugCmd.AddCommand(debugBlockIdsCmd) debugCmd.AddCommand(debugSendTelemetryCmd) + debugCmd.AddCommand(debugGetTabCmd) rootCmd.AddCommand(debugCmd) } +func debugGetTabRun(cmd *cobra.Command, args []string) error { + tab, err := wshclient.GetTabCommand(RpcClient, RpcContext.TabId, nil) + if err != nil { + return err + } + barr, err := json.MarshalIndent(tab, "", " ") + if err != nil { + return err + } + WriteStdout("%s\n", string(barr)) + return nil +} + func debugSendTelemetryRun(cmd *cobra.Command, args []string) error { err := wshclient.SendTelemetryCommand(RpcClient, nil) return err diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 1c6903104e..f27f4d0ee9 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -232,6 +232,11 @@ class RpcApiType { return client.wshRpcCall("getmeta", data, opts); } + // command "gettab" [call] + GetTabCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("gettab", data, opts); + } + // command "getupdatechannel" [call] GetUpdateChannelCommand(client: WshClient, opts?: RpcOpts): Promise { return client.wshRpcCall("getupdatechannel", null, opts); diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 6fdbaf7473..034365eecb 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -284,6 +284,12 @@ func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wsh return resp, err } +// command "gettab", wshserver.GetTabCommand +func GetTabCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*waveobj.Tab, error) { + resp, err := sendRpcRequestCallHelper[*waveobj.Tab](w, "gettab", data, opts) + return resp, err +} + // command "getupdatechannel", wshserver.GetUpdateChannelCommand func GetUpdateChannelCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (string, error) { resp, err := sendRpcRequestCallHelper[string](w, "getupdatechannel", nil, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index bca78a4ad5..8dc06a894b 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -194,6 +194,7 @@ type WshRpcInterface interface { PathCommand(ctx context.Context, data PathCommandData) (string, error) SendTelemetryCommand(ctx context.Context) error FetchSuggestionsCommand(ctx context.Context, data FetchSuggestionsData) (*FetchSuggestionsResponse, error) + GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error) // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 9db25050a7..84af61fd2b 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -914,3 +914,11 @@ func (ws *WshServer) PathCommand(ctx context.Context, data wshrpc.PathCommandDat func (ws *WshServer) FetchSuggestionsCommand(ctx context.Context, data wshrpc.FetchSuggestionsData) (*wshrpc.FetchSuggestionsResponse, error) { return suggestion.FetchSuggestions(ctx, data) } + +func (ws *WshServer) GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error) { + tab, err := wstore.DBGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return nil, fmt.Errorf("error getting tab: %w", err) + } + return tab, nil +} From e018858c34a253f6dc6df16022093837e2562e9e Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 14 Feb 2025 13:12:51 -0800 Subject: [PATCH 09/11] delete the block when switching --- frontend/app/store/global.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 0cf54433b1..4b4b6afee4 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -456,6 +456,9 @@ async function replaceBlock(blockId: string, blockDef: BlockDef): Promise { + await ObjectService.DeleteBlock(blockId); + }, 300); const targetNodeId = layoutModel.getNodeByBlockId(blockId)?.id; if (targetNodeId == null) { throw new Error(`targetNodeId not found for blockId: ${blockId}`); From 390ae7a0270c59cee1cc4a7ed874a662ba819680 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 14 Feb 2025 13:15:55 -0800 Subject: [PATCH 10/11] set timeout val to null after clear --- emain/emain-tabview.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts index c978e7c6a0..dae2517cd9 100644 --- a/emain/emain-tabview.ts +++ b/emain/emain-tabview.ts @@ -105,6 +105,7 @@ export class WaveTabView extends WebContentsView { } else { if (this.resetChordModeTimeout) { clearTimeout(this.resetChordModeTimeout); + this.resetChordModeTimeout = null; } } } From a178ab27d35d79030920cf496ecfa322be7c47c1 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 14 Feb 2025 13:21:33 -0800 Subject: [PATCH 11/11] share a chord_timeout const between emain and the fe --- emain/emain-tabview.ts | 3 ++- frontend/app/store/keymodel.ts | 3 ++- frontend/util/sharedconst.ts | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 frontend/util/sharedconst.ts diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts index dae2517cd9..c174cbf630 100644 --- a/emain/emain-tabview.ts +++ b/emain/emain-tabview.ts @@ -3,6 +3,7 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { adaptFromElectronKeyEvent } from "@/util/keyutil"; +import { CHORD_TIMEOUT } from "@/util/sharedconst"; import { Rectangle, shell, WebContentsView } from "electron"; import { getWaveWindowById } from "emain/emain-window"; import path from "path"; @@ -101,7 +102,7 @@ export class WaveTabView extends WebContentsView { } this.resetChordModeTimeout = setTimeout(() => { this.keyboardChordMode = false; - }, 2000); + }, CHORD_TIMEOUT); } else { if (this.resetChordModeTimeout) { clearTimeout(this.resetChordModeTimeout); diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 64590861ef..b673925584 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -25,6 +25,7 @@ import { } from "@/layout/index"; import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks"; import * as keyutil from "@/util/keyutil"; +import { CHORD_TIMEOUT } from "@/util/sharedconst"; import { fireAndForget } from "@/util/util"; import * as jotai from "jotai"; import { modalsModel } from "./modalmodel"; @@ -53,7 +54,7 @@ function setActiveChord(activeChordArg: string) { clearTimeout(chordTimeout); } activeChord = activeChordArg; - chordTimeout = setTimeout(() => resetChord(), 2000); + chordTimeout = setTimeout(() => resetChord(), CHORD_TIMEOUT); } export function keyboardMouseDownHandler(e: MouseEvent) { diff --git a/frontend/util/sharedconst.ts b/frontend/util/sharedconst.ts new file mode 100644 index 0000000000..cac939d8c7 --- /dev/null +++ b/frontend/util/sharedconst.ts @@ -0,0 +1,4 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +export const CHORD_TIMEOUT = 2000;