@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
@@ -13,26 +13,31 @@
13
13
  // `rows` environment. Components sharing a presentation bind the same source
14
14
  // (`Breadcrumbs` uses `RowList.xml`).
15
15
  //
16
- // TUI-only components (`Help`) live in `tui/` and are registered here only.
16
+ // The workspace chrome (`Frame.js`) and the key reference (`Help.js`) are
17
+ // shared logic bound here to their `.xml` presentations; the browser view
18
+ // binds the same modules to `.html`.
17
19
 
18
- import { extend, ui } from "select.tui"
19
- import { unwrap } from "select/state/cells"
20
+ import { extend, getTags, ui } from "select.tui"
21
+ import { effect, unwrap } from "select/state/cells"
20
22
  import bookmarkListXml from "../components/BookmarkList.xml" with { type: "text" }
21
23
  import bookmarkRowXml from "../components/BookmarkRow.xml" with { type: "text" }
22
24
  import commandBarXml from "../components/CommandBar.xml" with { type: "text" }
23
25
  import commandArgRowXml from "../components/CommandArgRow.xml" with { type: "text" }
24
26
  import commandItemXml from "../components/CommandItem.xml" with { type: "text" }
25
27
  import commandPanelXml from "../components/CommandPanel.xml" with { type: "text" }
26
- import commandPaletteXml from "../components/CommandPalette.xml" with { type: "text" }
27
28
  import confirmXml from "../components/Confirm.xml" with { type: "text" }
28
29
  import crumbXml from "../components/Crumb.xml" with { type: "text" }
29
30
  import entryDetailXml from "../components/EntryDetail.xml" with { type: "text" }
30
31
  import fileListXml from "../components/FileList.xml" with { type: "text" }
32
+ import filePickerXml from "../components/FilePicker.xml" with { type: "text" }
33
+ import filePickerRowXml from "../components/FilePickerRow.xml" with { type: "text" }
31
34
  import filePreviewXml from "../components/FilePreview.xml" with { type: "text" }
32
35
  import fileRowXml from "../components/FileRow.xml" with { type: "text" }
33
36
  import filterBarXml from "../components/FilterBar.xml" with { type: "text" }
34
37
  import helpXml from "../components/Help.xml" with { type: "text" }
35
- import paletteRowXml from "../components/PaletteRow.xml" with { type: "text" }
38
+ import helpRowXml from "../components/HelpRow.xml" with { type: "text" }
39
+ import overlayListXml from "../components/OverlayList.xml" with { type: "text" }
40
+ import overlayRowXml from "../components/OverlayRow.xml" with { type: "text" }
36
41
  import rowListXml from "../components/RowList.xml" with { type: "text" }
37
42
  import summaryXml from "../components/Summary.xml" with { type: "text" }
38
43
 
@@ -43,35 +48,58 @@ import {
43
48
  bookmarkRows,
44
49
  breadcrumbsBehaviors,
45
50
  commandBarBehaviors,
51
+ commandBarFit,
52
+ commandBarMarksItem,
46
53
  commandArgRowBehaviors,
47
54
  commandItemBehaviors,
48
55
  commandItemRows,
49
- commandBarMarksItem,
50
56
  commandPanelBehaviors,
51
- commandPaletteBehaviors,
52
57
  confirmBehaviors,
53
58
  crumbBehaviors,
54
59
  crumbRows,
55
60
  entryDetailBehaviors,
56
61
  fileListBehaviors,
62
+ filePickerBehaviors,
63
+ filePickerRowBehaviors,
64
+ filePickerRows,
57
65
  filePreviewBehaviors,
58
66
  fileRowBehaviors,
59
67
  fileRows,
60
- fileWindow,
61
68
  filterBarBehaviors,
69
+ helpRowBehaviors,
70
+ helpRows,
62
71
  iconsEnabled,
63
- paletteRowBehaviors,
64
- paletteRows,
72
+ listingWindow,
73
+ overlayListBehaviors,
74
+ overlayRowBehaviors,
75
+ overlayRows,
65
76
  panelArgRows,
66
77
  panelOpRows,
78
+ panelRunRows,
67
79
  summaryBehaviors,
68
80
  } from "../components/index.js"
