From 8ab83157eece049cec70e68fb1d90b7d6026faf9 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 11 Mar 2026 23:36:42 +0100 Subject: [PATCH] feat: add Cmd+Shift+T shortcut, menu item, and full wiring Add "Reopen Closed Tab" to File menu with CmdOrCtrl+Shift+T accelerator. Wire onReopenClosedTab through useAppKeyboard, useMenuEvents, useAppCommands, and App.tsx. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/menu.rs | 7 +++++++ src/App.tsx | 1 + src/hooks/useAppCommands.ts | 3 +++ src/hooks/useAppKeyboard.test.ts | 17 +++++++++++++++++ src/hooks/useAppKeyboard.ts | 11 +++++++++-- src/hooks/useMenuEvents.test.ts | 8 ++++++++ src/hooks/useMenuEvents.ts | 3 +++ 7 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 5b8c3151..5d79beca 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -13,6 +13,7 @@ const FILE_DAILY_NOTE: &str = "file-daily-note"; const FILE_QUICK_OPEN: &str = "file-quick-open"; const FILE_SAVE: &str = "file-save"; const FILE_CLOSE_TAB: &str = "file-close-tab"; +const FILE_REOPEN_CLOSED_TAB: &str = "file-reopen-closed-tab"; const EDIT_FIND_IN_VAULT: &str = "edit-find-in-vault"; const EDIT_TOGGLE_RAW_EDITOR: &str = "edit-toggle-raw-editor"; @@ -62,6 +63,7 @@ const CUSTOM_IDS: &[&str] = &[ FILE_QUICK_OPEN, FILE_SAVE, FILE_CLOSE_TAB, + FILE_REOPEN_CLOSED_TAB, EDIT_FIND_IN_VAULT, EDIT_TOGGLE_RAW_EDITOR, EDIT_TOGGLE_DIFF, @@ -167,6 +169,10 @@ fn build_file_menu(app: &App) -> MenuResult { .id(FILE_CLOSE_TAB) .accelerator("CmdOrCtrl+W") .build(app)?; + let reopen_closed_tab = MenuItemBuilder::new("Reopen Closed Tab") + .id(FILE_REOPEN_CLOSED_TAB) + .accelerator("CmdOrCtrl+Shift+T") + .build(app)?; Ok(SubmenuBuilder::new(app, "File") .item(&new_note) @@ -176,6 +182,7 @@ fn build_file_menu(app: &App) -> MenuResult { .separator() .item(&save) .item(&close_tab) + .item(&reopen_closed_tab) .build()?) } diff --git a/src/App.tsx b/src/App.tsx index f0d9792b..1835da1a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -596,6 +596,7 @@ function App() { onInstallMcp: installMcp, onEmptyTrash: handleEmptyTrash, trashedCount, + onReopenClosedTab: notes.handleReopenClosedTab, onReindexVault: indexing.triggerFullReindex, onReloadVault: vault.reloadVault, onRepairVault: handleRepairVault, diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts index a878b67d..02c1ce03 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -67,6 +67,7 @@ interface AppCommandsConfig { onInstallMcp?: () => void onEmptyTrash?: () => void trashedCount?: number + onReopenClosedTab?: () => void onReindexVault?: () => void onReloadVault?: () => void onRepairVault?: () => void @@ -116,6 +117,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onGoForward: config.onGoForward, onToggleAIChat: config.onToggleAIChat, onToggleRawEditor: config.onToggleRawEditor, + onReopenClosedTab: config.onReopenClosedTab, activeTabPathRef: config.activeTabPathRef, handleCloseTabRef: config.handleCloseTabRef, }) @@ -156,6 +158,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onReloadVault: config.onReloadVault, onRepairVault: config.onRepairVault, onEmptyTrash: config.onEmptyTrash, + onReopenClosedTab: config.onReopenClosedTab, activeTabPathRef: config.activeTabPathRef, handleCloseTabRef: config.handleCloseTabRef, activeTabPath: config.activeTabPath, diff --git a/src/hooks/useAppKeyboard.test.ts b/src/hooks/useAppKeyboard.test.ts index a931e20c..4c6dfa02 100644 --- a/src/hooks/useAppKeyboard.test.ts +++ b/src/hooks/useAppKeyboard.test.ts @@ -183,6 +183,23 @@ describe('useAppKeyboard', () => { expect(actions.onZoomReset).toHaveBeenCalled() }) + it('Cmd+Shift+T triggers reopen closed tab', () => { + const actions = makeActions() + const onReopenClosedTab = vi.fn() + renderHook(() => useAppKeyboard({ ...actions, onReopenClosedTab })) + fireKey('t', { metaKey: true, shiftKey: true }) + expect(onReopenClosedTab).toHaveBeenCalled() + }) + + it('Cmd+Shift+T does not trigger other shortcuts', () => { + const actions = makeActions() + const onReopenClosedTab = vi.fn() + renderHook(() => useAppKeyboard({ ...actions, onReopenClosedTab })) + fireKey('t', { metaKey: true, shiftKey: true }) + expect(actions.onQuickOpen).not.toHaveBeenCalled() + expect(actions.onCreateNote).not.toHaveBeenCalled() + }) + it('Cmd+I triggers toggle AI chat', () => { const actions = makeActions() const onToggleAIChat = vi.fn() diff --git a/src/hooks/useAppKeyboard.ts b/src/hooks/useAppKeyboard.ts index c988c1fa..7ac891b4 100644 --- a/src/hooks/useAppKeyboard.ts +++ b/src/hooks/useAppKeyboard.ts @@ -19,6 +19,7 @@ interface KeyboardActions { onGoForward?: () => void onToggleAIChat?: () => void onToggleRawEditor?: () => void + onReopenClosedTab?: () => void activeTabPathRef: React.MutableRefObject handleCloseTabRef: React.MutableRefObject<(path: string) => void> } @@ -64,7 +65,7 @@ function handleCmdKey(e: KeyboardEvent, keyMap: Record) export function useAppKeyboard({ onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, - onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, activeTabPathRef, handleCloseTabRef, + onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onReopenClosedTab, activeTabPathRef, handleCloseTabRef, }: KeyboardActions) { useEffect(() => { const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => { @@ -100,11 +101,17 @@ export function useAppKeyboard({ onSearch() return } + // Cmd+Shift+T: reopen last closed tab + if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 't' || e.key === 'T')) { + e.preventDefault() + onReopenClosedTab?.() + return + } if (!handleViewModeKey(e, onSetViewMode)) { handleCmdKey(e, cmdKeyMap) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor]) + }, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onReopenClosedTab]) } diff --git a/src/hooks/useMenuEvents.test.ts b/src/hooks/useMenuEvents.test.ts index 0a6afbe1..b44712c4 100644 --- a/src/hooks/useMenuEvents.test.ts +++ b/src/hooks/useMenuEvents.test.ts @@ -36,6 +36,7 @@ function makeHandlers(): MenuEventHandlers { onInstallMcp: vi.fn(), onReindexVault: vi.fn(), onReloadVault: vi.fn(), + onReopenClosedTab: vi.fn(), activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject, handleCloseTabRef: { current: vi.fn() } as React.MutableRefObject<(path: string) => void>, activeTabPath: '/vault/test.md', @@ -313,6 +314,13 @@ describe('dispatchMenuEvent', () => { expect(h.onReloadVault).toHaveBeenCalled() }) + // File menu: reopen closed tab + it('file-reopen-closed-tab triggers reopen closed tab', () => { + const h = makeHandlers() + dispatchMenuEvent('file-reopen-closed-tab', h) + expect(h.onReopenClosedTab).toHaveBeenCalled() + }) + // Edge cases it('unknown event ID does nothing', () => { const h = makeHandlers() diff --git a/src/hooks/useMenuEvents.ts b/src/hooks/useMenuEvents.ts index 7faa8ff1..f68a08d9 100644 --- a/src/hooks/useMenuEvents.ts +++ b/src/hooks/useMenuEvents.ts @@ -35,6 +35,7 @@ export interface MenuEventHandlers { onResolveConflicts?: () => void onViewChanges?: () => void onInstallMcp?: () => void + onReopenClosedTab?: () => void onReindexVault?: () => void onReloadVault?: () => void onRepairVault?: () => void @@ -83,6 +84,7 @@ type OptionalHandler = | 'onCreateTheme' | 'onRestoreDefaultThemes' | 'onCommitPush' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp' | 'onReindexVault' | 'onReloadVault' | 'onRepairVault' | 'onEmptyTrash' + | 'onReopenClosedTab' const OPTIONAL_EVENT_MAP: Record = { 'view-go-back': 'onGoBack', @@ -105,6 +107,7 @@ const OPTIONAL_EVENT_MAP: Record = { 'vault-reload': 'onReloadVault', 'vault-repair': 'onRepairVault', 'note-empty-trash': 'onEmptyTrash', + 'file-reopen-closed-tab': 'onReopenClosedTab', } function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean {