@tooee/shell 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/command-palette-provider.d.ts +5 -0
  2. package/dist/command-palette-provider.d.ts.map +1 -0
  3. package/dist/command-palette-provider.js +28 -0
  4. package/dist/command-palette-provider.js.map +1 -0
  5. package/dist/commands.d.ts +9 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +52 -1
  8. package/dist/commands.js.map +1 -1
  9. package/dist/copy-hook.d.ts +12 -0
  10. package/dist/copy-hook.d.ts.map +1 -0
  11. package/dist/copy-hook.js +35 -0
  12. package/dist/copy-hook.js.map +1 -0
  13. package/dist/copy-on-select.d.ts.map +1 -1
  14. package/dist/copy-on-select.js +1 -3
  15. package/dist/copy-on-select.js.map +1 -1
  16. package/dist/index.d.ts +8 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/navigation.d.ts +24 -0
  21. package/dist/navigation.d.ts.map +1 -0
  22. package/dist/navigation.js +239 -0
  23. package/dist/navigation.js.map +1 -0
  24. package/dist/provider.d.ts.map +1 -1
  25. package/dist/provider.js +4 -1
  26. package/dist/provider.js.map +1 -1
  27. package/dist/search-hook.d.ts +14 -0
  28. package/dist/search-hook.d.ts.map +1 -0
  29. package/dist/search-hook.js +120 -0
  30. package/dist/search-hook.js.map +1 -0
  31. package/package.json +20 -20
  32. package/src/command-palette-provider.tsx +39 -0
  33. package/src/commands.ts +55 -1
  34. package/src/copy-hook.ts +45 -0
  35. package/src/copy-on-select.ts +1 -4
  36. package/src/index.ts +15 -5
  37. package/src/navigation.ts +297 -0
  38. package/src/provider.tsx +6 -1
  39. package/src/search-hook.ts +147 -0
  40. package/dist/command-palette.d.ts +0 -9
  41. package/dist/command-palette.d.ts.map +0 -1
  42. package/dist/command-palette.js +0 -51
  43. package/dist/command-palette.js.map +0 -1
  44. package/dist/modal.d.ts +0 -33
  45. package/dist/modal.d.ts.map +0 -1
  46. package/dist/modal.js +0 -395
  47. package/dist/modal.js.map +0 -1
  48. package/src/command-palette.ts +0 -73
  49. package/src/modal.ts +0 -454
