Skip to content
Merged
21 changes: 21 additions & 0 deletions cmd/wsh/cmd/wshcmd-debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 29 additions & 22 deletions docs/docs/keybindings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

<PlatformProvider>
Expand All @@ -15,32 +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

<PlatformSelectorButton />
<div style={{ marginBottom: 20 }}></div>

| Key | Function |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <Kbd k="Cmd:t"/> | Open a new tab |
| <Kbd k="Cmd:n"/> | Open a new block (defaults to a terminal block with the same connection and working directory). Switch to launcher using `app:defaultnewblock` setting |
| <Kbd k="Cmd:d"/> | Split horizontally, open a new block to the right |
| <Kbd k="Cmd:Shift:d"/> | Split vertically, open a new block below |
| <Kbd k="Cmd:Shift:n"/> | Open a new window |
| <Kbd k="Cmd:w"/> | Close the current block |
| <Kbd k="Cmd:Shift:w"/> | Close the current tab |
| <Kbd k="Cmd:m"/> | Magnify / Un-Magnify the current block |
| <Kbd k="Cmd:g"/> | Open the "connection" switcher |
| <Kbd k="Cmd:i"/> | Refocus the current block (useful if the block has lost input focus) |
| <Kbd k="Ctrl:Shift"/> | Show block numbers |
| <Kbd k="Ctrl:Shift:1-9"/> | Switch to block number |
| <Kbd k="Ctrl:Shift:Arrows"/> | Move left, right, up, down between blocks |
| <Kbd k="Cmd:1-9"/> | Switch to tab number |
| <Kbd k="Cmd:["/> | Switch tab left |
| <Kbd k="Cmd:]"/> | Switch tab right |
| <Kbd k="Cmd:Ctrl:1-9"/> | Switch to workspace number |
| <Kbd k="Cmd:Shift:r"/> | Refresh the UI |
| <Kbd k="Ctrl:Shift:i"/> | Toggle terminal multi-input mode |
| Key | Function |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <Kbd k="Cmd:t"/> | Open a new tab |
| <Kbd k="Cmd:n"/> | Open a new block (defaults to a terminal block with the same connection and working directory). Switch to launcher using `app:defaultnewblock` setting |
| <Kbd k="Cmd:d"/> | Split horizontally, open a new block to the right |
| <Kbd k="Cmd:Shift:d"/> | Split vertically, open a new block below |
| <KbdChord karr={["Ctrl:Shift:s", "ArrowUp"]}/> | Split vertically, open a new block above |
| <KbdChord karr={["Ctrl:Shift:s", "ArrowDown"]}/> | Split vertically, open a new block below |
| <KbdChord karr={["Ctrl:Shift:s", "ArrowLeft"]}/> | Split horizontally, open a new block to the left |
| <KbdChord karr={["Ctrl:Shift:s", "ArrowRight"]}/> | Split horizontally, open a new block to the right |
| <Kbd k="Cmd:Shift:n"/> | Open a new window |
| <Kbd k="Cmd:w"/> | Close the current block |
| <Kbd k="Cmd:Shift:w"/> | Close the current tab |
| <Kbd k="Cmd:m"/> | Magnify / Un-Magnify the current block |
| <Kbd k="Cmd:g"/> | Open the "connection" switcher |
| <Kbd k="Cmd:i"/> | Refocus the current block (useful if the block has lost input focus) |
| <Kbd k="Ctrl:Shift"/> | Show block numbers |
| <Kbd k="Ctrl:Shift:1-9"/> | Switch to block number |
| <Kbd k="Ctrl:Shift:Arrows"/> | Move left, right, up, down between blocks |
| <Kbd k="Ctrl:Shift:k"/> | Replace the current block with a launcher block |
| <Kbd k="Cmd:1-9"/> | Switch to tab number |
| <Kbd k="Cmd:["/> | Switch tab left |
| <Kbd k="Cmd:]"/> | Switch tab right |
| <Kbd k="Cmd:Ctrl:1-9"/> | Switch to workspace number |
| <Kbd k="Cmd:Shift:r"/> | Refresh the UI |
| <Kbd k="Ctrl:Shift:i"/> | Toggle terminal multi-input mode |

