@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
|
@@ -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
|
|
@@ -36,7 +36,7 @@ function isCell(value) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Function: isSelected
|
|
39
|
-
// Whether a
|
|
39
|
+
// Whether a bookmark row is the current selection. The plain
|
|
40
40
|
// `selectedRow` flag (snapshotted by the row builder) wins over the shared
|
|
41
41
|
// `selected` cell, so a move only rewrites the rows that actually changed.
|
|
42
42
|
function isSelected(data) {
|
|
@@ -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,
|
|
@@ -119,21 +148,6 @@ function fileRows(FileRow) {
|
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
|
|
122
|
-
// Function: paletteRows
|
|
123
|
-
// `{ item: command, index, selectedRow }` bags for `PaletteRow` rows.
|
|
124
|
-
// `selectedRow` is the plain per-row selection flag so a palette move rewrites
|
|
125
|
-
// only the changed rows.
|
|
126
|
-
function paletteRows(PaletteRow) {
|
|
127
|
-
return (self, data) => {
|
|
128
|
-
const selected = isCell(data.selected) ? unwrap(data.selected) : data.selected
|
|
129
|
-
return PaletteRow.map(
|
|
130
|
-
data.items ?? [],
|
|
131
|
-
(item, index) => ({ item, index, selectedRow: index === selected }),
|
|
132
|
-
(command) => command.id,
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
151
|
// Function: commandItemRows
|
|
138
152
|
// `{ item: command, index }` bags for `CommandItem` rows.
|
|
139
153
|
function commandItemRows(CommandItem) {
|
|
@@ -146,67 +160,181 @@ function commandItemRows(CommandItem) {
|
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
// Function: panelOpRows
|
|
149
|
-
// `{ item
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// name stripped); `risk` tints destructive/caution operations; `cursor` is the
|
|
154
|
-
// column's selected index.
|
|
163
|
+
// `{ item, index, row, cursor, focus }` bags for the command panel's command
|
|
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.
|
|
155
167
|
function panelOpRows(CommandArgRow) {
|
|
156
168
|
return (self, data) =>
|
|
157
169
|
CommandArgRow.map(
|
|
158
170
|
data.items ?? [],
|
|
159
171
|
(item, index) => ({
|
|
160
172
|
item,
|
|
161
|
-
index,
|
|
173
|
+
index: item.type === "op" ? item.entryIndex ?? index : -1,
|
|
162
174
|
row: {
|
|
163
|
-
type:
|
|
164
|
-
key: item.
|
|
165
|
-
name: item.
|
|
166
|
-
|
|
175
|
+
type: item.type,
|
|
176
|
+
key: item.key,
|
|
177
|
+
name: item.name,
|
|
178
|
+
group: item.group,
|
|
167
179
|
risk: item.risk ?? null,
|
|
168
180
|
},
|
|
169
181
|
cursor: data.opCursor,
|
|
170
182
|
focus: data.focus,
|
|
171
183
|
}),
|
|
172
|
-
(item) => item.
|
|
184
|
+
(item) => item.key,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
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,
|
|
173
214
|
)
|
|
215
|
+
}
|
|
174
216
|
}
|
|
175
217
|
|
|
176
218
|
// Function: panelArgRows
|
|
177
|
-
// `{ item
|
|
178
|
-
// argument
|
|
179
|
-
// `core/panel.panelRowList`;
|
|
219
|
+
// `{ item, index, row, cursor, focus, value }` bags for the command
|
|
220
|
+
// panel's scrolling argument pane (`CommandArgRow`). The rows are the flat
|
|
221
|
+
// `core/panel.panelRowList`; this wraps each selection's item run in a top and
|
|
222
|
+
// bottom `frame` row so the list reads as one bordered box. `index` is the
|
|
223
|
+
// row's flat position (the frame rows take none), so the pane cursor still
|
|
224
|
+
// addresses the same rows.
|
|
180
225
|
function panelArgRows(CommandArgRow) {
|
|
181
|
-
return (self, data) =>
|
|
182
|
-
|
|
183
|
-
|
|
226
|
+
return (self, data) => {
|
|
227
|
+
const rows = (unwrap(data.rows) ?? []).filter((row) => row.type !== "run")
|
|
228
|
+
const visual = []
|
|
229
|
+
for (let i = 0; i < rows.length; i++) {
|
|
230
|
+
const row = rows[i]
|
|
231
|
+
if (row.type !== "item") {
|
|
232
|
+
visual.push(row)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
const first = i === 0 || rows[i - 1].type !== "item"
|
|
236
|
+
const last = i === rows.length - 1 || rows[i + 1].type !== "item"
|
|
237
|
+
if (first) visual.push({ ...row, type: "frame", variant: "top", key: `frame:top:${row.argIndex}` })
|
|
238
|
+
visual.push(row)
|
|
239
|
+
if (last) visual.push({ ...row, type: "frame", variant: "bottom", key: `frame:bottom:${row.argIndex}` })
|
|
240
|
+
}
|
|
241
|
+
return CommandArgRow.map(
|
|
242
|
+
visual,
|
|
184
243
|
(item, index) => ({
|
|
185
244
|
item,
|
|
186
|
-
index,
|
|
245
|
+
index: item.type === "frame" ? -1 : item.rowIndex ?? index,
|
|
187
246
|
row: item,
|
|
188
247
|
cursor: data.argCursor,
|
|
189
248
|
focus: data.focus,
|
|
249
|
+
permBit: data.permBit,
|
|
250
|
+
value: data.value,
|
|
190
251
|
}),
|
|
191
|
-
(
|
|
252
|
+
(item) => item.key,
|
|
192
253
|
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
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
|
+
}
|
|
193
290
|
}
|
|
194
291
|
|
|
195
292
|
// Function: bookmarkRows
|
|
196
|
-
// `{ item: { name, path }, index, selectedRow }` bags for `BookmarkRow`
|
|
197
|
-
// `selectedRow` is the plain per-row selection flag so a picker move
|
|
198
|
-
// 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.
|
|
199
297
|
function bookmarkRows(BookmarkRow) {
|
|
200
298
|
return (self, data) => {
|
|
201
299
|
const selected = isCell(data.selected) ? unwrap(data.selected) : data.selected
|
|
202
300
|
return BookmarkRow.map(
|
|
203
301
|
data.items ?? [],
|
|
204
302
|
(item, index) => ({ item, index, selectedRow: index === selected }),
|
|
205
|
-
(bookmark) => bookmark.name,
|
|
303
|
+
(bookmark) => bookmark.key ?? bookmark.name,
|
|
206
304
|
)
|
|
207
305
|
}
|
|
208
306
|
}
|
|
209
307
|
|
|
210
|
-
|
|
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,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
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
|
+
}
|
|
211
339
|
|
|
212
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
|
}
|