@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,61 +4,87 @@
|
|
|
4
4
|
// Created: 2026-10-09
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandArgRow
|
|
7
|
-
// CommandArgRow: one row shared by
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// passes `{ type: "arg" | "item"
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// `
|
|
7
|
+
// CommandArgRow: one row shared by the command panel's two panes. The left pane
|
|
8
|
+
// passes `{ type: "op" }` name-only rows (`risk` tints caution/destructive
|
|
9
|
+
// categories) and `{ type: "group" }` headers (Command / File / Create / Edit / Delete). The
|
|
10
|
+
// right pane passes `{ type: "arg" | "item" | "perm" | "frame" | "entry" |
|
|
11
|
+
// "parent" | "run" }` rows: an argument header stacks its label over its value
|
|
12
|
+
// — a bracketed field with a distinct background for textual arguments, a
|
|
13
|
+
// summary otherwise; an item shows a `[x]`/`[ ]` (or radio) marker with its
|
|
14
|
+
// basename and parent; `perm` is a `u/g/o` permission class with three r/w/x
|
|
15
|
+
// cells; a `frame` draws a selection list's border; and `parent` is the
|
|
16
|
+
// picker's `..` navigation row. Reads the row bag
|
|
17
|
+
// `{ item, index, row, cursor, focus, permBit, value }`
|
|
18
|
+
// built by `rows.js`.
|
|
15
19
|
//
|
|
16
20
|
// Environment: `accentFg` (focused row), `textFg`, `textSecondaryFg` (summary),
|
|
17
|
-
// `
|
|
18
|
-
// `
|
|
19
|
-
// (
|
|
21
|
+
// `textHeadingFg` (general headings), `accentSecondaryFg` (a selected item),
|
|
22
|
+
// `errorFg` (destructive op/error), `warningFg` (caution op), `selectionBg`
|
|
23
|
+
// (row background), `fieldBg`/`fieldFg` (editable values), `readonlyFg`
|
|
24
|
+
// (read-only values), `borderSecondaryFg` (the list border), `focusedClass`
|
|
25
|
+
// (web), `icons` (namespace marker).
|
|
20
26
|
|
|
21
27
|
import { unwrap } from "select/state/cells"
|
|
28
|
+
import { PERMISSION_BITS } from "../core/permissions.js"
|
|
22
29
|
|
|
23
30
|
// Function: riskFg
|
|
24
|
-
// The colour a row's
|
|
25
|
-
//
|
|
31
|
+
// The colour a row's category maps to: red for Delete, amber for Edit, null
|
|
32
|
+
// when the row is neither.
|
|
26
33
|
function riskFg(row, env) {
|
|
27
|
-
if (row?.type === "op" && row.risk === "destructive") return env.errorFg
|
|
28
|
-
if (row?.type === "op" && row.risk === "caution") return env.warningFg
|
|
34
|
+
if ((row?.type === "op" || row?.type === "run") && row.risk === "destructive") return env.errorFg
|
|
35
|
+
if ((row?.type === "op" || row?.type === "run") && row.risk === "caution") return env.warningFg
|
|
29
36
|
return null
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
// Function: groupFg
|
|
40
|
+
// Color the purpose-group headings: Edit is amber and Delete is red.
|
|
41
|
+
function groupFg(group, env) {
|
|
42
|
+
if (group === "Edit") return env.warningFg
|
|
43
|
+
if (group === "Delete") return env.errorFg
|
|
44
|
+
return env.textFg
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
// Function: rowName
|
|
33
|
-
// The row's
|
|
34
|
-
// `[x] basename
|
|
48
|
+
// The row's label: a group heading, operation name, argument label, `[Run]`
|
|
49
|
+
// action, `[x] basename`, or picker entry (directories carry a trailing slash).
|
|
35
50
|
function rowName(row) {
|
|
36
51
|
if (!row) return ""
|
|
52
|
+
if (row.type === "group") return row.name ?? ""
|
|
53
|
+
if (row.type === "run") return row.error ? "[Retry]" : row.armed ? "[Confirm]" : "[Run]"
|
|
37
54
|
if (row.type === "item") return `${row.marker ?? "[ ]"} ${row.name ?? ""}`
|
|
55
|
+
if (row.type === "perm") return row.label ?? ""
|
|
56
|
+
if (row.type === "parent") return ".."
|
|
57
|
+
if (row.type === "entry") return `${row.name ?? ""}${row.isDir ? "/" : ""}`
|
|
38
58
|
return row.name ?? ""
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
// Function: rowSummary
|
|
42
|
-
// The row's right cell:
|
|
43
|
-
//
|
|
62
|
+
// The row's right cell: an argument's binding summary (or its value), an
|
|
63
|
+
// item's parent directory, or a picker entry's kind.
|
|
44
64
|
function rowSummary(row) {
|
|
45
65
|
if (!row) return ""
|
|
46
66
|
if (row.type === "item") return row.parent ?? ""
|
|
47
|
-
if (row.type === "
|
|
67
|
+
if (row.type === "parent") return "parent directory"
|
|
68
|
+
if (row.type === "entry") {
|
|
69
|
+
if (row.isSymlink && row.target) return `${row.target}`
|
|
70
|
+
return row.isDir ? "dir" : ""
|
|
71
|
+
}
|
|
48
72
|
return row.summary ?? ""
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
// Function: wantsFocus
|
|
52
|
-
// The focus token a row belongs to: the
|
|
53
|
-
//
|
|
76
|
+
// The focus token a row belongs to: the command list ("ops"), the argument
|
|
77
|
+
// pane ("args"), or none (a group header or a list frame never takes the
|
|
78
|
+
// cursor).
|
|
54
79
|
function wantsFocus(row) {
|
|
80
|
+
if (row?.type === "group" || row?.type === "frame") return "none"
|
|
55
81
|
return row?.type === "op" ? "ops" : "args"
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
// Function: isFocused
|
|
59
|
-
// True when this row owns the cursor in the focused
|
|
85
|
+
// True when this row owns the cursor in the focused region.
|
|
60
86
|
function isFocused(row, index, cursor, focus) {
|
|
61
|
-
if (!row || !focus) return false
|
|
87
|
+
if (!row || !focus || row.type === "group" || row.type === "frame") return false
|
|
62
88
|
return focus === wantsFocus(row) && index === cursor
|
|
63
89
|
}
|
|
64
90
|
|
|
@@ -66,25 +92,115 @@ function isFocused(row, index, cursor, focus) {
|
|
|
66
92
|
// Builds the CommandArgRow behavior map for the given renderer environment.
|
|
67
93
|
function createBehaviors(env) {
|
|
68
94
|
const focused = (data) => isFocused(data.row, data.index, unwrap(data.cursor), unwrap(data.focus))
|
|
69
|
-
|
|
95
|
+
const type = (data) => data.row?.type
|
|
96
|
+
const isList = (data) => type(data) === "item" || type(data) === "frame"
|
|
97
|
+
// A permission row has three bit cells; the bit cursor marks the one Space
|
|
98
|
+
// toggles, and a click toggles the cell it lands on.
|
|
99
|
+
const bits = (data) => data.row?.bits ?? []
|
|
100
|
+
const bitAt = (data, index) => bits(data)[index]
|
|
101
|
+
const bitFocused = (data, index) => focused(data) && (unwrap(data.permBit) ?? 0) === index
|
|
102
|
+
// The web classes that draw a selection's list box: the item rows carry the
|
|
103
|
+
// sides, the frame rows the top/bottom.
|
|
104
|
+
const listClass = (data) => {
|
|
105
|
+
if (!isList(data)) return null
|
|
106
|
+
if (type(data) === "frame") return data.row?.variant === "top" ? "is-list-top" : "is-list-bottom"
|
|
107
|
+
return "is-list-item"
|
|
108
|
+
}
|
|
109
|
+
// A textual argument header edits in place while it owns the cursor: its
|
|
110
|
+
// summary yields to a value input bound to the panel's edit cell.
|
|
111
|
+
const editing = (_self, data) => focused(data) && type(data) === "arg" && data.row?.textual === true
|
|
112
|
+
const behaviors = {
|
|
113
|
+
editing,
|
|
114
|
+
// --- shared row content ---
|
|
70
115
|
marker: (_self, data) => (focused(data) ? ">" : " "),
|
|
71
116
|
markerFg: () => env.accentFg,
|
|
117
|
+
// An argument's label sits on its own line, above the value.
|
|
72
118
|
name: (_self, data) => rowName(data.row),
|
|
119
|
+
// A permission row: its class label and three bit cells.
|
|
120
|
+
permClass: (_self, data) => data.row?.label ?? data.row?.classId ?? "",
|
|
121
|
+
isPerm: (_self, data) => type(data) === "perm",
|
|
73
122
|
rowBg: (_self, data) => (focused(data) ? env.selectionBg : undefined),
|
|
74
|
-
rowClass: (_self, data) =>
|
|
123
|
+
rowClass: (_self, data) =>
|
|
124
|
+
[focused(data) ? env.focusedClass : null, listClass(data)].filter(Boolean).join(" ") || null,
|
|
125
|
+
rowSelected: (_self, data) => `${data.row?.selected === true}`,
|
|
75
126
|
rowFg: (_self, data) => {
|
|
76
|
-
|
|
77
|
-
// focused — so danger reads at a glance; focus is carried by the
|
|
78
|
-
// `>` marker and the row background instead.
|
|
127
|
+
if (type(data) === "group") return groupFg(data.row?.group ?? data.row?.name, env)
|
|
79
128
|
const risk = riskFg(data.row, env)
|
|
80
129
|
if (risk) return risk
|
|
81
130
|
if (focused(data)) return env.accentFg
|
|
82
|
-
if (data
|
|
131
|
+
if (type(data) === "item" && data.row?.selected) return env.accentSecondaryFg
|
|
83
132
|
return env.textFg
|
|
84
133
|
},
|
|
85
134
|
summary: (_self, data) => rowSummary(data.row),
|
|
86
135
|
summaryFg: () => env.textSecondaryFg,
|
|
136
|
+
// The list branch repeats the marker/name/summary nodes, so it carries
|
|
137
|
+
// its own out-names (duplicate `out` names in one template collide).
|
|
138
|
+
listMarker: (_self, data) => (focused(data) ? ">" : " "),
|
|
139
|
+
listName: (_self, data) => rowName(data.row, env.icons),
|
|
140
|
+
listSummary: (_self, data) => rowSummary(data.row),
|
|
141
|
+
// --- layout branches ---
|
|
142
|
+
isArg: (_self, data) => type(data) === "arg",
|
|
143
|
+
isItem: (_self, data) => type(data) === "item",
|
|
144
|
+
isOp: (_self, data) => type(data) === "op",
|
|
145
|
+
isGroup: (_self, data) => type(data) === "group",
|
|
146
|
+
isEntry: (_self, data) => type(data) === "entry" || type(data) === "parent",
|
|
147
|
+
isRun: (_self, data) => type(data) === "run",
|
|
148
|
+
hasRunError: (_self, data) => type(data) === "run" && Boolean(data.row?.error),
|
|
149
|
+
isFrame: (_self, data) => type(data) === "frame",
|
|
150
|
+
isList: (_self, data) => isList(data),
|
|
151
|
+
showPlain: (_self, data) => type(data) === "op" || type(data) === "entry" || type(data) === "parent",
|
|
152
|
+
isPlain: (_self, data) => type(data) === "entry" || type(data) === "parent",
|
|
153
|
+
// An argument stacks its label over its value; every other row is one
|
|
154
|
+
// line tall.
|
|
155
|
+
rowHeight: (_self, data) => (type(data) === "arg" ? 2 : 1),
|
|
156
|
+
runMarginTop: (_self, data) => (type(data) === "run" ? 1 : 0),
|
|
157
|
+
runLabel: (_self, data) => (data?.row?.error ? "[Retry]" : data?.row?.armed ? "[Confirm]" : "[Run]"),
|
|
158
|
+
runAriaLabel: (_self, data) =>
|
|
159
|
+
data?.row?.error ? "Retry command" : data?.row?.armed ? "Confirm command" : "Run command",
|
|
160
|
+
runError: (_self, data) => data.row?.error ?? "",
|
|
161
|
+
runErrorFg: () => env.errorFg,
|
|
162
|
+
runFg: (_self, data) => riskFg(data.row, env) ?? env.textFg,
|
|
163
|
+
groupAttributes: () => env.groupAttributes ?? 0,
|
|
164
|
+
editable: (_self, data) => `${data.row?.editable === true}`,
|
|
165
|
+
// Editable values have a filled bracketed field; read-only summaries
|
|
166
|
+
// stay plain and muted.
|
|
167
|
+
fieldBg: (_self, data) => (data.row?.textual ? env.fieldBg : undefined),
|
|
168
|
+
fieldFg: (_self, data) =>
|
|
169
|
+
data.row?.textual ? (env.fieldFg ?? env.textFg) : (env.readonlyFg ?? env.textSecondaryFg),
|
|
170
|
+
textual: (_self, data) => data.row?.textual === true,
|
|
171
|
+
fieldOpen: (_self, data) => (data.row?.textual ? "[" : ""),
|
|
172
|
+
fieldClose: (_self, data) => (data.row?.textual ? "]" : ""),
|
|
173
|
+
// Selection lists share the argument pane's one-cell inset; the spacer
|
|
174
|
+
// is NBSP so the renderers do not collapse the leading run.
|
|
175
|
+
rowIndent: (_self, data) => (isList(data) ? 1 : 0),
|
|
176
|
+
listIndent: (_self, data) => 1,
|
|
177
|
+
listSpacer: (_self, data) => "\u00a0",
|
|
178
|
+
listBorder: (_self, data) => {
|
|
179
|
+
if (type(data) !== "frame") return ["left", "right"]
|
|
180
|
+
return data.row?.variant === "top" ? ["top", "left", "right"] : ["bottom", "left", "right"]
|
|
181
|
+
},
|
|
182
|
+
borderFg: () => env.borderSecondaryFg,
|
|
183
|
+
}
|
|
184
|
+
// r/w/x cells of a permission row, in the shared permission order so the
|
|
185
|
+
// templates can bind each cell without looping.
|
|
186
|
+
for (let i = 0; i < PERMISSION_BITS.length; i++) {
|
|
187
|
+
const bit = PERMISSION_BITS[i]
|
|
188
|
+
const name = bit.id.toUpperCase()
|
|
189
|
+
behaviors[`perm${name}`] = (_self, data) => (bitAt(data, i)?.selected ? bit.id : "-")
|
|
190
|
+
behaviors[`perm${name}On`] = (_self, data) => `${bitAt(data, i)?.selected === true}`
|
|
191
|
+
behaviors[`perm${name}Fg`] = (_self, data) =>
|
|
192
|
+
bitAt(data, i)?.selected ? env.accentSecondaryFg : env.textSecondaryFg
|
|
193
|
+
behaviors[`perm${name}Bg`] = (_self, data) =>
|
|
194
|
+
bitFocused(data, i) ? env.accentSecondaryBg : undefined
|
|
195
|
+
behaviors[`perm${name}Class`] = (_self, data) =>
|
|
196
|
+
[
|
|
197
|
+
bitAt(data, i)?.selected ? "is-on" : "is-off",
|
|
198
|
+
bitFocused(data, i) ? "is-bit-focused" : null,
|
|
199
|
+
]
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.join(" ")
|
|
87
202
|
}
|
|
203
|
+
return behaviors
|
|
88
204
|
}
|
|
89
205
|
|
|
90
206
|
export { createBehaviors, isFocused, riskFg, rowName, rowSummary, wantsFocus }
|
|
@@ -1,5 +1,56 @@
|
|
|
1
|
-
<box flexDirection="row" height="
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<text
|
|
1
|
+
<box flexDirection="row" out:height="rowHeight" out:marginTop="runMarginTop" out:backgroundColor="rowBg" on:click="row.key!panel:select">
|
|
2
|
+
<!-- A selection's item list: indented under the value column and framed
|
|
3
|
+
by the top/bottom border rows. -->
|
|
4
|
+
<text when="isList" out="listSpacer" wrapMode="none"></text>
|
|
5
|
+
<box when="isList" flexGrow="1" flexShrink="1" minWidth="0" flexDirection="row" columnGap="1"
|
|
6
|
+
out:border="listBorder" out:borderColor="borderFg">
|
|
7
|
+
<text when="isItem" out="listMarker" width="2" out:fg="markerFg"></text>
|
|
8
|
+
<text when="isItem" out="listName" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
9
|
+
<text when="isItem" out="listSummary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
10
|
+
</box>
|
|
11
|
+
<!-- An argument header: its label on one line, the value field under it.
|
|
12
|
+
Both start one cell in from the pane border. -->
|
|
13
|
+
<box when="isArg" flexDirection="column" flexGrow="1" flexShrink="1" minWidth="0" paddingLeft="1">
|
|
14
|
+
<box flexDirection="row" height="1" columnGap="1" minWidth="0">
|
|
15
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
16
|
+
</box>
|
|
17
|
+
<box flexDirection="row" height="1" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
18
|
+
<box when="editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
|
|
19
|
+
<text when="textual" out="fieldOpen" out:fg="fieldFg"></text>
|
|
20
|
+
<input inout="value" flexGrow="1" minWidth="6" height="1" out:focused="editing"></input>
|
|
21
|
+
<text when="textual" out="fieldClose" out:fg="fieldFg"></text>
|
|
22
|
+
</box>
|
|
23
|
+
<box when="!editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
|
|
24
|
+
<text when="textual" out="fieldOpen" out:fg="fieldFg"></text>
|
|
25
|
+
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
26
|
+
<text when="textual" out="fieldClose" out:fg="fieldFg"></text>
|
|
27
|
+
</box>
|
|
28
|
+
</box>
|
|
29
|
+
</box>
|
|
30
|
+
<!-- A permission class: its label and three clickable r/w/x cells. The bit
|
|
31
|
+
cursor marks the cell Space toggles. -->
|
|
32
|
+
<box when="isPerm" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" height="1" columnGap="1" paddingLeft="1">
|
|
33
|
+
<text out="permClass" width="7" out:fg="rowFg" wrapMode="none"></text>
|
|
34
|
+
<box height="1" on:click="row.keyR!panel:perm." out:backgroundColor="permRBg"><text out="permR" width="1" out:fg="permRFg"></text></box>
|
|
35
|
+
<box height="1" on:click="row.keyW!panel:perm." out:backgroundColor="permWBg"><text out="permW" width="1" out:fg="permWFg"></text></box>
|
|
36
|
+
<box height="1" on:click="row.keyX!panel:perm." out:backgroundColor="permXBg"><text out="permX" width="1" out:fg="permXFg"></text></box>
|
|
37
|
+
</box>
|
|
38
|
+
<box when="isRun" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1" justifyContent="flex-start">
|
|
39
|
+
<text out="runLabel" out:fg="runFg" wrapMode="none"></text>
|
|
40
|
+
</box>
|
|
41
|
+
<!-- Bold color-group title; operations below it are indented. -->
|
|
42
|
+
<box when="isGroup" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" paddingLeft="1">
|
|
43
|
+
<text out="name" out:fg="rowFg" out:attributes="groupAttributes" wrapMode="none"></text>
|
|
44
|
+
</box>
|
|
45
|
+
<!-- Name-only operation row, indented beneath its group title. -->
|
|
46
|
+
<box when="isOp" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1" paddingLeft="2">
|
|
47
|
+
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
48
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
49
|
+
</box>
|
|
50
|
+
<!-- Picker entries retain their cursor marker and trailing kind summary. -->
|
|
51
|
+
<box when="isEntry" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
|
|
52
|
+
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
53
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
54
|
+
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
55
|
+
</box>
|
|
5
56
|
</box>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<footer class="command-bar">
|
|
1
|
+
<footer class="command-bar" ref="box" when="showBar">
|
|
2
2
|
<nav when="showKeys" out:class="listClass" out="rows"></nav>
|
|
3
|
-
<span when="showStatus" out="status" out:class="statusClass"></span>
|
|
3
|
+
<span when="showStatus" out="status" out:class="statusClass" out:data-kind="notice.kind" role="status"></span>
|
|
4
4
|
</footer>
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandBar
|
|
7
7
|
// CommandBar: the bottom bar. By default it shows the keys available in the
|
|
8
|
-
// current context (the command registry's `
|
|
9
|
-
//
|
|
10
|
-
// its place. Entries are bracket shorthands (see `CommandItem.js`) that
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// current context (the command registry's `BAR_COMMANDS`, or the hints of the
|
|
9
|
+
// overlay that owns the keyboard); a pane error or a session notice takes
|
|
10
|
+
// its place. Entries are bracket shorthands (see `CommandItem.js`) that stay
|
|
11
|
+
// on one line: entries that do not fit are dropped and a trailing ellipsis
|
|
12
|
+
// takes their place, while the active pane's marked count joins the list as a
|
|
13
|
+
// dimmed, non-clickable entry. The row logic lives in `CommandItem.js`; this
|
|
14
|
+
// parent maps its `items` to rows.
|
|
14
15
|
//
|
|
15
16
|
// The web presentation swaps the key list for a single status line
|
|
16
17
|
// (`showKeys`/`showStatus`/`status`/`statusClass`); the TUI presentation
|
|
17
|
-
// keeps the
|
|
18
|
+
// keeps the single-line key list and swaps it for the status when there is one.
|
|
18
19
|
//
|
|
19
20
|
// Environment: `rows(data)` returning the keyed row list, `listClass` (web
|
|
20
21
|
// root class), `errorClass` (web), and the TUI colors plus the
|
|
@@ -23,6 +24,10 @@
|
|
|
23
24
|
import { unwrap } from "select/state/cells"
|
|
24
25
|
import { commandWidth } from "./CommandItem.js"
|
|
25
26
|
|
|
27
|
+
// Constant: ELLIPSIS
|
|
28
|
+
// The trailing marker shown when the bar cannot fit every entry.
|
|
29
|
+
const ELLIPSIS = { id: null, title: "…", dim: true }
|
|
30
|
+
|
|
26
31
|
// Function: commandBarStatus
|
|
27
32
|
// The one-line status the bar shows when something needs attention: the
|
|
28
33
|
// session notice first, then a pane error. Empty when everything is fine.
|
|
@@ -52,25 +57,25 @@ function commandBarMarksItem(data) {
|
|
|
52
57
|
return marks ? { id: null, title: marks, dim: true } : null
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
// Function:
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
// Function: commandBarFit
|
|
61
|
+
// Truncates entries to `width` columns, appending a dim ellipsis when an entry
|
|
62
|
+
// had to be dropped. `width <= 0` (not measured yet) keeps every entry and
|
|
63
|
+
// lets the renderer clip. The bar is always one line: entries never wrap.
|
|
64
|
+
function commandBarFit(items, width) {
|
|
65
|
+
const list = items ?? []
|
|
66
|
+
if (!width || width <= 0) return list
|
|
67
|
+
let used = 0
|
|
68
|
+
for (let i = 0; i < list.length; i++) {
|
|
69
|
+
const next = used + commandWidth(list[i])
|
|
70
|
+
if (next > width) {
|
|
71
|
+
const kept = list.slice(0, i)
|
|
72
|
+
// The ellipsis needs its own column plus the separating space.
|
|
73
|
+
while (kept.length && used + 2 > width) used -= commandWidth(kept.pop())
|
|
74
|
+
return [...kept, ELLIPSIS]
|
|
69
75
|
}
|
|
70
|
-
|
|
71
|
-
index++
|
|
76
|
+
used = next
|
|
72
77
|
}
|
|
73
|
-
return
|
|
78
|
+
return list
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
// Function: isInfo
|
|
@@ -91,7 +96,8 @@ function statusKind(data) {
|
|
|
91
96
|
function createBehaviors(env) {
|
|
92
97
|
const behaviors = {
|
|
93
98
|
listClass: () => env?.listClass,
|
|
94
|
-
|
|
99
|
+
showBar: (_self, data) => !unwrap(data?.desktopMode) || Boolean(commandBarStatus(data)),
|
|
100
|
+
showKeys: (_self, data) => !unwrap(data?.desktopMode) && !commandBarStatus(data),
|
|
95
101
|
showStatus: (_self, data) => Boolean(commandBarStatus(data)),
|
|
96
102
|
status: (_self, data) => commandBarStatus(data),
|
|
97
103
|
statusBg: (_self, data) => {
|
|
@@ -116,11 +122,11 @@ function createBehaviors(env) {
|
|
|
116
122
|
},
|
|
117
123
|
}
|
|
118
124
|
if (env?.rows) {
|
|
119
|
-
behaviors.rows = (
|
|
125
|
+
behaviors.rows = (self, data) => env.rows(self, data)
|
|
120
126
|
}
|
|
121
127
|
return behaviors
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
export { commandBarMarks, commandBarMarksItem,
|
|
130
|
+
export { commandBarFit, commandBarMarks, commandBarMarksItem, commandBarStatus, createBehaviors }
|
|
125
131
|
|
|
126
132
|
// EOF
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<box flexDirection="column" overflow="hidden">
|
|
2
|
-
<box when="showKeys" out="rows" flexDirection="row" flexWrap="
|
|
1
|
+
<box ref="box" flexDirection="column" overflow="hidden">
|
|
2
|
+
<box when="showKeys" out="rows" flexDirection="row" flexWrap="nowrap" flexGrow="1" overflow="hidden"></box>
|
|
3
3
|
<text when="showStatus" out="status" out:fg="statusFg" out:bg="statusBg" flexShrink="0"></text>
|
|
4
4
|
</box>
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
// both engines. `item.id` may be null for pure key hints, in which case a
|
|
9
9
|
// click dispatches nothing.
|
|
10
10
|
//
|
|
11
|
-
// Each entry renders as a plain bracket shorthand
|
|
12
|
-
// divider (except the first entry):
|
|
11
|
+
// Each entry renders as a plain bracket shorthand:
|
|
13
12
|
//
|
|
14
|
-
// [Open ⏎] — the
|
|
15
|
-
// the accent (primary) or secondary
|
|
16
|
-
// space separating it from the next
|
|
13
|
+
// [Open ⏎] — the brackets in the dim text colour, the title in the text
|
|
14
|
+
// colour, the key glyph in the accent (primary) or secondary
|
|
15
|
+
// accent, and a trailing space separating it from the next
|
|
16
|
+
// entry. Entries are separated by that space alone — no
|
|
17
|
+
// divider glyph.
|
|
17
18
|
//
|
|
18
19
|
// Entries without a key (the marked count) render as a dimmed plain word
|
|
19
|
-
// with no brackets. The web renders the same label as a single string
|
|
20
|
-
// buttons are separated by their gap, not the pipe).
|
|
20
|
+
// with no brackets. The web renders the same label as a single string.
|
|
21
21
|
//
|
|
22
22
|
// Environment: `accentFg` (primary), `accentSecondaryFg` (secondary),
|
|
23
23
|
// `textFg`, `textSecondaryFg`.
|
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
// Function: keyGlyph
|
|
26
26
|
// The shorthand glyph for a shortcut label: ⏎ for Enter, ⇥ for Tab.
|
|
27
27
|
function keyGlyph(keys) {
|
|
28
|
+
if (!keys) return keys
|
|
28
29
|
const GLYPHS = { Enter: "⏎", "↵": "⏎", Tab: "⇥" }
|
|
29
|
-
return
|
|
30
|
+
return keys
|
|
31
|
+
.split("/")
|
|
32
|
+
.map((key) => GLYPHS[key] ?? (key.endsWith("Tab") ? `${key.slice(0, -3)}⇥` : key))
|
|
33
|
+
.join("/")
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
// Function: commandLabel
|
|
@@ -36,16 +40,10 @@ function commandLabel(item) {
|
|
|
36
40
|
return item.keys ? `[${item.title} ${keyGlyph(item.keys)}] ` : `${item.title} `
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
// Constant: COMMAND_SEP
|
|
40
|
-
// The divider drawn before every entry but the first: a dim pipe and the
|
|
41
|
-
// space that keeps it clear of the button.
|
|
42
|
-
const COMMAND_SEP = "| "
|
|
43
|
-
|
|
44
43
|
// Function: commandWidth
|
|
45
|
-
// Columns the entry occupies: its label
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return commandLabel(item).length + (index > 0 ? COMMAND_SEP.length : 0)
|
|
44
|
+
// Columns the entry occupies: its label, brackets and trailing space included.
|
|
45
|
+
function commandWidth(item) {
|
|
46
|
+
return commandLabel(item).length
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
// Function: isPrimary
|
|
@@ -57,22 +55,24 @@ function isPrimary(item) {
|
|
|
57
55
|
// Function: createBehaviors
|
|
58
56
|
// Builds the CommandItem behavior map for the given renderer environment.
|
|
59
57
|
function createBehaviors(env) {
|
|
60
|
-
const head = (_self, data) => (data.item?.keys ? `[${data.item.title} ` : `${data.item?.title ?? ""} `)
|
|
61
58
|
return {
|
|
62
59
|
// Web: the whole shorthand as one label.
|
|
63
60
|
itemLabel: (_self, data) => commandLabel(data.item),
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
itemHint: (_self, data) => !data.item?.id,
|
|
62
|
+
// TUI: the dim brackets around the bright title, then the accent key
|
|
63
|
+
// glyph and the closer. Keyless entries (the marked count) render as a
|
|
64
|
+
// plain dim word, with no brackets.
|
|
65
|
+
itemOpen: (_self, data) => (data.item?.keys ? "[" : ""),
|
|
66
|
+
itemOpenFg: () => env?.textSecondaryFg,
|
|
67
|
+
itemHead: (_self, data) => `${data.item?.title ?? ""} `,
|
|
69
68
|
itemFg: (_self, data) => (data.item?.keys ? env?.textFg : env?.textSecondaryFg),
|
|
70
69
|
keyText: (_self, data) => (data.item?.keys ? keyGlyph(data.item.keys) : ""),
|
|
71
70
|
keyFg: (_self, data) => (isPrimary(data.item) ? env?.accentFg : env?.accentSecondaryFg),
|
|
72
|
-
|
|
71
|
+
itemClose: (_self, data) => (data.item?.keys ? "] " : ""),
|
|
72
|
+
itemCloseFg: () => env?.textSecondaryFg,
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export {
|
|
76
|
+
export { commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
|
|
77
77
|
|
|
78
78
|
// EOF
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<box flexDirection="row" height="1" flexShrink="0" on:click="item.id!commandbar:run">
|
|
2
|
-
<text out="
|
|
2
|
+
<text out="itemOpen" out:fg="itemOpenFg" wrapMode="none"></text>
|
|
3
3
|
<text out="itemHead" out:fg="itemFg" wrapMode="none"></text>
|
|
4
4
|
<text out="keyText" out:fg="keyFg" wrapMode="none"></text>
|
|
5
|
-
<text out="
|
|
5
|
+
<text out="itemClose" out:fg="itemCloseFg" wrapMode="none"></text>
|
|
6
6
|
</box>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<section class="command-panel" when="open" role="dialog" aria-modal="true" aria-label="Command panel" tabindex="-1" on:click="!dialog:panel-cancel">
|
|
2
|
+
<div class="command-dialog" on:click="dialogStop">
|
|
3
|
+
<header class="panel-title" out="title"></header>
|
|
4
|
+
<button class="panel-close" type="button" tabindex="-1" aria-label="Close command panel" on:click="!panel:close." out="closeHint"></button>
|
|
5
|
+
<div class="panel-columns">
|
|
6
|
+
<div class="panel-command-column">
|
|
7
|
+
<label class="panel-query-line">
|
|
8
|
+
<span out="sign"></span>
|
|
9
|
+
<input class="panel-query" type="text" inout="query" aria-label="Filter commands" />
|
|
10
|
+
</label>
|
|
11
|
+
<ul class="panel-ops" aria-label="Commands" out="opRows"></ul>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="panel-args-column">
|
|
14
|
+
<p class="panel-command-name" out="commandName"></p>
|
|
15
|
+
<div when="picking" out="pickerView|FilePicker"></div>
|
|
16
|
+
<div class="panel-args-scroll" when="!picking">
|
|
17
|
+
<ul class="panel-args" aria-label="Arguments" out="argRows"></ul>
|
|
18
|
+
<pre class="panel-preview" when="hasPreview" out="preview"></pre>
|
|
19
|
+
<ul class="panel-run-action" aria-label="Run command" out="runRows"></ul>
|
|
20
|
+
<div class="panel-run-error-block" when="hasRunError">
|
|
21
|
+
<p out="runError"></p>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="panel-confirm-command" when="hasConfirmCommand" out="confirmCommand" out:data-risk="confirmRisk"></p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<footer out="footer"></footer>
|
|
28
|
+
</div>
|
|
29
|
+
</section>
|