@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.
Files changed (134) hide show
  1. package/README.md +256 -62
  2. package/bin/tcmd +10 -2
  3. package/bin/tcmd.js +13 -2
  4. package/desktop/dist/index.html +13 -0
  5. package/desktop/dist/main.js +20298 -0
  6. package/desktop/dist/style.css +1515 -0
  7. package/desktop/frontend/index.html +13 -0
  8. package/package.json +19 -6
  9. package/public/fonts/OFL.txt +93 -0
  10. package/public/index.html +14 -0
  11. package/public/licenses/LUCIDE.txt +43 -0
  12. package/public/main.js +20679 -0
  13. package/public/style.css +1515 -0
  14. package/scripts/build-desktop.js +31 -0
  15. package/scripts/serve-web.js +61 -0
  16. package/src/js/tcmd/components/App.html +80 -14
  17. package/src/js/tcmd/components/App.js +339 -114
  18. package/src/js/tcmd/components/App.xml +76 -22
  19. package/src/js/tcmd/components/BookmarkList.html +5 -5
  20. package/src/js/tcmd/components/BookmarkList.js +15 -41
  21. package/src/js/tcmd/components/BookmarkList.xml +1 -2
  22. package/src/js/tcmd/components/BookmarkRow.html +2 -1
  23. package/src/js/tcmd/components/BookmarkRow.js +14 -9
  24. package/src/js/tcmd/components/BookmarkRow.xml +1 -1
  25. package/src/js/tcmd/components/Breadcrumbs.js +1 -1
  26. package/src/js/tcmd/components/CommandArgRow.html +28 -0
  27. package/src/js/tcmd/components/CommandArgRow.js +146 -30
  28. package/src/js/tcmd/components/CommandArgRow.xml +55 -4
  29. package/src/js/tcmd/components/CommandBar.html +2 -2
  30. package/src/js/tcmd/components/CommandBar.js +33 -27
  31. package/src/js/tcmd/components/CommandBar.xml +2 -2
  32. package/src/js/tcmd/components/CommandItem.html +1 -1
  33. package/src/js/tcmd/components/CommandItem.js +25 -25
  34. package/src/js/tcmd/components/CommandItem.xml +2 -2
  35. package/src/js/tcmd/components/CommandPanel.html +29 -0
  36. package/src/js/tcmd/components/CommandPanel.js +78 -52
  37. package/src/js/tcmd/components/CommandPanel.xml +49 -25
  38. package/src/js/tcmd/components/Confirm.html +2 -2
  39. package/src/js/tcmd/components/EntryDetail.html +7 -7
  40. package/src/js/tcmd/components/EntryDetail.js +22 -8
  41. package/src/js/tcmd/components/FileList.html +5 -1
  42. package/src/js/tcmd/components/FileList.js +2 -1
  43. package/src/js/tcmd/components/FilePicker.html +10 -0
  44. package/src/js/tcmd/components/FilePicker.js +43 -0
  45. package/src/js/tcmd/components/FilePicker.xml +13 -0
  46. package/src/js/tcmd/components/FilePickerRow.html +6 -0
  47. package/src/js/tcmd/components/FilePickerRow.js +27 -0
  48. package/src/js/tcmd/components/FilePickerRow.xml +10 -0
  49. package/src/js/tcmd/components/FilePreview.html +14 -4
  50. package/src/js/tcmd/components/FilePreview.js +111 -12
  51. package/src/js/tcmd/components/FilePreview.xml +28 -8
  52. package/src/js/tcmd/components/FileRow.html +2 -1
  53. package/src/js/tcmd/components/FileRow.js +70 -12
  54. package/src/js/tcmd/components/FilterBar.html +1 -1
  55. package/src/js/tcmd/components/FilterBar.js +14 -4
  56. package/src/js/tcmd/components/FilterBar.xml +1 -1
  57. package/src/js/tcmd/components/Frame.js +324 -0
  58. package/src/js/tcmd/components/Help.html +15 -0
  59. package/src/js/tcmd/components/Help.js +260 -0
  60. package/src/js/tcmd/components/Help.xml +13 -4
  61. package/src/js/tcmd/components/HelpRow.html +8 -0
  62. package/src/js/tcmd/components/HelpRow.xml +7 -0
  63. package/src/js/tcmd/components/OverlayList.html +8 -0
  64. package/src/js/tcmd/components/OverlayList.js +69 -0
  65. package/src/js/tcmd/components/{CommandPalette.xml → OverlayList.xml} +2 -5
  66. package/src/js/tcmd/components/OverlayRow.html +5 -0
  67. package/src/js/tcmd/components/OverlayRow.js +47 -0
  68. package/src/js/tcmd/components/OverlayRow.xml +9 -0
  69. package/src/js/tcmd/components/Summary.html +1 -1
  70. package/src/js/tcmd/components/Summary.js +1 -6
  71. package/src/js/tcmd/components/format.js +4 -23
  72. package/src/js/tcmd/components/icons.js +176 -5
  73. package/src/js/tcmd/components/index.js +9 -5
  74. package/src/js/tcmd/components/rows.js +176 -48
  75. package/src/js/tcmd/core/base64.js +64 -0
  76. package/src/js/tcmd/core/bookmarks.js +45 -8
  77. package/src/js/tcmd/core/commands.js +978 -260
  78. package/src/js/tcmd/core/concurrency.js +59 -0
  79. package/src/js/tcmd/core/decompress.js +117 -0
  80. package/src/js/tcmd/core/directory.js +219 -93
  81. package/src/js/tcmd/core/format.js +36 -0
  82. package/src/js/tcmd/core/fs.js +438 -54
  83. package/src/js/tcmd/core/history.js +58 -0
  84. package/src/js/tcmd/core/index.js +3 -9
  85. package/src/js/tcmd/core/journal.js +93 -0
  86. package/src/js/tcmd/core/keymap.js +42 -6
  87. package/src/js/tcmd/core/operations.js +813 -48
  88. package/src/js/tcmd/core/os.js +29 -3
  89. package/src/js/tcmd/core/pane.js +33 -8
  90. package/src/js/tcmd/core/panel.js +280 -104
  91. package/src/js/tcmd/core/path.js +30 -3
  92. package/src/js/tcmd/core/permissions.js +107 -0
  93. package/src/js/tcmd/core/picker.js +156 -0
  94. package/src/js/tcmd/core/preview.js +102 -0
  95. package/src/js/tcmd/core/script.js +7 -95
  96. package/src/js/tcmd/core/selection.js +9 -1
  97. package/src/js/tcmd/core/session.js +498 -200
  98. package/src/js/tcmd/core/watch.js +153 -0
  99. package/src/js/tcmd/desktop/backend/api.js +165 -0
  100. package/src/js/tcmd/desktop/backend/fs.js +626 -0
  101. package/src/js/tcmd/desktop/backend/main.ts +9 -0
  102. package/src/js/tcmd/desktop/bookmarks.js +23 -0
  103. package/src/js/tcmd/desktop/fs.js +91 -0
  104. package/src/js/tcmd/desktop/main.js +194 -0
  105. package/src/js/tcmd/desktop/os.js +33 -0
  106. package/src/js/tcmd/index.js +0 -1
  107. package/src/js/tcmd/tui/app.js +118 -60
  108. package/src/js/tcmd/tui/catalogue.js +238 -42
  109. package/src/js/tcmd/tui/index.js +18 -19
  110. package/src/js/tcmd/tui/opentui.js +33 -0
  111. package/src/js/tcmd/tui/syntax.js +91 -0
  112. package/src/js/tcmd/tui/theme.js +32 -2
  113. package/src/js/tcmd/vendor/bzip2.js +276 -0
  114. package/src/js/tcmd/web/app.js +225 -41
  115. package/src/js/tcmd/web/bookmarks.js +36 -0
  116. package/src/js/tcmd/web/catalogue.js +81 -38
  117. package/src/js/tcmd/web/effects.js +223 -0
  118. package/src/js/tcmd/web/header.html +1 -1
  119. package/src/js/tcmd/web/header.js +15 -11
  120. package/src/js/tcmd/web/http.js +13 -0
  121. package/src/js/tcmd/web/index.js +2 -2
  122. package/src/js/tcmd/web/main.js +6 -66
  123. package/src/js/tcmd/web/opfs.js +297 -30
  124. package/src/js/tcmd/web/seed.js +4 -4
  125. package/src/js/tcmd/web/server.js +124 -21
  126. package/src/js/tcmd/web/theme.js +4 -0
  127. package/tinyjs.json +16 -0
  128. package/src/js/tcmd/components/CommandPalette.html +0 -7
  129. package/src/js/tcmd/components/CommandPalette.js +0 -58
  130. package/src/js/tcmd/components/PaletteRow.html +0 -3
  131. package/src/js/tcmd/components/PaletteRow.js +0 -43
  132. package/src/js/tcmd/components/PaletteRow.xml +0 -4
  133. package/src/js/tcmd/tui/frame.js +0 -269
  134. 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 both command-panel columns. The operation
