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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ For the supported operator catalog—including boolean grouping, folder scoping,

### Keyboard shortcuts & previews

All shortcuts below are configurable in `Preferences -> Shortcuts`. The list shows default bindings.

- `Cmd+Shift+Space` – toggle the Cardinal window globally via the quick-launch hotkey.
- `Cmd+,` – open Preferences.
- `Esc` – hide the Cardinal window.
Expand All @@ -56,6 +58,7 @@ For the supported operator catalog—including boolean grouping, folder scoping,
- `Space` – Quick Look the currently selected row without leaving Cardinal.
- `Cmd+O` – open the highlighted result.
- `Cmd+R` – reveal the highlighted result in Finder.
- `Cmd+Shift+F` – copy selected file names.
- `Cmd+C` – copy the selected files to the clipboard.
- `Cmd+Shift+C` – copy the selected paths to the clipboard.
- `Cmd+F` – jump focus back to the search bar.
Expand Down
6 changes: 3 additions & 3 deletions cardinal/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 138 additions & 0 deletions cardinal/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,31 @@ button:active:not(:disabled) {
outline-offset: 2px;
}

.preferences-manage-button {
border: 1px solid var(--color-elevated-border, var(--color-border));
background: var(--color-elevated-bg);
color: var(--color-text);
border-radius: 10px;
padding: 6px 12px;
font-size: 0.85rem;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.15s ease;
}

.preferences-manage-button:hover {
border-color: var(--color-accent);
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.12);
transform: translateY(-1px);
}

.preferences-manage-button:focus-visible {
outline: 2px solid rgba(var(--color-accent-rgb), 0.4);
outline-offset: 2px;
}

.preferences-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
Expand Down Expand Up @@ -1792,6 +1817,119 @@ button:active:not(:disabled) {
border-color: var(--color-accent);
}

/* === Shortcut Settings Modal === */
.shortcut-settings-overlay {
position: fixed;
inset: 0;
z-index: 2300;
display: grid;
place-items: center;
padding: 32px;
background: rgba(15, 17, 26, 0.84);
backdrop-filter: blur(8px);
}

.shortcut-settings-card {
width: 100%;
max-width: 520px;
max-height: calc(100vh - 96px);
background: var(--color-elevated-bg, var(--color-bg));
color: var(--color-text);
border-radius: 16px;
padding: 24px;
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.36);
display: flex;
flex-direction: column;
}

.shortcut-settings-card__header {
padding-bottom: 12px;
margin-bottom: 20px;
border-bottom: 1px solid var(--color-border);
}

.shortcut-settings-card__title {
font-size: 1.05rem;
font-weight: 600;
color: var(--color-header);
}

.shortcut-settings-section {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
overflow-y: auto;
padding-right: 4px;
}

.shortcut-settings-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
padding: 4px 0;
}

.shortcut-settings-row__details {
min-width: 0;
}

.shortcut-settings-hint {
margin-top: 4px;
font-size: 0.82rem;
color: var(--color-muted);
}

.shortcut-settings-capture-hint {
margin: 0;
font-size: 0.82rem;
color: var(--color-muted);
}

.shortcut-settings-capture-button {
min-width: 220px;
border: 1px solid var(--color-elevated-border, var(--color-border));
background: var(--color-elevated-bg);
color: var(--color-text);
border-radius: 8px;
padding: 7px 10px;
text-align: center;
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.01em;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}

.shortcut-settings-capture-button:hover {
border-color: var(--color-accent);
}

.shortcut-settings-capture-button:focus-visible {
outline: 2px solid rgba(var(--color-accent-rgb), 0.4);
outline-offset: 2px;
}

.shortcut-settings-capture-button--recording {
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(var(--color-accent-rgb), 0.22);
}

.shortcut-settings-error {
margin: 0;
}

.shortcut-settings-card__footer {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
}

