Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
120 changes: 120 additions & 0 deletions apps/desktop/src/keybindings/NativeKeybindingCapture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, expect, it, vi } from "vite-plus/test";

import {
dispatchNativeKeybindingCaptureInput,
nativeKeybindingCaptureInput,
} from "./NativeKeybindingCapture.ts";

describe("nativeKeybindingCaptureInput", () => {
it("forwards Command-Escape with its modifiers", () => {
expect(
nativeKeybindingCaptureInput(
{
type: "keyDown",
key: "Escape",
meta: true,
control: false,
alt: false,
shift: true,
},
"darwin",
),
).toEqual({
key: "Escape",
metaKey: true,
ctrlKey: false,
altKey: false,
shiftKey: true,
});
});

it("forwards Control-Escape as mod+esc on non-macOS platforms", () => {
expect(
nativeKeybindingCaptureInput(
{
type: "keyDown",
key: "Escape",
meta: false,
control: true,
alt: true,
shift: false,
},
"win32",
),
).toEqual({
key: "Escape",
metaKey: false,
ctrlKey: true,
altKey: true,
shiftKey: false,
});
});

it.each([
["bare Escape", { type: "keyDown", key: "Escape", meta: false }, "darwin"],
["Command keyup", { type: "keyUp", key: "Escape", meta: true }, "darwin"],
["another Command shortcut", { type: "keyDown", key: "k", meta: true }, "darwin"],
["Meta-Escape on Windows", { type: "keyDown", key: "Escape", meta: true }, "win32"],
] satisfies ReadonlyArray<
readonly [
string,
{ readonly type: string; readonly key: string; readonly meta: boolean },
NodeJS.Platform,
]
>)("ignores %s", (_name, input, platform) => {
expect(
nativeKeybindingCaptureInput(
{
control: false,
alt: false,
shift: false,
...input,
},
platform,
),
).toBeNull();
});

it("dispatches native shortcuts at the app window unless a keybinding recorder is active", () => {
const appDispatch = vi.fn(() => true);
const activeDispatch = vi.fn(() => true);
const activeElement = {
dispatchEvent: activeDispatch,
hasAttribute: vi.fn(() => false),
};
vi.stubGlobal("window", { dispatchEvent: appDispatch });
vi.stubGlobal("document", { activeElement });
vi.stubGlobal(
"KeyboardEvent",
class {
readonly type: string;
readonly init: KeyboardEventInit;

constructor(type: string, init: KeyboardEventInit) {
this.type = type;
this.init = init;
}
},
);

const input = {
key: "Escape" as const,
metaKey: true,
ctrlKey: false,
altKey: false,
shiftKey: false,
};

try {
dispatchNativeKeybindingCaptureInput(input);
expect(appDispatch).toHaveBeenCalledOnce();
expect(activeDispatch).not.toHaveBeenCalled();

activeElement.hasAttribute.mockReturnValue(true);
dispatchNativeKeybindingCaptureInput(input);
expect(activeDispatch).toHaveBeenCalledOnce();
} finally {
vi.unstubAllGlobals();
}
});
});
66 changes: 66 additions & 0 deletions apps/desktop/src/keybindings/NativeKeybindingCapture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Input } from "electron";

export const NATIVE_KEYBINDING_CAPTURE_CHANNEL = "desktop:native-keybinding-capture";

export interface NativeKeybindingCaptureInput {
readonly key: "Escape";
readonly metaKey: boolean;
readonly ctrlKey: boolean;
readonly altKey: boolean;
readonly shiftKey: boolean;
}

export function nativeKeybindingCaptureInput(
input: Pick<Input, "type" | "key" | "meta" | "control" | "alt" | "shift">,
platform: NodeJS.Platform,
): NativeKeybindingCaptureInput | null {
const key = input.key.toLowerCase();
const modPressed = platform === "darwin" ? input.meta : input.control;
if (input.type !== "keyDown" || (key !== "escape" && key !== "esc") || !modPressed) {
return null;
}

return {
key: "Escape",
metaKey: input.meta,
ctrlKey: input.control,
altKey: input.alt,
shiftKey: input.shift,
};
}

export function dispatchNativeKeybindingCaptureInput(input: unknown): void {
if (
typeof input !== "object" ||
input === null ||
!("key" in input) ||
input.key !== "Escape" ||
!("metaKey" in input) ||
typeof input.metaKey !== "boolean" ||
!("ctrlKey" in input) ||
typeof input.ctrlKey !== "boolean" ||
(!input.metaKey && !input.ctrlKey) ||
!("altKey" in input) ||
typeof input.altKey !== "boolean" ||
!("shiftKey" in input) ||
typeof input.shiftKey !== "boolean"
) {
return;
}

const activeElement = document.activeElement;
const target = activeElement?.hasAttribute("data-keybinding-capture") ? activeElement : window;
Comment thread
cursor[bot] marked this conversation as resolved.

target.dispatchEvent(
new KeyboardEvent("keydown", {
Comment thread
jakeleventhal marked this conversation as resolved.
key: input.key,
code: "Escape",
metaKey: input.metaKey,
ctrlKey: input.ctrlKey,
altKey: input.altKey,
shiftKey: input.shiftKey,
bubbles: true,
cancelable: true,
}),
);
}
8 changes: 8 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ import { exposeClerkBridge } from "@clerk/electron/preload";
import { contextBridge, ipcRenderer } from "electron";

