@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
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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 "
|
|
70
|
-
import { INSPECTOR_INLINE_WIDTH } from "
|
|
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.
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
197
|
-
//
|
|
198
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/js/tcmd/tui/index.js
CHANGED
|
@@ -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]
|
|
66
|
-
const right = createPane({ fs, path: paths[1]
|
|
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, {
|
|
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,
|
|
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
|