@ssebastien/tcmd 0.1.4 → 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 +169 -58
- 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 +17 -5
- package/public/fonts/OFL.txt +93 -0
- package/public/licenses/LUCIDE.txt +43 -0
- package/public/main.js +5066 -1888
- package/public/style.css +660 -46
- package/scripts/build-desktop.js +31 -0
- package/src/js/tcmd/components/App.html +56 -6
- package/src/js/tcmd/components/App.js +233 -58
- package/src/js/tcmd/components/App.xml +27 -22
- package/src/js/tcmd/components/BookmarkList.html +4 -5
- package/src/js/tcmd/components/BookmarkList.js +14 -40
- 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/CommandArgRow.html +18 -7
- package/src/js/tcmd/components/CommandArgRow.js +100 -44
- package/src/js/tcmd/components/CommandArgRow.xml +39 -15
- package/src/js/tcmd/components/CommandBar.html +1 -1
- package/src/js/tcmd/components/CommandBar.js +31 -25
- package/src/js/tcmd/components/CommandBar.xml +2 -2
- package/src/js/tcmd/components/CommandItem.js +24 -25
- package/src/js/tcmd/components/CommandItem.xml +2 -2
- package/src/js/tcmd/components/CommandPanel.html +13 -13
- package/src/js/tcmd/components/CommandPanel.js +57 -33
- package/src/js/tcmd/components/CommandPanel.xml +30 -20
- package/src/js/tcmd/components/Confirm.html +2 -2
- package/src/js/tcmd/components/EntryDetail.js +21 -7
- 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 +1 -2
- package/src/js/tcmd/components/FilePreview.js +40 -4
- package/src/js/tcmd/components/FilePreview.xml +17 -10
- package/src/js/tcmd/components/FileRow.html +1 -0
- package/src/js/tcmd/components/FileRow.js +60 -9
- package/src/js/tcmd/components/Frame.js +57 -31
- package/src/js/tcmd/components/Help.html +12 -4
- package/src/js/tcmd/components/Help.js +198 -47
- 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/OverlayList.xml +13 -0
- 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/format.js +4 -23
- package/src/js/tcmd/components/icons.js +137 -0
- package/src/js/tcmd/components/index.js +8 -3
- package/src/js/tcmd/components/rows.js +149 -25
- 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 +612 -136
- 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 +81 -5
- package/src/js/tcmd/core/format.js +36 -0
- package/src/js/tcmd/core/fs.js +410 -53
- package/src/js/tcmd/core/journal.js +93 -0
- package/src/js/tcmd/core/keymap.js +39 -3
- package/src/js/tcmd/core/operations.js +735 -63
- package/src/js/tcmd/core/os.js +29 -3
- package/src/js/tcmd/core/pane.js +4 -2
- package/src/js/tcmd/core/panel.js +179 -93
- 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/session.js +286 -116
- 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/tui/app.js +111 -74
- package/src/js/tcmd/tui/catalogue.js +232 -23
- package/src/js/tcmd/tui/index.js +18 -4
- 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 +27 -2
- package/src/js/tcmd/vendor/bzip2.js +276 -0
- package/src/js/tcmd/web/app.js +114 -56
- package/src/js/tcmd/web/catalogue.js +41 -4
- package/src/js/tcmd/web/effects.js +94 -14
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/http.js +13 -0
- package/src/js/tcmd/web/index.js +2 -2
- package/src/js/tcmd/web/opfs.js +297 -30
- package/src/js/tcmd/web/seed.js +1 -1
- package/src/js/tcmd/web/server.js +118 -17
- package/src/js/tcmd/web/theme.js +3 -0
- package/tinyjs.json +16 -0
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
// shared logic bound here to their `.xml` presentations; the browser view
|
|
18
18
|
// binds the same modules to `.html`.
|
|
19
19
|
|
|
20
|
-
import { extend, ui } from "select.tui"
|
|
21
|
-
import { unwrap } from "select/state/cells"
|
|
20
|
+
import { extend, getTags, ui } from "select.tui"
|
|
21
|
+
import { effect, unwrap } from "select/state/cells"
|
|
22
22
|
import bookmarkListXml from "../components/BookmarkList.xml" with { type: "text" }
|
|
23
23
|
import bookmarkRowXml from "../components/BookmarkRow.xml" with { type: "text" }
|
|
24
24
|
import commandBarXml from "../components/CommandBar.xml" with { type: "text" }
|
|
@@ -29,10 +29,15 @@ import confirmXml from "../components/Confirm.xml" with { type: "text" }
|
|
|
29
29
|
import crumbXml from "../components/Crumb.xml" with { type: "text" }
|
|
30
30
|
import entryDetailXml from "../components/EntryDetail.xml" with { type: "text" }
|
|
31
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" }
|
|
32
34
|
import filePreviewXml from "../components/FilePreview.xml" with { type: "text" }
|
|
33
35
|
import fileRowXml from "../components/FileRow.xml" with { type: "text" }
|
|
34
36
|
import filterBarXml from "../components/FilterBar.xml" with { type: "text" }
|
|
35
37
|
import helpXml from "../components/Help.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,32 +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
57
|
confirmBehaviors,
|
|
52
58
|
crumbBehaviors,
|
|
53
59
|
crumbRows,
|
|
54
60
|
entryDetailBehaviors,
|
|
55
61
|
fileListBehaviors,
|
|
62
|
+
filePickerBehaviors,
|
|
63
|
+
filePickerRowBehaviors,
|
|
64
|
+
filePickerRows,
|
|
56
65
|
filePreviewBehaviors,
|
|
57
66
|
fileRowBehaviors,
|
|
58
67
|
fileRows,
|
|
59
|
-
fileWindow,
|
|
60
68
|
filterBarBehaviors,
|
|
69
|
+
helpRowBehaviors,
|
|
70
|
+
helpRows,
|
|
61
71
|
iconsEnabled,
|
|
72
|
+
listingWindow,
|
|
73
|
+
overlayListBehaviors,
|
|
74
|
+
overlayRowBehaviors,
|
|
75
|
+
overlayRows,
|
|
62
76
|
panelArgRows,
|
|
63
77
|
panelOpRows,
|
|
78
|
+
panelRunRows,
|
|
64
79
|
summaryBehaviors,
|
|
65
80
|
} from "../components/index.js"
|
|
66
81
|
import { createBehaviors as helpBehaviors } from "../components/Help.js"
|
|
67
82
|
import { INSPECTOR_INLINE_WIDTH } from "../components/Frame.js"
|
|
83
|
+
import { CodeRenderable, ImageRenderable, pathToFiletype, createTextAttributes } from "./opentui.js"
|
|
84
|
+
import { syntaxStyle } from "./syntax.js"
|
|
68
85
|
import { tuiTheme } from "./theme.js"
|
|
69
86
|
|
|
70
87
|
let registered = false
|
|
71
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
|
+
|
|
72
103
|
// Function: tuiEnv
|
|
73
104
|
// The TUI renderer environment: OpenTUI colors plus TUI-only hooks.
|
|
74
105
|
function tuiEnv() {
|
|
@@ -87,14 +118,23 @@ function tuiEnv() {
|
|
|
87
118
|
dirFg: tuiTheme.dirFg,
|
|
88
119
|
errorBg: tuiTheme.errorBg,
|
|
89
120
|
errorFg: tuiTheme.errorFg,
|
|
121
|
+
execFg: tuiTheme.execFg,
|
|
90
122
|
fieldBg: tuiTheme.fieldBg,
|
|
91
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"],
|
|
92
131
|
// Nerd Font icons in rows, on unless `TCMD_ICONS` opts out.
|
|
93
132
|
icons: iconsEnabled(process.env),
|
|
94
133
|
lineFg: tuiTheme.textSecondaryFg,
|
|
95
134
|
linkFg: tuiTheme.linkFg,
|
|
96
135
|
mutedBg: tuiTheme.mutedBg,
|
|
97
136
|
overlayBg: tuiTheme.overlayBg,
|
|
137
|
+
readonlyFg: tuiTheme.readonlyFg,
|
|
98
138
|
selectionBg: tuiTheme.selectionBg,
|
|
99
139
|
surfaceBg: tuiTheme.surfaceBg,
|
|
100
140
|
textFg: tuiTheme.textFg,
|
|
@@ -164,17 +204,23 @@ function keepVisible(box, index, total) {
|
|
|
164
204
|
return top
|
|
165
205
|
}
|
|
166
206
|
|
|
167
|
-
// Keep the file-list cursor row inside the viewport.
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
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).
|
|
171
212
|
function fileListScrollTop(self, data) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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)
|
|
178
224
|
}
|
|
179
225
|
|
|
180
226
|
// Keep the bookmark picker selection inside its viewport.
|
|
@@ -182,21 +228,101 @@ function bookmarkScrollTop(self, data) {
|
|
|
182
228
|
return keepVisible(self.ref.list, unwrap(data.selected) ?? 0, unwrap(data.items)?.length ?? 0)
|
|
183
229
|
}
|
|
184
230
|
|
|
185
|
-
// 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.
|
|
186
262
|
function panelArgScrollTop(self, data) {
|
|
187
|
-
|
|
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
|
|
188
307
|
}
|
|
189
308
|
|
|
190
309
|
// Function: commandBarRows
|
|
191
|
-
// Appends the active pane's marked count as a trailing dimmed entry,
|
|
192
|
-
//
|
|
193
|
-
//
|
|
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.
|
|
194
313
|
function commandBarRows(commandItem) {
|
|
195
314
|
const base = commandItemRows(commandItem)
|
|
196
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
|
|
197
322
|
const items = unwrap(data.items) ?? []
|
|
198
323
|
const marks = commandBarMarksItem(data)
|
|
199
|
-
|
|
324
|
+
const entries = marks ? [...items, marks] : items
|
|
325
|
+
return base(self, { ...data, items: commandBarFit(entries, width) })
|
|
200
326
|
}
|
|
201
327
|
}
|
|
202
328
|
|
|
@@ -208,8 +334,11 @@ function createComponents() {
|
|
|
208
334
|
const commandArgRow = bind(ui, "CommandArgRow", commandArgRowXml, commandArgRowBehaviors(env))
|
|
209
335
|
const crumb = bind(ui, "Crumb", crumbXml, crumbBehaviors(env))
|
|
210
336
|
const fileRow = bind(ui, "FileRow", fileRowXml, fileRowBehaviors(env))
|
|
337
|
+
const filePickerRow = bind(ui, "FilePickerRow", filePickerRowXml, filePickerRowBehaviors(env))
|
|
211
338
|
const filterBar = bind(ui, "FilterBar", filterBarXml, filterBarBehaviors(env))
|
|
212
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 }))
|
|
213
342
|
return {
|
|
214
343
|
BookmarkRow: bookmarkRow,
|
|
215
344
|
CommandArgRow: commandArgRow,
|
|
@@ -231,8 +360,11 @@ function createComponents() {
|
|
|
231
360
|
})),
|
|
232
361
|
CommandPanel: bind(ui, "CommandPanel", commandPanelXml, commandPanelBehaviors({
|
|
233
362
|
...env,
|
|
363
|
+
panelHeight: panelDialogHeight,
|
|
234
364
|
opRows: panelOpRows(commandArgRow),
|
|
235
365
|
argRows: panelArgRows(commandArgRow),
|
|
366
|
+
runRows: panelRunRows(commandArgRow),
|
|
367
|
+
opScrollTop: panelOpScrollTop,
|
|
236
368
|
argScrollTop: panelArgScrollTop,
|
|
237
369
|
})),
|
|
238
370
|
Confirm: bind(ui, "Confirm", confirmXml, confirmBehaviors(env)),
|
|
@@ -247,9 +379,37 @@ function createComponents() {
|
|
|
247
379
|
rows: fileRows(fileRow),
|
|
248
380
|
scrollTop: fileListScrollTop,
|
|
249
381
|
})),
|
|
250
|
-
|
|
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
|
+
}),
|
|
251
406
|
FilterBar: filterBar,
|
|
252
|
-
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
|
+
})),
|
|
253
413
|
Summary: bind(ui, "Summary", summaryXml, summaryBehaviors({
|
|
254
414
|
...env,
|
|
255
415
|
width: paneWidth,
|
|
@@ -261,27 +421,76 @@ function createComponents() {
|
|
|
261
421
|
|
|
262
422
|
const components = createComponents()
|
|
263
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
|
+
|
|
264
464
|
// Function: registerComponents
|
|
265
465
|
// Idempotent registration of the parent components as select.tui host tags.
|
|
266
466
|
function registerComponents() {
|
|
267
467
|
if (registered) return components
|
|
268
468
|
registered = true
|
|
269
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 },
|
|
270
473
|
bookmarklist: components.BookmarkList,
|
|
271
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 },
|
|
272
479
|
commandbar: components.CommandBar,
|
|
273
480
|
commandpanel: components.CommandPanel,
|
|
274
481
|
confirm: components.Confirm,
|
|
275
482
|
entrydetail: components.EntryDetail,
|
|
276
483
|
filelist: components.FileList,
|
|
484
|
+
filepicker: components.FilePicker,
|
|
277
485
|
filepreview: components.FilePreview,
|
|
278
486
|
filterbar: components.FilterBar,
|
|
279
487
|
help: components.Help,
|
|
488
|
+
overlaylist: components.OverlayList,
|
|
280
489
|
summary: components.Summary,
|
|
281
490
|
})
|
|
282
491
|
return components
|
|
283
492
|
}
|
|
284
493
|
|
|
285
|
-
export { components, fileListScrollTop, keepVisible, registerComponents }
|
|
494
|
+
export { components, fileListScrollTop, keepVisible, panelDialogHeight, panelOpScrollTop, registerComponents }
|
|
286
495
|
|
|
287
496
|
// EOF
|
package/src/js/tcmd/tui/index.js
CHANGED
|
@@ -44,16 +44,27 @@ function bookmarkFile() {
|
|
|
44
44
|
return nodePath.join(configHome, "tcmd", "bookmarks.json")
|
|
45
45
|
}
|
|
46
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
|
+
|
|
47
56
|
// Function: main
|
|
48
57
|
// Runs the TUI. `paths` are optional initial directories [left, right].
|
|
49
58
|
async function main(paths = []) {
|
|
50
59
|
const fs = createNodeFs()
|
|
51
|
-
const left = createPane({ fs, path: paths[0]
|
|
52
|
-
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()) })
|
|
53
62
|
let instance = null
|
|
54
63
|
const os = createNodeOs({
|
|
55
64
|
suspend: () => instance?._ownedRenderer?.suspend?.(),
|
|
56
65
|
resume: () => instance?._ownedRenderer?.resume?.(),
|
|
66
|
+
// OSC 52: the renderer knows whether the terminal supports it.
|
|
67
|
+
copy: (text) => instance?._ownedRenderer?.copyToClipboardOSC52?.(text) ?? false,
|
|
57
68
|
})
|
|
58
69
|
const bookmarks = createBookmarks({
|
|
59
70
|
store: createFileBookmarkStore({
|
|
@@ -68,7 +79,10 @@ async function main(paths = []) {
|
|
|
68
79
|
const size = cell({ width: 0, height: 0 })
|
|
69
80
|
const help = cell(false)
|
|
70
81
|
const App = createAppComponent(session, { size, help })
|
|
71
|
-
instance = await render(App, {
|
|
82
|
+
instance = await render(App, {
|
|
83
|
+
useMouse: true,
|
|
84
|
+
exitOnCtrlC: false,
|
|
85
|
+
})
|
|
72
86
|
const renderer = instance?.ctx ?? instance?.renderer
|
|
73
87
|
const current = renderer?.width || renderer?.height
|
|
74
88
|
? { width: renderer.width ?? 0, height: renderer.height ?? 0 }
|
|
@@ -84,6 +98,6 @@ async function main(paths = []) {
|
|
|
84
98
|
return session
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
export { main, readSize }
|
|
101
|
+
export { main, readSize, startupPath }
|
|
88
102
|
|
|
89
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
|
package/src/js/tcmd/tui/theme.js
CHANGED
|
@@ -23,11 +23,16 @@
|
|
|
23
23
|
// from the cursor alone.
|
|
24
24
|
// - Text: primary, secondary (hints) and heading (titles/breadcrumbs).
|
|
25
25
|
// - Borders: secondary (pane box borders, header rules, active/inactive) and
|
|
26
|
-
// primary (the frame and the modal overlays).
|
|
26
|
+
// primary (the frame and the modal overlays). The active pane's
|
|
27
|
+
// border and head rule follow the view mode: the Tiny blue in
|
|
28
|
+
// explorer, the gold accent in commander.
|
|
27
29
|
// - Accent: primary (cursor, active pane label) and secondary (marks).
|
|
28
30
|
// - Semantic: error, warning and tip, each with a background + foreground
|
|
29
31
|
// for filled badges (the command bar status, the confirm dialog).
|
|
30
|
-
// - Content: the file-listing colors (file/link/device/directory
|
|
32
|
+
// - Content: the file-listing colors (file/link/device/directory plus the
|
|
33
|
+
// executable and read-only file states).
|
|
34
|
+
// - Scrollbar: the track and thumb of every scrollable surface (the TUI's
|
|
35
|
+
// slider, mirrored by the web's CSS variables).
|
|
31
36
|
// - Brand: the identity-bar gradient endpoints — "Tiny" stays blue, "Cmd"
|
|
32
37
|
// runs gold to saturated orange.
|
|
33
38
|
|
|
@@ -52,6 +57,8 @@ const DARK = {
|
|
|
52
57
|
// Borders
|
|
53
58
|
borderSecondaryFg: "#33508f",
|
|
54
59
|
borderSecondaryActiveFg: "#f0b429",
|
|
60
|
+
// The active pane's border/head rule in explorer mode: the Tiny blue.
|
|
61
|
+
borderActiveExplorerFg: "#4088ff",
|
|
55
62
|
borderPrimaryFg: "#3d5590",
|
|
56
63
|
borderPrimaryActiveFg: "#ffc233",
|
|
57
64
|
// Accent
|
|
@@ -71,6 +78,13 @@ const DARK = {
|
|
|
71
78
|
linkFg: "#4fc3ff",
|
|
72
79
|
devFg: "#b48eff",
|
|
73
80
|
dirFg: "#5c8cff",
|
|
81
|
+
// Executable files go green, read-only files dimmer than a plain file
|
|
82
|
+
// (`ls --color` / Midnight Commander habits).
|
|
83
|
+
execFg: "#5fd67a",
|
|
84
|
+
readonlyFg: "#7d8ab8",
|
|
85
|
+
// Scrollbar
|
|
86
|
+
scrollbarTrackBg: "transparent",
|
|
87
|
+
scrollbarThumbBg: "#22345e",
|
|
74
88
|
// Brand
|
|
75
89
|
brandTinyFrom: "#4088ff",
|
|
76
90
|
brandTinyTo: "#7cd0ff",
|
|
@@ -99,6 +113,7 @@ const LIGHT = {
|
|
|
99
113
|
// Borders
|
|
100
114
|
borderSecondaryFg: "#9aabd6",
|
|
101
115
|
borderSecondaryActiveFg: "#926000",
|
|
116
|
+
borderActiveExplorerFg: "#1d4ed8",
|
|
102
117
|
borderPrimaryFg: "#8c9cc9",
|
|
103
118
|
borderPrimaryActiveFg: "#a06a00",
|
|
104
119
|
// Accent
|
|
@@ -118,6 +133,11 @@ const LIGHT = {
|
|
|
118
133
|
linkFg: "#0369a1",
|
|
119
134
|
devFg: "#7c3aed",
|
|
120
135
|
dirFg: "#1d4ed8",
|
|
136
|
+
execFg: "#0f7a3d",
|
|
137
|
+
readonlyFg: "#6b7280",
|
|
138
|
+
// Scrollbar
|
|
139
|
+
scrollbarTrackBg: "transparent",
|
|
140
|
+
scrollbarThumbBg: "#34548f",
|
|
121
141
|
// Brand
|
|
122
142
|
brandTinyFrom: "#1d4ed8",
|
|
123
143
|
brandTinyTo: "#3b82f6",
|
|
@@ -141,6 +161,7 @@ const MONO = {
|
|
|
141
161
|
textHeadingFg: undefined,
|
|
142
162
|
borderSecondaryFg: undefined,
|
|
143
163
|
borderSecondaryActiveFg: undefined,
|
|
164
|
+
borderActiveExplorerFg: undefined,
|
|
144
165
|
borderPrimaryFg: undefined,
|
|
145
166
|
borderPrimaryActiveFg: undefined,
|
|
146
167
|
accentFg: undefined,
|
|
@@ -157,6 +178,10 @@ const MONO = {
|
|
|
157
178
|
linkFg: undefined,
|
|
158
179
|
devFg: undefined,
|
|
159
180
|
dirFg: undefined,
|
|
181
|
+
execFg: undefined,
|
|
182
|
+
readonlyFg: undefined,
|
|
183
|
+
scrollbarTrackBg: undefined,
|
|
184
|
+
scrollbarThumbBg: undefined,
|
|
160
185
|
brandTinyFrom: undefined,
|
|
161
186
|
brandTinyTo: undefined,
|
|
162
187
|
brandCmdFrom: undefined,
|