@@ -0,0 +1,120 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+ import { useCommand, useMode, useSetMode } from "@tooee/commands";
3
+ const EMPTY = [];
4
+ const CURSOR_MODES = ["cursor"];
5
+ const ALL_MODES = ["cursor", "select", "insert"];
6
+ export function useSearch({ match, onJump }) {
7
+ const mode = useMode();
8
+ const setMode = useSetMode();
9
+ const [searchQuery, setSearchQuery] = useState("");
10
+ const [searchActive, setSearchActive] = useState(false);
11
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
12
+ const [committedQuery, setCommittedQuery] = useState("");
13
+ const preSearchModeRef = useRef("cursor");
14
+ const matchRef = useRef(match);
15
+ matchRef.current = match;
16
+ const onJumpRef = useRef(onJump);
17
+ onJumpRef.current = onJump;
18
+ const activeQuery = searchActive ? searchQuery : committedQuery;
19
+ const matchingLines = useMemo(() => {
20
+ if (!activeQuery)
21
+ return EMPTY;
22
+ return matchRef.current(activeQuery);
23
+ }, [activeQuery]);
24
+ const matchingLinesRef = useRef(matchingLines);
25
+ matchingLinesRef.current = matchingLines;
26
+ // Imperatively set search query, reset match index, and jump to first match.
27
+ const updateSearchQuery = useCallback((query) => {
28
+ setSearchQuery(query);
29
+ setCurrentMatchIndex(0);
30
+ const matches = query ? matchRef.current(query) : [];
31
+ if (matches[0] != null) {
32
+ onJumpRef.current(matches[0]);
33
+ }
34
+ }, []);
35
+ useCommand({
36
+ id: "cursor-search-start",
37
+ title: "Search",
38
+ hotkey: "/",
39
+ modes: CURSOR_MODES,
40
+ handler: () => {
41
+ preSearchModeRef.current = mode;
42
+ setSearchActive(true);
43
+ setSearchQuery("");
44
+ setMode("insert");
45
+ },
46
+ });
47
+ useCommand({
48
+ id: "cursor-search-next",
49
+ title: "Next match",
50
+ hotkey: "n",
51
+ modes: CURSOR_MODES,
52
+ when: () => !searchActive,
53
+ handler: () => {
54
+ const matches = matchingLinesRef.current;
55
+ if (matches.length === 0)
56
+ return;
57
+ setCurrentMatchIndex((index) => {
58
+ const nextIndex = (index + 1) % matches.length;
59
+ const nextMatch = matches[nextIndex];
60
+ if (nextMatch != null) {
61
+ onJumpRef.current(nextMatch);
62
+ }
63
+ return nextIndex;
64
+ });
65
+ },
66
+ });
67
+ useCommand({
68
+ id: "cursor-search-prev",
69
+ title: "Previous match",
70
+ hotkey: "shift+n",
71
+ modes: CURSOR_MODES,
72
+ when: () => !searchActive,
73
+ handler: () => {
74
+ const matches = matchingLinesRef.current;
75
+ if (matches.length === 0)
76
+ return;
77
+ setCurrentMatchIndex((index) => {
78
+ const nextIndex = (index - 1 + matches.length) % matches.length;
79
+ const nextMatch = matches[nextIndex];
80
+ if (nextMatch != null) {
81
+ onJumpRef.current(nextMatch);
82
+ }
83
+ return nextIndex;
84
+ });
85
+ },
86
+ });
87
+ useCommand({
88
+ id: "search-cancel",
89
+ title: "Cancel search",
90
+ hotkey: "escape",
91
+ modes: ALL_MODES,
92
+ when: () => searchActive,
93
+ handler: () => {
94
+ setSearchActive(false);
95
+ setSearchQuery("");
96
+ setCommittedQuery("");
97
+ setCurrentMatchIndex(0);
98
+ setMode(preSearchModeRef.current);
99
+ },
100
+ });
101
+ const submitSearch = useCallback(() => {
102
+ setCommittedQuery(searchQuery);
103
+ setSearchActive(false);
104
+ setCurrentMatchIndex(0);
105
+ const matches = searchQuery ? matchRef.current(searchQuery) : [];
106
+ if (matches[0] != null) {
107
+ onJumpRef.current(matches[0]);
108
+ }
109
+ setMode(preSearchModeRef.current);
110
+ }, [searchQuery, setMode]);
111
+ return {
112
+ searchQuery,
113
+ searchActive,
114
+ setSearchQuery: updateSearchQuery,
115
+ matchingLines,
116
+ currentMatchIndex,
117
+ submitSearch,
118
+ };
119
+ }
120
+ //# sourceMappingURL=search-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-hook.js","sourceRoot":"","sources":["../src/search-hook.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@tooee/shell",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Composition layer wiring Tooee apps together",
5
+ "keywords": [
6
+ "cli",
7
+ "opentui",
8
+ "terminal",
9
+ "tui"
10
+ ],
11
+ "homepage": "https://github.com/gingerhendrix/tooee",
12
+ "bugs": "https://github.com/gingerhendrix/tooee/issues",
5
13
  "license": "MIT",
6
14
  "author": "Gareth Andrew",
7
15
  "repository": {
@@ -9,13 +17,9 @@
9
17
  "url": "https://github.com/gingerhendrix/tooee.git",
10
18
  "directory": "packages/shell"
11
19
  },
12
- "homepage": "https://github.com/gingerhendrix/tooee",
13
- "bugs": "https://github.com/gingerhendrix/tooee/issues",
14
- "keywords": [
15
- "tui",
16
- "terminal",
17
- "cli",
18
- "opentui"
20
+ "files": [
21
+ "dist",
22
+ "src"
19
23
  ],
20
24
  "type": "module",
21
25
  "exports": {
@@ -26,22 +30,18 @@
26
30
  }
27
31
  }
