@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,90 +4,170 @@
|
|
|
4
4
|
// Created: 2026-10-09
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/core/panel
|
|
7
|
-
// The command panel (Commando):
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
7
|
+
// The command panel (Commando): the one entry point for the CLI operations
|
|
8
|
+
// (`copy`, `move`, `mkdir`, `new`, `chmod`, `hardlink`, `symlink`, `delete`,
|
|
9
|
+
// `run` — the `core/operations` registry). They are grouped by purpose
|
|
10
|
+
// (Command, File, Info, Create, Edit, Delete), using each operation's own
|
|
11
|
+
// `group` metadata, and alphabetized within each group (see
|
|
12
|
+
// `filterPanelEntries`). The workspace-view toggle and Quit are listed
|
|
13
|
+
// alongside the CLI operations; other TinyCmd actions keep their keys.
|
|
14
|
+
//
|
|
15
|
+
// For the selected operation, the panel binds its arguments from the live
|
|
16
|
+
// context — a selection argument shows the SRC/DST pane's marked paths (falling
|
|
17
|
+
// back to the cursor entry) and lets the user toggle individual items; a
|
|
18
|
+
// destination argument resolves to the DST pane's directory; textual arguments
|
|
19
|
+
// (a name, a buffer label, a shell command) are typed in place. `chmod` binds
|
|
20
|
+
// its MODE to the permission grid from `core/permissions`.
|
|
15
21
|
//
|
|
16
22
|
// The selection the user tweaks is *transient*: it lives in the draft bindings
|
|
17
23
|
// (`session.panelDraft`), never in the real pane marks. When the draft runs,
|
|
18
24
|
// each selection argument is encoded as a self-contained `$paths:[…]` token, so
|
|
19
|
-
// the resolved paths survive an async confirm (see `expandArg`). The
|
|
20
|
-
//
|
|
21
|
-
//
|
|
25
|
+
// the resolved paths survive an async confirm (see `expandArg`). The draft can
|
|
26
|
+
// also be rendered as a shell-quoted command (`panelCommand`), while the panel
|
|
27
|
+
// itself presents the arguments and an explicit Run action.
|
|
22
28
|
// Everything here is pure: the caller owns the cells; the panel commands in
|
|
23
|
-
// `core/commands` assemble and run.
|
|
29
|
+
// `core/commands` assemble and run. Picker path resolution and row shaping live
|
|
30
|
+
// in `core/picker`.
|
|
24
31
|
|
|
25
|
-
import { hasMatch
|
|
26
|
-
import { PANE_BUFFERS, PANE_LABELS } from "./pane.js"
|
|
32
|
+
import { hasMatch } from "../vendor/fzy.js"
|
|
33
|
+
import { currentPaneId, PANE_BUFFERS, PANE_LABELS } from "./pane.js"
|
|
27
34
|
import { basename, parent } from "./path.js"
|
|
28
|
-
import { getOperations, pathToken } from "./operations.js"
|
|
35
|
+
import { getOperations, pathToken, shellQuote } from "./operations.js"
|
|
36
|
+
import { PERMISSION_BITS, PERMISSION_CLASSES, permissionItems, permissionOctal, permissionText } from "./permissions.js"
|
|
29
37
|
|
|
30
38
|
// Constant: PANEL_FOCUS
|
|
31
|
-
// The two
|
|
32
|
-
// list
|
|
33
|
-
//
|
|
39
|
+
// The two regions the panel's focus ring moves through: "ops" (the filter and
|
|
40
|
+
// the command list on the left) and "args" (an argument row or item in the
|
|
41
|
+
// right pane, where a textual argument owns the keyboard and Space toggles a
|
|
42
|
+
// selection item).
|
|
34
43
|
const PANEL_FOCUS = { OPS: "ops", ARGS: "args" }
|
|
44
|
+
const PANEL_GROUPS = ["Command", "File", "Info", "Create", "Edit", "Delete"]
|
|
35
45
|
|
|
36
|
-
// Function:
|
|
37
|
-
// The
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
// Function: panelEntries
|
|
47
|
+
// The entries the panel lists: the categorized CLI operations (each carries its
|
|
48
|
+
// own `group`), the workspace-view toggle (named `view` like File's preview
|
|
49
|
+
// operation, but in the Command group) and Quit.
|
|
50
|
+
function panelEntries() {
|
|
51
|
+
const entries = getOperations()
|
|
52
|
+
.filter((op) => !op.hidden && op.group)
|
|
53
|
+
.map((op) => ({
|
|
54
|
+
id: `op:${op.name}`,
|
|
55
|
+
name: op.name,
|
|
56
|
+
group: op.group,
|
|
57
|
+
risk: op.risk ?? null,
|
|
58
|
+
matchNames: [op.name, ...(op.aliases ?? [])],
|
|
59
|
+
op,
|
|
60
|
+
}))
|
|
61
|
+
entries.push({
|
|
62
|
+
id: "command:view",
|
|
63
|
+
name: "view",
|
|
64
|
+
group: "Command",
|
|
65
|
+
risk: null,
|
|
66
|
+
matchNames: ["view"],
|
|
67
|
+
commandId: "view.toggleMode",
|
|
68
|
+
// A no-argument command still shows where it acts.
|
|
69
|
+
showPath: true,
|
|
70
|
+
})
|
|
71
|
+
entries.push({
|
|
72
|
+
id: "command:reload",
|
|
73
|
+
name: "reload",
|
|
74
|
+
group: "Command",
|
|
75
|
+
risk: null,
|
|
76
|
+
matchNames: ["reload", "refresh"],
|
|
77
|
+
commandId: "pane.reload",
|
|
78
|
+
// A no-argument command still shows where it acts.
|
|
79
|
+
showPath: true,
|
|
80
|
+
})
|
|
81
|
+
entries.push({
|
|
82
|
+
id: "command:quit",
|
|
83
|
+
name: "quit",
|
|
84
|
+
group: "Command",
|
|
85
|
+
risk: null,
|
|
86
|
+
matchNames: ["quit", "q"],
|
|
87
|
+
commandId: "app.quit",
|
|
88
|
+
})
|
|
89
|
+
return entries
|
|
41
90
|
}
|
|
42
91
|
|
|
43
|
-
// Function:
|
|
44
|
-
//
|
|
45
|
-
function
|
|
46
|
-
|
|
92
|
+
// Function: compareEntries
|
|
93
|
+
// The panel's fixed category order, then alphabetical command order.
|
|
94
|
+
function compareEntries(a, b) {
|
|
95
|
+
const groupA = PANEL_GROUPS.indexOf(`${a?.group ?? "File"}`)
|
|
96
|
+
const groupB = PANEL_GROUPS.indexOf(`${b?.group ?? "File"}`)
|
|
97
|
+
const rankA = groupA < 0 ? PANEL_GROUPS.length : groupA
|
|
98
|
+
const rankB = groupB < 0 ? PANEL_GROUPS.length : groupB
|
|
99
|
+
return rankA - rankB || `${a?.name ?? ""}`.localeCompare(`${b?.name ?? ""}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Function: filterPanelEntries
|
|
103
|
+
// Narrows the entry list by command name or alias and keeps the fixed category
|
|
104
|
+
// order and alphabetical order within each group.
|
|
105
|
+
function filterPanelEntries(entries, query) {
|
|
106
|
+
const list = entries ?? []
|
|
107
|
+
const q = `${query ?? ""}`.trim().toLowerCase()
|
|
108
|
+
if (!q) return [...list].sort(compareEntries)
|
|
109
|
+
return list
|
|
110
|
+
.filter((entry) => (entry.matchNames ?? []).some((name) => hasMatch(q, `${name}`.toLowerCase())))
|
|
111
|
+
.sort(compareEntries)
|
|
47
112
|
}
|
|
48
113
|
|
|
49
|
-
// Function:
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
for (
|
|
57
|
-
|
|
114
|
+
// Function: panelEntryRows
|
|
115
|
+
// Projects filtered entries into the command-list column: a group heading and
|
|
116
|
+
// one name-only `op` row per entry, tagged with its index in the *entries* list
|
|
117
|
+
// (what the cursor `panelOpIndex` addresses).
|
|
118
|
+
function panelEntryRows(entries) {
|
|
119
|
+
const rows = []
|
|
120
|
+
let previousGroup = null
|
|
121
|
+
for (let index = 0; index < (entries ?? []).length; index++) {
|
|
122
|
+
const entry = entries[index]
|
|
123
|
+
const group = entry.group ?? "File"
|
|
124
|
+
if (group !== previousGroup) {
|
|
125
|
+
rows.push({ type: "group", key: `group:${group.toLowerCase()}`, name: group, group })
|
|
126
|
+
previousGroup = group
|
|
127
|
+
}
|
|
128
|
+
rows.push({
|
|
129
|
+
type: "op",
|
|
130
|
+
key: entry.id,
|
|
131
|
+
entryIndex: index,
|
|
132
|
+
name: entry.name,
|
|
133
|
+
group,
|
|
134
|
+
risk: entry.risk,
|
|
135
|
+
})
|
|
58
136
|
}
|
|
59
|
-
return
|
|
137
|
+
return rows
|
|
60
138
|
}
|
|
61
139
|
|
|
62
|
-
// Function:
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
ranked.push({ op, score: rank })
|
|
140
|
+
// Function: explorerColumnId
|
|
141
|
+
// The column an explorer argument draws from: the *focused* one, so focusing
|
|
142
|
+
// the parent mirror selects from its own listing — the current directory, a
|
|
143
|
+
// directory — and focusing the preview selects from the directory it mirrors.
|
|
144
|
+
// A focused column without a listing (the inspector over a file) falls back to
|
|
145
|
+
// the pane that owns the current directory, matching `core/commands#pane`.
|
|
146
|
+
function explorerColumnId(session) {
|
|
147
|
+
const active = session.active?.value
|
|
148
|
+
if (active === "parent") return active
|
|
149
|
+
if (
|
|
150
|
+
active === "preview" &&
|
|
151
|
+
session.showPreview?.value &&
|
|
152
|
+
session.previewEntry?.value?.isDir &&
|
|
153
|
+
!session.preview?.value
|
|
154
|
+
) {
|
|
155
|
+
return active
|
|
79
156
|
}
|
|
80
|
-
|
|
81
|
-
return ranked.map((match) => match.op)
|
|
157
|
+
return currentPaneId(session)
|
|
82
158
|
}
|
|
83
159
|
|
|
84
160
|
// Function: resolvePaneId
|
|
85
|
-
// Maps an argument role to a pane id: "source" ->
|
|
86
|
-
//
|
|
161
|
+
// Maps an argument role to a pane id: "source" -> the working pane (SRC in
|
|
162
|
+
// commander; in explorer the focused column, which may be either side of the
|
|
163
|
+
// chain), "dest" -> DST (right), anything else -> the pane holding the current
|
|
164
|
+
// directory (the focused column in explorer, never an auxiliary column with no
|
|
165
|
+
// listing, so operations run against what the cursor shows).
|
|
87
166
|
function resolvePaneId(session, role) {
|
|
88
|
-
if (role === "source") return "left"
|
|
89
167
|
if (role === "dest") return "right"
|
|
90
|
-
|
|
168
|
+
if (session.viewMode?.value === "explorer") return explorerColumnId(session)
|
|
169
|
+
if (role === "source") return "left"
|
|
170
|
+
return currentPaneId(session)
|
|
91
171
|
}
|
|
92
172
|
|
|
93
173
|
// Function: paneSelectionPaths
|
|
@@ -131,12 +211,16 @@ function bindArg(session, arg, index) {
|
|
|
131
211
|
const role = arg.role ?? "active"
|
|
132
212
|
const common = {
|
|
133
213
|
index,
|
|
214
|
+
// Display order in the argument pane; the argv order stays the
|
|
215
|
+
// operation's own (see `panelRowList` and `argTokens`).
|
|
216
|
+
panelOrder: arg.panelOrder ?? index,
|
|
134
217
|
name: arg.name,
|
|
135
218
|
kind: arg.kind,
|
|
136
219
|
role,
|
|
137
220
|
optional: arg.optional === true,
|
|
138
221
|
variadic: arg.variadic === true,
|
|
139
|
-
textual:
|
|
222
|
+
textual:
|
|
223
|
+
arg.kind === "literal" || arg.kind === "raw" || arg.kind === "buffer" || arg.kind === "path",
|
|
140
224
|
}
|
|
141
225
|
if (arg.kind === "selection") {
|
|
142
226
|
const paneId = resolvePaneId(session, role)
|
|
@@ -145,26 +229,57 @@ function bindArg(session, arg, index) {
|
|
|
145
229
|
return {
|
|
146
230
|
...common,
|
|
147
231
|
paneId,
|
|
232
|
+
// A variadic selection accepts many marked paths (checkboxes); a
|
|
233
|
+
// single-path argument accepts one (a radio list).
|
|
234
|
+
multi: arg.variadic === true,
|
|
148
235
|
items: paneSelectionPaths(pane).map((path) => itemFor(entries, path)),
|
|
149
236
|
value: "",
|
|
150
237
|
}
|
|
151
238
|
}
|
|
152
|
-
if (arg.kind === "
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
239
|
+
if (arg.kind === "permissions") {
|
|
240
|
+
// Seed the checkboxes from the first selected path's current mode, so
|
|
241
|
+
// opening chmod starts on the file's real permissions rather than a
|
|
242
|
+
// generic 644. A path outside the listing falls back by kind.
|
|
243
|
+
const pane = session.panes[resolvePaneId(session, role)]
|
|
244
|
+
const entries = entryIndex(pane)
|
|
245
|
+
const path = paneSelectionPaths(pane)[0]
|
|
246
|
+
const entry = path ? entries.get(path) : null
|
|
247
|
+
const mode = Number.isInteger(entry?.mode)
|
|
248
|
+
? entry.mode & 0o777
|
|
249
|
+
: entry?.isDir
|
|
250
|
+
? 0o755
|
|
251
|
+
: 0o644
|
|
252
|
+
return { ...common, multi: true, items: permissionItems(mode) }
|
|
253
|
+
}
|
|
254
|
+
if (arg.kind === "dest" || arg.kind === "path") {
|
|
255
|
+
// A destination or a path argument is typeable in place, like a
|
|
256
|
+
// literal, and additionally browsable through the panel's file picker
|
|
257
|
+
// (`picker: true`, opened with `panel.browse`). In commander mode the
|
|
258
|
+
// DST pane is the natural destination, so a directory argument
|
|
259
|
+
// pre-fills from it; explorer mode has no destination pane, so it opens
|
|
260
|
+
// empty for the picker (or the user) to fill.
|
|
261
|
+
const mode = arg.kind === "dest" ? "dir" : arg.mode ?? "any"
|
|
262
|
+
const commander = (session.viewMode?.value ?? "commander") !== "explorer"
|
|
263
|
+
const value =
|
|
264
|
+
arg.prefill === "current"
|
|
265
|
+
? (session.activePath?.value ?? "")
|
|
266
|
+
: mode === "dir" && commander
|
|
267
|
+
? (session.panes?.right?.path?.value ?? "")
|
|
268
|
+
: ""
|
|
269
|
+
return { ...common, textual: true, picker: true, mode, items: [], value }
|
|
157
270
|
}
|
|
158
271
|
if (arg.kind === "buffer") {
|
|
159
|
-
|
|
272
|
+
const label = PANE_BUFFERS[session.currentId?.value ?? session.active.value] ?? "A"
|
|
273
|
+
return { ...common, items: [], value: `+${label}` }
|
|
160
274
|
}
|
|
161
275
|
return { ...common, items: [], value: "" }
|
|
162
276
|
}
|
|
163
277
|
|
|
164
278
|
// Function: bindArgs
|
|
165
|
-
// Binds every argument of
|
|
166
|
-
|
|
167
|
-
|
|
279
|
+
// Binds every argument of the selected entry to the live context. Every
|
|
280
|
+
// operation carries an `args` signature.
|
|
281
|
+
function bindArgs(session, entry) {
|
|
282
|
+
return (entry?.op?.args ?? []).map((arg, index) => bindArg(session, arg, index))
|
|
168
283
|
}
|
|
169
284
|
|
|
170
285
|
// Function: selectedPaths
|
|
@@ -184,6 +299,7 @@ function argTokens(bindings) {
|
|
|
184
299
|
const paths = selectedPaths(binding)
|
|
185
300
|
return paths.length ? pathToken(paths) : ""
|
|
186
301
|
}
|
|
302
|
+
if (binding.kind === "permissions") return permissionOctal(binding)
|
|
187
303
|
if (binding.kind === "dest") return binding.value ?? ""
|
|
188
304
|
return `${binding.value ?? ""}`.trim()
|
|
189
305
|
})
|
|
@@ -200,32 +316,29 @@ function argvFor(bindings) {
|
|
|
200
316
|
return args
|
|
201
317
|
}
|
|
202
318
|
|
|
203
|
-
// Function:
|
|
204
|
-
// The
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
319
|
+
// Function: missingBinding
|
|
320
|
+
// The first binding that is not runnable: a required argument left blank, an
|
|
321
|
+
// optional one before a filled one (a positional gap would shift the following
|
|
322
|
+
// tokens), or a selection with nothing selected. Returns null when the argv is
|
|
323
|
+
// runnable. The panel uses it both to flag an unrunnable draft and to focus the
|
|
324
|
+
// next thing to fill when Enter lands on an incomplete command.
|
|
325
|
+
function missingBinding(bindings) {
|
|
209
326
|
const list = bindings ?? []
|
|
210
327
|
const args = argTokens(list)
|
|
211
328
|
for (let i = 0; i < args.length - 1; i++) {
|
|
212
|
-
if (args[i] === "") return list[i]
|
|
329
|
+
if (args[i] === "") return list[i]
|
|
213
330
|
}
|
|
214
331
|
// A trailing required argument must still be filled.
|
|
215
332
|
const trailing = list[list.length - 1]
|
|
216
|
-
if (trailing && args[args.length - 1] === "" && trailing.optional !== true) return trailing
|
|
333
|
+
if (trailing && args[args.length - 1] === "" && trailing.optional !== true) return trailing
|
|
217
334
|
return null
|
|
218
335
|
}
|
|
219
336
|
|
|
220
|
-
// Function:
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
function shellQuote(token) {
|
|
226
|
-
const text = `${token ?? ""}`
|
|
227
|
-
if (text === "") return "''"
|
|
228
|
-
return /^[A-Za-z0-9_@%+=:,.\/-]+$/.test(text) ? text : `'${text.replace(/'/g, `'\\''`)}'`
|
|
337
|
+
// Function: missingArg
|
|
338
|
+
// The name of the first argument that is not runnable (see `missingBinding`),
|
|
339
|
+
// or null when the argv is runnable.
|
|
340
|
+
function missingArg(bindings) {
|
|
341
|
+
return missingBinding(bindings)?.name ?? null
|
|
229
342
|
}
|
|
230
343
|
|
|
231
344
|
// Function: panelCommand
|
|
@@ -244,6 +357,10 @@ function panelCommand(bindings) {
|
|
|
244
357
|
else if (binding.optional !== true) parts.push(`«${binding.name}»`)
|
|
245
358
|
continue
|
|
246
359
|
}
|
|
360
|
+
if (binding.kind === "permissions") {
|
|
361
|
+
parts.push(permissionOctal(binding))
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
247
364
|
const value = `${binding.value ?? ""}`.trim()
|
|
248
365
|
if (value) parts.push(shellQuote(value))
|
|
249
366
|
else if (binding.optional !== true) parts.push(`«${binding.name}»`)
|
|
@@ -269,41 +386,94 @@ function panelSummary(binding) {
|
|
|
269
386
|
const count = selectedPaths(binding).length
|
|
270
387
|
return `${pane} · ${count} selected`
|
|
271
388
|
}
|
|
272
|
-
if (binding.kind === "
|
|
389
|
+
if (binding.kind === "permissions") return permissionText(binding)
|
|
390
|
+
if (binding.kind === "dest" || binding.kind === "path") return binding.value || "(none)"
|
|
273
391
|
if (binding.kind === "buffer") return binding.value || "(active)"
|
|
274
392
|
return binding.value ? binding.value : "(empty)"
|
|
275
393
|
}
|
|
276
394
|
|
|
277
395
|
// Function: panelRowList
|
|
278
|
-
// Flattens the draft into the rows the panel's argument
|
|
279
|
-
// header row per argument
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
396
|
+
// Flattens the draft into the rows the panel's right-hand argument pane
|
|
397
|
+
// renders: one header row per argument (its label and value/summary),
|
|
398
|
+
// followed by one item row per selected path of a selection argument (so
|
|
399
|
+
// individual paths can be toggled). A variadic selection shows checkboxes
|
|
400
|
+
// (`[x]`/`[ ]`); a single-path one shows radios (`(•)`/`( )`). Row focus is
|
|
401
|
+
// assigned by the view (comparing the row index to the cursor), the same way
|
|
402
|
+
// list rows are; this stays pure and index-stable.
|
|
283
403
|
function panelRowList(bindings) {
|
|
284
404
|
const rows = []
|
|
285
|
-
|
|
405
|
+
// The pane may show arguments in a different order from the argv (PATH
|
|
406
|
+
// before MODE/OWNER); the binding `index` still keys the draft.
|
|
407
|
+
const ordered = [...(bindings ?? [])].sort(
|
|
408
|
+
(a, b) => (a.panelOrder ?? a.index) - (b.panelOrder ?? b.index),
|
|
409
|
+
)
|
|
410
|
+
for (const binding of ordered) {
|
|
411
|
+
const multi = binding.multi === true
|
|
412
|
+
const items = binding.items ?? []
|
|
413
|
+
// A selection that resolves to a single path is not a list: it renders
|
|
414
|
+
// as a pre-filled, read-only path (no checkbox/radio to pick from).
|
|
415
|
+
const single = items.length === 1
|
|
286
416
|
rows.push({
|
|
287
417
|
type: "arg",
|
|
288
418
|
key: `arg:${binding.index}`,
|
|
419
|
+
rowIndex: rows.length,
|
|
289
420
|
argIndex: binding.index,
|
|
290
421
|
itemIndex: -1,
|
|
291
422
|
name: binding.name,
|
|
292
|
-
summary: panelSummary(binding),
|
|
423
|
+
summary: single ? items[0].path : panelSummary(binding),
|
|
293
424
|
kind: binding.kind,
|
|
294
425
|
role: binding.role,
|
|
295
426
|
optional: binding.optional,
|
|
296
427
|
variadic: binding.variadic,
|
|
297
428
|
textual: binding.textual,
|
|
429
|
+
editable: binding.textual === true,
|
|
430
|
+
picker: binding.picker === true,
|
|
431
|
+
mode: binding.mode ?? null,
|
|
432
|
+
multi,
|
|
433
|
+
readonly: single,
|
|
298
434
|
value: binding.value,
|
|
299
|
-
count:
|
|
435
|
+
count: items.length,
|
|
300
436
|
})
|
|
301
|
-
|
|
302
|
-
|
|
437
|
+
if (single) continue
|
|
438
|
+
// A permission argument renders as one row per class (u/g/o) with its
|
|
439
|
+
// three bits as clickable cells, rather than nine checkbox rows. The
|
|
440
|
+
// bit cursor (`panelPermBit`) selects the cell Space toggles.
|
|
441
|
+
if (binding.kind === "permissions") {
|
|
442
|
+
for (let c = 0; c < PERMISSION_CLASSES.length; c++) {
|
|
443
|
+
const permissionClass = PERMISSION_CLASSES[c]
|
|
444
|
+
const rowIndex = rows.length
|
|
445
|
+
const bits = PERMISSION_BITS.map((bit, i) => {
|
|
446
|
+
const itemIndex = c * PERMISSION_BITS.length + i
|
|
447
|
+
return {
|
|
448
|
+
id: bit.id,
|
|
449
|
+
selected: items[itemIndex]?.selected === true,
|
|
450
|
+
itemIndex,
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
rows.push({
|
|
454
|
+
type: "perm",
|
|
455
|
+
key: `perm:${binding.index}:${permissionClass.id}`,
|
|
456
|
+
rowIndex,
|
|
457
|
+
argIndex: binding.index,
|
|
458
|
+
classId: permissionClass.id,
|
|
459
|
+
label: permissionClass.label,
|
|
460
|
+
bits,
|
|
461
|
+
// Click payloads: arg index, item index, row index.
|
|
462
|
+
keyR: `${binding.index}:${bits[0].itemIndex}:${rowIndex}`,
|
|
463
|
+
keyW: `${binding.index}:${bits[1].itemIndex}:${rowIndex}`,
|
|
464
|
+
keyX: `${binding.index}:${bits[2].itemIndex}:${rowIndex}`,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
for (let i = 0; i < items.length; i++) {
|
|
470
|
+
const item = items[i]
|
|
303
471
|
const selected = item.selected
|
|
472
|
+
const marker = multi ? (selected ? "[x]" : "[ ]") : selected ? "(•)" : "( )"
|
|
304
473
|
rows.push({
|
|
305
474
|
type: "item",
|
|
306
475
|
key: `item:${binding.index}:${item.path}`,
|
|
476
|
+
rowIndex: rows.length,
|
|
307
477
|
argIndex: binding.index,
|
|
308
478
|
itemIndex: i,
|
|
309
479
|
name: item.name,
|
|
@@ -311,7 +481,8 @@ function panelRowList(bindings) {
|
|
|
311
481
|
parent: item.parent ?? "",
|
|
312
482
|
isDir: item.isDir,
|
|
313
483
|
selected,
|
|
314
|
-
|
|
484
|
+
multi,
|
|
485
|
+
marker,
|
|
315
486
|
})
|
|
316
487
|
}
|
|
317
488
|
}
|
|
@@ -333,12 +504,13 @@ function isTextualRow(row) {
|
|
|
333
504
|
|
|
334
505
|
// Function: withToggledItem
|
|
335
506
|
// Returns a new draft with item `itemIndex` of argument `argIndex` toggled,
|
|
336
|
-
// leaving the other bindings untouched.
|
|
337
|
-
|
|
507
|
+
// leaving the other bindings untouched. A single-path argument (`multi` false)
|
|
508
|
+
// is a radio list: choosing one selects it alone.
|
|
509
|
+
function withToggledItem(draft, argIndex, itemIndex, multi = true) {
|
|
338
510
|
return (draft ?? []).map((binding) => {
|
|
339
511
|
if (binding.index !== argIndex) return binding
|
|
340
512
|
const items = (binding.items ?? []).map((item, i) =>
|
|
341
|
-
i === itemIndex ? { ...item, selected: !item.selected } : item,
|
|
513
|
+
multi ? (i === itemIndex ? { ...item, selected: !item.selected } : item) : { ...item, selected: i === itemIndex },
|
|
342
514
|
)
|
|
343
515
|
return { ...binding, items }
|
|
344
516
|
})
|
|
@@ -364,14 +536,18 @@ export { decodePathToken, pathToken } from "./operations.js"
|
|
|
364
536
|
|
|
365
537
|
export {
|
|
366
538
|
PANEL_FOCUS,
|
|
539
|
+
PANEL_GROUPS,
|
|
367
540
|
argvFor,
|
|
541
|
+
bindArg,
|
|
368
542
|
bindArgs,
|
|
369
543
|
bindingAt,
|
|
370
|
-
|
|
544
|
+
filterPanelEntries,
|
|
371
545
|
isTextualRow,
|
|
372
546
|
missingArg,
|
|
547
|
+
missingBinding,
|
|
373
548
|
panelCommand,
|
|
374
|
-
|
|
549
|
+
panelEntries,
|
|
550
|
+
panelEntryRows,
|
|
375
551
|
panelRowList,
|
|
376
552
|
panelRunnable,
|
|
377
553
|
panelSummary,
|
package/src/js/tcmd/core/path.js
CHANGED
|
@@ -37,9 +37,11 @@ function split(p) {
|
|
|
37
37
|
|
|
38
38
|
// Function: extname
|
|
39
39
|
// Lowercased extension of the last segment, without the dot; null when the
|
|
40
|
-
// name has no extension (or is a dotfile).
|
|
40
|
+
// name has no extension (or is a dotfile). A bare filename (no separator), the
|
|
41
|
+
// common case when building a listing, skips the `basename` split.
|
|
41
42
|
function extname(p) {
|
|
42
|
-
const
|
|
43
|
+
const text = `${p ?? ""}`
|
|
44
|
+
const name = text.indexOf(SEP) < 0 ? text : basename(text)
|
|
43
45
|
const at = name.lastIndexOf(".")
|
|
44
46
|
return at > 0 ? name.slice(at + 1).toLowerCase() : null
|
|
45
47
|
}
|
|
@@ -52,6 +54,31 @@ function prefix(p, count) {
|
|
|
52
54
|
return SEP + parts.slice(0, Math.max(0, count)).join(SEP)
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
// Function: relative
|
|
58
|
+
// Expresses `target` relative to the absolute directory `base`, using `..`
|
|
59
|
+
// segments when it sits outside: `relative("/a", "/a/b/c")` is `"b/c"` and
|
|
60
|
+
// `relative("/a/b", "/a/c")` is `"../c"`. Equal paths collapse to `"."`. A
|
|
61
|
+
// non-absolute input comes back unchanged, since there is nothing meaningful
|
|
62
|
+
// to relativize against.
|
|
63
|
+
function relative(base, target) {
|
|
64
|
+
const from = `${base ?? ""}`
|
|
65
|
+
const to = `${target ?? ""}`
|
|
66
|
+
if (!from.startsWith(SEP) || !to.startsWith(SEP)) return to
|
|
67
|
+
const baseParts = split(from)
|
|
68
|
+
const targetParts = split(to)
|
|
69
|
+
let common = 0
|
|
70
|
+
while (
|
|
71
|
+
common < baseParts.length &&
|
|
72
|
+
common < targetParts.length &&
|
|
73
|
+
baseParts[common] === targetParts[common]
|
|
74
|
+
) {
|
|
75
|
+
common += 1
|
|
76
|
+
}
|
|
77
|
+
const up = Array.from({ length: baseParts.length - common }, () => "..")
|
|
78
|
+
const down = targetParts.slice(common)
|
|
79
|
+
return [...up, ...down].join(SEP) || "."
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { SEP, basename, extname, join, normalize, parent, prefix, relative, split }
|
|
56
83
|
|
|
57
84
|
// EOF
|