/* === Utilities (extracted from inline JSX styles) === */
.flex-fill {
flex: 1;
Expand Down
29 changes: 26 additions & 3 deletions cardinal/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SearchBar } from './components/SearchBar';
import { FilesTabContent } from './components/FilesTabContent';
import { PermissionOverlay } from './components/PermissionOverlay';
import PreferencesOverlay from './components/PreferencesOverlay';
import ShortcutSettingsOverlay from './components/ShortcutSettingsOverlay';
import StatusBar from './components/StatusBar';
import type { SearchResultItem } from './types/search';
import { useColumnResize } from './hooks/useColumnResize';
Expand All @@ -30,6 +31,7 @@ import { useAppPreferences } from './hooks/useAppPreferences';
import { useAppWindowListeners } from './hooks/useAppWindowListeners';
import { useFilesTabEffects } from './hooks/useFilesTabEffects';
import { useFilesTabState } from './hooks/useFilesTabState';
import { useShortcutSettingsController } from './hooks/useShortcutSettingsController';

function App() {
const {
Expand Down Expand Up @@ -66,6 +68,13 @@ function App() {
const { caseSensitive } = searchParams;
const { eventColWidths, onEventResizeStart, autoFitEventColumns } = useEventColumnWidths();
const { t, i18n } = useTranslation();
const {
isShortcutSettingsOpen,
shortcuts,
openShortcutSettings,
closeShortcutSettings,
handleShortcutSettingsSave,
} = useShortcutSettingsController();
// `resultsVersion` tracks raw backend search result-set changes.
// `displayedResultsVersion` additionally tracks UI ordering/projection changes (e.g. sort toggle).
const {
Expand Down Expand Up @@ -96,6 +105,8 @@ function App() {
} = useFilesTabState({
searchQuery: searchParams.query,
queueSearch,
shortcuts,
shortcutsEnabled: !isShortcutSettingsOpen,
});
const { filteredEvents } = useRecentFSEvents({
caseSensitive,
Expand Down Expand Up @@ -128,12 +139,12 @@ function App() {
const {
showContextMenu: showFilesContextMenu,
showHeaderContextMenu: showFilesHeaderContextMenu,
} = useContextMenu(autoFitColumns, toggleQuickLook);
} = useContextMenu(autoFitColumns, toggleQuickLook, shortcuts);

const {
showContextMenu: showEventsContextMenu,
showHeaderContextMenu: showEventsHeaderContextMenu,
} = useContextMenu(autoFitEventColumns);
} = useContextMenu(autoFitEventColumns, undefined, shortcuts);

const {
status: fullDiskAccessStatus,
Expand Down Expand Up @@ -194,6 +205,8 @@ function App() {
activeTab,
selectedPaths,
selectedIndicesRef,
shortcuts,
enabled: !isShortcutSettingsOpen,
focusSearchInput,
navigateSelection,
triggerQuickLook,
Expand Down Expand Up @@ -333,7 +346,10 @@ function App() {

return (
<>
<main className="container" aria-hidden={showFullDiskAccessOverlay || isPreferencesOpen}>
<main
className="container"
aria-hidden={showFullDiskAccessOverlay || isPreferencesOpen || isShortcutSettingsOpen}
>
<SearchBar
inputRef={searchInputRef}
placeholder={searchPlaceholder}
Expand Down Expand Up @@ -398,6 +414,7 @@ function App() {
<PreferencesOverlay
open={isPreferencesOpen}
onClose={closePreferences}
onOpenShortcutSettings={openShortcutSettings}
sortThreshold={sortThreshold}
defaultSortThreshold={DEFAULT_SORTABLE_RESULT_THRESHOLD}
onSortThresholdChange={setSortThreshold}
Expand All @@ -411,6 +428,12 @@ function App() {
onReset={handleResetPreferences}
themeResetToken={preferencesResetToken}
/>
<ShortcutSettingsOverlay
open={isShortcutSettingsOpen}
onClose={closeShortcutSettings}
shortcuts={shortcuts}
onShortcutSettingsSave={handleShortcutSettingsSave}
/>
{showFullDiskAccessOverlay && (
<PermissionOverlay
title={t('app.fullDiskAccess.title')}
Expand Down
13 changes: 13 additions & 0 deletions cardinal/src/__tests__/App.contextMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,23 @@ vi.mock('../components/PreferencesOverlay', () => ({
default: () => null,
}));

vi.mock('../components/ShortcutSettingsOverlay', () => ({
default: () => null,
}));

vi.mock('../components/StatusBar', () => ({
default: () => null,
}));

vi.mock('../tray', () => ({
refreshTrayMenu: vi.fn(),
}));

vi.mock('../menu', () => ({
refreshAppMenu: vi.fn(),
setMenuShortcutsEnabled: vi.fn(),
}));

vi.mock('../components/FSEventsPanel', () => ({
default: forwardRef(function MockFSEventsPanel(
{
Expand Down
59 changes: 59 additions & 0 deletions cardinal/src/__tests__/shortcuts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it } from 'vitest';
import {
DEFAULT_SHORTCUTS,
SHORTCUTS_STORAGE_KEY,
getStoredShortcutAccelerators,
getStoredShortcuts,
persistShortcuts,
} from '../shortcuts';

describe('shortcuts storage', () => {
beforeEach(() => {
window.localStorage.clear();
});

it('hydrates defaults when storage is empty', () => {
expect(getStoredShortcuts()).toEqual(DEFAULT_SHORTCUTS);
});

it('persists and restores local shortcut overrides', () => {
persistShortcuts({
...DEFAULT_SHORTCUTS,
openResult: 'Command+P',
searchHistoryUp: 'Control+K',
});

expect(getStoredShortcuts()).toMatchObject({
...DEFAULT_SHORTCUTS,
openResult: 'Command+P',
searchHistoryUp: 'Control+K',
});
});

it('writes unified shortcuts to localStorage', () => {
persistShortcuts(DEFAULT_SHORTCUTS);

expect(window.localStorage.getItem(SHORTCUTS_STORAGE_KEY)).toBeTruthy();
});

it('falls back to defaults when storage payload is invalid', () => {
window.localStorage.setItem(SHORTCUTS_STORAGE_KEY, '{bad json');

expect(getStoredShortcuts()).toEqual(DEFAULT_SHORTCUTS);
});

it('builds app menu/tray accelerator values from stored shortcuts', () => {
persistShortcuts({
...DEFAULT_SHORTCUTS,
openPreferences: 'Command+Comma',
hideWindow: 'Esc',
quickLaunch: 'Command+Shift+Space',
});

expect(getStoredShortcutAccelerators()).toEqual({
quickLaunch: 'Command+Shift+Space',
openPreferences: 'Cmd+,',
hideWindow: 'Esc',
});
});
});
12 changes: 12 additions & 0 deletions cardinal/src/components/PreferencesOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import LanguageSwitcher from './LanguageSwitcher';
type PreferencesOverlayProps = {
open: boolean;
onClose: () => void;
onOpenShortcutSettings: () => void;
sortThreshold: number;
defaultSortThreshold: number;
onSortThresholdChange: (value: number) => void;
Expand All @@ -24,6 +25,7 @@ type PreferencesOverlayProps = {
export function PreferencesOverlay({
open,
onClose,
onOpenShortcutSettings,
sortThreshold,
defaultSortThreshold,
onSortThresholdChange,
Expand Down Expand Up @@ -170,6 +172,16 @@ export function PreferencesOverlay({
<p className="preferences-label">{t('preferences.language')}</p>
<LanguageSwitcher className="preferences-control" />
</div>
<div className="preferences-row">
<p className="preferences-label">{t('preferences.shortcuts.label')}</p>
<button
className="preferences-manage-button"
type="button"
onClick={onOpenShortcutSettings}
>
{t('preferences.shortcuts.configure')}
</button>
</div>
<div className="preferences-row">
<p className="preferences-label">{t('preferences.trayIcon.label')}</p>
<div className="preferences-control">
Expand Down
Loading
Loading