8
- // column passes `{ type: "op" }` rows (name = operation title, summary =
9
- // signature, `risk` tints destructive/caution operations); the argument column
10
- // passes `{ type: "arg" | "item" }` rows from `core/panel.panelRowList`. An
11
- // item shows a `[x]`/`[ ]` toggle with its basename and, dimmed after it, the
12
- // parent directory; a focused row carries a `>` marker so focus is not
13
- // colour-only. Reads the row bag `{ item, index, row, cursor, focus }` built by
14
- // `rows.js`.
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
- // `accentSecondaryFg` (a selected item), `errorFg` (destructive op),
18
- // `warningFg` (caution op), `selectionBg` (row background), `focusedClass`
19
- // (web).
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 risk maps to: red for destructive operations, amber for
25
- // caution (a move), null when the row is not risky.
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 left cell: the operation title, an argument's name, or an item'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: the operation signature, an argument's binding
43
- // summary, or an item's parent directory.
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 === "op") return row.summary ?? ""
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 operation column ("ops") or the
53
- // argument column ("args").
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 column.
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
- return {
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) => (focused(data) ? env.focusedClass : null),
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
- // A risky operation (delete/erase/move) is always tinted — even
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.row?.type === "item" && data.row?.selected) return env.accentSecondaryFg
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="1" columnGap="1" out:backgroundColor="rowBg" on:click="row.key!panel:select">
2
- <text out="marker" width="2" out:fg="markerFg"></text>
3
- <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
4
- <text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></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 `bar` commands, or the palette
9
- // hints while the palette is open); a pane error or a session notice takes
10
- // its place. Entries are bracket shorthands (see `CommandItem.js`) that wrap
11
- // onto as many rows as the width demands; the active pane's marked count
12
- // joins the list as a dimmed, non-clickable entry. The row logic lives in
13
- // `CommandItem.js`; this parent maps its `items` to rows.
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 wrapped key list and swaps it for the status when there is one.
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: commandBarRowCount
56
- // How many rows the wrapped entries occupy at `width` columns (greedy fill,
57
- // matching the flex layout, separators included). `width <= 0` (unknown)
58
- // assumes a single row.
59
- function commandBarRowCount(items, width) {
60
- if (!width || width <= 0) return 1
61
- let rows = 1
62
- let cols = 0
63
- let index = 0
64
- for (const item of items ?? []) {
65
- const w = commandWidth(item, index)
66
- if (cols > 0 && cols + w > width) {
67
- rows++
68
- cols = 0
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
- cols += w
71
- index++
76
+ used = next
72
77
  }
73
- return rows
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
- showKeys: (_self, data) => !commandBarStatus(data),
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 = (_self, data) => env.rows(self, data)
125
+ behaviors.rows = (self, data) => env.rows(self, data)
120
126
  }
121
127
  return behaviors
122
128
  }