28
32
  },
29
- "files": [
30
- "dist",
31
- "src"
32
- ],
33
33
  "scripts": {
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
36
  "dependencies": {
37
- "@tooee/commands": "0.1.11",
38
- "@tooee/config": "0.1.11",
39
- "@tooee/clipboard": "0.1.11",
40
- "@tooee/toasts": "0.1.11",
41
- "@tooee/layout": "0.1.11",
42
- "@tooee/overlays": "0.1.11",
43
- "@tooee/renderers": "0.1.11",
44
- "@tooee/themes": "0.1.11"
37
+ "@tooee/clipboard": "0.1.12",
38
+ "@tooee/commands": "0.1.12",
39
+ "@tooee/config": "0.1.12",
40
+ "@tooee/layout": "0.1.12",
41
+ "@tooee/overlays": "0.1.12",
42
+ "@tooee/renderers": "0.1.12",
43
+ "@tooee/themes": "0.1.12",
44
+ "@tooee/toasts": "0.1.12"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@opentui/core": "^0.1.86",
@@ -0,0 +1,39 @@
1
+ import { useCallback, useRef, type ReactNode } from "react"
2
+ import { createElement } from "react"
3
+ import { useCommand, useMode } from "@tooee/commands"
4
+ import type { Mode } from "@tooee/commands"
5
+ import { useOverlay } from "@tooee/overlays"
6
+ import type { OverlayCloseReason } from "@tooee/overlays"
7
+ import { CommandPaletteOverlay } from "./CommandPaletteOverlay.js"
8
+
9
+ const OVERLAY_ID = "command-palette"
10
+
11
+ export function CommandPaletteProvider({ children }: { children: ReactNode }) {
12
+ const mode = useMode()
13
+ const overlay = useOverlay()
14
+ const launchModeRef = useRef<Mode>(mode)
15
+
16
+ const open = useCallback(() => {
17
+ launchModeRef.current = mode
18
+ overlay.open(
19
+ OVERLAY_ID,
20
+ ({ close }: { close: (reason?: OverlayCloseReason) => void }) =>
21
+ createElement(CommandPaletteOverlay, {
22
+ launchMode: mode,
23
+ close: () => close(),
24
+ }),
25
+ null,
26
+ { mode: "insert", dismissOnEscape: true },
27
+ )
28
+ }, [overlay, mode])
29
+
30
+ useCommand({
31
+ id: "command-palette",
32
+ title: "Command Palette",
33
+ hotkey: ":",
34
+ modes: ["cursor", "select"],
35
+ handler: open,
36
+ })
37
+
38
+ return <>{children}</>
39
+ }
package/src/commands.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useRenderer } from "@opentui/react"
2
- import { copyToClipboard } from "@tooee/clipboard"
2
+ import { copyToClipboard, readClipboardText, readPrimaryText } from "@tooee/clipboard"
3
3
  import { useCommand, type CommandWhen } from "@tooee/commands"
4
4
  import { useToast } from "@tooee/toasts"
5
5
  import { useThemePicker, type ThemePickerState } from "./theme-picker.js"
@@ -73,6 +73,60 @@ export function useCopyCommand(opts: { getText: () => string | undefined; when?:
73
73
  })
74
74
  }
75
75
 