import * as IpcChannels from "./ipc/channels.ts";
import {
dispatchNativeKeybindingCaptureInput,
NATIVE_KEYBINDING_CAPTURE_CHANNEL,
} from "./keybindings/NativeKeybindingCapture.ts";

exposeClerkBridge({ passkeys: true });

ipcRenderer.on(NATIVE_KEYBINDING_CAPTURE_CHANNEL, (_event, input: unknown) => {
dispatchNativeKeybindingCaptureInput(input);
});

function unwrapEnsureSshEnvironmentResult(result: unknown) {
if (
typeof result === "object" &&
Expand Down
33 changes: 33 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { beforeEach, describe, expect, it, vi } from "vite-plus/test";

import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { NATIVE_KEYBINDING_CAPTURE_CHANNEL } from "../keybindings/NativeKeybindingCapture.ts";
import * as BrowserSession from "./BrowserSession.ts";
import * as PreviewManager from "./Manager.ts";

Expand Down Expand Up @@ -499,6 +500,38 @@ describe("PreviewManager", () => {

expect(loadURL).toHaveBeenCalledOnce();
expect(loadURL).toHaveBeenCalledWith("http://localhost:3200/");

const mainWindowSend = vi.fn();
yield* manager.setMainWindow({
isDestroyed: () => false,
once: vi.fn(),
webContents: { send: mainWindowSend },
} as never);
const beforeInputEvent = { preventDefault: vi.fn() };
listeners.get("before-input-event")?.(
beforeInputEvent as never,
{
type: "keyDown",
key: "Escape",
meta: true,
control: false,
alt: false,
shift: false,
} as never,
);
yield* Effect.yieldNow;
expect(mainWindowSend).toHaveBeenCalledWith(NATIVE_KEYBINDING_CAPTURE_CHANNEL, {
key: "Escape",
metaKey: true,
ctrlKey: false,
altKey: false,
shiftKey: false,
});
expect(webviewSend).not.toHaveBeenCalledWith(
NATIVE_KEYBINDING_CAPTURE_CHANNEL,
expect.anything(),
);
expect(beforeInputEvent.preventDefault).toHaveBeenCalledOnce();
}),
),
);
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ import * as SynchronizedRef from "effect/SynchronizedRef";

import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import { PREVIEW_PICTURE_IN_PICTURE_FRAME_CHANNEL } from "../ipc/channels.ts";
import {
type NativeKeybindingCaptureInput,
nativeKeybindingCaptureInput,
NATIVE_KEYBINDING_CAPTURE_CHANNEL,
} from "../keybindings/NativeKeybindingCapture.ts";
import * as BrowserSession from "./BrowserSession.ts";
import {
ANNOTATION_CAPTURED_CHANNEL,
Expand Down Expand Up @@ -1661,6 +1666,15 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
],
});
});
const forwardNativeKeybindingCapture = Effect.fn(
"PreviewManager.forwardNativeKeybindingCapture",
)(function* (input: NativeKeybindingCaptureInput) {
const mainWindow = yield* Ref.get(mainWindowRef);
if (Option.isNone(mainWindow) || mainWindow.value.isDestroyed()) {
return;
}
mainWindow.value.webContents.send(NATIVE_KEYBINDING_CAPTURE_CHANNEL, input);
});
const beforeInput = (event: Electron.Event, input: Electron.Input): void => {
if (isPreviewRefreshShortcut(input)) {
event.preventDefault();
Expand All @@ -1671,6 +1685,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
);
return;
}
const captureInput = nativeKeybindingCaptureInput(input, hostPlatform);
if (captureInput) {
event.preventDefault();
runFork(forwardNativeKeybindingCapture(captureInput));
return;
}
runFork(forwardShortcut(event, input));
};
yield* Scope.addFinalizer(
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preview/PickPreload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @effect-diagnostics globalDate:off - This isolated Electron preload does not run inside an Effect runtime.
import { ipcRenderer } from "electron";

import { getElementContext } from "react-grab/primitives";
import type {
DesktopPreviewAnnotationTheme,
Expand All @@ -25,6 +26,7 @@ import {
MOUSE_NAVIGATE_CHANNEL,
START_PICK_CHANNEL,
} from "./GuestProtocol.ts";

const OVERLAY_ATTRIBUTE = "data-t3code-annotation-ui";
const Z_INDEX_OVERLAY = 2147483646;
const PRIMARY = "var(--t3-primary)";
Expand Down
Loading
Loading