@ssebastien/tcmd 0.1.3 → 0.1.7
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/README.md +256 -62
- package/bin/tcmd +10 -2
- package/bin/tcmd.js +13 -2
- package/desktop/dist/index.html +13 -0
- package/desktop/dist/main.js +20298 -0
- package/desktop/dist/style.css +1515 -0
- package/desktop/frontend/index.html +13 -0
- package/package.json +19 -6
- package/public/fonts/OFL.txt +93 -0
- package/public/index.html +14 -0
- package/public/licenses/LUCIDE.txt +43 -0
- package/public/main.js +20679 -0
- package/public/style.css +1515 -0
- package/scripts/build-desktop.js +31 -0
- package/scripts/serve-web.js +61 -0
- package/src/js/tcmd/components/App.html +80 -14
- package/src/js/tcmd/components/App.js +339 -114
- package/src/js/tcmd/components/App.xml +76 -22
- package/src/js/tcmd/components/BookmarkList.html +5 -5
- package/src/js/tcmd/components/BookmarkList.js +15 -41
- package/src/js/tcmd/components/BookmarkList.xml +1 -2
- package/src/js/tcmd/components/BookmarkRow.html +2 -1
- package/src/js/tcmd/components/BookmarkRow.js +14 -9
- package/src/js/tcmd/components/BookmarkRow.xml +1 -1
- package/src/js/tcmd/components/Breadcrumbs.js +1 -1
- package/src/js/tcmd/components/CommandArgRow.html +28 -0
- package/src/js/tcmd/components/CommandArgRow.js +146 -30
- package/src/js/tcmd/components/CommandArgRow.xml +55 -4
- package/src/js/tcmd/components/CommandBar.html +2 -2
- package/src/js/tcmd/components/CommandBar.js +33 -27
- package/src/js/tcmd/components/CommandBar.xml +2 -2
- package/src/js/tcmd/components/CommandItem.html +1 -1
- package/src/js/tcmd/components/CommandItem.js +25 -25
- package/src/js/tcmd/components/CommandItem.xml +2 -2
- package/src/js/tcmd/components/CommandPanel.html +29 -0
- package/src/js/tcmd/components/CommandPanel.js +78 -52
- package/src/js/tcmd/components/CommandPanel.xml +49 -25
- package/src/js/tcmd/components/Confirm.html +2 -2
- package/src/js/tcmd/components/EntryDetail.html +7 -7
- package/src/js/tcmd/components/EntryDetail.js +22 -8
- package/src/js/tcmd/components/FileList.html +5 -1
- package/src/js/tcmd/components/FileList.js +2 -1
- package/src/js/tcmd/components/FilePicker.html +10 -0
- package/src/js/tcmd/components/FilePicker.js +43 -0
- package/src/js/tcmd/components/FilePicker.xml +13 -0
- package/src/js/tcmd/components/FilePickerRow.html +6 -0
- package/src/js/tcmd/components/FilePickerRow.js +27 -0
- package/src/js/tcmd/components/FilePickerRow.xml +10 -0
- package/src/js/tcmd/components/FilePreview.html +14 -4
- package/src/js/tcmd/components/FilePreview.js +111 -12
- package/src/js/tcmd/components/FilePreview.xml +28 -8
- package/src/js/tcmd/components/FileRow.html +2 -1
- package/src/js/tcmd/components/FileRow.js +70 -12
- package/src/js/tcmd/components/FilterBar.html +1 -1
- package/src/js/tcmd/components/FilterBar.js +14 -4
- package/src/js/tcmd/components/FilterBar.xml +1 -1
- package/src/js/tcmd/components/Frame.js +324 -0
- package/src/js/tcmd/components/Help.html +15 -0
- package/src/js/tcmd/components/Help.js +260 -0
- package/src/js/tcmd/components/Help.xml +13 -4
- package/src/js/tcmd/components/HelpRow.html +8 -0
- package/src/js/tcmd/components/HelpRow.xml +7 -0
- package/src/js/tcmd/components/OverlayList.html +8 -0
- package/src/js/tcmd/components/OverlayList.js +69 -0
- package/src/js/tcmd/components/{CommandPalette.xml → OverlayList.xml} +2 -5
- package/src/js/tcmd/components/OverlayRow.html +5 -0
- package/src/js/tcmd/components/OverlayRow.js +47 -0
- package/src/js/tcmd/components/OverlayRow.xml +9 -0
- package/src/js/tcmd/components/Summary.html +1 -1
- package/src/js/tcmd/components/Summary.js +1 -6
- package/src/js/tcmd/components/format.js +4 -23
- package/src/js/tcmd/components/icons.js +176 -5
- package/src/js/tcmd/components/index.js +9 -5
- package/src/js/tcmd/components/rows.js +176 -48
- package/src/js/tcmd/core/base64.js +64 -0
- package/src/js/tcmd/core/bookmarks.js +45 -8
- package/src/js/tcmd/core/commands.js +978 -260
- package/src/js/tcmd/core/concurrency.js +59 -0
- package/src/js/tcmd/core/decompress.js +117 -0
- package/src/js/tcmd/core/directory.js +219 -93
- package/src/js/tcmd/core/format.js +36 -0
- package/src/js/tcmd/core/fs.js +438 -54
- package/src/js/tcmd/core/history.js +58 -0
- package/src/js/tcmd/core/index.js +3 -9
- package/src/js/tcmd/core/journal.js +93 -0
- package/src/js/tcmd/core/keymap.js +42 -6
- package/src/js/tcmd/core/operations.js +813 -48
- package/src/js/tcmd/core/os.js +29 -3
- package/src/js/tcmd/core/pane.js +33 -8
- package/src/js/tcmd/core/panel.js +280 -104
- package/src/js/tcmd/core/path.js +30 -3
- package/src/js/tcmd/core/permissions.js +107 -0
- package/src/js/tcmd/core/picker.js +156 -0
- package/src/js/tcmd/core/preview.js +102 -0
- package/src/js/tcmd/core/script.js +7 -95
- package/src/js/tcmd/core/selection.js +9 -1
- package/src/js/tcmd/core/session.js +498 -200
- package/src/js/tcmd/core/watch.js +153 -0
- package/src/js/tcmd/desktop/backend/api.js +165 -0
- package/src/js/tcmd/desktop/backend/fs.js +626 -0
- package/src/js/tcmd/desktop/backend/main.ts +9 -0
- package/src/js/tcmd/desktop/bookmarks.js +23 -0
- package/src/js/tcmd/desktop/fs.js +91 -0
- package/src/js/tcmd/desktop/main.js +194 -0
- package/src/js/tcmd/desktop/os.js +33 -0
- package/src/js/tcmd/index.js +0 -1
- package/src/js/tcmd/tui/app.js +118 -60
- package/src/js/tcmd/tui/catalogue.js +238 -42
- package/src/js/tcmd/tui/index.js +18 -19
- package/src/js/tcmd/tui/opentui.js +33 -0
- package/src/js/tcmd/tui/syntax.js +91 -0
- package/src/js/tcmd/tui/theme.js +32 -2
- package/src/js/tcmd/vendor/bzip2.js +276 -0
- package/src/js/tcmd/web/app.js +225 -41
- package/src/js/tcmd/web/bookmarks.js +36 -0
- package/src/js/tcmd/web/catalogue.js +81 -38
- package/src/js/tcmd/web/effects.js +223 -0
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/header.js +15 -11
- package/src/js/tcmd/web/http.js +13 -0
- package/src/js/tcmd/web/index.js +2 -2
- package/src/js/tcmd/web/main.js +6 -66
- package/src/js/tcmd/web/opfs.js +297 -30
- package/src/js/tcmd/web/seed.js +4 -4
- package/src/js/tcmd/web/server.js +124 -21
- package/src/js/tcmd/web/theme.js +4 -0
- package/tinyjs.json +16 -0
- package/src/js/tcmd/components/CommandPalette.html +0 -7
- package/src/js/tcmd/components/CommandPalette.js +0 -58
- package/src/js/tcmd/components/PaletteRow.html +0 -3
- package/src/js/tcmd/components/PaletteRow.js +0 -43
- package/src/js/tcmd/components/PaletteRow.xml +0 -4
- package/src/js/tcmd/tui/frame.js +0 -269
- package/src/js/tcmd/tui/help.js +0 -32
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// Created: 2026-09-26
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/App
|
|
7
|
-
// Root component: a header, two panes (breadcrumbs + FileList +
|
|
8
|
-
// an optional preview column, the command bar and the command
|
|
7
|
+
// Root component: a header, two panes (breadcrumbs + FileList + detail each),
|
|
8
|
+
// an optional preview column, the command bar and the command panel overlay.
|
|
9
9
|
// App is the controller seam: component events (published bubbles) and key
|
|
10
10
|
// chords are translated into `session.dispatch(commandId, payload)` calls; it
|
|
11
|
-
// never mutates model state directly.
|
|
11
|
+
// never mutates model state directly. It also owns the shared modal key
|
|
12
|
+
// routing — the fullscreen viewer overlay included — so both renderers behave
|
|
13
|
+
// alike.
|
|
12
14
|
//
|
|
13
15
|
// Environment: `normalizeKey(event)` (optional; identity when omitted) and
|
|
14
16
|
// `quit(self)`. Renderer-specific bindings (pane borders, status color) live in
|
|
@@ -17,31 +19,40 @@
|
|
|
17
19
|
import { derived, unwrap } from "select/state/cells"
|
|
18
20
|
import { DEFAULT_KEYMAP, matchKeymap } from "../core/keymap.js"
|
|
19
21
|
import { PANEL_FOCUS } from "../core/panel.js"
|
|
22
|
+
import { isNarrow } from "./Frame.js"
|
|
23
|
+
|
|
24
|
+
// Constant: DOUBLE_CLICK_MS
|
|
25
|
+
// Two clicks on the same listing row within this window count as a double
|
|
26
|
+
// click, which opens the row like Enter (see `createEvents#list:select`).
|
|
27
|
+
const DOUBLE_CLICK_MS = 400
|
|
20
28
|
|
|
21
29
|
// Function: createInit
|
|
22
30
|
// State initializer binding the root component to a `session`.
|
|
23
31
|
function createInit(session) {
|
|
24
32
|
const left = session.panes.left
|
|
25
33
|
const right = session.panes.right
|
|
34
|
+
const parent = session.panes.parent ?? {}
|
|
35
|
+
const preview = session.panes.preview ?? {}
|
|
26
36
|
// Pane-focus flags so the listing can render the cursor differently in the
|
|
27
37
|
// inactive pane. Shared by both engines.
|
|
28
38
|
const leftActive = derived([session.active], ([active]) => active === "left")
|
|
29
39
|
const rightActive = derived([session.active], ([active]) => active === "right")
|
|
40
|
+
const parentActive = derived([session.active], ([active]) => active === "parent")
|
|
41
|
+
const previewActive = derived([session.active], ([active]) => active === "preview")
|
|
30
42
|
return () => ({
|
|
31
43
|
active: session.active,
|
|
32
|
-
// The preview target entry, so the inspector can show a directory
|
|
33
|
-
// even when there is no text/card preview.
|
|
44
|
+
// The preview target entry, so the inspector can show a directory
|
|
45
|
+
// column even when there is no text/card preview.
|
|
34
46
|
appEntry: session.previewEntry,
|
|
35
47
|
appHost: session.host,
|
|
36
48
|
appPath: session.activePath,
|
|
37
49
|
bookmarkIndex: session.bookmarkIndex,
|
|
38
50
|
bookmarkItems: session.bookmarkItems,
|
|
39
|
-
bookmarkMode: session.bookmarkMode,
|
|
40
51
|
bookmarkOpen: session.bookmarkOpen,
|
|
41
52
|
bookmarkQuery: session.bookmarkQuery,
|
|
42
|
-
bookmarkTarget: session.bookmarkTarget,
|
|
43
53
|
commandBarItems: session.commandBarItems,
|
|
44
54
|
confirm: session.confirm,
|
|
55
|
+
currentId: session.currentId,
|
|
45
56
|
dispatch: session.dispatch,
|
|
46
57
|
filtering: session.filtering,
|
|
47
58
|
leftActive,
|
|
@@ -55,28 +66,53 @@ function createInit(session) {
|
|
|
55
66
|
leftLoading: left.loading,
|
|
56
67
|
leftMarked: left.marked,
|
|
57
68
|
leftSegments: left.segments,
|
|
69
|
+
leftSetFilter: (value) => left.setFilter(value),
|
|
58
70
|
notice: session.notice,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
paletteQuery: session.paletteQuery,
|
|
64
|
-
panelCommand: session.panelCommand,
|
|
71
|
+
// The shared list overlay (`u` history / `⇧T` trash).
|
|
72
|
+
overlay: session.overlay,
|
|
73
|
+
overlayIndex: session.overlayIndex,
|
|
74
|
+
overlayItems: session.overlayItems,
|
|
65
75
|
panelDraft: session.panelDraft,
|
|
66
76
|
panelCursorRow: session.panelCursorRow,
|
|
77
|
+
panelEntry: session.panelEntry,
|
|
67
78
|
panelFiltered: session.panelFiltered,
|
|
68
79
|
panelFocus: session.panelFocus,
|
|
69
80
|
panelOpIndex: session.panelOpIndex,
|
|
70
81
|
panelItems: session.panelItems,
|
|
71
82
|
panelOpen: session.panelOpen,
|
|
72
|
-
|
|
83
|
+
panelPermBit: session.panelPermBit,
|
|
84
|
+
panelPreview: session.panelPreview,
|
|
73
85
|
panelQuery: session.panelQuery,
|
|
74
86
|
panelRowIndex: session.panelRowIndex,
|
|
75
87
|
panelRows: session.panelRows,
|
|
76
|
-
panelRunnable: session.panelRunnable,
|
|
77
88
|
panelTextual: session.panelTextual,
|
|
78
89
|
panelValue: session.panelValue,
|
|
90
|
+
// The explorer's first column: the parent of the current directory.
|
|
91
|
+
parentActive,
|
|
92
|
+
parentCursor: parent.cursor,
|
|
93
|
+
parentDisk: parent.disk,
|
|
94
|
+
parentEntries: parent.entries,
|
|
95
|
+
parentEntry: session.parentCursorEntry,
|
|
96
|
+
parentError: parent.error,
|
|
97
|
+
parentFilter: parent.filter,
|
|
98
|
+
parentId: "parent",
|
|
99
|
+
parentLoading: parent.loading,
|
|
100
|
+
parentMarked: parent.marked,
|
|
101
|
+
parentSegments: parent.segments,
|
|
102
|
+
parentSetFilter: (value) => parent.setFilter(value),
|
|
79
103
|
preview: session.preview,
|
|
104
|
+
previewFull: session.previewFull,
|
|
105
|
+
// The full previews apply body scroll requests through this cell.
|
|
106
|
+
previewScroll: session.previewScroll,
|
|
107
|
+
// The third column: the directory the inspector previews, so it reads
|
|
108
|
+
// as a pane. Over a file the inspector stays a card and these idle.
|
|
109
|
+
previewActive,
|
|
110
|
+
previewCursor: preview.cursor,
|
|
111
|
+
previewEntries: preview.entries,
|
|
112
|
+
previewId: "preview",
|
|
113
|
+
previewLoading: preview.loading,
|
|
114
|
+
previewMarked: preview.marked,
|
|
115
|
+
previewSegments: preview.segments,
|
|
80
116
|
rightActive,
|
|
81
117
|
rightCursor: right.cursor,
|
|
82
118
|
rightDisk: right.disk,
|
|
@@ -88,52 +124,149 @@ function createInit(session) {
|
|
|
88
124
|
rightLoading: right.loading,
|
|
89
125
|
rightMarked: right.marked,
|
|
90
126
|
rightSegments: right.segments,
|
|
127
|
+
rightSetFilter: (value) => right.setFilter(value),
|
|
91
128
|
showPreview: session.showPreview,
|
|
129
|
+
viewMode: session.viewMode,
|
|
130
|
+
// The panel's file picker: its open mode, editable path, focus region
|
|
131
|
+
// and rows, so the panel can render the picker view.
|
|
132
|
+
pickerOpen: session.pickerOpen,
|
|
133
|
+
pickerPathText: session.pickerPathText,
|
|
134
|
+
pickerFocus: session.pickerFocus,
|
|
135
|
+
pickerRows: session.pickerRows,
|
|
136
|
+
pickerQuery: session.pickerQuery,
|
|
92
137
|
})
|
|
93
138
|
}
|
|
94
139
|
|
|
140
|
+
// Function: fieldOwnsKeys
|
|
141
|
+
// True when one of the panel's inputs owns the keyboard: the command filter
|
|
142
|
+
// (`PANEL_FOCUS.OPS`) or a textual argument's value field (`panelTextual`).
|
|
143
|
+
// While an input owns the keyboard the shared handler leaves text editing to
|
|
144
|
+
// it — typing, Backspace, Delete, caret movement. A plain argument row or an
|
|
145
|
+
// item row has no input pressed, so the handler handles the key itself.
|
|
146
|
+
function fieldOwnsKeys(data, focus) {
|
|
147
|
+
return focus === PANEL_FOCUS.OPS || Boolean(unwrap(data.panelTextual))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Function: isFilePreview
|
|
151
|
+
// True when the inspector shows a file — a text body or a metadata card — as
|
|
152
|
+
// opposed to a directory column, which keeps its listing cursor navigation.
|
|
153
|
+
function isFilePreview(data) {
|
|
154
|
+
const entry = unwrap(data.appEntry)
|
|
155
|
+
return Boolean(entry) && !entry.isDir
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Function: routePreviewKeys
|
|
159
|
+
// The arrows of a full preview: Up/Down scroll the body, Left/Right step to
|
|
160
|
+
// the previous/next entry (the cursor movement the preview already follows).
|
|
161
|
+
// Returns true when the key was handled, so the caller can stop routing.
|
|
162
|
+
function routePreviewKeys(data, key) {
|
|
163
|
+
if (!isFilePreview(data)) return false
|
|
164
|
+
const name = key?.name
|
|
165
|
+
if (name === "up" || name === "down") {
|
|
166
|
+
data.dispatch("preview.scroll", { delta: name === "up" ? -1 : 1 })
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
if (name === "left" || name === "right") {
|
|
170
|
+
data.dispatch(name === "left" ? "nav.up" : "nav.down")
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
95
176
|
// Function: createBehaviors
|
|
96
177
|
// Builds the App data/key behavior map for the given renderer environment.
|
|
97
178
|
// Published events are registered separately via `createEvents()` (`.sub`).
|
|
98
179
|
function createBehaviors(env) {
|
|
99
180
|
return {
|
|
100
|
-
// Global key handling:
|
|
101
|
-
// keymap.
|
|
181
|
+
// Global key handling: the fullscreen viewer modal first, then confirm
|
|
182
|
+
// dialogs, the command panel, and finally the keymap.
|
|
102
183
|
keys: (self, data, event) => {
|
|
103
184
|
const key = env.normalizeKey ? env.normalizeKey(event) : event
|
|
185
|
+
// The fullscreen inspector overlay is modal at any width: it opens
|
|
186
|
+
// from Enter on a file the host cannot edit (see `core/commands`)
|
|
187
|
+
// and from `v` below the inline breakpoint (below). While open it
|
|
188
|
+
// owns the keyboard — Esc or `v` closes it, ⇧O/⇧E act on the file,
|
|
189
|
+
// everything else is swallowed.
|
|
190
|
+
if (unwrap(data.viewerOpen)) {
|
|
191
|
+
if (key?.name === "escape" || (key?.name === "v" && !key.shift)) {
|
|
192
|
+
data.dispatch("view.toggleViewer")
|
|
193
|
+
} else if (key?.shift && key?.name === "o") {
|
|
194
|
+
data.dispatch("file.open", { paneId: unwrap(data.active) })
|
|
195
|
+
} else if (key?.shift && key?.name === "e") {
|
|
196
|
+
data.dispatch("file.edit", { paneId: unwrap(data.active) })
|
|
197
|
+
} else {
|
|
198
|
+
routePreviewKeys(data, key)
|
|
199
|
+
}
|
|
200
|
+
return
|
|
201
|
+
}
|
|
104
202
|
if (unwrap(data.confirm)) {
|
|
105
203
|
const name = key?.name
|
|
106
204
|
if (name === "y" || name === "return" || name === "enter") data.dispatch("confirm.yes")
|
|
107
205
|
else if (name === "n" || name === "escape") data.dispatch("confirm.no")
|
|
108
206
|
return
|
|
109
207
|
}
|
|
110
|
-
if (unwrap(data.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
208
|
+
if (unwrap(data.panelOpen)) {
|
|
209
|
+
// The picker is a sub-mode of the panel: its editable path owns
|
|
210
|
+
// typing, and the entry list owns navigation once focused.
|
|
211
|
+
if (unwrap(data.pickerOpen)) {
|
|
212
|
+
const field = unwrap(data.pickerFocus) === "rows" ? "rows" : "path"
|
|
213
|
+
if (key?.name === "escape") {
|
|
214
|
+
data.dispatch("picker.cancel")
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (key?.name === "tab") {
|
|
218
|
+
data.dispatch(field === "path" ? "picker.focusRows" : "picker.focusPath")
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if (field === "path") {
|
|
222
|
+
// Typing, Backspace and caret movement belong to the path
|
|
223
|
+
// input. Up/Down hand over to the entries, Enter commits
|
|
224
|
+
// the typed path.
|
|
225
|
+
if (key?.name === "up" || key?.name === "down") {
|
|
226
|
+
data.dispatch("picker.focusRows")
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
230
|
+
data.dispatch("picker.path")
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (key?.name === "up") {
|
|
236
|
+
data.dispatch("picker.up")
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (key?.name === "down") {
|
|
240
|
+
data.dispatch("picker.down")
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (key?.name === "left") {
|
|
244
|
+
data.dispatch("picker.focusPath")
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (key?.name === "right") {
|
|
248
|
+
data.dispatch("picker.descend")
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (key?.name === "backspace") {
|
|
252
|
+
// An empty path (the current directory) steps up; a
|
|
253
|
+
// typed fragment is edited in the path input instead.
|
|
254
|
+
if (`${unwrap(data.pickerQuery) ?? ""}` === "") data.dispatch("picker.parent")
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
258
|
+
data.dispatch(key.ctrl ? "picker.chooseDir" : "picker.enter")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
// Typing anywhere in the picker narrows the path.
|
|
262
|
+
const char = key?.name === "space" ? " " : key?.name
|
|
263
|
+
if (char && char.length === 1 && !key.ctrl && !key.meta) {
|
|
264
|
+
data.dispatch("picker.type", { char })
|
|
265
|
+
}
|
|
130
266
|
return
|
|
131
267
|
}
|
|
132
|
-
// Let the focused input handle typing.
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
if (unwrap(data.panelOpen)) {
|
|
136
268
|
const focus = unwrap(data.panelFocus)
|
|
269
|
+
const fieldOwns = fieldOwnsKeys(data, focus)
|
|
137
270
|
// Escape closes; Tab cycles the focus ring (command ⇄ args);
|
|
138
271
|
// arrows move within the focused column; Enter runs the command.
|
|
139
272
|
if (key?.name === "escape") {
|
|
@@ -152,29 +285,40 @@ function createBehaviors(env) {
|
|
|
152
285
|
data.dispatch(focus === PANEL_FOCUS.ARGS ? "panel.nextArg" : "panel.down")
|
|
153
286
|
return
|
|
154
287
|
}
|
|
288
|
+
if (key?.name === "o" && key.ctrl) {
|
|
289
|
+
// Browse a path/destination argument in the file picker.
|
|
290
|
+
data.dispatch("panel.browse")
|
|
291
|
+
return
|
|
292
|
+
}
|
|
155
293
|
if (key?.name === "return" || key?.name === "enter") {
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
// Enter runs the highlighted command with the current draft.
|
|
294
|
+
// Enter runs a complete command, or walks to the next
|
|
295
|
+
// missing field (see `panel.run`); the root routes it for
|
|
296
|
+
// every renderer, so no per-input `on:enter` is needed.
|
|
160
297
|
data.dispatch("panel.run")
|
|
161
298
|
return
|
|
162
299
|
}
|
|
163
|
-
//
|
|
164
|
-
|
|
300
|
+
// While an input owns the keyboard (the command filter, or a
|
|
301
|
+
// textual argument's value field) it also owns text editing:
|
|
302
|
+
// typing, Backspace, Delete and caret movement pass through to
|
|
303
|
+
// it. Intercepting Backspace here used to bounce the cursor
|
|
304
|
+
// back to the command filter mid-edit.
|
|
305
|
+
if (fieldOwns) return
|
|
306
|
+
// A focused permission row: Left/Right walk its r/w/x cells.
|
|
307
|
+
if ((key?.name === "left" || key?.name === "right") && focus === PANEL_FOCUS.ARGS) {
|
|
308
|
+
data.dispatch("panel.moveBit", { delta: key.name === "left" ? -1 : 1 })
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
// A focused non-textual argument row: Space toggles its
|
|
312
|
+
// selection, any other printable key jumps back to the command
|
|
313
|
+
// filter and appends the character; Backspace edits the filter.
|
|
314
|
+
if (key?.name === "space") {
|
|
165
315
|
data.dispatch("panel.toggleItem")
|
|
166
316
|
return
|
|
167
317
|
}
|
|
168
|
-
if (key?.name === "backspace"
|
|
169
|
-
// Editing the command filter from the argument column.
|
|
318
|
+
if (key?.name === "backspace") {
|
|
170
319
|
data.dispatch("panel.type", { backspace: true })
|
|
171
320
|
return
|
|
172
321
|
}
|
|
173
|
-
// While the command column is focused, or a textual argument
|
|
174
|
-
// owns the cursor, the focused input handles typing.
|
|
175
|
-
if (focus !== PANEL_FOCUS.ARGS || unwrap(data.panelTextual)) return
|
|
176
|
-
// Typing on a non-textual argument jumps back to the command
|
|
177
|
-
// filter and appends the character.
|
|
178
322
|
if (key?.name && key.name.length === 1 && !key.ctrl && !key.meta) {
|
|
179
323
|
data.dispatch("panel.type", { char: key.name })
|
|
180
324
|
return
|
|
@@ -182,20 +326,14 @@ function createBehaviors(env) {
|
|
|
182
326
|
return
|
|
183
327
|
}
|
|
184
328
|
if (unwrap(data.bookmarkOpen)) {
|
|
185
|
-
|
|
329
|
+
// One panel: the filter input owns typing, arrows move the
|
|
330
|
+
// list, Enter activates the row. Backspace edits the query while
|
|
331
|
+
// it has text and removes the selected bookmark once it is
|
|
332
|
+
// empty; Delete always removes.
|
|
186
333
|
if (key?.name === "escape") {
|
|
187
334
|
data.dispatch("bookmark.cancel")
|
|
188
335
|
return
|
|
189
336
|
}
|
|
190
|
-
if (mode === "name") {
|
|
191
|
-
// The input owns typing; Enter saves.
|
|
192
|
-
if (key?.name === "return" || key?.name === "enter") {
|
|
193
|
-
self.pub("bookmark:confirm")
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
// List mode: arrows move, Enter jumps.
|
|
199
337
|
if (key?.name === "up") {
|
|
200
338
|
data.dispatch("bookmark.up")
|
|
201
339
|
return
|
|
@@ -204,11 +342,42 @@ function createBehaviors(env) {
|
|
|
204
342
|
data.dispatch("bookmark.down")
|
|
205
343
|
return
|
|
206
344
|
}
|
|
345
|
+
if (key?.name === "delete") {
|
|
346
|
+
data.dispatch("bookmark.remove")
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
if (key?.name === "backspace" && !`${unwrap(data.bookmarkQuery) ?? ""}`.trim()) {
|
|
350
|
+
data.dispatch("bookmark.remove")
|
|
351
|
+
return
|
|
352
|
+
}
|
|
207
353
|
if (key?.name === "return" || key?.name === "enter") {
|
|
208
354
|
self.pub("bookmark:confirm")
|
|
209
355
|
return
|
|
210
356
|
}
|
|
211
|
-
// Let the focused input handle typing the
|
|
357
|
+
// Let the focused input handle typing the filter.
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
// The shared list overlay (history / trash) is modal: arrows move,
|
|
361
|
+
// Enter activates the row (jump in history, restore from trash), Esc
|
|
362
|
+
// closes. The key that opened it also closes it.
|
|
363
|
+
if (unwrap(data.overlay)) {
|
|
364
|
+
const mode = unwrap(data.overlay)
|
|
365
|
+
if (key?.name === "escape" || (key?.name === "u" && mode === "history") || (key?.name === "t" && key?.shift && mode === "trash")) {
|
|
366
|
+
data.dispatch("overlay.cancel")
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
if (key?.name === "up") {
|
|
370
|
+
data.dispatch("overlay.up")
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
if (key?.name === "down") {
|
|
374
|
+
data.dispatch("overlay.down")
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
378
|
+
data.dispatch("overlay.activate")
|
|
379
|
+
return
|
|
380
|
+
}
|
|
212
381
|
return
|
|
213
382
|
}
|
|
214
383
|
const active = unwrap(data.active)
|
|
@@ -244,21 +413,34 @@ function createBehaviors(env) {
|
|
|
244
413
|
// filter (or on Shift-Esc) the keymap decides — Esc steps back
|
|
245
414
|
// through the pane's directory history.
|
|
246
415
|
if (key?.name === "escape" && !key.shift) {
|
|
247
|
-
const
|
|
416
|
+
const filters = {
|
|
417
|
+
left: data.leftFilter,
|
|
418
|
+
parent: data.parentFilter,
|
|
419
|
+
right: data.rightFilter,
|
|
420
|
+
}
|
|
421
|
+
const filter = unwrap(filters[active])
|
|
248
422
|
if (filter) {
|
|
249
423
|
data.dispatch("filter.clear")
|
|
250
424
|
return
|
|
251
425
|
}
|
|
252
426
|
}
|
|
427
|
+
// The maximized inspector (`⇧V`) previews a file full-width: the
|
|
428
|
+
// arrows scroll and step through files instead of driving the pane
|
|
429
|
+
// underneath. A directory column keeps its listing navigation.
|
|
430
|
+
if (unwrap(data.previewFull) && routePreviewKeys(data, key)) return
|
|
431
|
+
// Below the inline breakpoint the viewer overlay replaces the side
|
|
432
|
+
// panel, so `v` opens it instead of toggling `showPreview` (⇧V
|
|
433
|
+
// still maximizes over the columns, via the keymap).
|
|
434
|
+
if (key?.name === "v" && !key?.shift && isNarrow(data, self)) {
|
|
435
|
+
data.dispatch("view.toggleViewer")
|
|
436
|
+
return
|
|
437
|
+
}
|
|
253
438
|
const id = matchKeymap(DEFAULT_KEYMAP, key)
|
|
254
439
|
if (!id) return
|
|
255
440
|
if (id === "app.quit") {
|
|
256
441
|
env.quit(self)
|
|
257
442
|
return
|
|
258
443
|
}
|
|
259
|
-
// The command panel is a TUI-only view; the web renderer omits the
|
|
260
|
-
// capability so `m` never opens an invisible modal there.
|
|
261
|
-
if (id === "panel.toggle" && !env.panel) return
|
|
262
444
|
data.dispatch(id, { paneId: unwrap(data.active) })
|
|
263
445
|
},
|
|
264
446
|
}
|
|
@@ -266,90 +448,133 @@ function createBehaviors(env) {
|
|
|
266
448
|
|
|
267
449
|
// Function: createEvents
|
|
268
450
|
// App event subscriptions, registered with `.sub(...)`. Handlers receive
|
|
269
|
-
// `(self, data, event)` with the published payload in `event.data`.
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
|
|
451
|
+
// `(self, data, event)` with the published payload in `event.data`.
|
|
452
|
+
function createEvents() {
|
|
453
|
+
// Last listing row clicked, for the double-click-to-open gesture: a second
|
|
454
|
+
// click on the same row within `DOUBLE_CLICK_MS` opens it like Enter.
|
|
455
|
+
let lastRowClick = { paneId: null, index: -1, at: 0 }
|
|
273
456
|
return {
|
|
274
|
-
"commandbar:run": (
|
|
275
|
-
|
|
276
|
-
|
|
457
|
+
"commandbar:run": (self, data, event) => {
|
|
458
|
+
// Below the inline breakpoint the bar's `[Preview v]` opens the
|
|
459
|
+
// viewer overlay, the same as the key (see `createBehaviors#keys`).
|
|
460
|
+
if (event.data === "view.togglePreview" && isNarrow(data, self)) {
|
|
461
|
+
data.dispatch("view.toggleViewer")
|
|
277
462
|
return
|
|
278
463
|
}
|
|
279
464
|
if (event.data) data.dispatch(event.data)
|
|
280
465
|
},
|
|
281
466
|
"panel:select": (_self, data, event) => {
|
|
282
|
-
// Clicking
|
|
283
|
-
// argument/item row focuses the
|
|
467
|
+
// Clicking a command row moves the cursor to it (a group header is
|
|
468
|
+
// inert); clicking an argument/item row focuses the right pane
|
|
469
|
+
// (toggling an item); clicking a picker row chooses or navigates it.
|
|
284
470
|
const items = unwrap(data.panelItems) ?? []
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
287
|
-
data.dispatch("panel.setOp", { index })
|
|
471
|
+
const item = items.find((row) => row.key === event.data)
|
|
472
|
+
if (item?.type === "op") {
|
|
473
|
+
data.dispatch("panel.setOp", { index: item.entryIndex })
|
|
288
474
|
return
|
|
289
475
|
}
|
|
476
|
+
if (item) return
|
|
290
477
|
const rows = unwrap(data.panelRows) ?? []
|
|
291
478
|
const rowIndex = rows.findIndex((row) => row.key === event.data)
|
|
292
479
|
if (rowIndex < 0) return
|
|
480
|
+
const row = rows[rowIndex]
|
|
481
|
+
if (row?.type === "entry" || row?.type === "parent") {
|
|
482
|
+
data.dispatch("panel.focusArg")
|
|
483
|
+
data.dispatch("picker.setCursor", { index: rowIndex })
|
|
484
|
+
data.dispatch(row.type === "parent" ? "picker.parent" : "picker.enter")
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
if (row?.type === "run") {
|
|
488
|
+
data.dispatch("panel.focusArg")
|
|
489
|
+
data.dispatch("panel.setRow", { index: rowIndex })
|
|
490
|
+
data.dispatch("panel.run")
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
// A read-only value (a single-path selection, resolved from the
|
|
494
|
+
// pane) is a path worth copying: clicking it sends the value to the
|
|
495
|
+
// host clipboard. Copy runs last so its confirmation is not cleared
|
|
496
|
+
// by the focus dispatches (each dispatch resets the notice).
|
|
293
497
|
data.dispatch("panel.focusArg")
|
|
294
498
|
data.dispatch("panel.setRow", { index: rowIndex })
|
|
295
|
-
const row = rows[rowIndex]
|
|
296
499
|
if (row?.type === "item") data.dispatch("panel.toggleItem")
|
|
500
|
+
if (row?.type === "arg" && row.readonly === true && row.summary) {
|
|
501
|
+
data.dispatch("panel.copy", { text: row.summary })
|
|
502
|
+
}
|
|
297
503
|
},
|
|
298
504
|
"panel:confirm": (_self, data) => {
|
|
505
|
+
// Published by a focused panel input's `on:enter`; the key handler
|
|
506
|
+
// leaves Enter to it (see `fieldOwnsKeys`).
|
|
299
507
|
if (!unwrap(data.panelOpen)) return
|
|
508
|
+
if (unwrap(data.pickerOpen)) {
|
|
509
|
+
data.dispatch("picker.enter")
|
|
510
|
+
return
|
|
511
|
+
}
|
|
300
512
|
data.dispatch("panel.run")
|
|
301
513
|
},
|
|
514
|
+
"panel:perm": (_self, data, event) => {
|
|
515
|
+
// A click on one r/w/x cell: focus its class row, then toggle the
|
|
516
|
+
// bit. The payload is `argIndex:itemIndex:rowIndex` (see
|
|
517
|
+
// `core/panel#panelRowList`).
|
|
518
|
+
const [argIndex, itemIndex, rowIndex] = `${event.data ?? ""}`.split(":").map(Number)
|
|
519
|
+
if (![argIndex, itemIndex, rowIndex].every(Number.isInteger)) return
|
|
520
|
+
data.dispatch("panel.focusArg")
|
|
521
|
+
data.dispatch("panel.setRow", { index: rowIndex })
|
|
522
|
+
data.dispatch("panel.togglePerm", { argIndex, itemIndex })
|
|
523
|
+
},
|
|
524
|
+
"panel:close": (_self, data) => {
|
|
525
|
+
// The dialog's upper-right close shorthand: back out of the picker
|
|
526
|
+
// sub-mode first, otherwise close the panel.
|
|
527
|
+
if (!unwrap(data.panelOpen)) return
|
|
528
|
+
data.dispatch(unwrap(data.pickerOpen) ? "picker.cancel" : "panel.cancel")
|
|
529
|
+
},
|
|
302
530
|
"crumbs:go": (_self, data, event) => {
|
|
303
531
|
// The "…" truncation marker carries no path.
|
|
304
532
|
if (!event.data?.path) return
|
|
305
533
|
data.dispatch("pane.setPath", event.data)
|
|
306
534
|
},
|
|
307
|
-
"list:select": (_self, data, event) =>
|
|
535
|
+
"list:select": (_self, data, event) => {
|
|
536
|
+
const paneId = event.data?.paneId
|
|
537
|
+
const index = event.data?.index
|
|
538
|
+
const now = Date.now()
|
|
539
|
+
// A click selects (focus + cursor); a second click on the same row
|
|
540
|
+
// within the window opens it like Enter. `list.cursor` activates the
|
|
541
|
+
// pane synchronously, so `pane.enter` then acts on the clicked pane.
|
|
542
|
+
const isDouble =
|
|
543
|
+
paneId === lastRowClick.paneId &&
|
|
544
|
+
index === lastRowClick.index &&
|
|
545
|
+
now - lastRowClick.at <= DOUBLE_CLICK_MS
|
|
546
|
+
lastRowClick = { paneId, index, at: isDouble ? 0 : now }
|
|
547
|
+
data.dispatch("list.cursor", event.data)
|
|
548
|
+
if (isDouble) data.dispatch("pane.enter", { paneId })
|
|
549
|
+
},
|
|
308
550
|
"pane:activate": (_self, data, event) => data.dispatch("pane.activate", { paneId: event.data }),
|
|
551
|
+
// Clicking the identity bar's view-mode word flips explorer/commander.
|
|
552
|
+
"view:toggleMode": (_self, data) => data.dispatch("view.toggleMode"),
|
|
309
553
|
"bookmark:select": (_self, data, event) => {
|
|
310
|
-
// Clicking a bookmark row selects and
|
|
554
|
+
// Clicking a bookmark (or the add) row selects and activates it.
|
|
311
555
|
if (!unwrap(data.bookmarkOpen)) return
|
|
312
556
|
const items = unwrap(data.bookmarkItems) ?? []
|
|
313
|
-
const index = items.findIndex((row) => row.
|
|
557
|
+
const index = items.findIndex((row) => row.key === event.data)
|
|
314
558
|
if (index < 0) return
|
|
315
559
|
data.dispatch("bookmark.setIndex", { index })
|
|
316
560
|
data.dispatch("bookmark.select")
|
|
317
561
|
},
|
|
318
562
|
"bookmark:confirm": (_self, data) => {
|
|
319
563
|
if (!unwrap(data.bookmarkOpen)) return
|
|
320
|
-
|
|
321
|
-
else data.dispatch("bookmark.select")
|
|
322
|
-
},
|
|
323
|
-
"palette:confirm": (_self, data, event) => {
|
|
324
|
-
// The global key handler and the input's on:enter can both fire;
|
|
325
|
-
// only the first one (while the palette is open) acts.
|
|
326
|
-
if (!unwrap(data.paletteOpen)) return
|
|
327
|
-
if (unwrap(data.paletteMode) === "script") {
|
|
328
|
-
// Submit the typed line: run it, or complete the highlighted row
|
|
329
|
-
// when the input is empty.
|
|
330
|
-
data.dispatch("palette.submit")
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
const items = unwrap(data.paletteItems) ?? []
|
|
334
|
-
const index = Math.max(0, Math.min(unwrap(data.paletteIndex) ?? 0, items.length - 1))
|
|
335
|
-
const match = items[index] ?? items[0]
|
|
336
|
-
if (match?.id) data.dispatch(match.id)
|
|
337
|
-
data.dispatch("palette.toggle")
|
|
564
|
+
data.dispatch("bookmark.select")
|
|
338
565
|
},
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
if (unwrap(data.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
data.dispatch(event.data)
|
|
348
|
-
data.dispatch("palette.toggle")
|
|
566
|
+
"overlay:select": (_self, data, event) => {
|
|
567
|
+
// Clicking an overlay row moves the cursor to it and activates it.
|
|
568
|
+
if (!unwrap(data.overlay)) return
|
|
569
|
+
const rows = unwrap(data.overlayItems) ?? []
|
|
570
|
+
const index = rows.findIndex((row) => row.key === event.data)
|
|
571
|
+
if (index < 0) return
|
|
572
|
+
data.dispatch("overlay.setIndex", { index })
|
|
573
|
+
data.dispatch("overlay.activate")
|
|
349
574
|
},
|
|
350
575
|
}
|
|
351
576
|
}
|
|
352
577
|
|
|
353
|
-
export { createBehaviors, createEvents, createInit }
|
|
578
|
+
export { createBehaviors, createEvents, createInit, DOUBLE_CLICK_MS }
|
|
354
579
|
|
|
355
580
|
// EOF
|