@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.
- package/dist/command-palette-provider.d.ts +5 -0
- package/dist/command-palette-provider.d.ts.map +1 -0
- package/dist/command-palette-provider.js +28 -0
- package/dist/command-palette-provider.js.map +1 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +52 -1
- package/dist/commands.js.map +1 -1
- package/dist/copy-hook.d.ts +12 -0
- package/dist/copy-hook.d.ts.map +1 -0
- package/dist/copy-hook.js +35 -0
- package/dist/copy-hook.js.map +1 -0
- package/dist/copy-on-select.d.ts.map +1 -1
- package/dist/copy-on-select.js +1 -3
- package/dist/copy-on-select.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation.d.ts +24 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +239 -0
- package/dist/navigation.js.map +1 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +4 -1
- package/dist/provider.js.map +1 -1
- package/dist/search-hook.d.ts +14 -0
- package/dist/search-hook.d.ts.map +1 -0
- package/dist/search-hook.js +120 -0
- package/dist/search-hook.js.map +1 -0
- package/package.json +20 -20
- package/src/command-palette-provider.tsx +39 -0
- package/src/commands.ts +55 -1
- package/src/copy-hook.ts +45 -0
- package/src/copy-on-select.ts +1 -4
- package/src/index.ts +15 -5
- package/src/navigation.ts +297 -0
- package/src/provider.tsx +6 -1
- package/src/search-hook.ts +147 -0
- package/dist/command-palette.d.ts +0 -9
- package/dist/command-palette.d.ts.map +0 -1
- package/dist/command-palette.js +0 -51
- package/dist/command-palette.js.map +0 -1
- package/dist/modal.d.ts +0 -33
- package/dist/modal.d.ts.map +0 -1
- package/dist/modal.js +0 -395
- package/dist/modal.js.map +0 -1
- package/src/command-palette.ts +0 -73
- 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":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC9D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAa,MAAM,iBAAiB,CAAA;AAgB5E,MAAM,KAAK,GAAa,EAAE,CAAA;AAC1B,MAAM,YAAY,GAAW,CAAC,QAAQ,CAAC,CAAA;AACvC,MAAM,SAAS,GAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAExD,MAAM,UAAU,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAoB;IAC3D,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAE5B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAClD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvD,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC7D,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACxD,MAAM,gBAAgB,GAAG,MAAM,CAAO,QAAQ,CAAC,CAAA;IAE/C,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;IAExB,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAChC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAA;IAE1B,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAA;IAE/D,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE;QACjC,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAA;QAC9B,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IACtC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAA;IAEjB,MAAM,gBAAgB,GAAG,MAAM,CAAC,aAAa,CAAC,CAAA;IAC9C,gBAAgB,CAAC,OAAO,GAAG,aAAa,CAAA;IAExC,6EAA6E;IAC7E,MAAM,iBAAiB,GAAG,WAAW,CAAC,CAAC,KAAa,EAAE,EAAE;QACtD,cAAc,CAAC,KAAK,CAAC,CAAA;QACrB,oBAAoB,CAAC,CAAC,CAAC,CAAA;QACvB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACpD,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YACvB,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,UAAU,CAAC;QACT,EAAE,EAAE,qBAAqB;QACzB,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,YAAY;QACnB,OAAO,EAAE,GAAG,EAAE;YACZ,gBAAgB,CAAC,OAAO,GAAG,IAAI,CAAA;YAC/B,eAAe,CAAC,IAAI,CAAC,CAAA;YACrB,cAAc,CAAC,EAAE,CAAC,CAAA;YAClB,OAAO,CAAC,QAAQ,CAAC,CAAA;QACnB,CAAC;KACF,CAAC,CAAA;IAEF,UAAU,CAAC;QACT,EAAE,EAAE,oBAAoB;QACxB,KAAK,EAAE,YAAY;QACnB,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,YAAY;QACnB,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY;QACzB,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAA;YACxC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAM;YAEhC,oBAAoB,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC7B,MAAM,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAA;gBAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;gBACpC,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;oBACtB,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;gBAC9B,CAAC;gBACD,OAAO,SAAS,CAAA;YAClB,CAAC,CAAC,CAAA;QACJ,CAAC;KACF,CAAC,CAAA;IAEF,UAAU,CAAC;QACT,EAAE,EAAE,oBAAoB;QACxB,KAAK,EAAE,gBAAgB;QACvB,MAAM,EAAE,SAAS;QACjB,KAAK,EAAE,YAAY;QACnB,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY;QACzB,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAA;YACxC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAM;YAEhC,oBAAoB,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC7B,MAAM,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAA;gBAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;gBACpC,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;oBACtB,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;gBAC9B,CAAC;gBACD,OAAO,SAAS,CAAA;YAClB,CAAC,CAAC,CAAA;QACJ,CAAC;KACF,CAAC,CAAA;IAEF,UAAU,CAAC;QACT,EAAE,EAAE,eAAe;QACnB,KAAK,EAAE,eAAe;QACtB,MAAM,EAAE,QAAQ;QAChB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,GAAG,EAAE,CAAC,YAAY;QACxB,OAAO,EAAE,GAAG,EAAE;YACZ,eAAe,CAAC,KAAK,CAAC,CAAA;YACtB,cAAc,CAAC,EAAE,CAAC,CAAA;YAClB,iBAAiB,CAAC,EAAE,CAAC,CAAA;YACrB,oBAAoB,CAAC,CAAC,CAAC,CAAA;YACvB,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;QACnC,CAAC;KACF,CAAC,CAAA;IAEF,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;QACpC,iBAAiB,CAAC,WAAW,CAAC,CAAA;QAC9B,eAAe,CAAC,KAAK,CAAC,CAAA;QACtB,oBAAoB,CAAC,CAAC,CAAC,CAAA;QACvB,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAChE,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YACvB,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;QAC/B,CAAC;QACD,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACnC,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAA;IAE1B,OAAO;QACL,WAAW;QACX,YAAY;QACZ,cAAc,EAAE,iBAAiB;QACjC,aAAa;QACb,iBAAiB;QACjB,YAAY;KACb,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tooee/shell",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
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/
|
|
38
|
-
"@tooee/
|
|
39
|
-
"@tooee/
|
|
40
|
-
"@tooee/
|
|
41
|
-
"@tooee/
|
|
42
|
-
"@tooee/
|
|
43
|
-
"@tooee/
|
|
44
|
-
"@tooee/
|
|
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
|
package/src/copy-hook.ts
ADDED
|
@@ -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
|
+
}
|
package/src/copy-on-select.ts
CHANGED
|
@@ -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 {
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
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
|
+
}
|