Skip to content
Merged
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
29 changes: 29 additions & 0 deletions apps/desktop/src/components/hotkeys/hotkeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest"
import { getTabIdForNumberShortcut } from "./hotkeys"

describe("getTabIdForNumberShortcut", () => {
it("selects the first, second, and ninth tabs by number", () => {
const tabs = Array.from({ length: 9 }, (_, index) => ({
id: index + 101,
}))

expect(getTabIdForNumberShortcut(tabs, 1)).toBe(101)
expect(getTabIdForNumberShortcut(tabs, 2)).toBe(102)
expect(getTabIdForNumberShortcut(tabs, 9)).toBe(109)
})

it("returns null when the requested tab number does not exist", () => {
const tabs = [{ id: 11 }, { id: 22 }, { id: 33 }]

expect(getTabIdForNumberShortcut(tabs, 4)).toBeNull()
expect(getTabIdForNumberShortcut(tabs, 9)).toBeNull()
})

it("maps shortcut 9 to the ninth tab instead of the last tab", () => {
const tabs = Array.from({ length: 12 }, (_, index) => ({
id: index + 1,
}))

expect(getTabIdForNumberShortcut(tabs, 9)).toBe(9)
})
})
37 changes: 36 additions & 1 deletion apps/desktop/src/components/hotkeys/hotkeys.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
APP_HOTKEY_DEFINITIONS,
type AppHotkeyActionId,
FIXED_TAB_SHORTCUT_DIGITS,
} from "@mdit/store/hotkeys"
Comment thread
hhhjin marked this conversation as resolved.
import { useHotkey } from "@tanstack/react-hotkeys"
import { useCallback, useMemo } from "react"
Expand All @@ -9,7 +10,6 @@ import { closeTabOrHideWindow } from "@/lib/close-tab-or-hide-window"
import { useStore } from "@/store"

const HOTKEY_OPTIONS = { preventDefault: true } as const

type HotkeyBindingProps = {
binding: string
onTrigger: () => void
Expand All @@ -24,6 +24,17 @@ function HotkeyBinding({ binding, onTrigger }: HotkeyBindingProps) {
return null
}

export function getTabIdForNumberShortcut<T extends { id: number }>(
tabs: readonly T[],
digit: number,
): number | null {
if (!Number.isInteger(digit) || digit < 1 || digit > 9) {
return null
}

return tabs[digit - 1]?.id ?? null
}

export function Hotkeys() {
const {
hotkeys,
Expand All @@ -32,6 +43,7 @@ export function Hotkeys() {
isEditMode,
closeActiveTab,
openFolderPicker,
activateTabById,
activatePreviousTab,
activateNextTab,
workspacePath,
Expand All @@ -57,6 +69,7 @@ export function Hotkeys() {
isEditMode: s.isEditMode,
closeActiveTab: s.closeActiveTab,
openFolderPicker: s.openFolderPicker,
activateTabById: s.activateTabById,
activatePreviousTab: s.activatePreviousTab,
activateNextTab: s.activateNextTab,
workspacePath: s.workspacePath,
Expand Down Expand Up @@ -85,6 +98,21 @@ export function Hotkeys() {
})
}, [activeTabId, closeActiveTab, isEditMode])

const handleActivateTabByNumber = useCallback(
(digit: number) => {
const targetTabId = getTabIdForNumberShortcut(
useStore.getState().tabs,
digit,
)
if (targetTabId === null) {
return
}

activateTabById(targetTabId)
},
[activateTabById],
)
Comment thread
hhhjin marked this conversation as resolved.

const actionHandlers = useMemo<Record<AppHotkeyActionId, () => void>>(
() => ({
"create-note": () => {
Expand Down Expand Up @@ -183,6 +211,13 @@ export function Hotkeys() {
/>
)
})}
{FIXED_TAB_SHORTCUT_DIGITS.map((digit) => (
<HotkeyBinding
key={`fixed-tab-shortcut-${digit}`}
binding={`Mod+${digit}`}
onTrigger={() => handleActivateTabByNumber(digit)}
/>
))}
</>
)
}
2 changes: 2 additions & 0 deletions packages/store/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ export {
APP_HOTKEY_CATEGORY_LABELS,
APP_HOTKEY_DEFINITIONS,
createDefaultAppHotkeys,
FIXED_TAB_SHORTCUT_DIGITS,
findHotkeyConflict,
hotkeyToDisplayTokens,
hotkeyToMenuAccelerator,
isAppHotkeyActionId,
isReservedAppHotkeyBinding,
mergeWithDefaultHotkeys,
normalizeHotkeyBinding,
} from "./hotkeys/hotkey-utils"
Expand Down
13 changes: 13 additions & 0 deletions packages/store/src/hotkeys/hotkey-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type AppHotkeyDefinition = {

export type AppHotkeyMap = Record<AppHotkeyActionId, string>

export const FIXED_TAB_SHORTCUT_DIGITS = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const

export const APP_HOTKEY_DEFINITIONS: readonly AppHotkeyDefinition[] = [
{
id: "create-note",
Expand Down Expand Up @@ -252,6 +254,17 @@ export function findHotkeyConflict(
return null
}

export function isReservedAppHotkeyBinding(binding: string): boolean {
const normalizedBinding = normalizeHotkeyBinding(binding)
if (!normalizedBinding) {
return false
}

return FIXED_TAB_SHORTCUT_DIGITS.some(
(digit) => normalizedBinding === `Mod+${digit}`,
)
Comment thread
hhhjin marked this conversation as resolved.
}

export function hotkeyToMenuAccelerator(binding: string): string | undefined {
const normalizedBinding = normalizeHotkeyBinding(binding)
if (!normalizedBinding) {
Expand Down
12 changes: 12 additions & 0 deletions packages/store/src/hotkeys/hotkeys-slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ describe("hotkeys-slice", () => {
expect(storage.save).not.toHaveBeenCalled()
})

it("rejects fixed tab shortcut bindings", async () => {
const store = createHotkeysStore(storage)

const result = await store
.getState()
.setHotkeyBinding("create-note", "mod+1")

expect(result.success).toBe(false)
expect(result.error).toBe("Shortcut is reserved for tab switching")
expect(storage.save).not.toHaveBeenCalled()
})

it("allows unassigned hotkeys", async () => {
const store = createHotkeysStore(storage)

Expand Down
8 changes: 8 additions & 0 deletions packages/store/src/hotkeys/hotkeys-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type AppHotkeyMap,
createDefaultAppHotkeys,
findHotkeyConflict,
isReservedAppHotkeyBinding,
normalizeHotkeyBinding,
} from "./hotkey-utils"

Expand Down Expand Up @@ -74,6 +75,13 @@ export const prepareHotkeysSlice =
error: validationResult.errors[0] ?? "Invalid hotkey format",
}
}

if (isReservedAppHotkeyBinding(normalizedBinding)) {
return {
success: false,
error: "Shortcut is reserved for tab switching",
}
}
}

const currentHotkeys = get().hotkeys
Expand Down
Loading