@ssebastien/tcmd 0.0.0-stage → 0.1.0
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 +234 -2
- package/bin/tcmd +7 -0
- package/package.json +37 -5
- package/src/js/tcmd/components/App.html +28 -0
- package/src/js/tcmd/components/App.js +355 -0
- package/src/js/tcmd/components/App.xml +98 -0
- package/src/js/tcmd/components/BookmarkList.html +12 -0
- package/src/js/tcmd/components/BookmarkList.js +73 -0
- package/src/js/tcmd/components/BookmarkList.xml +18 -0
- package/src/js/tcmd/components/BookmarkRow.html +5 -0
- package/src/js/tcmd/components/BookmarkRow.js +40 -0
- package/src/js/tcmd/components/BookmarkRow.xml +10 -0
- package/src/js/tcmd/components/Breadcrumbs.js +27 -0
- package/src/js/tcmd/components/CommandArgRow.js +90 -0
- package/src/js/tcmd/components/CommandArgRow.xml +5 -0
- package/src/js/tcmd/components/CommandBar.html +4 -0
- package/src/js/tcmd/components/CommandBar.js +126 -0
- package/src/js/tcmd/components/CommandBar.xml +4 -0
- package/src/js/tcmd/components/CommandItem.html +3 -0
- package/src/js/tcmd/components/CommandItem.js +78 -0
- package/src/js/tcmd/components/CommandItem.xml +6 -0
- package/src/js/tcmd/components/CommandPalette.html +7 -0
- package/src/js/tcmd/components/CommandPalette.js +58 -0
- package/src/js/tcmd/components/CommandPalette.xml +16 -0
- package/src/js/tcmd/components/CommandPanel.js +98 -0
- package/src/js/tcmd/components/CommandPanel.xml +35 -0
- package/src/js/tcmd/components/Confirm.html +3 -0
- package/src/js/tcmd/components/Confirm.js +36 -0
- package/src/js/tcmd/components/Confirm.xml +7 -0
- package/src/js/tcmd/components/Crumb.html +4 -0
- package/src/js/tcmd/components/Crumb.js +48 -0
- package/src/js/tcmd/components/Crumb.xml +4 -0
- package/src/js/tcmd/components/EntryDetail.html +15 -0
- package/src/js/tcmd/components/EntryDetail.js +194 -0
- package/src/js/tcmd/components/EntryDetail.xml +16 -0
- package/src/js/tcmd/components/FileList.html +1 -0
- package/src/js/tcmd/components/FileList.js +41 -0
- package/src/js/tcmd/components/FileList.xml +7 -0
- package/src/js/tcmd/components/FilePreview.html +5 -0
- package/src/js/tcmd/components/FilePreview.js +110 -0
- package/src/js/tcmd/components/FilePreview.xml +10 -0
- package/src/js/tcmd/components/FileRow.html +5 -0
- package/src/js/tcmd/components/FileRow.js +135 -0
- package/src/js/tcmd/components/FileRow.xml +10 -0
- package/src/js/tcmd/components/FilterBar.html +6 -0
- package/src/js/tcmd/components/FilterBar.js +50 -0
- package/src/js/tcmd/components/FilterBar.xml +6 -0
- package/src/js/tcmd/components/Help.xml +11 -0
- package/src/js/tcmd/components/PaletteRow.html +3 -0
- package/src/js/tcmd/components/PaletteRow.js +43 -0
- package/src/js/tcmd/components/PaletteRow.xml +4 -0
- package/src/js/tcmd/components/RowList.html +1 -0
- package/src/js/tcmd/components/RowList.xml +1 -0
- package/src/js/tcmd/components/Summary.html +3 -0
- package/src/js/tcmd/components/Summary.js +77 -0
- package/src/js/tcmd/components/Summary.xml +3 -0
- package/src/js/tcmd/components/bind.js +20 -0
- package/src/js/tcmd/components/format.js +155 -0
- package/src/js/tcmd/components/icons.js +185 -0
- package/src/js/tcmd/components/index.js +41 -0
- package/src/js/tcmd/components/rows.js +212 -0
- package/src/js/tcmd/core/bookmarks.js +175 -0
- package/src/js/tcmd/core/commands.js +930 -0
- package/src/js/tcmd/core/directory.js +194 -0
- package/src/js/tcmd/core/entries.js +88 -0
- package/src/js/tcmd/core/fs.js +266 -0
- package/src/js/tcmd/core/index.js +39 -0
- package/src/js/tcmd/core/keymap.js +60 -0
- package/src/js/tcmd/core/operations.js +453 -0
- package/src/js/tcmd/core/os.js +128 -0
- package/src/js/tcmd/core/pane.js +74 -0
- package/src/js/tcmd/core/panel.js +383 -0
- package/src/js/tcmd/core/path.js +57 -0
- package/src/js/tcmd/core/script.js +186 -0
- package/src/js/tcmd/core/selection.js +43 -0
- package/src/js/tcmd/core/session.js +405 -0
- package/src/js/tcmd/index.js +53 -0
- package/src/js/tcmd/tui/app.js +140 -0
- package/src/js/tcmd/tui/catalogue.js +300 -0
- package/src/js/tcmd/tui/frame.js +269 -0
- package/src/js/tcmd/tui/help.js +32 -0
- package/src/js/tcmd/tui/index.js +104 -0
- package/src/js/tcmd/tui/theme.js +177 -0
- package/src/js/tcmd/vendor/fzy.js +243 -0
- package/src/js/tcmd/web/app.js +154 -0
- package/src/js/tcmd/web/catalogue.js +159 -0
- package/src/js/tcmd/web/header.html +1 -0
- package/src/js/tcmd/web/header.js +38 -0
- package/src/js/tcmd/web/http.js +79 -0
- package/src/js/tcmd/web/index.js +22 -0
- package/src/js/tcmd/web/main.js +151 -0
- package/src/js/tcmd/web/opfs.js +282 -0
- package/src/js/tcmd/web/seed.js +90 -0
- package/src/js/tcmd/web/server.js +191 -0
- package/src/js/tcmd/web/theme.js +23 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-26
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FilePreview
|
|
7
|
+
// FilePreview: inline preview of the entry under the cursor. Text files show
|
|
8
|
+
// their leading content; other kinds show a metadata card. Renderer-agnostic
|
|
9
|
+
// logic; `FilePreview.xml` / `FilePreview.html` are the presentations.
|
|
10
|
+
//
|
|
11
|
+
// Environment: `textFg`, `textSecondaryFg`, `textHeadingFg`.
|
|
12
|
+
|
|
13
|
+
import { unwrap } from "select/state/cells"
|
|
14
|
+
import { formatSize } from "./format.js"
|
|
15
|
+
|
|
16
|
+
const KIND_LABELS = {
|
|
17
|
+
directory: "directory",
|
|
18
|
+
image: "image",
|
|
19
|
+
media: "audio/video",
|
|
20
|
+
text: "text",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Function: cardText
|
|
24
|
+
// Metadata card for non-text previews; `preview.details` (set by `stat`) adds
|
|
25
|
+
// one indented line each.
|
|
26
|
+
function cardText(preview) {
|
|
27
|
+
if (!preview) return ""
|
|
28
|
+
const lines = []
|
|
29
|
+
lines.push("")
|
|
30
|
+
lines.push(` ${KIND_LABELS[preview.kind] ?? "file"}: ${preview.title}`)
|
|
31
|
+
lines.push(` size: ${formatSize(preview.size)}`)
|
|
32
|
+
for (const detail of preview.details ?? []) lines.push(` ${detail}`)
|
|
33
|
+
lines.push("")
|
|
34
|
+
return lines.join("\n")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Function: hasCard
|
|
38
|
+
// True when the preview is a metadata card (no text body).
|
|
39
|
+
function hasCard(preview) {
|
|
40
|
+
return Boolean(preview && preview.text === null)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Function: hasText
|
|
44
|
+
// True when the preview carries a text body.
|
|
45
|
+
function hasText(preview) {
|
|
46
|
+
return Boolean(preview?.text)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Function: previewTitle
|
|
50
|
+
// "Filename <EXT>" style title; falls back to the kind label when the file has
|
|
51
|
+
// no extension.
|
|
52
|
+
function previewTitle(preview) {
|
|
53
|
+
if (!preview) return "preview"
|
|
54
|
+
const badge = (preview.ext || KIND_LABELS[preview.kind] || "file").toUpperCase()
|
|
55
|
+
return `${preview.title} <${badge}>`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Function: entryCard
|
|
59
|
+
// Metadata card for the entry the session did not preview: a directory, or a
|
|
60
|
+
// file whose kind has no inline preview.
|
|
61
|
+
function entryCard(entry) {
|
|
62
|
+
if (!entry) return ""
|
|
63
|
+
const lines = []
|
|
64
|
+
lines.push("")
|
|
65
|
+
lines.push(` ${entry.isDir ? "directory" : "file"}: ${entry.name}${entry.isDir ? "/" : ""}`)
|
|
66
|
+
if (entry.isDir) lines.push(" open it to list its contents")
|
|
67
|
+
else lines.push(` size: ${formatSize(entry.size)}`)
|
|
68
|
+
lines.push("")
|
|
69
|
+
return lines.join("\n")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Function: headerTitle
|
|
73
|
+
// The inspector title: the preview's own title, else the cursor entry.
|
|
74
|
+
function headerTitle(preview, entry) {
|
|
75
|
+
if (preview) return previewTitle(preview)
|
|
76
|
+
if (entry) {
|
|
77
|
+
const badge = entry.isDir ? "DIR" : (entry.ext || "file").toUpperCase()
|
|
78
|
+
return `${entry.name}${entry.isDir ? "/" : ""} <${badge}>`
|
|
79
|
+
}
|
|
80
|
+
return "inspector"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Function: createBehaviors
|
|
84
|
+
// Builds the FilePreview behavior map for the given renderer environment.
|
|
85
|
+
function createBehaviors(env) {
|
|
86
|
+
return {
|
|
87
|
+
actions: () => " [Open O] [Edit E]",
|
|
88
|
+
actionsFg: () => env.textSecondaryFg,
|
|
89
|
+
cardText: (_self, data) => cardText(unwrap(data.preview)),
|
|
90
|
+
dimFg: () => env.textSecondaryFg,
|
|
91
|
+
emptyText: () => " No file selected\n Move the cursor or press v",
|
|
92
|
+
entryText: (_self, data) => entryCard(unwrap(data.entry)),
|
|
93
|
+
hasCard: (_self, data) => hasCard(unwrap(data.preview)),
|
|
94
|
+
// The metadata card fills in whenever there is a cursor entry but no
|
|
95
|
+
// preview payload (a directory, or a non-text file).
|
|
96
|
+
hasEntryCard: (_self, data) => !unwrap(data.preview) && Boolean(unwrap(data.entry)),
|
|
97
|
+
hasText: (_self, data) => hasText(unwrap(data.preview)),
|
|
98
|
+
headerTitle: (_self, data) => headerTitle(unwrap(data.preview), unwrap(data.entry)),
|
|
99
|
+
isEmpty: (_self, data) => !unwrap(data.preview) && !unwrap(data.entry),
|
|
100
|
+
previewText: (_self, data) => unwrap(data.preview)?.text ?? "",
|
|
101
|
+
previewTitle: (_self, data) => previewTitle(unwrap(data.preview)),
|
|
102
|
+
previewTitleColor: () => env.textHeadingFg,
|
|
103
|
+
show: (_self, data) => Boolean(unwrap(data.show)),
|
|
104
|
+
textFg: () => env.textFg,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { cardText, createBehaviors, entryCard, hasCard, hasText, headerTitle, previewTitle }
|
|
109
|
+
|
|
110
|
+
// EOF
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<box when="show" out:title="headerTitle" out:titleColor="previewTitleColor" border borderStyle="rounded"
|
|
2
|
+
flexDirection="column" flexGrow="1" overflow="hidden">
|
|
3
|
+
<scrollbox flexGrow="1">
|
|
4
|
+
<text when="hasText" out="previewText" out:fg="textFg"></text>
|
|
5
|
+
<text when="hasCard" out="cardText" out:fg="dimFg"></text>
|
|
6
|
+
<text when="hasEntryCard" out="entryText" out:fg="dimFg"></text>
|
|
7
|
+
<text when="isEmpty" out="emptyText" out:fg="dimFg"></text>
|
|
8
|
+
</scrollbox>
|
|
9
|
+
<box height="1"><text out="actions" out:fg="actionsFg"></text></box>
|
|
10
|
+
</box>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-26
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FileRow
|
|
7
|
+
// File listing row behaviors. Read `{ item: entry, index, currentRow,
|
|
8
|
+
// markedRow, active, paneId }`, the same bag shape in both engines.
|
|
9
|
+
// `currentRow`/`markedRow` are plain per-row booleans snapshotted by the
|
|
10
|
+
// `fileRows` builder (see `rows.js`); `active` is the pane-focus cell, read
|
|
11
|
+
// with `unwrap` at the point of use.
|
|
12
|
+
//
|
|
13
|
+
// Environment: `cursorBg`, `cursorInactiveBg`, `currentClass`,
|
|
14
|
+
// `accentSecondaryFg`, `accentFg`, `linkFg`, `dirFg`, `devFg`, `fileFg`,
|
|
15
|
+
// `textSecondaryFg`, `dirClass`, `icons` (show Nerd Font icons).
|
|
16
|
+
|
|
17
|
+
import { unwrap } from "select/state/cells"
|
|
18
|
+
import { formatSize } from "./format.js"
|
|
19
|
+
import { iconFor } from "./icons.js"
|
|
20
|
+
|
|
21
|
+
// Function: rowName
|
|
22
|
+
// The entry name with the directory slash (the icon is its own slot, so it
|
|
23
|
+
// can be dimmer than the name — see `rowIcon`).
|
|
24
|
+
function rowName(entry) {
|
|
25
|
+
if (!entry) return ""
|
|
26
|
+
return entry.isDir ? `${entry.name}/` : entry.name
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Function: rowIcon
|
|
30
|
+
// The entry's Nerd Font icon, empty when the renderer environment keeps the
|
|
31
|
+
// rows icon-free.
|
|
32
|
+
function rowIcon(entry, icons) {
|
|
33
|
+
return icons ? iconFor(entry) : ""
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Function: rowSize
|
|
37
|
+
function rowSize(entry) {
|
|
38
|
+
if (!entry) return ""
|
|
39
|
+
return entry.isDir ? "<dir>" : formatSize(entry.size)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Function: isCurrent
|
|
43
|
+
function isCurrent(index, cursor) {
|
|
44
|
+
return index === cursor
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Function: isMarked
|
|
48
|
+
function isMarked(entry, marked) {
|
|
49
|
+
return Boolean(entry && marked?.has?.(entry.path))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Function: rowMarker
|
|
53
|
+
// The 2-char row marker: "*" for a marked row, ">" for the cursor row — marks
|
|
54
|
+
// win, so a marked-and-current row still reads as marked.
|
|
55
|
+
function rowMarker(data) {
|
|
56
|
+
return rowIsMarked(data) ? "* " : rowCurrent(data) ? "> " : " "
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Function: rowCurrent
|
|
60
|
+
// Whether the row is the pane cursor. The plain `currentRow` flag (snapshotted
|
|
61
|
+
// by the row builder) wins; otherwise fall back to comparing the row index
|
|
62
|
+
// against the shared cursor cell (an unknown index never reads as current).
|
|
63
|
+
// Because the flag is plain, a cursor move rewrites only the two changed row
|
|
64
|
+
// bags instead of re-rendering every row.
|
|
65
|
+
function rowCurrent(data) {
|
|
66
|
+
if (data.currentRow !== undefined) return data.currentRow === true
|
|
67
|
+
if (data.index === undefined) return false
|
|
68
|
+
return isCurrent(data.index, unwrap(data.cursor))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Function: rowIsMarked
|
|
72
|
+
// Whether the row's entry is marked. `bag.markedRow` (a plain per-row boolean)
|
|
73
|
+
// wins; otherwise compare against the shared marked set.
|
|
74
|
+
function rowIsMarked(data) {
|
|
75
|
+
if (data.markedRow !== undefined) return data.markedRow === true
|
|
76
|
+
return isMarked(data.item, unwrap(data.marked))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Function: nameFg
|
|
80
|
+
// The listing color for an entry: a symlink reads as a link, a directory as a
|
|
81
|
+
// directory, a device node as a device, otherwise the plain file color.
|
|
82
|
+
// Symlinks win over directories, matching `dircolors` (a link to a directory
|
|
83
|
+
// renders as a link).
|
|
84
|
+
function nameFg(item, env) {
|
|
85
|
+
if (item?.isSymlink) return env.linkFg
|
|
86
|
+
if (item?.isDir) return env.dirFg
|
|
87
|
+
if (item?.isDevice) return env.devFg
|
|
88
|
+
return env.fileFg
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Function: rowNameFg
|
|
92
|
+
// The name color for a row: a marked row reads orange (the selection signal),
|
|
93
|
+
// the cursor row the bright heading color, otherwise the entry's kind color.
|
|
94
|
+
// Marks win over the cursor so a marked-and-current row still reads as
|
|
95
|
+
// marked, the same precedence the row marker follows.
|
|
96
|
+
function rowNameFg(data, env) {
|
|
97
|
+
if (rowIsMarked(data)) return env.accentSecondaryFg
|
|
98
|
+
if (rowCurrent(data)) return env.textHeadingFg
|
|
99
|
+
return nameFg(data.item, env)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Function: createBehaviors
|
|
103
|
+
// Builds the FileRow behavior map for the given renderer environment.
|
|
104
|
+
function createBehaviors(env) {
|
|
105
|
+
return {
|
|
106
|
+
rowBg: (_self, data) => {
|
|
107
|
+
if (rowCurrent(data)) {
|
|
108
|
+
// The active pane's cursor is the strong accent; the inactive
|
|
109
|
+
// pane keeps a muted cursor so focus is visible from the row
|
|
110
|
+
// alone. A missing `active` (web, single-pane hosts) reads as
|
|
111
|
+
// active.
|
|
112
|
+
return unwrap(data.active) === false ? env.cursorInactiveBg : env.cursorBg
|
|
113
|
+
}
|
|
114
|
+
if (rowIsMarked(data)) return env.accentSecondaryBg
|
|
115
|
+
return undefined
|
|
116
|
+
},
|
|
117
|
+
rowClass: (_self, data) => (rowCurrent(data) ? env.currentClass : null),
|
|
118
|
+
// The icon is its own slot so it can sit dimmer than the name; it
|
|
119
|
+
// collapses when the environment keeps rows icon-free.
|
|
120
|
+
hasIcon: () => Boolean(env.icons),
|
|
121
|
+
rowIcon: (_self, data) => rowIcon(data.item, Boolean(env.icons)),
|
|
122
|
+
rowIconFg: () => env.textSecondaryFg,
|
|
123
|
+
rowMarker: (_self, data) => rowMarker(data),
|
|
124
|
+
rowMarkerFg: (_self, data) => (rowIsMarked(data) ? env.accentSecondaryFg : env.accentFg),
|
|
125
|
+
rowName: (_self, data) => rowName(data.item),
|
|
126
|
+
rowNameClass: (_self, data) => (data.item?.isDir ? env.dirClass : null),
|
|
127
|
+
rowNameFg: (_self, data) => rowNameFg(data, env),
|
|
128
|
+
rowSize: (_self, data) => rowSize(data.item),
|
|
129
|
+
rowSizeFg: () => env.textSecondaryFg,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { createBehaviors, isMarked, nameFg, rowIcon, rowName, rowNameFg, rowSize }
|
|
134
|
+
|
|
135
|
+
// EOF
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="paneId:paneId,index:index!list:select">
|
|
2
|
+
<text out="rowMarker" width="2" out:fg="rowMarkerFg"></text>
|
|
3
|
+
<box flexGrow="1" minWidth="0" overflow="hidden" flexDirection="row" columnGap="1">
|
|
4
|
+
<text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
|
|
5
|
+
<text out="rowName" out:fg="rowNameFg" wrapMode="none" truncate="true"></text>
|
|
6
|
+
</box>
|
|
7
|
+
<box width="9" justifyContent="flex-end">
|
|
8
|
+
<text out="rowSize" out:fg="rowSizeFg" wrapMode="none"></text>
|
|
9
|
+
</box>
|
|
10
|
+
</box>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="filter-bar" when="visible">
|
|
2
|
+
<span class="filter-prompt" out="prompt"></span>
|
|
3
|
+
<input when="editing" ref="query" inout="query" class="filter-input" />
|
|
4
|
+
<span when="applied" class="filter-label" out="label"></span>
|
|
5
|
+
<span class="filter-alert" out="alert"></span>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-30
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FilterBar
|
|
7
|
+
// FilterBar: the fuzzy filter indicator, folded into the pane header so it
|
|
8
|
+
// never shifts the listing. While the pane owns the filter input
|
|
9
|
+
// (`filtering === paneId`) it shows a compact focused input bound to the pane's
|
|
10
|
+
// `query`; once accepted it degrades to the same `/ query` indication, so the
|
|
11
|
+
// pane is navigable again and the filter can still be cleared with Esc. When
|
|
12
|
+
// the query matches no entry it shows a `no match` alert. The key hints live in
|
|
13
|
+
// the command bar, not here.
|
|
14
|
+
//
|
|
15
|
+
// Environment: `accentFg` (the applied label), `textSecondaryFg` (the `/`
|
|
16
|
+
// prompt), `warningFg` (the no-match alert).
|
|
17
|
+
|
|
18
|
+
import { unwrap } from "select/state/cells"
|
|
19
|
+
|
|
20
|
+
// Function: createBehaviors
|
|
21
|
+
// Builds the FilterBar behavior map for the given renderer environment.
|
|
22
|
+
function createBehaviors(env) {
|
|
23
|
+
// The pane that owns the input is the one typing into it.
|
|
24
|
+
const editing = (_self, data) => unwrap(data.filtering) === unwrap(data.paneId)
|
|
25
|
+
const queryText = (_self, data) => `${unwrap(data.query) ?? ""}`
|
|
26
|
+
const activeQuery = (self, data) => queryText(self, data).trim()
|
|
27
|
+
const matchCount = (_self, data) => (unwrap(data.entries) ?? []).length
|
|
28
|
+
return {
|
|
29
|
+
// The row exists while editing or once a non-empty filter is applied.
|
|
30
|
+
alert: (self, data) =>
|
|
31
|
+
activeQuery(self, data).length > 0 && matchCount(self, data) === 0 ? "no match" : "",
|
|
32
|
+
alertFg: () => env.warningFg,
|
|
33
|
+
applied: (self, data) => !editing(self, data) && activeQuery(self, data).length > 0,
|
|
34
|
+
editing,
|
|
35
|
+
focused: editing,
|
|
36
|
+
// The bare query (shared with the web presentation).
|
|
37
|
+
label: (self, data) => queryText(self, data),
|
|
38
|
+
labelFg: () => env.accentFg,
|
|
39
|
+
// TUI: the static label renders the full `/ query`, so the editing
|
|
40
|
+
// input and the accepted state read the same.
|
|
41
|
+
labelText: (self, data) => `/ ${queryText(self, data)}`,
|
|
42
|
+
prompt: () => "/ ",
|
|
43
|
+
promptFg: () => env.textSecondaryFg,
|
|
44
|
+
visible: (self, data) => editing(self, data) || activeQuery(self, data).length > 0,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { createBehaviors }
|
|
49
|
+
|
|
50
|
+
// EOF
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<box when="visible" flexDirection="row" height="1" flexShrink="0">
|
|
2
|
+
<text out="prompt" out:fg="promptFg"></text>
|
|
3
|
+
<input when="editing" ref="query" inout="query" out:focused="focused" width="14" height="1"></input>
|
|
4
|
+
<text when="applied" out="labelText" out:fg="labelFg" wrapMode="none" truncate="true"></text>
|
|
5
|
+
<text out="alert" out:fg="alertFg" wrapMode="none"></text>
|
|
6
|
+
</box>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<box when="open" width="100%" height="100%" alignItems="center" justifyContent="center">
|
|
2
|
+
<box width="72%" height="82%" border borderStyle="rounded"
|
|
3
|
+
out:title="title" out:titleColor="titleFg" out:borderColor="borderFg"
|
|
4
|
+
out:backgroundColor="bg" flexDirection="column"
|
|
5
|
+
paddingLeft="2" paddingRight="2" paddingTop="1" paddingBottom="1">
|
|
6
|
+
<scrollbox flexGrow="1">
|
|
7
|
+
<text out="text" out:fg="textFg"></text>
|
|
8
|
+
</scrollbox>
|
|
9
|
+
<text out="hint" out:fg="dimFg"></text>
|
|
10
|
+
</box>
|
|
11
|
+
</box>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-26
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/PaletteRow
|
|
7
|
+
// Command-palette row behaviors. Read `{ item: command, index, selected }`, the
|
|
8
|
+
// same bag shape in both engines. Selection is snapshotted by the row builder
|
|
9
|
+
// into a plain `selectedRow` boolean and read through the shared `isSelected`
|
|
10
|
+
// predicate, with the `selected` cell as fallback for hand-built bags. Script
|
|
11
|
+
// rows carry a `detail` (operation signature, fileset count) appended to the
|
|
12
|
+
// label.
|
|
13
|
+
//
|
|
14
|
+
// Environment: `accentFg`, `textFg`, `selectionBg` (TUI row background),
|
|
15
|
+
// `selectedClass` (web).
|
|
16
|
+
|
|
17
|
+
import { isSelected } from "./rows.js"
|
|
18
|
+
|
|
19
|
+
// Function: paletteLabel
|
|
20
|
+
// "Switch [Tab]" for commands; script rows append their detail.
|
|
21
|
+
function paletteLabel(command) {
|
|
22
|
+
if (!command) return ""
|
|
23
|
+
const keys = command.keys ? ` [${command.keys}]` : ""
|
|
24
|
+
const detail = command.detail ? ` ${command.detail}` : ""
|
|
25
|
+
return `${command.title}${keys}${detail}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Function: createBehaviors
|
|
29
|
+
// Builds the PaletteRow behavior map for the given renderer environment.
|
|
30
|
+
function createBehaviors(env) {
|
|
31
|
+
return {
|
|
32
|
+
paletteLabel: (_self, data) => paletteLabel(data.item),
|
|
33
|
+
rowBg: (_self, data) => (isSelected(data) ? env.selectionBg : undefined),
|
|
34
|
+
rowClass: (_self, data) => (isSelected(data) ? env.selectedClass : null),
|
|
35
|
+
rowFg: (_self, data) => (isSelected(data) ? env.accentFg : env.textFg),
|
|
36
|
+
// TUI: a marker so the selection is not color-only.
|
|
37
|
+
rowMarker: (_self, data) => (isSelected(data) ? ">" : " "),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { createBehaviors, paletteLabel }
|
|
42
|
+
|
|
43
|
+
// EOF
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<nav out:class="listClass" out="rows"></nav>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<box out="rows" flexDirection="row" height="1"></box>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-26
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/Summary
|
|
7
|
+
// PathSummary: one compact line with entry counts and disk availability.
|
|
8
|
+
// Renderer-agnostic logic; `Summary.xml` / `Summary.html` provide the TUI and
|
|
9
|
+
// web presentations.
|
|
10
|
+
//
|
|
11
|
+
// Environment (injected by the renderer binding):
|
|
12
|
+
// - `lineFg`: color/token for the summary text (bound by the TUI presentation)
|
|
13
|
+
// - `width(self)`: pane width in cells (optional; defaults to 0)
|
|
14
|
+
// - `elide(text, width)`: truncate to the pane width (optional; defaults to
|
|
15
|
+
// the full text)
|
|
16
|
+
// - `measure(text)`: display width of `text` (optional; defaults to
|
|
17
|
+
// `text.length`)
|
|
18
|
+
|
|
19
|
+
import { unwrap } from "select/state/cells"
|
|
20
|
+
import { formatCounts, formatDisk, formatSize } from "./format.js"
|
|
21
|
+
|
|
22
|
+
// Constant: INDENT
|
|
23
|
+
// Leading inset so the summary aligns with the detail line above it.
|
|
24
|
+
const INDENT = " "
|
|
25
|
+
|
|
26
|
+
// Function: summaryText
|
|
27
|
+
// The full, untruncated summary line for `entries` and `disk`.
|
|
28
|
+
function summaryText(entries, disk) {
|
|
29
|
+
let text = formatCounts(entries)
|
|
30
|
+
const diskText = formatDisk(disk)
|
|
31
|
+
if (diskText) text += ` · ${diskText}`
|
|
32
|
+
return `${INDENT}${text}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Function: summaryCounts
|
|
36
|
+
// Just the counts part, the first thing a summary drops information for.
|
|
37
|
+
function summaryCounts(entries) {
|
|
38
|
+
return formatCounts(entries)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function: summaryShortDisk
|
|
42
|
+
// Compact disk availability, e.g. "12G free".
|
|
43
|
+
function summaryShortDisk(disk) {
|
|
44
|
+
return disk ? `${formatSize(disk.free)} free` : ""
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Function: createBehaviors
|
|
48
|
+
// Builds the Summary behavior map for the given renderer environment.
|
|
49
|
+
function createBehaviors(env) {
|
|
50
|
+
const measure = env.measure ?? ((text) => `${text}`.length)
|
|
51
|
+
return {
|
|
52
|
+
line: (self, data) => {
|
|
53
|
+
const text = summaryText(unwrap(data.entries), unwrap(data.disk))
|
|
54
|
+
const width = env.width ? env.width(self) : 0
|
|
55
|
+
return env.elide ? env.elide(text, width) : text
|
|
56
|
+
},
|
|
57
|
+
lineFg: () => env.lineFg,
|
|
58
|
+
// TUI: degrade by information value instead of chopping mid-token:
|
|
59
|
+
// full line -> short disk -> counts only -> elided counts.
|
|
60
|
+
priorityLine: (self, data) => {
|
|
61
|
+
const entries = unwrap(data.entries)
|
|
62
|
+
const disk = unwrap(data.disk)
|
|
63
|
+
const width = env.width ? env.width(self) : 0
|
|
64
|
+
const full = summaryText(entries, disk)
|
|
65
|
+
if (!width || measure(full) <= width) return full
|
|
66
|
+
const counts = `${INDENT}${summaryCounts(entries)}`
|
|
67
|
+
const short = disk ? `${counts} · ${summaryShortDisk(disk)}` : counts
|
|
68
|
+
if (measure(short) <= width) return short
|
|
69
|
+
if (measure(counts) <= width) return counts
|
|
70
|
+
return env.elide ? env.elide(counts, width) : counts
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { createBehaviors, summaryCounts, summaryShortDisk, summaryText }
|
|
76
|
+
|
|
77
|
+
// EOF
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-27
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/bind
|
|
7
|
+
// Binds shared component logic to a renderer template. Both select.js and
|
|
8
|
+
// select.tui implement the same `ui.component(name).does(behaviors).using(source)`
|
|
9
|
+
// contract, so the catalogues share this one binding idiom and only differ in
|
|
10
|
+
// the `ui` runtime they pass in.
|
|
11
|
+
|
|
12
|
+
// Function: bind
|
|
13
|
+
// Returns the component `name` bound to `source` with `behaviors`.
|
|
14
|
+
function bind(ui, name, source, behaviors) {
|
|
15
|
+
return ui.component(name).does(behaviors).using(source)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { bind }
|
|
19
|
+
|
|
20
|
+
// EOF
|