Skip to content

Commit 6298990

Browse files
committed
Shortcuts!
- `⌥↑` and `⌥↓` for home/end - `Fn←` and `Fn→` also for home/end - `Fn↑` and `Fn--↓` for page up/down. - And a bit of a refactor
1 parent a42eda5 commit 6298990

6 files changed

Lines changed: 210 additions & 4 deletions

File tree

docs/features/shortcuts.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Keyboard shortcuts
2+
3+
This document lists all keyboard shortcuts available in Rusty Commander.
4+
5+
## Navigation
6+
7+
### Basic navigation
8+
9+
| Shortcut | Action | Mode |
10+
| ----------- | ------------------------------- | ---------- |
11+
| `` | Move selection up one item | Both |
12+
| `` | Move selection down one item | Both |
13+
| `` | Move selection left one column | Brief only |
14+
| `` | Move selection right one column | Brief only |
15+
| `Enter` | Open selected file/folder | Both |
16+
| `Backspace` | Navigate to parent directory | Both |
17+
18+
### Jump shortcuts
19+
20+
| Shortcut | Action | Mode |
21+
| -------- | ------------------------- | ---- |
22+
| `⌥↑` | Jump to first item (Home) | Both |
23+
| `⌥↓` | Jump to last item (End) | Both |
24+
| `Fn←` | Jump to first item (Home) | Both |
25+
| `Fn→` | Jump to last item (End) | Both |
26+
| `Fn↑` | Page up | Both |
27+
| `Fn↓` | Page down | Both |
28+
29+
**Note**: On macOS, `Fn+Arrow` keys generate `Home`, `End`, `PageUp`, and `PageDown` key events.
30+
31+
### Pane navigation
32+
33+
| Shortcut | Action |
34+
| -------- | ----------------------------------- |
35+
| `Tab` | Switch between left and right panes |
36+
37+
## Notes
38+
39+
- **Brief mode**: The file list is displayed in multiple columns. Arrow keys navigate within and between columns.
40+
- Page Up/Down: Moves horizontally by (number of visible columns - 1) and jumps to the bottommost item in the target
41+
column. If the target would be at or past the leftmost/rightmost edge, it jumps to the first/last item instead.
42+
This allows quick navigation across large file sets while maintaining context.
43+
- **Full mode**: The file list is displayed in a single column with detailed metadata (size, date). Only up/down arrow
44+
keys navigate items; left/right arrows are not used.
45+
- Page Up/Down: Moves vertically by (number of visible items - 1), adapting to the current window size.
46+
- All jump shortcuts (Home/End) work consistently in both Brief and Full modes.
47+
48+
## Shortcuts by category
49+
50+
### Quick jumps
51+
52+
- Go to start: `⌥↑` or `Fn←`
53+
- Go to end: `⌥↓` or `Fn→`
54+
- Page up: `Fn↑` (Brief: move left by visible columns - 1, or to first item if near edge; Full: move up by visible
55+
items - 1)
56+
- Page down: `Fn↓` (Brief: move right by visible columns - 1, or to last item if near edge; Full: move down by visible
57+
items - 1)
58+
59+
### File operations
60+
61+
- Open file/folder: `Enter`
62+
- Go up one directory: `Backspace`
63+
64+
### Interface
65+
66+
- Switch panes: `Tab`

docs/todo.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
- [ ] Load Google Drive sync statuses, too
2828
- [ ] Load OneDrive sync statuses, too?
2929
- [ ] Enable file drag&drop from the app to other apps.
30+
- [x] `⌥↑` and `⌥↓` should be home/end, `Fn←` and `Fn→` same, then `Fn↑` and `Fn--↓` should be page up/down.
3031

3132
## Cleanup
3233

