@ssebastien/tcmd 0.1.4 → 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 +169 -58
- 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 +17 -5
- package/public/fonts/OFL.txt +93 -0
- package/public/licenses/LUCIDE.txt +43 -0
- package/public/main.js +5066 -1888
- package/public/style.css +660 -46
- package/scripts/build-desktop.js +31 -0
- package/src/js/tcmd/components/App.html +56 -6
- package/src/js/tcmd/components/App.js +233 -58
- package/src/js/tcmd/components/App.xml +27 -22
- package/src/js/tcmd/components/BookmarkList.html +4 -5
- package/src/js/tcmd/components/BookmarkList.js +14 -40
- 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/CommandArgRow.html +18 -7
- package/src/js/tcmd/components/CommandArgRow.js +100 -44
- package/src/js/tcmd/components/CommandArgRow.xml +39 -15
- package/src/js/tcmd/components/CommandBar.html +1 -1
- package/src/js/tcmd/components/CommandBar.js +31 -25
- package/src/js/tcmd/components/CommandBar.xml +2 -2
- package/src/js/tcmd/components/CommandItem.js +24 -25
- package/src/js/tcmd/components/CommandItem.xml +2 -2
- package/src/js/tcmd/components/CommandPanel.html +13 -13
- package/src/js/tcmd/components/CommandPanel.js +57 -33
- package/src/js/tcmd/components/CommandPanel.xml +30 -20
- package/src/js/tcmd/components/Confirm.html +2 -2
- package/src/js/tcmd/components/EntryDetail.js +21 -7
- 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 +1 -2
- package/src/js/tcmd/components/FilePreview.js +40 -4
- package/src/js/tcmd/components/FilePreview.xml +17 -10
- package/src/js/tcmd/components/FileRow.html +1 -0
- package/src/js/tcmd/components/FileRow.js +60 -9
- package/src/js/tcmd/components/Frame.js +57 -31
- package/src/js/tcmd/components/Help.html +12 -4
- package/src/js/tcmd/components/Help.js +198 -47
- 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/OverlayList.xml +13 -0
- 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/format.js +4 -23
- package/src/js/tcmd/components/icons.js +137 -0
- package/src/js/tcmd/components/index.js +8 -3
- package/src/js/tcmd/components/rows.js +149 -25
- 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 +612 -136
- 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 +81 -5
- package/src/js/tcmd/core/format.js +36 -0
- package/src/js/tcmd/core/fs.js +410 -53
- package/src/js/tcmd/core/journal.js +93 -0
- package/src/js/tcmd/core/keymap.js +39 -3
- package/src/js/tcmd/core/operations.js +735 -63
- package/src/js/tcmd/core/os.js +29 -3
- package/src/js/tcmd/core/pane.js +4 -2
- package/src/js/tcmd/core/panel.js +179 -93
- 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/session.js +286 -116
- 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/tui/app.js +111 -74
- package/src/js/tcmd/tui/catalogue.js +232 -23
- package/src/js/tcmd/tui/index.js +18 -4
- 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 +27 -2
- package/src/js/tcmd/vendor/bzip2.js +276 -0
- package/src/js/tcmd/web/app.js +114 -56
- package/src/js/tcmd/web/catalogue.js +41 -4
- package/src/js/tcmd/web/effects.js +94 -14
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/http.js +13 -0
- package/src/js/tcmd/web/index.js +2 -2
- package/src/js/tcmd/web/opfs.js +297 -30
- package/src/js/tcmd/web/seed.js +1 -1
- package/src/js/tcmd/web/server.js +118 -17
- package/src/js/tcmd/web/theme.js +3 -0
- package/tinyjs.json +16 -0
|
@@ -179,6 +179,7 @@ function extensionOf(name) {
|
|
|
179
179
|
// file icon.
|
|
180
180
|
function iconFor(entry) {
|
|
181
181
|
if (!entry) return ""
|
|
182
|
+
if (entry.add) return entry.isDir ? DIR_ICON : FILE_ICON
|
|
182
183
|
if (entry.isSymlink) return SYMLINK_ICON
|
|
183
184
|
if (entry.isDevice) return DEVICE_ICON
|
|
184
185
|
if (entry.isDir) return DIR_ICON
|
|
@@ -187,6 +188,141 @@ function iconFor(entry) {
|
|
|
187
188
|
return NAME_ICONS[plain] ?? EXTENSION_ICONS[extensionOf(name)] ?? FILE_ICON
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// Function: lucideIconFor
|
|
192
|
+
// Lucide's outline icons are used by the web renderers, where SVG works without
|
|
193
|
+
// a patched terminal font. The TUI keeps the detailed Nerd Font mapping above;
|
|
194
|
+
// the web uses a smaller set of semantic file-kind icons.
|
|
195
|
+
const LUCIDE_NAME_ICONS = {
|
|
196
|
+
cmakelists: "file-code",
|
|
197
|
+
dockerfile: "file-code",
|
|
198
|
+
env: "settings",
|
|
199
|
+
gitattributes: "settings",
|
|
200
|
+
gitconfig: "settings",
|
|
201
|
+
gitignore: "settings",
|
|
202
|
+
gitmodules: "settings",
|
|
203
|
+
gnumakefile: "file-code",
|
|
204
|
+
license: "file-text",
|
|
205
|
+
licence: "file-text",
|
|
206
|
+
makefile: "file-code",
|
|
207
|
+
npmrc: "settings",
|
|
208
|
+
readme: "file-text",
|
|
209
|
+
}
|
|
210
|
+
const LUCIDE_EXTENSION_ICONS = {
|
|
211
|
+
"7z": "file-archive",
|
|
212
|
+
aac: "audio-lines",
|
|
213
|
+
avi: "video",
|
|
214
|
+
bmp: "file-image",
|
|
215
|
+
bz2: "file-archive",
|
|
216
|
+
c: "file-code",
|
|
217
|
+
cc: "file-code",
|
|
218
|
+
cjs: "file-code",
|
|
219
|
+
clj: "file-code",
|
|
220
|
+
cmake: "file-code",
|
|
221
|
+
csv: "file-text",
|
|
222
|
+
cfg: "settings",
|
|
223
|
+
conf: "settings",
|
|
224
|
+
cpp: "file-code",
|
|
225
|
+
cs: "file-code",
|
|
226
|
+
css: "file-code",
|
|
227
|
+
d: "file-code",
|
|
228
|
+
db: "database",
|
|
229
|
+
diff: "file-code",
|
|
230
|
+
ex: "file-code",
|
|
231
|
+
exs: "file-code",
|
|
232
|
+
env: "settings",
|
|
233
|
+
flac: "audio-lines",
|
|
234
|
+
gif: "file-image",
|
|
235
|
+
go: "file-code",
|
|
236
|
+
gql: "file-code",
|
|
237
|
+
graphql: "file-code",
|
|
238
|
+
gz: "file-archive",
|
|
239
|
+
h: "file-code",
|
|
240
|
+
hbs: "file-code",
|
|
241
|
+
hs: "file-code",
|
|
242
|
+
hpp: "file-code",
|
|
243
|
+
htm: "file-code",
|
|
244
|
+
html: "file-code",
|
|
245
|
+
ini: "settings",
|
|
246
|
+
java: "file-code",
|
|
247
|
+
jar: "file-archive",
|
|
248
|
+
jpeg: "file-image",
|
|
249
|
+
jpg: "file-image",
|
|
250
|
+
js: "file-code",
|
|
251
|
+
jsx: "file-code",
|
|
252
|
+
json: "file-code",
|
|
253
|
+
kt: "file-code",
|
|
254
|
+
kts: "file-code",
|
|
255
|
+
less: "file-code",
|
|
256
|
+
lock: "settings",
|
|
257
|
+
log: "file-text",
|
|
258
|
+
lua: "file-code",
|
|
259
|
+
m4a: "audio-lines",
|
|
260
|
+
md: "file-text",
|
|
261
|
+
markdown: "file-text",
|
|
262
|
+
mk: "file-code",
|
|
263
|
+
mkv: "video",
|
|
264
|
+
mov: "video",
|
|
265
|
+
mp3: "audio-lines",
|
|
266
|
+
mp4: "video",
|
|
267
|
+
mjs: "file-code",
|
|
268
|
+
nim: "file-code",
|
|
269
|
+
ogg: "audio-lines",
|
|
270
|
+
pdf: "file-text",
|
|
271
|
+
perl: "file-code",
|
|
272
|
+
patch: "file-code",
|
|
273
|
+
php: "file-code",
|
|
274
|
+
pl: "file-code",
|
|
275
|
+
png: "file-image",
|
|
276
|
+
ps1: "file-code",
|
|
277
|
+
py: "file-code",
|
|
278
|
+
r: "file-code",
|
|
279
|
+
rb: "file-code",
|
|
280
|
+
rar: "file-archive",
|
|
281
|
+
rs: "file-code",
|
|
282
|
+
sass: "file-code",
|
|
283
|
+
scala: "file-code",
|
|
284
|
+
scss: "file-code",
|
|
285
|
+
sh: "file-code",
|
|
286
|
+
sql: "file-code",
|
|
287
|
+
sqlite: "database",
|
|
288
|
+
sqlite3: "database",
|
|
289
|
+
svg: "file-image",
|
|
290
|
+
swift: "file-code",
|
|
291
|
+
tar: "file-archive",
|
|
292
|
+
text: "file-text",
|
|
293
|
+
toml: "settings",
|
|
294
|
+
ts: "file-code",
|
|
295
|
+
tsv: "file-text",
|
|
296
|
+
txt: "file-text",
|
|
297
|
+
tsx: "file-code",
|
|
298
|
+
vim: "file-code",
|
|
299
|
+
vue: "file-code",
|
|
300
|
+
wav: "audio-lines",
|
|
301
|
+
webm: "video",
|
|
302
|
+
webp: "file-image",
|
|
303
|
+
xml: "file-code",
|
|
304
|
+
xz: "file-archive",
|
|
305
|
+
yaml: "file-code",
|
|
306
|
+
yml: "file-code",
|
|
307
|
+
zip: "file-archive",
|
|
308
|
+
zsh: "file-code",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function lucideIconFor(entry) {
|
|
312
|
+
if (!entry) return ""
|
|
313
|
+
if (entry.add) return "folder-plus"
|
|
314
|
+
if (entry.isSymlink) return "link-2"
|
|
315
|
+
if (entry.isDevice) return "cpu"
|
|
316
|
+
if (entry.isDir) return "folder"
|
|
317
|
+
|
|
318
|
+
const name = `${entry.name ?? ""}`
|
|
319
|
+
const basename = name.slice(name.lastIndexOf("/") + 1)
|
|
320
|
+
const extension = extensionOf(basename)
|
|
321
|
+
const stem = extension ? basename.slice(0, -(extension.length + 1)) : basename
|
|
322
|
+
const plain = (basename.startsWith(".") ? basename.slice(1) : basename).toLowerCase()
|
|
323
|
+
return LUCIDE_NAME_ICONS[plain] ?? LUCIDE_NAME_ICONS[stem.toLowerCase()] ?? LUCIDE_EXTENSION_ICONS[extension] ?? "file"
|
|
324
|
+
}
|
|
325
|
+
|
|
190
326
|
// Function: iconsEnabled
|
|
191
327
|
// Whether rows show icons: on by default, `TCMD_ICONS=0|false|off` opts out
|
|
192
328
|
// for terminals without a Nerd Font.
|
|
@@ -213,6 +349,7 @@ export {
|
|
|
213
349
|
extensionOf,
|
|
214
350
|
iconFor,
|
|
215
351
|
iconsEnabled,
|
|
352
|
+
lucideIconFor,
|
|
216
353
|
sourceMarker,
|
|
217
354
|
}
|
|
218
355
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
// Binding
|
|
13
13
|
export { bind } from "./bind.js"
|
|
14
|
-
export { commandBarMarks, commandBarMarksItem,
|
|
14
|
+
export { commandBarFit, commandBarMarks, commandBarMarksItem, commandBarStatus } from "./CommandBar.js"
|
|
15
15
|
|
|
16
16
|
// Behaviors
|
|
17
17
|
export { createBehaviors as appBehaviors, createEvents as appEvents, createInit as appInit } from "./App.js"
|
|
@@ -26,15 +26,20 @@ export { createBehaviors as confirmBehaviors } from "./Confirm.js"
|
|
|
26
26
|
export { createBehaviors as crumbBehaviors } from "./Crumb.js"
|
|
27
27
|
export { createBehaviors as entryDetailBehaviors } from "./EntryDetail.js"
|
|
28
28
|
export { createBehaviors as fileListBehaviors } from "./FileList.js"
|
|
29
|
+
export { createBehaviors as filePickerBehaviors } from "./FilePicker.js"
|
|
30
|
+
export { createBehaviors as filePickerRowBehaviors } from "./FilePickerRow.js"
|
|
29
31
|
export { createBehaviors as filePreviewBehaviors } from "./FilePreview.js"
|
|
30
32
|
export { createBehaviors as fileRowBehaviors } from "./FileRow.js"
|
|
31
33
|
export { createBehaviors as filterBarBehaviors } from "./FilterBar.js"
|
|
32
34
|
export { createBehaviors as helpBehaviors } from "./Help.js"
|
|
35
|
+
export { createRowBehaviors as helpRowBehaviors } from "./Help.js"
|
|
36
|
+
export { createBehaviors as overlayListBehaviors } from "./OverlayList.js"
|
|
37
|
+
export { createBehaviors as overlayRowBehaviors } from "./OverlayRow.js"
|
|
33
38
|
export { createBehaviors as summaryBehaviors } from "./Summary.js"
|
|
34
39
|
|
|
35
40
|
// Helpers
|
|
36
41
|
export { formatCounts, formatDisk, formatMode, formatSize, formatTime } from "./format.js"
|
|
37
|
-
export { iconFor, iconsEnabled, sourceMarker } from "./icons.js"
|
|
38
|
-
export { bookmarkRows, commandItemRows, crumbRows, fileRows, fileWindow, panelArgRows, panelOpRows } from "./rows.js"
|
|
42
|
+
export { iconFor, iconsEnabled, lucideIconFor, sourceMarker } from "./icons.js"
|
|
43
|
+
export { bookmarkRows, commandItemRows, crumbRows, filePickerRows, fileRows, fileWindow, helpRows, listingWindow, overlayRows, panelArgRows, panelOpRows, panelRunRows } from "./rows.js"
|
|
39
44
|
|
|
40
45
|
// EOF
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// `(self, data) → Component.map(…)`: it turns a parent's data bag into the
|
|
9
9
|
// `{ item, index, … }` bags its row components receive, so the bag shape is
|
|
10
10
|
// defined once and both renderers stay in sync. `self` is the parent list
|
|
11
|
-
// instance
|
|
12
|
-
// height when no viewport is threaded down.
|
|
11
|
+
// instance, kept in the signature for the shared `(self, data)` contract.
|
|
13
12
|
//
|
|
14
13
|
// Row state (cursor, marks, selection) is snapshotted into plain per-row
|
|
15
14
|
// booleans (`currentRow`, `markedRow`, `selectedRow`) instead of broadcasting
|
|
@@ -27,6 +26,7 @@
|
|
|
27
26
|
// (`CommandArgRow.wantsFocus`), which the builders do not know.
|
|
28
27
|
|
|
29
28
|
import { unwrap } from "select/state/cells"
|
|
29
|
+
import { helpBlocks } from "./Help.js"
|
|
30
30
|
|
|
31
31
|
// Function: isCell
|
|
32
32
|
// True when `value` is a reactive cell, using the same `isReactive` marker the
|
|
@@ -49,6 +49,15 @@ function isSelected(data) {
|
|
|
49
49
|
// list, so a scroll step or a cursor nudge does not first expose a blank row.
|
|
50
50
|
const ROW_WINDOW_MARGIN = 8
|
|
51
51
|
|
|
52
|
+
// Constant: UNWINDOWED_ROWS
|
|
53
|
+
// How many rows a list without a threaded viewport (the inspector's directory
|
|
54
|
+
// column, the web) mounts before it windows around the cursor. Such a list
|
|
55
|
+
// renders its *whole* listing so its scrollbox can scroll all of it, but the
|
|
56
|
+
// terminal renderer cannot create unbounded rows — a many-thousand-row
|
|
57
|
+
// directory fails to render — so the mount is bounded here. The bound sits
|
|
58
|
+
// well under that limit and leaves normal directories fully mounted.
|
|
59
|
+
const UNWINDOWED_ROWS = 500
|
|
60
|
+
|
|
52
61
|
// Function: fileWindow
|
|
53
62
|
// The windowed slice of a file listing: only `[start, start+len)` rows are
|
|
54
63
|
// mounted around the cursor, so a large directory does not lay out thousands of
|
|
@@ -71,6 +80,26 @@ function fileWindow(data, viewport) {
|
|
|
71
80
|
return { start, len, local: cursor - start, total }
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
// Function: listingWindow
|
|
84
|
+
// The rows a `FileList` mounts, shared by the row builder and the scroll
|
|
85
|
+
// keeper so they always agree on the window. A threaded viewport (the panes)
|
|
86
|
+
// windows around the cursor with a margin. Without one (the inspector's
|
|
87
|
+
// directory column) the whole listing is mounted — so its scrollbox scrolls
|
|
88
|
+
// all of it — up to `UNWINDOWED_ROWS`; beyond that the same cursor-anchored
|
|
89
|
+
// window keeps the mount bounded while the cursor still reaches every row.
|
|
90
|
+
function listingWindow(data, viewport) {
|
|
91
|
+
const vp = Math.floor(viewport ?? 0)
|
|
92
|
+
if (vp > 0) return fileWindow(data, vp)
|
|
93
|
+
const entries = unwrap(data.entries) ?? []
|
|
94
|
+
const total = entries.length
|
|
95
|
+
const cursor = isCell(data.cursor) ? unwrap(data.cursor) : (data.cursor ?? 0)
|
|
96
|
+
if (total <= UNWINDOWED_ROWS) return { start: 0, len: total, local: cursor, total }
|
|
97
|
+
const maxStart = Math.max(0, total - UNWINDOWED_ROWS)
|
|
98
|
+
const start = Math.max(0, Math.min(cursor - Math.floor(UNWINDOWED_ROWS / 2), maxStart))
|
|
99
|
+
const len = Math.min(UNWINDOWED_ROWS, total - start)
|
|
100
|
+
return { start, len, local: cursor - start, total }
|
|
101
|
+
}
|
|
102
|
+
|
|
74
103
|
// Function: crumbRows
|
|
75
104
|
// `{ item: segment, index, last, paneId }` bags for `Crumb` rows; `last`
|
|
76
105
|
// marks the basename, which carries no separator.
|
|
@@ -99,11 +128,11 @@ function fileRows(FileRow) {
|
|
|
99
128
|
const entries = unwrap(data.entries) ?? []
|
|
100
129
|
const cursor = isCell(data.cursor) ? unwrap(data.cursor) : data.cursor
|
|
101
130
|
const marked = isCell(data.marked) ? unwrap(data.marked) : data.marked
|
|
102
|
-
// The
|
|
103
|
-
//
|
|
104
|
-
// scrollbox
|
|
105
|
-
|
|
106
|
-
const { start, len } =
|
|
131
|
+
// The window is shared with `fileListScrollTop` (see `listingWindow`).
|
|
132
|
+
// The panes window around their cursor; the inspector mounts its whole
|
|
133
|
+
// listing so its scrollbox can scroll all of it, bounded for the
|
|
134
|
+
// terminal renderer.
|
|
135
|
+
const { start, len } = listingWindow(data, unwrap(data.viewport) ?? 0)
|
|
107
136
|
const bag = (item, index) => ({
|
|
108
137
|
item,
|
|
109
138
|
index,
|
|
@@ -132,41 +161,70 @@ function commandItemRows(CommandItem) {
|
|
|
132
161
|
|
|
133
162
|
// Function: panelOpRows
|
|
134
163
|
// `{ item, index, row, cursor, focus }` bags for the command panel's command
|
|
135
|
-
// list (`CommandArgRow`).
|
|
136
|
-
// operation
|
|
137
|
-
//
|
|
138
|
-
// destructive/caution operations.
|
|
164
|
+
// list (`CommandArgRow`). `panelEntryRows` contributes group headings and
|
|
165
|
+
// name-only operation rows. `entryIndex` maps operations to the command cursor;
|
|
166
|
+
// group headings take no cursor. `risk` tints destructive/caution operations.
|
|
139
167
|
function panelOpRows(CommandArgRow) {
|
|
140
168
|
return (self, data) =>
|
|
141
169
|
CommandArgRow.map(
|
|
142
170
|
data.items ?? [],
|
|
143
171
|
(item, index) => ({
|
|
144
172
|
item,
|
|
145
|
-
index: item.entryIndex ?? index,
|
|
173
|
+
index: item.type === "op" ? item.entryIndex ?? index : -1,
|
|
146
174
|
row: {
|
|
147
|
-
type:
|
|
148
|
-
key: item.
|
|
175
|
+
type: item.type,
|
|
176
|
+
key: item.key,
|
|
149
177
|
name: item.name,
|
|
150
|
-
|
|
178
|
+
group: item.group,
|
|
151
179
|
risk: item.risk ?? null,
|
|
152
180
|
},
|
|
153
181
|
cursor: data.opCursor,
|
|
154
182
|
focus: data.focus,
|
|
155
183
|
}),
|
|
156
|
-
(item) => item.
|
|
184
|
+
(item) => item.key,
|
|
157
185
|
)
|
|
158
186
|
}
|
|
159
187
|
|
|
188
|
+
// Function: filePickerRows
|
|
189
|
+
// `{ item, index, key, currentRow, markedRow, active }` bags for the file
|
|
190
|
+
// picker's `FilePickerRow`s. The picker rows are already flat (parent row plus
|
|
191
|
+
// entries); the same bounded window as an unwindowed listing keeps a huge
|
|
192
|
+
// directory from mounting every row in the terminal.
|
|
193
|
+
function filePickerRows(FilePickerRow) {
|
|
194
|
+
return (self, data) => {
|
|
195
|
+
const rows = unwrap(data.rows) ?? []
|
|
196
|
+
const cursor = isCell(data.cursor) ? unwrap(data.cursor) : (data.cursor ?? 0)
|
|
197
|
+
const { start, len } = listingWindow({ entries: rows, cursor }, 0)
|
|
198
|
+
const bag = (item, index) => ({
|
|
199
|
+
item,
|
|
200
|
+
index,
|
|
201
|
+
key: item.key ?? item.path,
|
|
202
|
+
currentRow: index === cursor,
|
|
203
|
+
markedRow: false,
|
|
204
|
+
active: true,
|
|
205
|
+
paneId: null,
|
|
206
|
+
})
|
|
207
|
+
if (start === 0 && len === rows.length) {
|
|
208
|
+
return FilePickerRow.map(rows, bag, (item) => item.key ?? item.path)
|
|
209
|
+
}
|
|
210
|
+
return FilePickerRow.map(
|
|
211
|
+
rows.slice(start, start + len),
|
|
212
|
+
(item, index) => bag(item, start + index),
|
|
213
|
+
(item) => item.key ?? item.path,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
160
218
|
// Function: panelArgRows
|
|
161
|
-
// `{ item, index, row, cursor, focus, value
|
|
162
|
-
// panel's argument pane (`CommandArgRow`). The rows are the flat
|
|
219
|
+
// `{ item, index, row, cursor, focus, value }` bags for the command
|
|
220
|
+
// panel's scrolling argument pane (`CommandArgRow`). The rows are the flat
|
|
163
221
|
// `core/panel.panelRowList`; this wraps each selection's item run in a top and
|
|
164
222
|
// bottom `frame` row so the list reads as one bordered box. `index` is the
|
|
165
223
|
// row's flat position (the frame rows take none), so the pane cursor still
|
|
166
224
|
// addresses the same rows.
|
|
167
225
|
function panelArgRows(CommandArgRow) {
|
|
168
226
|
return (self, data) => {
|
|
169
|
-
const rows = unwrap(data.rows) ?? []
|
|
227
|
+
const rows = (unwrap(data.rows) ?? []).filter((row) => row.type !== "run")
|
|
170
228
|
const visual = []
|
|
171
229
|
for (let i = 0; i < rows.length; i++) {
|
|
172
230
|
const row = rows[i]
|
|
@@ -188,29 +246,95 @@ function panelArgRows(CommandArgRow) {
|
|
|
188
246
|
row: item,
|
|
189
247
|
cursor: data.argCursor,
|
|
190
248
|
focus: data.focus,
|
|
249
|
+
permBit: data.permBit,
|
|
191
250
|
value: data.value,
|
|
192
|
-
labelWidth: unwrap(data.labelWidth) ?? 8,
|
|
193
251
|
}),
|
|
194
252
|
(item) => item.key,
|
|
195
253
|
)
|
|
196
254
|
}
|
|
197
255
|
}
|
|
198
256
|
|
|
257
|
+
// Function: panelRunRows
|
|
258
|
+
// The explicit Run/Retry action is outside the scrolling argument list, while
|
|
259
|
+
// sharing its cursor/focus state and row component.
|
|
260
|
+
function panelRunRows(CommandArgRow) {
|
|
261
|
+
return (self, data) => {
|
|
262
|
+
const rows = (unwrap(data.rows) ?? []).filter((row) => row.type === "run")
|
|
263
|
+
return CommandArgRow.map(
|
|
264
|
+
rows,
|
|
265
|
+
(item, index) => ({
|
|
266
|
+
item,
|
|
267
|
+
index: item.rowIndex ?? index,
|
|
268
|
+
row: item,
|
|
269
|
+
cursor: data.argCursor,
|
|
270
|
+
focus: data.focus,
|
|
271
|
+
value: data.value,
|
|
272
|
+
}),
|
|
273
|
+
(item) => item.key,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Function: helpRows
|
|
279
|
+
// `{ item, index }` bags for the `?` reference's `HelpRow`s. The blocks are
|
|
280
|
+
// precomputed and balanced in `Help.helpBlocks`; `side` picks the column.
|
|
281
|
+
function helpRows(HelpRow) {
|
|
282
|
+
return (self, data, side) => {
|
|
283
|
+
const blocks = helpBlocks()[side] ?? []
|
|
284
|
+
return HelpRow.map(
|
|
285
|
+
blocks,
|
|
286
|
+
(item, index) => ({ item, index }),
|
|
287
|
+
(item) => item.key,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
199
292
|
// Function: bookmarkRows
|
|
200
|
-
// `{ item: { name, path }, index, selectedRow }` bags for `BookmarkRow`
|
|
201
|
-
// `selectedRow` is the plain per-row selection flag so a picker move
|
|
202
|
-
// only the changed rows
|
|
293
|
+
// `{ item: { key, name, path }, index, selectedRow }` bags for `BookmarkRow`
|
|
294
|
+
// rows. `selectedRow` is the plain per-row selection flag so a picker move
|
|
295
|
+
// rewrites only the changed rows; `key` separates the synthetic add row from
|
|
296
|
+
// stored bookmarks.
|
|
203
297
|
function bookmarkRows(BookmarkRow) {
|
|
204
298
|
return (self, data) => {
|
|
205
299
|
const selected = isCell(data.selected) ? unwrap(data.selected) : data.selected
|
|
206
300
|
return BookmarkRow.map(
|
|
207
301
|
data.items ?? [],
|
|
208
302
|
(item, index) => ({ item, index, selectedRow: index === selected }),
|
|
209
|
-
(bookmark) => bookmark.name,
|
|
303
|
+
(bookmark) => bookmark.key ?? bookmark.name,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Function: overlayRows
|
|
309
|
+
// `{ item, index, currentRow }` bags for the shared list overlay's `OverlayRow`s
|
|
310
|
+
// (history / trash). `currentRow` is the plain per-row cursor flag, so a move
|
|
311
|
+
// rewrites only the changed rows.
|
|
312
|
+
function overlayRows(OverlayRow) {
|
|
313
|
+
return (self, data) => {
|
|
314
|
+
const items = unwrap(data.items) ?? []
|
|
315
|
+
const cursor = isCell(data.cursor) ? unwrap(data.cursor) : data.cursor
|
|
316
|
+
return OverlayRow.map(
|
|
317
|
+
items,
|
|
318
|
+
(item, index) => ({ item, index, currentRow: index === cursor }),
|
|
319
|
+
(item) => item.key ?? item.text,
|
|
210
320
|
)
|
|
211
321
|
}
|
|
212
322
|
}
|
|
213
323
|
|
|
214
|
-
export {
|
|
324
|
+
export {
|
|
325
|
+
bookmarkRows,
|
|
326
|
+
commandItemRows,
|
|
327
|
+
crumbRows,
|
|
328
|
+
filePickerRows,
|
|
329
|
+
fileRows,
|
|
330
|
+
fileWindow,
|
|
331
|
+
helpRows,
|
|
332
|
+
isSelected,
|
|
333
|
+
listingWindow,
|
|
334
|
+
overlayRows,
|
|
335
|
+
panelArgRows,
|
|
336
|
+
panelOpRows,
|
|
337
|
+
panelRunRows,
|
|
338
|
+
}
|
|
215
339
|
|
|
216
340
|
// EOF
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-20
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/core/base64
|
|
7
|
+
// Standard base64 for adapters that can only carry bytes as JSON text — the
|
|
8
|
+
// web API and the tinyjs bridge. Pure JavaScript: neither `Buffer` nor
|
|
9
|
+
// `btoa`/`atob` is guaranteed in every target (the desktop backend runs under
|
|
10
|
+
// txiki.js), and compressed previews stay small enough for a string.
|
|
11
|
+
|
|
12
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
13
|
+
|
|
14
|
+
// Constant: LOOKUP
|
|
15
|
+
// Byte code -> 6-bit value, -1 for anything outside the alphabet.
|
|
16
|
+
const LOOKUP = (() => {
|
|
17
|
+
const table = new Int16Array(128).fill(-1)
|
|
18
|
+
for (let i = 0; i < ALPHABET.length; i++) table[ALPHABET.charCodeAt(i)] = i
|
|
19
|
+
return table
|
|
20
|
+
})()
|
|
21
|
+
|
|
22
|
+
// Function: encodeBase64
|
|
23
|
+
// Base64 text for a byte array, with `=` padding.
|
|
24
|
+
function encodeBase64(bytes) {
|
|
25
|
+
let text = ""
|
|
26
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
27
|
+
const first = bytes[i]
|
|
28
|
+
const second = i + 1 < bytes.length ? bytes[i + 1] : undefined
|
|
29
|
+
const third = i + 2 < bytes.length ? bytes[i + 2] : undefined
|
|
30
|
+
text += ALPHABET[first >> 2]
|
|
31
|
+
text += ALPHABET[((first & 0x03) << 4) | ((second ?? 0) >> 4)]
|
|
32
|
+
text += second === undefined ? "=" : ALPHABET[((second & 0x0f) << 2) | ((third ?? 0) >> 6)]
|
|
33
|
+
text += third === undefined ? "=" : ALPHABET[third & 0x3f]
|
|
34
|
+
}
|
|
35
|
+
return text
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Function: decodeBase64
|
|
39
|
+
// Bytes for base64 text; padding, whitespace and foreign characters are
|
|
40
|
+
// ignored, so a truncated or line-wrapped payload still decodes.
|
|
41
|
+
function decodeBase64(text) {
|
|
42
|
+
const source = `${text ?? ""}`
|
|
43
|
+
const bytes = new Uint8Array(Math.floor((source.length * 3) / 4))
|
|
44
|
+
let length = 0
|
|
45
|
+
let buffer = 0
|
|
46
|
+
let bits = 0
|
|
47
|
+
for (let i = 0; i < source.length; i++) {
|
|
48
|
+
const code = source.charCodeAt(i)
|
|
49
|
+
if (code === 61) break // "=" padding: nothing follows it.
|
|
50
|
+
const value = code < LOOKUP.length ? LOOKUP[code] : -1
|
|
51
|
+
if (value < 0) continue
|
|
52
|
+
buffer = (buffer << 6) | value
|
|
53
|
+
bits += 6
|
|
54
|
+
if (bits >= 8) {
|
|
55
|
+
bits -= 8
|
|
56
|
+
bytes[length++] = (buffer >> bits) & 0xff
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return bytes.slice(0, length)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { decodeBase64, encodeBase64 }
|
|
63
|
+
|
|
64
|
+
// EOF
|
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
// Created: 2026-10-09
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/core/bookmarks
|
|
7
|
-
// Named bookmarks: a `{ name -> { path, isDir } }` map
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// the
|
|
7
|
+
// Named bookmarks: a `{ name -> { path, isDir } }` map shown by the `b`
|
|
8
|
+
// bookmarks panel, which filters them and records the current path under an
|
|
9
|
+
// automatic name. A bookmark remembers whether its path is a file or a
|
|
10
|
+
// directory (or `null` when only a legacy path is known), so a jump can open a
|
|
11
|
+
// directory but select-and-preview a file. Bookmarks are pure model state (a
|
|
12
|
+
// reactive cell) plus a persistence layer: a `BookmarkStore` interface with an
|
|
13
|
+
// in-memory default (tests, web) and a Node/JSON-file implementation (the TUI).
|
|
14
|
+
// The session only ever sees the store interface, so the browser bundle stays
|
|
15
|
+
// free of Node builtins.
|
|
15
16
|
|
|
16
17
|
import { cell } from "select/state/cells"
|
|
18
|
+
import { basename } from "./path.js"
|
|
17
19
|
|
|
18
20
|
// Function: normalizeName
|
|
19
21
|
// Trims a bookmark name; empty names are rejected by the callers.
|
|
@@ -29,6 +31,27 @@ function isValidName(name) {
|
|
|
29
31
|
return text.length > 0 && !/[\s/]/.test(text)
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
// Function: suggestName
|
|
35
|
+
// The automatic name for a path: its basename with whitespace (and the root
|
|
36
|
+
// separator) turned into dashes. Paths without a usable segment fall back to
|
|
37
|
+
// `bookmark`, so the add action always has a valid name.
|
|
38
|
+
function suggestName(path) {
|
|
39
|
+
const text = basename(path).replace(/[\s/]+/g, "-").replace(/^-+|-+$/g, "")
|
|
40
|
+
return isValidName(text) ? text : "bookmark"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Function: uniqueName
|
|
44
|
+
// `base`, suffixed with `-2`, `-3`, … until the map has no entry under it.
|
|
45
|
+
function uniqueName(map, base) {
|
|
46
|
+
let name = base
|
|
47
|
+
let suffix = 2
|
|
48
|
+
while (Object.hasOwn(map ?? {}, name)) {
|
|
49
|
+
name = `${base}-${suffix}`
|
|
50
|
+
suffix += 1
|
|
51
|
+
}
|
|
52
|
+
return name
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
// Function: normalizeEntry
|
|
33
56
|
// Coerces a stored value to `{ path, isDir }`: a plain string is a legacy
|
|
34
57
|
// path with an unknown kind (`isDir: null`), resolved when it is jumped to.
|
|
@@ -91,6 +114,18 @@ function createBookmarks({ store = createMemoryBookmarkStore(), entries = {} } =
|
|
|
91
114
|
return clean
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
// Function: add
|
|
118
|
+
// Records `path` under an automatic name (`suggestName`, disambiguated on
|
|
119
|
+
// collision) and persists. Returns `{ name, added }`; an already-bookmarked
|
|
120
|
+
// path keeps its existing name and reports `added: false`.
|
|
121
|
+
bookmarks.add = async (path, isDir = null) => {
|
|
122
|
+
const existing = bookmarks.find(path)
|
|
123
|
+
if (existing) return { name: existing.name, added: false }
|
|
124
|
+
const name = uniqueName(bookmarks.entries.value, suggestName(path))
|
|
125
|
+
await bookmarks.record(name, path, isDir)
|
|
126
|
+
return { name, added: true }
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
// Function: remove
|
|
95
130
|
// Deletes a bookmark and persists. Returns true when one was removed.
|
|
96
131
|
bookmarks.remove = async (name) => {
|
|
@@ -172,4 +207,6 @@ export {
|
|
|
172
207
|
normalizeEntry,
|
|
173
208
|
normalizeMap,
|
|
174
209
|
normalizeName,
|
|
210
|
+
suggestName,
|
|
211
|
+
uniqueName,
|
|
175
212
|
}
|