76
+ export function usePasteCommands(opts: {
77
+ getTarget: () => { insertText: (text: string) => void } | null
78
+ when?: CommandWhen
79
+ }) {
80
+ useCommand({
81
+ id: "paste-clipboard",
82
+ title: "Paste from clipboard",
83
+ hotkey: "p",
84
+ when: opts.when,
85
+ handler: (ctx) => {
86
+ const target = opts.getTarget()
87
+ if (!target) return
88
+ void readClipboardText().then((text) => {
89
+ if (text) {
90
+ target.insertText(text)
91
+ } else {
92
+ ctx.toast.toast({ message: "Clipboard empty", level: "warning" })
93
+ }
94
+ })
95
+ },
96
+ })
97
+
98
+ useCommand({
99
+ id: "paste-primary",
100
+ title: "Paste from selection",
101
+ when: opts.when,
102
+ handler: (ctx) => {
103
+ const target = opts.getTarget()
104
+ if (!target) return
105
+ void readPrimaryText().then((text) => {
106
+ if (text) {
107
+ target.insertText(text)
108
+ } else {
109
+ ctx.toast.toast({ message: "Selection empty", level: "warning" })
110
+ }
111
+ })
112
+ },
113
+ })
114
+ }
115
+
116
+ export function useDebugConsoleCommand(opts?: { when?: CommandWhen }) {
117
+ const renderer = useRenderer()
118
+
119
+ useCommand({
120
+ id: "toggle-debug-console",
121
+ title: "Toggle debug console",
122
+ hotkey: "ctrl+shift+j",
123
+ when: opts?.when,
124
+ handler: () => {
125
+ renderer.console.toggle()
126
+ },
127
+ })
128
+ }
129
+
76
130
  export function useToggleLineNumbersCommand(opts: {
77
131
  showLineNumbers: boolean
78
132
  onToggle: () => void
@@ -0,0 +1,45 @@
1
+ import { useCommand, useSetMode } from "@tooee/commands"
2
+ import { copyToClipboard } from "@tooee/clipboard"
3
+ import type { Position } from "./navigation.js"
4
+
5
+ export interface UseCopyOptions {
6
+ getRowText: (index: number) => string
7
+ cursor: Position | null
8
+ selection: { start: Position; end: Position } | null
9
+ toggledIndices: Set<number>
10
+ }
11
+
12
+ export function useCopy({ getRowText, cursor, selection, toggledIndices }: UseCopyOptions): void {
13
+ const setMode = useSetMode()
14
+
15
+ useCommand({
16
+ id: "select-copy",
17
+ title: "Copy selection",
18
+ hotkey: "y",
19
+ modes: ["select"],
20
+ handler: () => {
21
+ let text = ""
22
+
23
+ if (toggledIndices.size > 0) {
24
+ text = Array.from(toggledIndices)
25
+ .sort((left, right) => left - right)
26
+ .map((index) => getRowText(index))
27
+ .join("\n")
28
+ } else if (selection) {
29
+ const rows: string[] = []
30
+ for (let index = selection.start.line; index <= selection.end.line; index++) {
31
+ rows.push(getRowText(index))
32
+ }
33
+ text = rows.join("\n")
34
+ } else if (cursor) {
35
+ text = getRowText(cursor.line)
36
+ }
37
+
38
+ if (text) {
39
+ void copyToClipboard(text)
40
+ }
41
+
42
+ setMode("cursor")
43
+ },
44
+ })
45
+ }
@@ -13,10 +13,7 @@ export function useCopyOnSelect() {
13
13
  const copyOnSelect = config.view?.copyOnSelect
14
14
 
15
15
  // Default: on for Linux, off elsewhere
16
- const effective =
17
- copyOnSelect === undefined
18
- ? platform() === "linux"
19
- : copyOnSelect
16
+ const effective = copyOnSelect === undefined ? platform() === "linux" : copyOnSelect
20
17
 
21
18
  if (!effective) return
22
19
 
package/src/index.ts CHANGED
@@ -1,11 +1,21 @@
1
- export { useThemeCommands, useQuitCommand, useCopyCommand, useToggleLineNumbersCommand } from "./commands.js"
2
- export { useModalNavigationCommands } from "./modal.js"
3
- export type { ModalNavigationState, ModalNavigationOptions, Position } from "./modal.js"
1
+ export {
2
+ useThemeCommands,
3
+ useQuitCommand,
4
+ useCopyCommand,
5
+ usePasteCommands,
6
+ useToggleLineNumbersCommand,
7
+ useDebugConsoleCommand,
8
+ } from "./commands.js"
9
+ export { useNavigation } from "./navigation.js"
10
+ export type { UseNavigationOptions, NavigationState, Position } from "./navigation.js"
11
+ export { useSearch } from "./search-hook.js"
12
+ export type { UseSearchOptions, SearchState } from "./search-hook.js"
13
+ export { useCopy } from "./copy-hook.js"
14
+ export type { UseCopyOptions } from "./copy-hook.js"
4
15
  export { findMatchingLines } from "./search.js"
5
16
  export { TooeeProvider } from "./provider.js"
6
17
  export { launchCli, guardTerminalHealth } from "./launch.js"
7
- export { useCommandPalette } from "./command-palette.js"
8
- export type { CommandPaletteState } from "./command-palette.js"
18
+ export { CommandPaletteProvider } from "./command-palette-provider.js"
9
19
  export { useThemePicker } from "./theme-picker.js"
10
20
  export type { ThemePickerState, ThemePickerEntry } from "./theme-picker.js"
11
21
  export { OverlayProvider } from "./overlay.js"
@@ -0,0 +1,297 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react"
2
+ import { useTerminalDimensions } from "@opentui/react"
3
+ import { useCommand, useMode, useSetMode, type Mode } from "@tooee/commands"
4
+
5
+ export interface Position {
6
+ line: number
7
+ col: number
8
+ }
9
+
10
+ const CURSOR_MODES: Mode[] = ["cursor"]
11
+ const SELECT_MODES: Mode[] = ["select"]
12
+
13
+ export interface UseNavigationOptions {
14
+ rowCount: number
15
+ isSelectable?: (index: number) => boolean
16
+ viewportHeight?: number
17
+ multiSelect?: boolean
18
+ }
19
+
20
+ export interface NavigationState {
21
+ mode: Mode
22
+ setMode: (mode: Mode) => void
23
+ cursor: Position | null
24
+ setCursor: (line: number) => void
25
+ selection: { start: Position; end: Position } | null
26
+ toggledIndices: Set<number>
27
+ }
28
+
29
+ function defaultIsSelectable(): boolean {
30
+ return true
31
+ }
32
+
33
+ export function useNavigation({
34
+ rowCount,
35
+ isSelectable = defaultIsSelectable,
36
+ viewportHeight,
37
+ multiSelect = false,
38
+ }: UseNavigationOptions): NavigationState {
39
+ const { height: terminalHeight } = useTerminalDimensions()
40
+ const effectiveViewportHeight = viewportHeight ?? Math.max(1, terminalHeight - 2)
41
+ const mode = useMode()
42
+ const setMode = useSetMode()
43
+
44
+ const maxIndex = Math.max(0, rowCount - 1)
45
+
46
+ const clampIndex = useCallback(
47
+ (index: number) => Math.max(0, Math.min(index, maxIndex)),
48
+ [maxIndex],
49
+ )
50
+
51
+ const findSelectable = useCallback(
52
+ (start: number, direction: 1 | -1) => {
53
+ if (rowCount <= 0) return null
54
+ const initial = Math.max(0, Math.min(start, maxIndex))
55
+ for (
56
+ let index = initial;
57
+ direction === 1 ? index <= maxIndex : index >= 0;
58
+ index += direction
59
+ ) {
60
+ if (isSelectable(index)) return index
61
+ }
62
+ return null
63
+ },
64
+ [rowCount, maxIndex, isSelectable],
65
+ )
66
+
67
+ const resolveSelectable = useCallback(
68
+ (target: number, preferredDirection: 1 | -1 = 1) => {
69
+ if (rowCount <= 0) return null
70
+ const clamped = Math.max(0, Math.min(target, maxIndex))
71
+ if (isSelectable(clamped)) return clamped
72
+ return preferredDirection === 1
73
+ ? findSelectable(clamped, 1) ?? findSelectable(clamped, -1)
74
+ : findSelectable(clamped, -1) ?? findSelectable(clamped, 1)
75
+ },
76
+ [rowCount, maxIndex, isSelectable, findSelectable],
77
+ )
78
+
79
+ const [rawCursor, setRawCursor] = useState<Position | null>(() => {
80
+ const line = resolveSelectable(0, 1)
81
+ return line == null ? null : { line, col: 0 }
82
+ })
83
+
84
+ // Derive valid cursor from raw cursor + constraints
85
+ const cursor = useMemo(() => {
86
+ if (rowCount <= 0) return null
87
+ if (!rawCursor) {
88
+ if (mode !== "cursor") return null
89
+ const line = resolveSelectable(0, 1)
90
+ return line == null ? null : { line, col: 0 }
91
+ }
92
+ const preferredDirection: 1 | -1 = rawCursor.line > maxIndex ? -1 : 1
93
+ const resolved = resolveSelectable(rawCursor.line, preferredDirection)
94
+ if (resolved == null) return null
95
+ if (resolved === rawCursor.line) return rawCursor
96
+ return { line: resolved, col: 0 }
97
+ }, [rawCursor, rowCount, maxIndex, mode, resolveSelectable])
98
+
99
+ const cursorRef = useRef(cursor)
100
+ cursorRef.current = cursor
101
+ const [selectionAnchor, setSelectionAnchor] = useState<Position | null>(null)
102
+ const [rawToggledIndices, setRawToggledIndices] = useState<Set<number>>(new Set())
103
+
104
+ // Derive valid toggled indices (filter out-of-bounds)
105
+ const toggledIndices = useMemo(() => {
106
+ if (rowCount <= 0) return new Set<number>()
107
+ let needsFilter = false
108
+ for (const i of rawToggledIndices) {
109
+ if (i >= rowCount) { needsFilter = true; break }
110
+ }
111
+ if (!needsFilter) return rawToggledIndices
112
+ return new Set(Array.from(rawToggledIndices).filter(i => i < rowCount))
113
+ }, [rawToggledIndices, rowCount])
114
+
115
+ const setCursor = useCallback(
116
+ (line: number) => {
117
+ setRawCursor((current) => {
118
+ const preferredDirection: 1 | -1 = current && line < current.line ? -1 : 1
119
+ const nextLine = resolveSelectable(line, preferredDirection)
120
+ if (nextLine == null) return null
121
+ if (current && nextLine === current.line) return current
122
+ return { line: nextLine, col: 0 }
123
+ })
124
+ },
125
+ [resolveSelectable],
126
+ )
127
+
128
+ const moveCursor = useCallback(
129
+ (delta: number) => {
130
+ setRawCursor((current) => {
131
+ if (!current) return current
132
+ const target = clampIndex(current.line + delta)
133
+ const preferredDirection: 1 | -1 = delta < 0 ? -1 : 1
134
+ const nextLine = resolveSelectable(target, preferredDirection)
135
+ if (nextLine == null) return current
136
+ if (nextLine === current.line) return current
137
+ return { line: nextLine, col: 0 }
138
+ })
139
+ },
140
+ [clampIndex, resolveSelectable],
141
+ )
142
+
143
+ const jumpCursor = useCallback(
144
+ (target: number, preferredDirection: 1 | -1) => {
145
+ const nextLine = resolveSelectable(target, preferredDirection)
146
+ setRawCursor(nextLine == null ? null : { line: nextLine, col: 0 })
147
+ },
148
+ [resolveSelectable],
149
+ )
150
+
151
+ const toggleCurrent = useCallback(() => {
152
+ const cur = cursorRef.current
153
+ if (!cur) return
154
+ setRawToggledIndices((prev) => {
155
+ const next = new Set(prev)
156
+ if (next.has(cur.line)) {
157
+ next.delete(cur.line)
158
+ } else {
159
+ next.add(cur.line)
160
+ }
161
+ return next
162
+ })
163
+ }, [])
164
+
165
+ useCommand({
166
+ id: "cursor-down",
167
+ title: "Cursor down",
168
+ hotkey: "j",
169
+ modes: CURSOR_MODES,
170
+ handler: () => moveCursor(1),
171
+ })
172
+
173
+ useCommand({
174
+ id: "cursor-up",
175
+ title: "Cursor up",
176
+ hotkey: "k",
177
+ modes: CURSOR_MODES,
178
+ handler: () => moveCursor(-1),
179
+ })
180
+
181
+ useCommand({
182
+ id: "cursor-half-down",
183
+ title: "Cursor half page down",
184
+ hotkey: "ctrl+d",
185
+ modes: CURSOR_MODES,
186
+ handler: () => moveCursor(Math.floor(effectiveViewportHeight / 2) || 1),
187
+ })
188
+
189
+ useCommand({
190
+ id: "cursor-half-up",
191
+ title: "Cursor half page up",
192
+ hotkey: "ctrl+u",
193
+ modes: CURSOR_MODES,
194
+ handler: () => moveCursor(-(Math.floor(effectiveViewportHeight / 2) || 1)),
195
+ })
196
+
197
+ useCommand({
198
+ id: "cursor-top",
199
+ title: "Cursor to top",
200
+ hotkey: "g g",
201
+ modes: CURSOR_MODES,
202
+ handler: () => jumpCursor(0, 1),
203
+ })
204
+
205
+ useCommand({
206
+ id: "cursor-bottom",
207
+ title: "Cursor to bottom",
208
+ hotkey: "shift+g",
209
+ modes: CURSOR_MODES,
210
+ handler: () => jumpCursor(maxIndex, -1),
211
+ })
212
+
213
+ useCommand({
214
+ id: "enter-select",
215
+ title: "Enter select mode",
216
+ hotkey: "v",
217
+ modes: CURSOR_MODES,
218
+ handler: () => {
219
+ setSelectionAnchor(cursorRef.current ? { ...cursorRef.current } : null)
220
+ setMode("select")
221
+ },
222
+ })
223
+
224
+ useCommand({
225
+ id: "cursor-toggle",
226
+ title: "Toggle selection",
227
+ hotkey: "tab",
228
+ modes: CURSOR_MODES,
229
+ when: () => multiSelect,
230
+ handler: toggleCurrent,
231
+ })
232
+
233
+ useCommand({
234
+ id: "cursor-toggle-up",
235
+ title: "Toggle and move up",
236
+ hotkey: "shift+tab",
237
+ modes: CURSOR_MODES,
238
+ when: () => multiSelect,
239
+ handler: () => {
240
+ toggleCurrent()
241
+ moveCursor(-1)
242
+ },
243
+ })
244
+
245
+ useCommand({
246
+ id: "select-down",
247
+ title: "Extend selection down",
248
+ hotkey: "j",
249
+ modes: SELECT_MODES,
250
+ handler: () => moveCursor(1),
251
+ })
252
+
253
+ useCommand({
254
+ id: "select-up",
255
+ title: "Extend selection up",
256
+ hotkey: "k",
257
+ modes: SELECT_MODES,
258
+ handler: () => moveCursor(-1),
259
+ })
260
+
261
+ useCommand({
262
+ id: "select-toggle",
263
+ title: "Toggle selection",
264
+ hotkey: "tab",
265
+ modes: SELECT_MODES,
266
+ when: () => multiSelect,
267
+ handler: toggleCurrent,
268
+ })
269
+
270
+ useCommand({
271
+ id: "select-cancel",
272
+ title: "Cancel selection",
273
+ hotkey: "escape",
274
+ modes: SELECT_MODES,
275
+ handler: () => {
276
+ setSelectionAnchor(null)
277
+ setMode("cursor")
278
+ },
279
+ })
280
+
281
+ const selection =
282
+ mode === "select" && selectionAnchor && cursor
283
+ ? {
284
+ start: selectionAnchor.line <= cursor.line ? selectionAnchor : cursor,
285
+ end: selectionAnchor.line <= cursor.line ? cursor : selectionAnchor,
286
+ }
287
+ : null
288
+
289
+ return {
290
+ mode,
291
+ setMode,
292
+ cursor,
293
+ setCursor,
294
+ selection,
295
+ toggledIndices,
296
+ }
297
+ }