Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const SSH_PASSWORD_PROMPT_CANCELLED_RESULT = "ssh-password-prompt-cancell
export const PREVIEW_CREATE_TAB_CHANNEL = "desktop:preview-create-tab";
export const PREVIEW_CLOSE_TAB_CHANNEL = "desktop:preview-close-tab";
export const PREVIEW_REGISTER_WEBVIEW_CHANNEL = "desktop:preview-register-webview";
export const PREVIEW_SET_RIGHT_PANEL_CLOSE_SHORTCUTS_CHANNEL =
"desktop:preview-set-right-panel-close-shortcuts";
export const PREVIEW_NAVIGATE_CHANNEL = "desktop:preview-navigate";
export const PREVIEW_GO_BACK_CHANNEL = "desktop:preview-go-back";
export const PREVIEW_GO_FORWARD_CHANNEL = "desktop:preview-go-forward";
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/ipc/methods/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
DesktopPreviewRecordingArtifactSchema,
DesktopPreviewRecordingSaveInputSchema,
DesktopPreviewRegisterWebviewInputSchema,
DesktopPreviewRightPanelCloseShortcutsInputSchema,
DesktopPreviewScreenshotArtifactSchema,
DesktopPreviewTabInputSchema,
DesktopPreviewWebviewConfigSchema,
Expand Down Expand Up @@ -75,6 +76,16 @@ export const registerWebview = DesktopIpc.makeIpcMethod({
}),
});

export const setRightPanelCloseShortcuts = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PREVIEW_SET_RIGHT_PANEL_CLOSE_SHORTCUTS_CHANNEL,
payload: DesktopPreviewRightPanelCloseShortcutsInputSchema,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.preview.setRightPanelCloseShortcuts")(function* ({ shortcuts }) {
const manager = yield* PreviewManager.PreviewManager;
yield* manager.setRightPanelCloseShortcuts(shortcuts);
}),
});

export const navigate = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PREVIEW_NAVIGATE_CHANNEL,
payload: DesktopPreviewNavigateInputSchema,
Expand Down Expand Up @@ -338,6 +349,7 @@ export const methods = [
createTab,
closeTab,
registerWebview,
setRightPanelCloseShortcuts,
navigate,
goBack,
goForward,
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ contextBridge.exposeInMainWorld("desktopBridge", {
closeTab: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, { tabId }),
registerWebview: (tabId, webContentsId) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, { tabId, webContentsId }),
setRightPanelCloseShortcuts: (shortcuts) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_RIGHT_PANEL_CLOSE_SHORTCUTS_CHANNEL, {
shortcuts,
}),
navigate: (tabId, url) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_NAVIGATE_CHANNEL, { tabId, url }),
goBack: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_GO_BACK_CHANNEL, { tabId }),
Expand Down
202 changes: 202 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import type * as Scope from "effect/Scope";
import { TestClock } from "effect/testing";
import type * as Electron from "electron";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";

import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
Expand Down Expand Up @@ -96,6 +97,135 @@ const layer = PreviewManager.layer.pipe(
);
const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError);

describe("resolveForwardedPreviewShortcutKey", () => {
const cmdW = {
type: "keyDown",
key: "w",
code: "KeyW",
meta: true,
control: false,
shift: false,
alt: false,
};

it("normalizes physical Cmd+W before forwarding it to the main renderer", () => {
expect(PreviewManager.resolveForwardedPreviewShortcutKey(cmdW, "darwin")).toBe("w");
expect(PreviewManager.resolveForwardedPreviewShortcutKey({ ...cmdW, key: "z" }, "darwin")).toBe(
"w",
);
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{
...cmdW,
meta: false,
control: true,
},
"linux",
),
).toBe("w");
});

it("prioritizes the configured close binding when logical and physical keys disagree", () => {
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{ ...cmdW, key: ",", code: "KeyW" },
"darwin",
),
).toBe("w");
});

it("preserves the existing Meta-based app shortcut forwarding", () => {
const cmdK = { ...cmdW, key: "k", code: "KeyK" };
expect(PreviewManager.resolveForwardedPreviewShortcutKey(cmdK, "linux", [])).toBe("k");
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{ ...cmdK, meta: false, control: true },
"linux",
[],
),
).toBeNull();
expect(
PreviewManager.resolveForwardedPreviewShortcutKey({ ...cmdK, alt: true }, "darwin", []),
).toBe("k");
});