src/lib/file-explorer/BriefList.svelte

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { getCachedIcon, prefetchIcons, iconCacheVersion } from '$lib/icon-cache'
44
import { calculateVirtualWindow, getScrollToPosition } from './virtual-scroll'
55
import { getFileRange } from '$lib/tauri-commands'
6+
import { handleNavigationShortcut } from './keyboard-shortcuts'
67
78
/** Prefetch buffer - load this many items around visible range */
89
const PREFETCH_BUFFER = 200
@@ -266,7 +267,23 @@
266267
}
267268
268269
// Handle keyboard navigation
269-
export function handleKeyNavigation(key: string): number | undefined {
270+
export function handleKeyNavigation(key: string, event?: KeyboardEvent): number | undefined {
271+
// Try navigation shortcuts first (Home/End/PageUp/PageDown)
272+
if (event) {
273+
// Calculate number of visible columns for PageUp/PageDown
274+
const visibleColumns = Math.ceil(containerWidth / maxFilenameWidth)
275+
const result = handleNavigationShortcut(event, {
276+
currentIndex: selectedIndex,
277+
totalCount,
278+
itemsPerColumn,
279+
visibleColumns,
280+
})
281+
if (result) {
282+
return result.newIndex
283+
}
284+
}
285+
286+
// Handle arrow keys
270287
if (key === 'ArrowUp') {
271288
return Math.max(0, selectedIndex - 1)
272289
}

src/lib/file-explorer/FilePane.svelte

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import SelectionInfo from './SelectionInfo.svelte'
2121
import LoadingIcon from '../LoadingIcon.svelte'
2222
import * as benchmark from '$lib/benchmark'
23+
import { handleNavigationShortcut } from './keyboard-shortcuts'
2324
2425
interface Props {
2526
initialPath: string
@@ -257,9 +258,9 @@
257258
258259
// Handle arrow keys based on view mode
259260
if (viewMode === 'brief') {
260-
// BriefList handles all arrow keys
261+
// BriefList handles all arrow keys and shortcuts
261262
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
262-
const newIndex: number | undefined = briefListRef?.handleKeyNavigation(e.key)
263+
const newIndex: number | undefined = briefListRef?.handleKeyNavigation(e.key, e)
263264
if (newIndex !== undefined) {
264265
e.preventDefault()
265266
selectedIndex = newIndex
@@ -268,7 +269,24 @@
268269
void fetchSelectedEntry()
269270
}
270271
} else {
271-
// Full mode: only Up/Down navigate
272+
// Full mode: try navigation shortcuts first
273+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
274+
const visibleItems: number = fullListRef?.getVisibleItemsCount() ?? 20
275+
const shortcutResult = handleNavigationShortcut(e, {
276+
currentIndex: selectedIndex,
277+
totalCount: effectiveTotalCount,
278+
visibleItems,
279+
})
280+
if (shortcutResult) {
281+
e.preventDefault()
282+
selectedIndex = shortcutResult.newIndex
283+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
284+
fullListRef?.scrollToIndex(shortcutResult.newIndex)
285+
void fetchSelectedEntry()
286+
return
287+
}
288+
289+
// Then handle Up/Down arrow navigation
272290
if (e.key === 'ArrowDown') {
273291
e.preventDefault()
274292
const newIndex = Math.min(selectedIndex + 1, effectiveTotalCount - 1)

src/lib/file-explorer/FullList.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@
304304
export function getVisiblePaths(): string[] {
305305
return visibleFiles.map((f) => f.file.path)
306306
}
307+
308+
// Returns the number of visible items (for Page Up/Down navigation)
309+
export function getVisibleItemsCount(): number {
310+
return Math.ceil(containerHeight / ROW_HEIGHT)
311+
}
307312
</script>
308313

309314
<div
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Keyboard shortcut handling for file lists
3+
*
4+
* Provides centralized logic for handling keyboard navigation shortcuts
5+
* across different view modes (Brief and Full).
6+
*/
7+
8+
export interface NavigationResult {
9+
/** The new index to select */
10+
newIndex: number
11+
/** Whether the event was handled */
12+
handled: boolean
13+
}
14+
15+
export interface NavigationContext {
16+
currentIndex: number
17+
totalCount: number
18+
/** For Brief mode: items per column */
19+
itemsPerColumn?: number
20+
/** For Brief mode: number of visible columns (for PageUp/PageDown) */
21+
visibleColumns?: number
22+
/** For Full mode: number of visible items (for PageUp/PageDown) */
23+
visibleItems?: number
24+
}
25+
26+
/**
27+
* Handles keyboard navigation shortcuts for file lists.
28+
* Returns the new index and whether the event was handled.
29+
*/
30+
export function handleNavigationShortcut(event: KeyboardEvent, context: NavigationContext): NavigationResult | null {
31+
const { currentIndex, totalCount, itemsPerColumn, visibleColumns, visibleItems } = context
32+
33+
// Home/End shortcuts (both Option+Arrow and Fn+Arrow)
34+
// Option+Up or Fn+Left = Home (go to first item)
35+
if ((event.altKey && event.key === 'ArrowUp') || (event.key === 'Home' && !event.metaKey)) {
36+
return { newIndex: 0, handled: true }
37+
}
38+
39+
// Option+Down or Fn+Right = End (go to last item)
40+
if ((event.altKey && event.key === 'ArrowDown') || (event.key === 'End' && !event.metaKey)) {
41+
return { newIndex: Math.max(0, totalCount - 1), handled: true }
42+
}
43+
44+
// Page Up/Down shortcuts (Fn+Up/Down)
45+
// In Brief mode: move horizontally by (visibleColumns - 1) and go to bottommost item
46+
// if near edge, go to first/last item instead
47+
// In Full mode: move vertically by (visibleItems - 1)
48+
if (event.key === 'PageUp') {
49+
if (visibleColumns !== undefined && itemsPerColumn !== undefined) {
50+
// Brief mode: horizontal page navigation
51+
const columnsToMove = Math.max(1, visibleColumns - 1)
52+
const currentColumn = Math.floor(currentIndex / itemsPerColumn)
53+
const targetColumn = currentColumn - columnsToMove
54+
55+
// If we'd go to or past the leftmost column, jump to first item
56+
if (targetColumn <= 0) {
57+
return { newIndex: 0, handled: true }
58+
}
59+
60+
// Otherwise, go to the bottommost item in the target column
61+
const targetColumnStart = targetColumn * itemsPerColumn
62+
const targetColumnEnd = Math.min(totalCount - 1, targetColumnStart + itemsPerColumn - 1)
63+
return { newIndex: targetColumnEnd, handled: true }
64+
} else {
65+
// Full mode: vertical page navigation by (visible items - 1)
66+
const pageSize = visibleItems ? Math.max(1, visibleItems - 1) : 20
67+
const newIndex = Math.max(0, currentIndex - pageSize)
68+
return { newIndex, handled: true }
69+
}
70+
}
71+
72+
if (event.key === 'PageDown') {
73+
if (visibleColumns !== undefined && itemsPerColumn !== undefined) {
74+
// Brief mode: horizontal page navigation
75+
const columnsToMove = Math.max(1, visibleColumns - 1)
76+
const currentColumn = Math.floor(currentIndex / itemsPerColumn)
77+
const totalColumns = Math.ceil(totalCount / itemsPerColumn)
78+
const targetColumn = currentColumn + columnsToMove
79+
80+
// If we'd go to or past the rightmost column, jump to last item
81+
if (targetColumn >= totalColumns - 1) {
82+
return { newIndex: totalCount - 1, handled: true }
83+
}
84+
85+
// Otherwise, go to the bottommost item in the target column
86+
const targetColumnStart = targetColumn * itemsPerColumn
87+
const targetColumnEnd = Math.min(totalCount - 1, targetColumnStart + itemsPerColumn - 1)
88+
return { newIndex: targetColumnEnd, handled: true }
89+
} else {
90+
// Full mode: vertical page navigation by (visible items - 1)
91+
const pageSize = visibleItems ? Math.max(1, visibleItems - 1) : 20
92+
const newIndex = Math.min(totalCount - 1, currentIndex + pageSize)
93+
return { newIndex, handled: true }
94+
}
95+
}
96+
97+
// Not a handled shortcut
98+
return null
99+
}

0 commit comments

Comments
 (0)