69
- import { createBehaviors as helpBehaviors } from "./help.js"
70
- import { INSPECTOR_INLINE_WIDTH } from "./frame.js"
81
+ import { createBehaviors as helpBehaviors } from "../components/Help.js"
82
+ import { INSPECTOR_INLINE_WIDTH } from "../components/Frame.js"
83
+ import { CodeRenderable, ImageRenderable, pathToFiletype, createTextAttributes } from "./opentui.js"
84
+ import { syntaxStyle } from "./syntax.js"
71
85
  import { tuiTheme } from "./theme.js"
72
86
 
73
87
  let registered = false
74
88
 
89
+ // Class: CodeView
90
+ // The `code` host tag: OpenTUI's tree-sitter `CodeRenderable` with the shared
91
+ // `SyntaxStyle` injected, because the renderable reads its style at
92
+ // construction time and a template attribute can only carry a string. Content
93
+ // and `filetype` still arrive through the node's own `out:` bindings.
94
+ class CodeView extends CodeRenderable {
95
+ constructor(ctx, options) {
96
+ super(ctx, {
97
+ ...options,
98
+ syntaxStyle: options.syntaxStyle ?? syntaxStyle(ctx.nativeScene),
99
+ })
100
+ }
101
+ }
102
+
75
103
  // Function: tuiEnv
76
104
  // The TUI renderer environment: OpenTUI colors plus TUI-only hooks.