it("forwards configured right-panel close shortcuts instead of assuming Mod+W", () => {
const modP = [
{
key: "p",
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
modKey: true,
},
];
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{ ...cmdW, key: "z", code: "KeyP" },
"darwin",
modP,
),
).toBe("p");
expect(PreviewManager.resolveForwardedPreviewShortcutKey(cmdW, "darwin", modP)).toBeNull();
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{ ...cmdW, key: "{", code: "BracketLeft", shift: true },
"darwin",
[{ ...modP[0]!, key: "[", shiftKey: true }],
),
).toBe("[");
});

it.each([
["ArrowLeft", "ArrowLeft", "arrowleft", "Left"],
["ArrowRight", "ArrowRight", "arrowright", "Right"],
["ArrowUp", "ArrowUp", "arrowup", "Up"],
["ArrowDown", "ArrowDown", "arrowdown", "Down"],
[" ", "Space", " ", "Space"],
["Escape", "Escape", "escape", "Escape"],
["PageDown", "PageDown", "pagedown", "PageDown"],
["Insert", "Insert", "insert", "Insert"],
["ContextMenu", "ContextMenu", "contextmenu", "ContextMenu"],
["PrintScreen", "PrintScreen", "printscreen", "PrintScreen"],
["MediaPlayPause", "MediaPlayPause", "mediaplaypause", "MediaPlayPause"],
["Pause", "Pause", "pause", "Pause"],
["F12", "F12", "f12", "F12"],
])("encodes the %s shortcut for Electron input", (key, code, shortcutKey, expected) => {
expect(
PreviewManager.resolveForwardedPreviewShortcutKey({ ...cmdW, key, code }, "darwin", [
{
key: shortcutKey,
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
modKey: true,
},
]),
).toBe(expected);
});

it("does not consume modified or non-app shortcuts", () => {
expect(
PreviewManager.resolveForwardedPreviewShortcutKey({ ...cmdW, alt: true }, "darwin"),
).toBeNull();
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{ ...cmdW, meta: false, control: true },
"darwin",
),
).toBeNull();
expect(PreviewManager.resolveForwardedPreviewShortcutKey(cmdW, "linux")).toBeNull();
expect(
PreviewManager.resolveForwardedPreviewShortcutKey(
{ ...cmdW, key: "x", code: "KeyX" },
"darwin",
),
).toBeNull();
});
});

const withManager = <A>(
use: (
manager: PreviewManager.PreviewManager["Service"],
Expand Down Expand Up @@ -146,6 +276,78 @@ describe("PreviewManager", () => {
),
);

effectIt.effect("uses the renderer-supplied close shortcut for embedded Browser input", () =>
withManager((manager) =>
Effect.gen(function* () {
let beforeInput:
| ((event: { preventDefault: () => void }, input: Electron.Input) => void)
| undefined;
const sendInputEvent = vi.fn();
fromId.mockReturnValue({
id: 42,
isDestroyed: () => false,
getType: () => "webview",
getURL: () => "https://example.com",
getTitle: () => "Example",
isLoading: () => false,
getZoomFactor: () => 1,
setZoomFactor: vi.fn(),
on: vi.fn((event: string, listener: typeof beforeInput) => {
if (event === "before-input-event") beforeInput = listener;
}),
off: vi.fn(),
ipc: { on: vi.fn(), off: vi.fn() },
send: webviewSend,
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
setWindowOpenHandler: vi.fn(),
debugger: {
isAttached: () => false,
attach: vi.fn(),
sendCommand: vi.fn(async () => undefined),
on: vi.fn(),
off: vi.fn(),
},
} as never);

yield* manager.createTab("tab_shortcut");
yield* manager.registerWebview("tab_shortcut", 42);
yield* manager.setMainWindow({
isDestroyed: () => false,
webContents: { sendInputEvent },
} as never);
yield* manager.setRightPanelCloseShortcuts([
{
key: "arrowleft",
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
modKey: true,
},
]);

const preventDefault = vi.fn();
beforeInput?.({ preventDefault }, {
type: "keyDown",
key: "ArrowLeft",
code: "ArrowLeft",
meta: false,
control: true,
shift: false,
alt: false,
} as Electron.Input);
yield* Effect.yieldNow;

expect(preventDefault).toHaveBeenCalledOnce();
expect(sendInputEvent).toHaveBeenCalledWith({
type: "keyDown",
keyCode: "Left",
modifiers: ["control"],
});
}),
),
);

effectIt.effect("isolates failed state listeners and continues delivery", () => {
const loggedErrors: Array<unknown> = [];
const logger = Logger.make(({ message }) => {
Expand Down
Loading
Loading