## File Preview Keybindings

Expand Down
12 changes: 12 additions & 0 deletions docs/src/components/kbd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,15 @@ const KbdInternal = ({ k }: { k: string }) => {
export const Kbd = ({ k }: { k: string }) => {
return <BrowserOnly fallback={<kbd>{k}</kbd>}>{() => <KbdInternal k={k} />}</BrowserOnly>;
};

export const KbdChord = ({ karr }: { karr: string[] }) => {
const elems: React.ReactNode[] = [];
for (let i = 0; i < karr.length; i++) {
if (i > 0) {
elems.push(<span style={{ padding: "0 2px" }}>+</span>);
}
elems.push(<Kbd key={i} k={karr[i]} />);
}
const fullElem = <span style={{ whiteSpace: "nowrap" }}>{elems}</span>;
return <BrowserOnly fallback={null}>{() => fullElem}</BrowserOnly>;
};
25 changes: 25 additions & 0 deletions emain/emain-tabview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -45,6 +46,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");
Expand Down Expand Up @@ -91,6 +94,23 @@ 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;
}, CHORD_TIMEOUT);
} else {
if (this.resetChordModeTimeout) {
clearTimeout(this.resetChordModeTimeout);
this.resetChordModeTimeout = null;
}
}
}

positionTabOnScreen(winBounds: Rectangle) {
const curBounds = this.getBounds();
if (
Expand Down Expand Up @@ -220,6 +240,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");
Expand Down
16 changes: 16 additions & 0 deletions emain/emain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down Expand Up @@ -312,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);
});
Comment on lines +325 to +329
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for keyboard chord mode.

The keyboard chord mode handler should handle potential errors:

  1. Missing tab view
  2. Failed mode setting
 electron.ipcMain.on("set-keyboard-chord-mode", (event) => {
     event.returnValue = null;
     const tabView = getWaveTabViewByWebContentsId(event.sender.id);
+    if (!tabView) {
+        console.error("No tab view found for keyboard chord mode");
+        return;
+    }
     tabView?.setKeyboardChordMode(true);
+    console.log("Keyboard chord mode enabled for tab view");
 });

Committable suggestion skipped: line range outside the PR's diff.


if (unamePlatform !== "darwin") {
const fac = new FastAverageColor();

Expand Down
4 changes: 3 additions & 1 deletion emain/preload.ts
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down Expand Up @@ -51,6 +51,8 @@ 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),
setKeyboardChordMode: () => ipcRenderer.send("set-keyboard-chord-mode"),
});

// Custom event for "new-window"
Expand Down
4 changes: 1 addition & 3 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -643,13 +643,11 @@ const BlockFrame = React.memo((props: BlockFrameProps) => {
const blockId = props.nodeModel.blockId;
const [blockData] = WOS.useWaveObjectValue<Block>(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 <FrameElem {...props} numBlocksInTab={numBlocks} />;
return <BlockFrame_Default {...props} numBlocksInTab={numBlocks} />;
});

export { BlockFrame, NumActiveConnColors };
4 changes: 4 additions & 0 deletions frontend/app/store/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ async function replaceBlock(blockId: string, blockDef: BlockDef): Promise<string
const layoutModel = getLayoutModelForTabById(tabId);
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
const newBlockId = await ObjectService.CreateBlock(blockDef, rtOpts);
setTimeout(async () => {
await ObjectService.DeleteBlock(blockId);
}, 300);
const targetNodeId = layoutModel.getNodeByBlockId(blockId)?.id;
if (targetNodeId == null) {
throw new Error(`targetNodeId not found for blockId: ${blockId}`);
Expand Down Expand Up @@ -763,6 +766,7 @@ export {
getBlockComponentModel,
getBlockMetaKeyAtom,
getConnStatusAtom,
getFocusedBlockId,
getHostName,
getObjectId,
getOverrideConfigAtom,
Expand Down
Loading
Loading