123
129
 
124
- export { commandBarMarks, commandBarMarksItem, commandBarRowCount, commandBarStatus, createBehaviors }
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="wrap" flexGrow="1" overflow="hidden"></box>
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>
@@ -1,3 +1,3 @@
1
- <button class="command-key" type="button" on:click="item.id!commandbar:run">
1
+ <button class="command-key" type="button" out:disabled="itemHint" on:click="item.id!commandbar:run">
2
2
  <span out="itemLabel"></span>
3
3
  </button>
@@ -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, preceded by a dim `|`
12
- // divider (except the first entry):
11
+ // Each entry renders as a plain bracket shorthand:
13
12
  //
14
- // [Open ⏎] — the bracket and title in the text colour, the key glyph in
15
- // the accent (primary) or secondary accent, and a trailing
16
- // space separating it from the next entry.
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 (its
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 GLYPHS[keys] ?? keys
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 plus the leading separator when it
46
- // is not the first entry.
47
- function commandWidth(item, index) {
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
- // TUI: the dim pipe divider, then the bracket + title head, the
65
- // accent key glyph, the closer.
66
- itemSep: (_self, data) => (data.index > 0 ? COMMAND_SEP : ""),
67
- itemSepFg: () => env?.textSecondaryFg,
68
- itemHead: head,
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
- endText: (_self, data) => (data.item?.keys ? "] " : ""),
71
+ itemClose: (_self, data) => (data.item?.keys ? "] " : ""),
72
+ itemCloseFg: () => env?.textSecondaryFg,
73
73
  }
74
74
  }
75
75
 
76
- export { COMMAND_SEP, commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
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="itemSep" out:fg="itemSepFg" wrapMode="none"></text>
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="endText" out:fg="itemFg" wrapMode="none"></text>
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>