77
105
  function tuiEnv() {
@@ -90,13 +118,23 @@ function tuiEnv() {
90
118
  dirFg: tuiTheme.dirFg,
91
119
  errorBg: tuiTheme.errorBg,
92
120
  errorFg: tuiTheme.errorFg,
121
+ execFg: tuiTheme.execFg,
122
+ fieldBg: tuiTheme.fieldBg,
93
123
  fileFg: tuiTheme.fileFg,
124
+ groupAttributes: createTextAttributes({ bold: true }),
125
+ // Resolves an entry path to a tree-sitter filetype (undefined when no
126
+ // bundled parser matches, and the preview then draws plain text).
127
+ filetype: (path) => pathToFiletype(path),
128
+ // Raster formats the `image` renderable decodes. `bmp`/`svg` are not
129
+ // among them, so those previews keep the metadata card.
130
+ imageExts: ["gif", "jpeg", "jpg", "png", "webp"],
94
131
  // Nerd Font icons in rows, on unless `TCMD_ICONS` opts out.
95
132
  icons: iconsEnabled(process.env),
96
133
  lineFg: tuiTheme.textSecondaryFg,
97
134
  linkFg: tuiTheme.linkFg,
98
135
  mutedBg: tuiTheme.mutedBg,
99
136
  overlayBg: tuiTheme.overlayBg,
137
+ readonlyFg: tuiTheme.readonlyFg,
100
138
  selectionBg: tuiTheme.selectionBg,
101
139
  surfaceBg: tuiTheme.surfaceBg,
102
140
  textFg: tuiTheme.textFg,
@@ -166,20 +204,23 @@ function keepVisible(box, index, total) {
166
204
  return top
167
205
  }
168
206
 
169
- // Keep the file-list cursor row inside the viewport. When the list is windowed
170
- // (a `viewport` height is threaded down) the scrollbox holds only the window,
171
- // so it scrolls to the cursor's index inside that window against the window
172
- // length; otherwise it scrolls the full listing.
207
+ // Keep the file-list cursor row inside the viewport. The window comes from
208
+ // `listingWindow`, the same helper the row builder uses, so the scrollbox
209
+ // content and the scroll keeper never disagree (a windowed pane scrolls the
210
+ // cursor inside its window; an inspector without a threaded viewport mounts
211
+ // the full listing and scrolls it by absolute index).
173
212
  function fileListScrollTop(self, data) {
174
- const viewport = unwrap(data.viewport)
175
- if (viewport > 0) {
176
- const { local, len } = fileWindow(data, viewport)
177
- return keepVisible(self.ref.list, local, len)
178
- }
179
- return keepVisible(self.ref.list, unwrap(data.cursor) ?? 0, unwrap(data.entries)?.length ?? 0)
180
- }// Keep the palette selection inside its viewport.
181
- function paletteScrollTop(self, data) {
182
- return keepVisible(self.ref.list, unwrap(data.selected) ?? 0, unwrap(data.items)?.length ?? 0)
213
+ const { local, len } = listingWindow(data, unwrap(data.viewport) ?? 0)
214
+ return keepVisible(self.ref.list, local, len)
215
+ }
216
+
217
+ // Keep the file picker's cursor row inside its viewport, using the same
218
+ // bounded window as the row builder.
219
+ function filePickerScrollTop(self, data) {
220
+ const rows = unwrap(data.rows) ?? []
221
+ const cursor = unwrap(data.cursor) ?? 0
222
+ const { local, len } = listingWindow({ entries: rows, cursor }, 0)
223
+ return keepVisible(self.ref.list, local, len)
183
224
  }
184
225
 
185
226
  // Keep the bookmark picker selection inside its viewport.
@@ -187,21 +228,101 @@ function bookmarkScrollTop(self, data) {
187
228
  return keepVisible(self.ref.list, unwrap(data.selected) ?? 0, unwrap(data.items)?.length ?? 0)
188
229
  }
189
230
 
190
- // Keep the command panel's argument cursor inside its viewport.
231
+ // Keep the shared list overlay's cursor inside its viewport.
232
+ function overlayScrollTop(self, data) {
233
+ return keepVisible(self.ref.list, unwrap(data.cursor) ?? 0, unwrap(data.items)?.length ?? 0)
234
+ }
235
+
236
+ // Keep the selected command visible in the command panel's scrolling list.
237
+ function panelOpScrollTop(self, data) {
238
+ const items = unwrap(data.items) ?? []
239
+ const cursor = unwrap(data.opCursor) ?? 0
240
+ const index = items.findIndex((item) => item.type === "op" && item.entryIndex === cursor)
241
+ const box = self.ref.ops
242
+ // OpenTUI's measured scrollbox height includes two clipped edge cells in a
243
+ // modal, so scroll against the visible content height rather than the frame.
244
+ const visibleBox = box?.height
245
+ ? { height: Math.max(1, Math.floor(box.height) - 2), scrollTop: box.scrollTop, scrollHeight: box.scrollHeight }
246
+ : box
247
+ return keepVisible(visibleBox, Math.max(0, index), items.length)
248
+ }
249
+
250
+ // Keep enough vertical room for the trailing Run row on compact terminals while
251
+ // retaining the usual shorter panel on a full-sized viewport.
252
+ function panelDialogHeight(self) {
253
+ const height = Math.floor(self.ctx?.height ?? 0)
254
+ if (!height) return "72%"
255
+ return Math.floor(height * (height <= 20 ? 0.85 : 0.72))
256
+ }
257
+
258
+ // Keep the command panel's argument cursor inside its viewport. Argument
259
+ // headers are two lines tall and a selection's item run gains a border row at
260
+ // each end, so the offset is measured in rendered lines rather than row
261
+ // indices.
191
262
  function panelArgScrollTop(self, data) {
192
- return keepVisible(self.ref.args, unwrap(data.argCursor) ?? 0, unwrap(data.rows)?.length ?? 0)
263
+ // Arguments, the preflight preview and Run share the argument scroller;
264
+ // the mapping must count the preview and Run's leading blank line so the
265
+ // cursor can still be kept in view at the bottom.
266
+ const rows = unwrap(data.rows) ?? []
267
+ const box = self.ref.args
268
+ if (!rows.length) return box?.scrollTop ?? 0
269
+ const cursor = Math.min(unwrap(data.argCursor) ?? 0, rows.length - 1)
270
+ const previewHeight = unwrap(data.preview) ? (self.ref.previewBox?.height ?? 0) : 0
271
+ let offset = 0
272
+ let cursorTop = 0
273
+ let cursorHeight = 1
274
+ for (let i = 0; i < rows.length; i++) {
275
+ const row = rows[i]
276
+ // Run is preceded by the preview (when there is one) and by its own
277
+ // one-line separation from the last field.
278
+ if (row.type === "run") offset += 1 + previewHeight
279
+ const firstItem = row.type === "item" && (i === 0 || rows[i - 1].type !== "item")
280
+ const lastItem = row.type === "item" && (i === rows.length - 1 || rows[i + 1].type !== "item")
281
+ if (firstItem) offset += 1
282
+ const height = row.type === "arg" ? 2 : 1
283
+ if (i === cursor) {
284
+ cursorTop = offset
285
+ cursorHeight = height
286
+ }
287
+ offset += height
288
+ if (lastItem) offset += 1
289
+ }
290
+ return keepRangeVisible(box, cursorTop, cursorHeight, offset)
291
+ }
292
+
293
+ // Function: keepRangeVisible
294
+ // The line-based counterpart of `keepVisible`: keeps the `[top, top + height)`
295
+ // run inside the scrollbox, then nudges one line at the same edge margins.
296
+ function keepRangeVisible(box, top, height, total) {
297
+ if (!box || !box.height) return box?.scrollTop ?? 0
298
+ const view = Math.max(1, Math.floor(box.height))
299
+ const maxTop = Math.max(0, total - view)
300
+ let scroll = Math.max(0, Math.min(box.scrollTop ?? 0, maxTop))
301
+ if (top < scroll) scroll = top
302
+ else if (top + height > scroll + view) scroll = top + height - view
303
+ const position = top - scroll
304
+ if (position > SCROLL_HIGH * view && scroll < maxTop) scroll += 1
305
+ else if (position < SCROLL_LOW * view && scroll > 0) scroll -= 1
306
+ return scroll
193
307
  }
194
308
 
195
309
  // Function: commandBarRows
196
- // Appends the active pane's marked count as a trailing dimmed entry, then
197
- // maps the items to rows. The entries wrap onto as many rows as the width
198
- // demands; nothing is trimmed.
310
+ // Appends the active pane's marked count as a trailing dimmed entry, fits the
311
+ // entries on one line (dropping what does not fit behind an ellipsis), then
312
+ // maps them to rows.
199
313
  function commandBarRows(commandItem) {
200
314
  const base = commandItemRows(commandItem)
201
315
  return (self, data) => {
316
+ const viewport = unwrap(data.uiSize)
317
+ const viewportWidth = Math.floor(viewport?.width ?? 0)
318
+ // Prefer the injected viewport: the box geometry still reports the
319
+ // previous width on the frame a resize arrives.
320
+ const own = Math.floor(self.ref.box?.width ?? 0)
321
+ const width = viewportWidth > 0 ? viewportWidth - 1 : own
202
322
  const items = unwrap(data.items) ?? []
203
323
  const marks = commandBarMarksItem(data)
204
- return base(self, { ...data, items: marks ? [...items, marks] : items })
324
+ const entries = marks ? [...items, marks] : items
325
+ return base(self, { ...data, items: commandBarFit(entries, width) })
205
326
  }
206
327
  }
207
328
 
@@ -213,16 +334,17 @@ function createComponents() {
213
334
  const commandArgRow = bind(ui, "CommandArgRow", commandArgRowXml, commandArgRowBehaviors(env))
214
335
  const crumb = bind(ui, "Crumb", crumbXml, crumbBehaviors(env))
215
336
  const fileRow = bind(ui, "FileRow", fileRowXml, fileRowBehaviors(env))
337
+ const filePickerRow = bind(ui, "FilePickerRow", filePickerRowXml, filePickerRowBehaviors(env))
216
338
  const filterBar = bind(ui, "FilterBar", filterBarXml, filterBarBehaviors(env))
217
- const paletteRow = bind(ui, "PaletteRow", paletteRowXml, paletteRowBehaviors(env))
218
339
  const bookmarkRow = bind(ui, "BookmarkRow", bookmarkRowXml, bookmarkRowBehaviors(env))
340
+ const overlayRow = bind(ui, "OverlayRow", overlayRowXml, overlayRowBehaviors(env))
341
+ const helpRow = bind(ui, "HelpRow", helpRowXml, helpRowBehaviors({ theme: tuiTheme }))
219
342
  return {
220
343
  BookmarkRow: bookmarkRow,
221
344
  CommandArgRow: commandArgRow,
222
345
  CommandItem: commandItem,
223
346
  Crumb: crumb,
224
347
  FileRow: fileRow,
225
- PaletteRow: paletteRow,
226
348
  Breadcrumbs: bind(ui, "Breadcrumbs", rowListXml, breadcrumbsBehaviors({
227
349
  ...env,
228
350
  rows: crumbRows(crumb),
@@ -238,15 +360,13 @@ function createComponents() {
238
360
  })),
239
361
  CommandPanel: bind(ui, "CommandPanel", commandPanelXml, commandPanelBehaviors({
240
362
  ...env,
363
+ panelHeight: panelDialogHeight,
241
364
  opRows: panelOpRows(commandArgRow),
242
365
  argRows: panelArgRows(commandArgRow),
366
+ runRows: panelRunRows(commandArgRow),
367
+ opScrollTop: panelOpScrollTop,
243
368
  argScrollTop: panelArgScrollTop,
244
369
  })),
245
- CommandPalette: bind(ui, "CommandPalette", commandPaletteXml, commandPaletteBehaviors({
246
- ...env,
247
- rows: paletteRows(paletteRow),
248
- scrollTop: paletteScrollTop,
249
- })),
250
370
  Confirm: bind(ui, "Confirm", confirmXml, confirmBehaviors(env)),
251
371
  EntryDetail: bind(ui, "EntryDetail", entryDetailXml, entryDetailBehaviors({
252
372
  ...env,
@@ -259,9 +379,37 @@ function createComponents() {
259
379
  rows: fileRows(fileRow),
260
380
  scrollTop: fileListScrollTop,
261
381
  })),
262
- FilePreview: bind(ui, "FilePreview", filePreviewXml, filePreviewBehaviors(env)),
382
+ FilePicker: bind(ui, "FilePicker", filePickerXml, filePickerBehaviors({
383
+ ...env,
384
+ rows: filePickerRows(filePickerRow),
385
+ scrollTop: filePickerScrollTop,
386
+ })),
387
+ FilePreview: bind(ui, "FilePreview", filePreviewXml, filePreviewBehaviors(env)).init((self, data) => {
388
+ // A new preview target starts at the top of the body. `entry` is the
389
+ // reactive cell the App threads down; the effect fires after mount,
390
+ // when `ref.body` exists, and the following render swaps the text.
391
+ if (data?.entry?.isReactive) {
392
+ self.effect(() => effect([data.entry], () => {
393
+ if (self.ref.body) self.ref.body.scrollTop = 0
394
+ }))
395
+ }
396
+ // A full-preview scroll request (App key routing) moves the body by
397
+ // one line; OpenTUI clamps the range, so the edges need no guard.
398
+ if (data?.scroll?.isReactive) {
399
+ self.effect(() => effect([data.scroll], ([request]) => {
400
+ const body = self.ref.body
401
+ if (!body || !request?.delta) return
402
+ body.scrollTop = Math.max(0, body.scrollTop + request.delta)
403
+ }))
404
+ }
405
+ }),
263
406
  FilterBar: filterBar,
264
- Help: bind(ui, "Help", helpXml, helpBehaviors({ theme: tuiTheme })),
407
+ Help: bind(ui, "Help", helpXml, helpBehaviors({ theme: tuiTheme, rows: helpRows(helpRow) })),
408
+ OverlayList: bind(ui, "OverlayList", overlayListXml, overlayListBehaviors({
409
+ ...env,
410
+ rows: overlayRows(overlayRow),
411
+ scrollTop: overlayScrollTop,
412
+ })),
265
413
  Summary: bind(ui, "Summary", summaryXml, summaryBehaviors({
266
414
  ...env,
267
415
  width: paneWidth,
@@ -273,28 +421,76 @@ function createComponents() {
273
421
 
274
422
  const components = createComponents()
275
423
 
424
+ // Function: showScrollBar
425
+ // select.tui hides scrollbars by default because cursor-aware lists keep the
426
+ // cursor on screen themselves. Restore automatic overflow visibility for every
427
+ // scrollbox; the wrapper width fix lets the vertical bar fit beside its content.
428
+ function showScrollBar(node) {
429
+ // The wrapper's content declares a 100% minimum width, which otherwise
430
+ // starves the 1-column bar and drops it over the first column. The wrapper
431
+ // is re-measured once the content is mounted, so apply it again on the next
432
+ // tick to land on the overlay's first frame.
433
+ const fix = () => {
434
+ if (node.wrapper) node.wrapper.minWidth = 0
435
+ node.requestRender?.()
436
+ }
437
+ fix()
438
+ queueMicrotask(fix)
439
+ const bar = node.verticalScrollBar
440
+ if (bar) {
441
+ // Override select.tui's hidden default and let overflow drive the bar
442
+ // again. OpenTUI 0.8 pins `visible = false`, so the reset is required.
443
+ bar.resetVisibilityControl?.()
444
+ bar.width = 1
445
+ bar.flexShrink = 0
446
+ // The default track/thumb are greys; the theme keeps the scrollbar part
447
+ // of the night-blue surfaces (mono leaves the defaults). OpenTUI 0.8
448
+ // paints the bar natively from `trackOptions`; older versions expose a
449
+ // child slider renderable.
450
+ if (bar.slider) {
451
+ if (tuiTheme.scrollbarThumbBg) {
452
+ bar.slider.backgroundColor = tuiTheme.scrollbarTrackBg
453
+ bar.slider.foregroundColor = tuiTheme.scrollbarThumbBg
454
+ }
455
+ } else if (tuiTheme.scrollbarThumbBg) {
456
+ bar.trackOptions = {
457
+ foregroundColor: tuiTheme.scrollbarThumbBg,
458
+ backgroundColor: tuiTheme.scrollbarTrackBg,
459
+ }
460
+ }
461
+ }
462
+ }
463
+
276
464
  // Function: registerComponents
277
465
  // Idempotent registration of the parent components as select.tui host tags.
278
466
  function registerComponents() {
279
467
  if (registered) return components
280
468
  registered = true
281
469
  extend({
470
+ // Preserve OpenTUI's scrollbox while re-enabling its overflow-aware bars.
471
+ scrollbox: { Class: getTags().scrollbox.Class, configure: showScrollBar },
472
+ helpscroll: { Class: getTags().scrollbox.Class, configure: showScrollBar },
282
473
  bookmarklist: components.BookmarkList,
283
474
  breadcrumbs: components.Breadcrumbs,
475
+ // Tree-sitter code preview (see `CodeView`) and the Kitty/sixel/blocks
476
+ // image renderable; `filepreview` binds both.
477
+ code: { Class: CodeView, contentProp: "content" },
478
+ image: { Class: ImageRenderable },
284
479
  commandbar: components.CommandBar,
285
480
  commandpanel: components.CommandPanel,
286
- commandpalette: components.CommandPalette,
287
481
  confirm: components.Confirm,
288
482
  entrydetail: components.EntryDetail,
289
483
  filelist: components.FileList,
484
+ filepicker: components.FilePicker,
290
485
  filepreview: components.FilePreview,
291
486
  filterbar: components.FilterBar,
292
487
  help: components.Help,
488
+ overlaylist: components.OverlayList,
293
489
  summary: components.Summary,
294
490
  })
295
491
  return components
296
492
  }
297
493
 
298
- export { components, fileListScrollTop, keepVisible, registerComponents }
494
+ export { components, fileListScrollTop, keepVisible, panelDialogHeight, panelOpScrollTop, registerComponents }
299
495
 
300
496
  // EOF
@@ -15,24 +15,10 @@ import nodePath from "node:path"
15
15
  import { mkdir, readFile, writeFile } from "node:fs/promises"
16
16
  import { cell } from "select/state/cells"
17
17
  import { render } from "select.tui"
18
- import { commandRow, getPaletteCommands } from "../core/commands.js"
19
18
  import { createBookmarks, createFileBookmarkStore } from "../core/bookmarks.js"
20
19
  import { createNodeFs, createNodeOs, createPane, createSession, load } from "../core/index.js"
21
20
  import { createAppComponent } from "./app.js"
22
21
 
23
- // The TUI palette lists meaningful actions; raw cursor movement is already on
24
- // the arrow keys and is noise in the list. Hiding it here (rather than in core)
25
- // keeps the web palette and the shared registry unchanged.
26
- const PALETTE_NOISE = /^nav\./
27
-
28
- // Function: tuiPaletteCommands
29
- // The command rows the TUI palette offers.
30
- function tuiPaletteCommands() {
31
- return getPaletteCommands()
32
- .filter((command) => !PALETTE_NOISE.test(command.id))
33
- .map(commandRow)
34
- }
35
-
36
22
  // Function: readSize
37
23
  // Normalizes a renderer resize payload. OpenTUI's live renderer emits
38
24
  // `(width, height)` as two arguments; the test renderer emits a bare width;
@@ -58,16 +44,27 @@ function bookmarkFile() {
58
44
  return nodePath.join(configHome, "tcmd", "bookmarks.json")
59
45
  }
60
46
 
47
+ // Function: startupPath
48
+ // Resolves a CLI path argument against the working directory. Relative paths
49
+ // (`../src`, `.`) would otherwise reach the panes verbatim and be rewritten
50
+ // from the filesystem root by the POSIX path helpers, corrupting breadcrumbs,
51
+ // history and the explorer's parent column.
52
+ function startupPath(value, fallback) {
53
+ return value ? nodePath.resolve(value) : fallback
54
+ }
55
+
61
56
  // Function: main
62
57
  // Runs the TUI. `paths` are optional initial directories [left, right].
63
58
  async function main(paths = []) {
64
59
  const fs = createNodeFs()
65
- const left = createPane({ fs, path: paths[0] || fs.cwd() })
66
- const right = createPane({ fs, path: paths[1] || fs.home() })
60
+ const left = createPane({ fs, path: startupPath(paths[0], fs.cwd()) })
61
+ const right = createPane({ fs, path: startupPath(paths[1], fs.home()) })
67
62
  let instance = null
68
63
  const os = createNodeOs({
69
64
  suspend: () => instance?._ownedRenderer?.suspend?.(),
70
65
  resume: () => instance?._ownedRenderer?.resume?.(),
66
+ // OSC 52: the renderer knows whether the terminal supports it.
67
+ copy: (text) => instance?._ownedRenderer?.copyToClipboardOSC52?.(text) ?? false,
71
68
  })
72
69
  const bookmarks = createBookmarks({
73
70
  store: createFileBookmarkStore({
@@ -78,12 +75,14 @@ async function main(paths = []) {
78
75
  }),
79
76
  })
80
77
  const session = createSession({ fs, os, left, right, host: nodeOs.hostname(), bookmarks })
81
- session.paletteCommands.set(tuiPaletteCommands())
82
78
  await load(session)
83
79
  const size = cell({ width: 0, height: 0 })
84
80
  const help = cell(false)
85
81
  const App = createAppComponent(session, { size, help })
86
- instance = await render(App, { useMouse: true, exitOnCtrlC: false })
82
+ instance = await render(App, {
83
+ useMouse: true,
84
+ exitOnCtrlC: false,
85
+ })
87
86
  const renderer = instance?.ctx ?? instance?.renderer
88
87
  const current = renderer?.width || renderer?.height
89
88
  ? { width: renderer.width ?? 0, height: renderer.height ?? 0 }
@@ -99,6 +98,6 @@ async function main(paths = []) {
99
98
  return session
100
99
  }
101
100
 
102
- export { main, readSize, tuiPaletteCommands }
101
+ export { main, readSize, startupPath }
103
102
 
104
103
  // EOF
@@ -0,0 +1,33 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-18
5
+
6
+ // Module: tcmd/tui/opentui
7
+ // The OpenTUI classes the TUI registers as host tags (`code`, `image`) — and
8
+ // the `SyntaxStyle`/filetype helpers they need — resolved from the *same*
9
+ // OpenTUI copy `select.tui` itself binds its renderables to.
10
+ //
11
+ // A plain `import … from "@opentui/core"` can land on a different physical
12
+ // copy: in a source checkout `select.tui` is symlinked with its own
13
+ // `node_modules`, so the repo and the renderer each get an `@opentui/core` and
14
+ // their renderable base classes stop matching (a code renderable from one copy
15
+ // cannot be mounted in the other's tree — OpenTUI throws on the first
16
+ // destroy). Resolving `@opentui/core` from wherever `select.tui` resolves its
17
+ // own dependencies keeps a single instance. In a normal install both resolve
18
+ // to the one hoisted copy, so this is a no-op there.
19
+
20
+ import { createRequire } from "node:module"
21
+ import { pathToFileURL } from "node:url"
22
+
23
+ const requireHere = createRequire(import.meta.url)
24
+ const requireFromSelectTui = createRequire(requireHere.resolve("select.tui"))
25
+ const coreUrl = pathToFileURL(requireFromSelectTui.resolve("@opentui/core")).href
26
+
27
+ const core = await import(coreUrl)
28
+
29
+ const { CodeRenderable, ImageRenderable, pathToFiletype, SyntaxStyle, createTextAttributes } = core
30
+
31
+ export { CodeRenderable, ImageRenderable, pathToFiletype, SyntaxStyle, createTextAttributes }
32
+
33
+ // EOF
@@ -0,0 +1,91 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-18
5
+
6
+ // Module: tcmd/tui/syntax
7
+ // The TUI's syntax-highlighting palette: one `SyntaxStyle` (OpenTUI's
8
+ // tree-sitter style table) per native scene, built from the active `tuiTheme`,
9
+ // so code previews read in the same colours as the rest of the workspace.
10
+ // Since OpenTUI 0.6, every style is tied to the scene that owns its native
11
+ // handle, so a renderer cannot share a style with another renderer. The
12
+ // TUI-only half of the preview — the web renders plain text and never imports
13
+ // OpenTUI.
14
+ //
15
+ // The scope names are the standard tree-sitter captures the bundled queries
16
+ // emit (`keyword.return`, `function.method`, `punctuation.bracket`…).
17
+ // `SyntaxStyle#getStyleId` falls back from a dotted capture to its base name,
18
+ // so registering the bases here covers every variant without enumerating them.
19
+ // In the mono theme every colour token is `undefined`, so a scope keeps only
20
+ // its attributes (comment stays italic, headings stay bold) and the terminal's
21
+ // own foreground carries the rest.
22
+
23
+ import { SyntaxStyle } from "./opentui.js"
24
+ import { tuiTheme } from "./theme.js"
25
+
26
+ // Function: scopeStyles
27
+ // Maps each base capture to a style drawn from `theme`. Content colours reuse
28
+ // the listing palette (comments dimmer than plain text, strings the executable
29
+ // green, types the directory blue, literals the device purple), so a preview
30
+ // and a pane never disagree about what a colour means.
31
+ function scopeStyles(theme) {
32
+ return {
33
+ attribute: { fg: theme.tipFg },
34
+ boolean: { fg: theme.tipFg },
35
+ comment: { fg: theme.readonlyFg, italic: true },
36
+ constant: { fg: theme.tipFg },
37
+ constructor: { fg: theme.dirFg },
38
+ embedded: { fg: theme.textFg },
39
+ field: { fg: theme.devFg },
40
+ function: { fg: theme.linkFg },
41
+ import: { fg: theme.accentFg },
42
+ keyword: { fg: theme.accentSecondaryFg, bold: true },
43
+ label: { fg: theme.accentSecondaryFg },
44
+ method: { fg: theme.linkFg },
45
+ module: { fg: theme.accentFg },
46
+ namespace: { fg: theme.accentFg },
47
+ number: { fg: theme.tipFg },
48
+ operator: { fg: theme.textSecondaryFg },
49
+ parameter: { fg: theme.textFg },
50
+ property: { fg: theme.devFg },
51
+ punctuation: { fg: theme.textSecondaryFg },
52
+ string: { fg: theme.execFg },
53
+ tag: { fg: theme.accentSecondaryFg },
54
+ type: { fg: theme.dirFg },
55
+ variable: { fg: theme.textFg },
56
+ "markup.heading": { fg: theme.textHeadingFg, bold: true },
57
+ "markup.italic": { italic: true },
58
+ "markup.link": { fg: theme.linkFg, underline: true },
59
+ "markup.link.label": { fg: theme.linkFg },
60
+ "markup.link.url": { fg: theme.linkFg, underline: true },
61
+ "markup.list": { fg: theme.accentSecondaryFg },
62
+ "markup.quote": { fg: theme.readonlyFg, italic: true },
63
+ "markup.raw": { fg: theme.execFg },
64
+ "markup.strong": { bold: true },
65
+ }
66
+ }
67
+
68
+ // Function: createSyntaxStyle
69
+ // Builds a `SyntaxStyle` for `theme`, owned by `scene`.
70
+ function createSyntaxStyle(scene, theme = tuiTheme) {
71
+ return SyntaxStyle.fromStyles(scopeStyles(theme), scene)
72
+ }
73
+
74
+ // The style is shared per scene: the theme resolves once per process, and the
75
+ // code renderables of one renderer share one native handle.
76
+ const sharedStyles = new WeakMap()
77
+
78
+ // Function: syntaxStyle
79
+ // The shared `SyntaxStyle` for `scene` and the active theme, built on first use.
80
+ function syntaxStyle(scene, theme = tuiTheme) {
81
+ let style = sharedStyles.get(scene)
82
+ if (!style) {
83
+ style = createSyntaxStyle(scene, theme)
84
+ sharedStyles.set(scene, style)
85
+ }
86
+ return style
87
+ }
88
+
89
+ export { syntaxStyle }
90
+
91
+ // EOF