Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
40 changes: 40 additions & 0 deletions apps/desktop/src/lib/shortcuts/hotkeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isSelectAllChord } from "$lib/shortcuts/hotkeys";
import { describe, expect, test } from "vitest";

function keydown(key: string, modifiers: { metaKey?: boolean; ctrlKey?: boolean } = {}) {
return new KeyboardEvent("keydown", { key, ...modifiers });
}

/** The values `Backend["platformName"]` takes where ⌘ is not the select-all modifier. */
const NON_MAC_PLATFORMS = ["windows", "linux", "web"] as const;

describe("isSelectAllChord", () => {
test("macOS selects all with cmd", () => {
expect(isSelectAllChord(keydown("a", { metaKey: true }), "macos")).toBe(true);
});

// Ctrl+A is the emacs "move to the start of the line" binding that macOS text fields have,
// so claiming it for select-all takes an editing key away instead of adding a shortcut.
test("macOS leaves ctrl alone", () => {
expect(isSelectAllChord(keydown("a", { ctrlKey: true }), "macos")).toBe(false);
});

test.each(NON_MAC_PLATFORMS)("%s selects all with ctrl", (platform) => {
expect(isSelectAllChord(keydown("a", { ctrlKey: true }), platform)).toBe(true);
});

// Cmd is not a select-all modifier off macOS, where that key is the Super/Windows key.
test.each(NON_MAC_PLATFORMS)("%s ignores meta", (platform) => {
expect(isSelectAllChord(keydown("a", { metaKey: true }), platform)).toBe(false);
});

test("a bare 'a' types rather than selects", () => {
expect(isSelectAllChord(keydown("a"), "macos")).toBe(false);
expect(isSelectAllChord(keydown("a"), "windows")).toBe(false);
});

test("the modifier only counts together with 'a'", () => {
expect(isSelectAllChord(keydown("b", { metaKey: true }), "macos")).toBe(false);
expect(isSelectAllChord(keydown("b", { ctrlKey: true }), "windows")).toBe(false);
});
});
16 changes: 16 additions & 0 deletions apps/desktop/src/lib/shortcuts/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ interface KeybindDefinitions {
[combo: string]: (event: KeyboardEvent) => void;
}

/**
* Whether `event` is `platform`'s "select all" chord.
*
* macOS selects all with ⌘ and leaves Ctrl+A as "move to the start of the line", the emacs
* binding its text fields carry everywhere else, so accepting either modifier there takes an
* editing key away rather than adding a shortcut. Everywhere else Ctrl is the one that selects.
*
* `platform` is a `Backend["platformName"]`, which the desktop app gets from the OS rather
* than from the user agent.
*/
export function isSelectAllChord(event: KeyboardEvent, platform: string): boolean {
if (event.key !== "a") return false;

return platform === "macos" ? event.metaKey : event.ctrlKey;
}

export const shortcuts = {
global: {
open_repository: {
Expand Down
8 changes: 3 additions & 5 deletions apps/desktop/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import { fModeEnabled } from "$lib/config/uiFeatureFlags";
import { PROJECTS_SERVICE } from "$lib/project/projectsService";
import { TERMINAL_SERVICE } from "$lib/settings/terminalService";
import { createKeybind } from "$lib/shortcuts/hotkeys";
import { createKeybind, isSelectAllChord } from "$lib/shortcuts/hotkeys";
import { SHORTCUT_SERVICE } from "$lib/shortcuts/shortcutService";
import { CLIENT_STATE } from "$lib/state/clientState.svelte";
import { initUserSettings, UI_STATE } from "$lib/state/uiState.svelte";
Expand Down Expand Up @@ -114,14 +114,12 @@
// =============================================================================

function handleKeyDown(e: KeyboardEvent) {
// Explicitly detect cmd/ctrl + A since Tauri gets in the way of default behavior.
// Explicitly detect the select-all chord since Tauri gets in the way of default behavior.
// To get default behavior you can add a "Select All" predefined menu item to the
// Edit menu, but that prevents the event from reaching the webview.
if (
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
(e.metaKey || e.ctrlKey) &&
e.key === "a" &&
e.target
isSelectAllChord(e, backend.platformName)
) {
e.target.select();
e.preventDefault();
Expand Down
Loading