@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
|
@@ -4,90 +4,185 @@
|
|
|
4
4
|
// Created: 2026-10-02
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/Help
|
|
7
|
-
// Shared behavior for the `?` key reference overlay, plus the reference
|
|
8
|
-
// itself
|
|
9
|
-
//
|
|
10
|
-
// (`
|
|
7
|
+
// Shared behavior for the `?` key reference overlay, plus the reference data
|
|
8
|
+
// itself. The categories are built from the live keymap and command registry,
|
|
9
|
+
// so they cannot drift from the actual bindings. The TUI keeps the bracket
|
|
10
|
+
// shorthand (`[Open ⏎]`); the web renders toolbar-like action chips with Lucide
|
|
11
|
+
// icons and key badges. Rows are balanced across two columns with bright
|
|
12
|
+
// uppercase section headings. Both presentations (`Help.xml` / `Help.html`)
|
|
13
|
+
// render the same row data through `HelpRow`.
|
|
11
14
|
|
|
12
15
|
import { unwrap } from "select/state/cells"
|
|
13
|
-
import { DEFAULT_KEYMAP } from "../core/keymap.js"
|
|
16
|
+
import { DEFAULT_KEYMAP, keyGlyph } from "../core/keymap.js"
|
|
14
17
|
import { getCommands } from "../core/commands.js"
|
|
18
|
+
import { commandLabel, keyGlyph as keyLabelGlyph } from "./CommandItem.js"
|
|
15
19
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
pageup: "PgUp",
|
|
30
|
-
pagedown: "PgDn",
|
|
31
|
-
escape: "Esc",
|
|
32
|
-
backspace: "Bksp",
|
|
33
|
-
home: "Home",
|
|
34
|
-
end: "End",
|
|
35
|
-
}
|
|
36
|
-
let base = named[name] ?? (name.length === 1 ? name.toUpperCase() : `${name}`)
|
|
37
|
-
if (chord.ctrl) base = `^${base}`
|
|
38
|
-
if (chord.meta) base = `⌘${base}`
|
|
39
|
-
if (chord.shift && name !== "tab") base = `⇧${base}`
|
|
40
|
-
return base
|
|
41
|
-
}
|
|
20
|
+
// Constant: BRACKET
|
|
21
|
+
// The shorthand delimiters, shared with the command bar.
|
|
22
|
+
const OPEN = "["
|
|
23
|
+
const CLOSE = "]"
|
|
24
|
+
|
|
25
|
+
// Constant: CLOSE_HINT
|
|
26
|
+
// The footer shorthand: Esc dismisses the overlay.
|
|
27
|
+
const CLOSE_HINT = { title: "Close", keys: "Esc" }
|
|
28
|
+
|
|
29
|
+
// Constant: BOLD
|
|
30
|
+
// OpenTUI's `TextAttributes.BOLD` bit, set on section headings through the
|
|
31
|
+
// TUI's `out:attributes` slot. The web bolds through CSS instead.
|
|
32
|
+
const BOLD = 1
|
|
42
33
|
|
|
43
34
|
// Constant: GROUP_ORDER / GROUP_TITLES
|
|
44
35
|
// Help sections, in display order.
|
|
45
|
-
const GROUP_ORDER = ["nav", "pane", "list", "bookmark", "view", "file", "panel", "filter", "app"]
|
|
36
|
+
const GROUP_ORDER = ["nav", "pane", "list", "bookmark", "view", "file", "history", "trash", "panel", "filter", "app"]
|
|
46
37
|
const GROUP_TITLES = {
|
|
47
38
|
app: "Session",
|
|
48
39
|
bookmark: "Bookmarks",
|
|
49
40
|
file: "Files",
|
|
50
41
|
filter: "Filter",
|
|
42
|
+
history: "History",
|
|
51
43
|
list: "Selection",
|
|
52
44
|
nav: "Navigation",
|
|
53
45
|
panel: "Commands",
|
|
54
46
|
pane: "Panes",
|
|
47
|
+
trash: "Trash",
|
|
55
48
|
view: "View",
|
|
56
49
|
}
|
|
57
50
|
|
|
58
|
-
//
|
|
59
|
-
// The
|
|
60
|
-
|
|
51
|
+
// Toolbar actions and key-reference entries share Lucide icons in the web UI.
|
|
52
|
+
// The TUI keeps its compact textual row layout.
|
|
53
|
+
const HELP_ICONS = {
|
|
54
|
+
"nav.up": "arrow-up",
|
|
55
|
+
"nav.down": "arrow-down",
|
|
56
|
+
"nav.jumpUp": "arrow-up",
|
|
57
|
+
"nav.jumpDown": "arrow-down",
|
|
58
|
+
"nav.pageUp": "arrow-up",
|
|
59
|
+
"nav.pageDown": "arrow-down",
|
|
60
|
+
"nav.home": "arrow-up",
|
|
61
|
+
"nav.end": "arrow-down",
|
|
62
|
+
"pane.enter": "arrow-right",
|
|
63
|
+
"pane.up": "arrow-up",
|
|
64
|
+
"pane.back": "arrow-left",
|
|
65
|
+
"pane.forward": "arrow-right",
|
|
66
|
+
"pane.toggle": "columns-2",
|
|
67
|
+
"pane.reload": "refresh-cw",
|
|
68
|
+
"list.mark": "check",
|
|
69
|
+
"bookmark.open": "bookmark",
|
|
70
|
+
"view.togglePreview": "panel-right",
|
|
71
|
+
"view.maximize": "maximize",
|
|
72
|
+
"view.toggleMode": "columns-2",
|
|
73
|
+
"file.mkdir": "folder-plus",
|
|
74
|
+
"file.open": "file",
|
|
75
|
+
"file.edit": "file-text",
|
|
76
|
+
"history.open": "refresh-cw",
|
|
77
|
+
"history.undo": "arrow-left",
|
|
78
|
+
"history.redo": "arrow-right",
|
|
79
|
+
"trash.open": "trash",
|
|
80
|
+
"panel.open": "terminal",
|
|
81
|
+
"panel.shell": "terminal",
|
|
82
|
+
"filter.start": "search",
|
|
83
|
+
"app.quit": "power",
|
|
84
|
+
"app.help": "circle-question-mark",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Function: helpGroups
|
|
88
|
+
// The categorized key reference as `{ title, rows }` groups in display order.
|
|
89
|
+
// One row per command, with every distinct chord from the live keymap. The
|
|
90
|
+
// registry provides the action title.
|
|
91
|
+
function helpGroups() {
|
|
61
92
|
const byId = new Map(getCommands().map((command) => [command.id, command]))
|
|
62
93
|
const groups = new Map()
|
|
63
|
-
const seen = new Set()
|
|
64
94
|
for (const binding of DEFAULT_KEYMAP) {
|
|
65
95
|
const command = byId.get(binding.command)
|
|
66
96
|
if (!command) continue
|
|
67
97
|
const group = binding.command.split(".")[0]
|
|
68
98
|
if (!groups.has(group)) groups.set(group, new Map())
|
|
69
99
|
const rows = groups.get(group)
|
|
70
|
-
if (!rows.has(binding.command)) rows.set(binding.command, { keys: [], title: command.title })
|
|
100
|
+
if (!rows.has(binding.command)) rows.set(binding.command, { keys: [], id: binding.command, title: command.title })
|
|
71
101
|
const row = rows.get(binding.command)
|
|
72
102
|
const glyph = keyGlyph(binding)
|
|
73
|
-
if (!row.keys.includes(glyph)
|
|
74
|
-
row.keys.push(glyph)
|
|
75
|
-
seen.add(`${binding.command}:${glyph}`)
|
|
76
|
-
}
|
|
103
|
+
if (!row.keys.includes(glyph)) row.keys.push(glyph)
|
|
77
104
|
}
|
|
78
|
-
const
|
|
105
|
+
const out = []
|
|
79
106
|
for (const group of GROUP_ORDER) {
|
|
80
107
|
const rows = groups.get(group)
|
|
81
108
|
if (!rows) continue
|
|
82
|
-
|
|
109
|
+
const list = []
|
|
83
110
|
for (const row of rows.values()) {
|
|
84
|
-
|
|
111
|
+
list.push({ id: row.id, title: row.title, keys: row.keys.map(keyLabelGlyph).join("/") })
|
|
85
112
|
}
|
|
113
|
+
out.push({ title: GROUP_TITLES[group] ?? group, rows: list })
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Function: helpLines
|
|
119
|
+
// The flat reference: group headings and rows, one per line, blank line
|
|
120
|
+
// between groups (the single-column fallback).
|
|
121
|
+
function helpLines() {
|
|
122
|
+
const lines = []
|
|
123
|
+
for (const group of helpGroups()) {
|
|
124
|
+
lines.push(group.title)
|
|
125
|
+
for (const row of group.rows) lines.push(commandLabel(row).trim())
|
|
86
126
|
lines.push("")
|
|
87
127
|
}
|
|
88
128
|
return lines
|
|
89
129
|
}
|
|
90
130
|
|
|
131
|
+
// Function: helpBlocks
|
|
132
|
+
// The reference split into two columns balanced by line count, as flat lists of
|
|
133
|
+
// renderable blocks: `{ type: "section", label }` headings,
|
|
134
|
+
// `{ type: "entry", open, head, keys, close }` rows and `{ type: "spacer" }`
|
|
135
|
+
// blank lines between groups. Every entry title and key is padded to the widest
|
|
136
|
+
// one across both columns, so all brackets share one width and the keys align
|
|
137
|
+
// down each column (keys right-aligned against the closing bracket).
|
|
138
|
+
function helpBlocks() {
|
|
139
|
+
const groups = helpGroups()
|
|
140
|
+
const commands = new Map(getCommands().map((command) => [command.id, command]))
|
|
141
|
+
let titleWidth = 0
|
|
142
|
+
let keyWidth = 0
|
|
143
|
+
for (const group of groups) {
|
|
144
|
+
for (const row of group.rows) {
|
|
145
|
+
titleWidth = Math.max(titleWidth, row.title.length)
|
|
146
|
+
keyWidth = Math.max(keyWidth, keyLabelGlyph(row.keys).length)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const columns = [[], []]
|
|
150
|
+
const loads = [0, 0]
|
|
151
|
+
for (const group of groups) {
|
|
152
|
+
const at = loads[0] <= loads[1] ? 0 : 1
|
|
153
|
+
columns[at].push({ type: "section", key: `section:${group.title}`, label: group.title.toUpperCase() })
|
|
154
|
+
for (const row of group.rows) {
|
|
155
|
+
const command = commands.get(row.id)
|
|
156
|
+
// The compact web badge shows the command's canonical key when the
|
|
157
|
+
// registry has one (lowercase `b`, `Spc`, `⇥`); otherwise fall back
|
|
158
|
+
// to the keymap chords, which may list several alternatives.
|
|
159
|
+
const shortcut = command?.keys ? keyLabelGlyph(command.keys) : keyLabelGlyph(row.keys)
|
|
160
|
+
columns[at].push({
|
|
161
|
+
type: "entry",
|
|
162
|
+
key: `entry:${row.id}`,
|
|
163
|
+
icon: HELP_ICONS[row.id] ?? "chevron-right",
|
|
164
|
+
title: row.title,
|
|
165
|
+
shortcut,
|
|
166
|
+
open: OPEN,
|
|
167
|
+
head: `${row.title.padEnd(titleWidth)} `,
|
|
168
|
+
keys: keyLabelGlyph(row.keys).padStart(keyWidth),
|
|
169
|
+
close: CLOSE,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
// A blank row separates the groups (and costs one line, like the
|
|
173
|
+
// heading).
|
|
174
|
+
columns[at].push({ type: "spacer", key: `spacer:${group.title}` })
|
|
175
|
+
// A group costs its rows plus its heading and the blank after it.
|
|
176
|
+
loads[at] += group.rows.length + 2
|
|
177
|
+
}
|
|
178
|
+
// The blank line separates groups, so the bottom of each column ends on its
|
|
179
|
+
// last entry instead.
|
|
180
|
+
for (const column of columns) {
|
|
181
|
+
if (column[column.length - 1]?.type === "spacer") column.pop()
|
|
182
|
+
}
|
|
183
|
+
return { left: columns[0], right: columns[1] }
|
|
184
|
+
}
|
|
185
|
+
|
|
91
186
|
// Function: createBehaviors
|
|
92
187
|
// Builds the Help behavior map for the given renderer environment.
|
|
93
188
|
function createBehaviors(env) {
|
|
@@ -95,15 +190,71 @@ function createBehaviors(env) {
|
|
|
95
190
|
bg: () => env.theme.surfaceBg,
|
|
96
191
|
borderFg: () => env.theme.borderPrimaryActiveFg,
|
|
97
192
|
dimFg: () => env.theme.textSecondaryFg,
|
|
98
|
-
|
|
193
|
+
leftRows: (self, data) => env.rows?.(self, data, "left") ?? [],
|
|
194
|
+
rightRows: (self, data) => env.rows?.(self, data, "right") ?? [],
|
|
195
|
+
// The reference has no cursor for the scrollbox to follow, so its
|
|
196
|
+
// vertical bar is driven here: visible only while the two columns
|
|
197
|
+
// actually overflow the viewport. The viewport height is derived from
|
|
198
|
+
// the injected `size` (so the bar tracks a resize) and the dialog's own
|
|
199
|
+
// geometry (`Help.xml`: 82% tall, minus padding, borders, rule and
|
|
200
|
+
// footer).
|
|
201
|
+
scrollbar: (self, data) => {
|
|
202
|
+
const scroll = self?.ref?.scroll
|
|
203
|
+
const bar = scroll?.verticalScrollBar
|
|
204
|
+
if (!bar) return ""
|
|
205
|
+
const height = unwrap(data?.size)?.height ?? self?.ctx?.height ?? 0
|
|
206
|
+
const viewport = height > 0 ? Math.floor(height * 0.82) - 6 : 0
|
|
207
|
+
const columns = helpBlocks()
|
|
208
|
+
const rows = Math.max(columns.left.length, columns.right.length)
|
|
209
|
+
const want = viewport > 0 && rows > viewport
|
|
210
|
+
if (bar.visible !== want) bar.visible = want
|
|
211
|
+
return ""
|
|
212
|
+
},
|
|
99
213
|
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
100
|
-
|
|
214
|
+
// The footer shorthand reads like the bar: dim brackets, bright title,
|
|
215
|
+
// accent key.
|
|
216
|
+
hintOpen: () => OPEN,
|
|
217
|
+
hintHead: () => `${CLOSE_HINT.title} `,
|
|
218
|
+
hintKeys: () => CLOSE_HINT.keys,
|
|
219
|
+
hintClose: () => CLOSE,
|
|
220
|
+
bracketFg: () => env.theme.textSecondaryFg,
|
|
221
|
+
headFg: () => env.theme.textFg,
|
|
222
|
+
keyFg: () => env.theme.accentFg,
|
|
223
|
+
// Separator rule above the footer (a top border on a one-row box).
|
|
224
|
+
rule: () => ["top"],
|
|
225
|
+
ruleFg: () => env.theme.textSecondaryFg,
|
|
101
226
|
textFg: () => env.theme.textFg,
|
|
102
227
|
title: () => " Keys ",
|
|
103
228
|
titleFg: () => env.theme.textHeadingFg,
|
|
104
229
|
}
|
|
105
230
|
}
|
|
106
231
|
|
|
107
|
-
|
|
232
|
+
// Function: createRowBehaviors
|
|
233
|
+
// Builds the HelpRow behavior map (one section heading or one key entry).
|
|
234
|
+
function createRowBehaviors(env) {
|
|
235
|
+
return {
|
|
236
|
+
isSection: (_self, data) => data.item?.type === "section",
|
|
237
|
+
isEntry: (_self, data) => data.item?.type === "entry",
|
|
238
|
+
section: (_self, data) => data.item?.label ?? "",
|
|
239
|
+
// Sections are bright; the TUI adds bold via OpenTUI's attribute bit
|
|
240
|
+
// (the web bolds through CSS instead).
|
|
241
|
+
sectionFg: () => env.theme.textHeadingFg,
|
|
242
|
+
sectionAttrs: () => BOLD,
|
|
243
|
+
open: (_self, data) => data.item?.open ?? "",
|
|
244
|
+
head: (_self, data) => data.item?.head ?? "",
|
|
245
|
+
keys: (_self, data) => data.item?.keys ?? "",
|
|
246
|
+
close: (_self, data) => data.item?.close ?? "",
|
|
247
|
+
hasIcon: (_self, data) => Boolean(env.theme.lucideIcons && data.item?.icon),
|
|
248
|
+
iconHref: (_self, data) =>
|
|
249
|
+
env.theme.lucideIcons && data.item?.icon ? `#lucide-${data.item.icon}` : "",
|
|
250
|
+
actionTitle: (_self, data) => data.item?.title ?? `${data.item?.head ?? ""}`.trim(),
|
|
251
|
+
shortcut: (_self, data) => data.item?.shortcut ?? `${data.item?.keys ?? ""}`.trim(),
|
|
252
|
+
bracketFg: () => env.theme.textSecondaryFg,
|
|
253
|
+
headFg: () => env.theme.textFg,
|
|
254
|
+
keyFg: () => env.theme.accentFg,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { createBehaviors, createRowBehaviors, helpBlocks, helpGroups, helpLines, keyGlyph }
|
|
108
259
|
|
|
109
260
|
// EOF
|
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
out:title="title" out:titleColor="titleFg" out:borderColor="borderFg"
|
|
4
4
|
out:backgroundColor="bg" flexDirection="column"
|
|
5
5
|
paddingLeft="2" paddingRight="2" paddingTop="1" paddingBottom="1">
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<helpscroll out="scrollbar" ref="scroll" flexGrow="1" overflow="hidden">
|
|
7
|
+
<box flexDirection="row" overflow="hidden">
|
|
8
|
+
<box out="leftRows" flexDirection="column" flexGrow="1" flexBasis="0" overflow="hidden"></box>
|
|
9
|
+
<box out="rightRows" flexDirection="column" flexGrow="1" flexBasis="0" overflow="hidden" paddingLeft="2"></box>
|
|
10
|
+
</box>
|
|
11
|
+
</helpscroll>
|
|
12
|
+
<box height="1" flexShrink="0" out:border="rule" out:borderColor="ruleFg"></box>
|
|
13
|
+
<box height="1" flexShrink="0" flexDirection="row" overflow="hidden">
|
|
14
|
+
<text out="hintOpen" out:fg="bracketFg" wrapMode="none"></text>
|
|
15
|
+
<text out="hintHead" out:fg="headFg" wrapMode="none"></text>
|
|
16
|
+
<text out="hintKeys" out:fg="keyFg" wrapMode="none"></text>
|
|
17
|
+
<text out="hintClose" out:fg="bracketFg" wrapMode="none"></text>
|
|
18
|
+
</box>
|
|
10
19
|
</box>
|
|
11
20
|
</box>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="help-block">
|
|
2
|
+
<span class="help-section" when="isSection" out="section"></span>
|
|
3
|
+
<span class="help-entry" when="isEntry" role="group">
|
|
4
|
+
<svg class="help-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="iconHref"></use></svg>
|
|
5
|
+
<span class="help-title" out="actionTitle"></span>
|
|
6
|
+
<kbd class="help-key" out="shortcut"></kbd>
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<box height="1" flexDirection="row" flexShrink="0" overflow="hidden">
|
|
2
|
+
<text when="isSection" out="section" out:fg="sectionFg" out:attributes="sectionAttrs" wrapMode="none"></text>
|
|
3
|
+
<text when="isEntry" out="open" out:fg="bracketFg" wrapMode="none"></text>
|
|
4
|
+
<text when="isEntry" out="head" out:fg="headFg" wrapMode="none"></text>
|
|
5
|
+
<text when="isEntry" out="keys" out:fg="keyFg" wrapMode="none"></text>
|
|
6
|
+
<text when="isEntry" out="close" out:fg="bracketFg" wrapMode="none"></text>
|
|
7
|
+
</box>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="overlay-list" when="open" role="dialog" aria-modal="true" aria-label="History and trash" on:click="!dialog:overlay-cancel">
|
|
2
|
+
<div class="overlay-dialog" on:click="dialogStop">
|
|
3
|
+
<h2 out="title"></h2>
|
|
4
|
+
<p class="overlay-empty" when="emptyVisible" out="empty"></p>
|
|
5
|
+
<ul class="overlay-rows" out="rows"></ul>
|
|
6
|
+
<footer out="footer"></footer>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-17
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/OverlayList
|
|
7
|
+
// The shared modal list overlay: the undo history (`u`) and the trash browser
|
|
8
|
+
// (`⇧T`). One component, two modes — `history` lists the journal (a row jumps
|
|
9
|
+
// the file state to that point), `trash` lists trashed entries (a row restores
|
|
10
|
+
// one). The row logic lives in `OverlayRow.js`; this parent maps its `items` to
|
|
11
|
+
// rows and switches title/footer/empty text on the mode.
|
|
12
|
+
//
|
|
13
|
+
// Environment: `overlayBg`, `borderPrimaryActiveFg`, `textHeadingFg` (title),
|
|
14
|
+
// `textSecondaryFg` (footer/empty), `rows(data)` returning the keyed rows and
|
|
15
|
+
// `scrollTop(self, data)` (TUI viewport tracking).
|
|
16
|
+
|
|
17
|
+
import { unwrap } from "select/state/cells"
|
|
18
|
+
|
|
19
|
+
// Function: overlayMode
|
|
20
|
+
// "trash" or "history" for the data's mode (an absent mode reads as history).
|
|
21
|
+
function overlayMode(mode) {
|
|
22
|
+
return unwrap(mode) === "trash" ? "trash" : "history"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Function: overlayTitle
|
|
26
|
+
function overlayTitle(mode) {
|
|
27
|
+
return overlayMode(mode)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Function: overlayFooter
|
|
31
|
+
function overlayFooter(mode) {
|
|
32
|
+
return overlayMode(mode) === "trash"
|
|
33
|
+
? "↑↓ move ↵ restore Esc close"
|
|
34
|
+
: "↑↓ move ↵ jump ^Z undo ^⇧Z redo Esc close"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Function: overlayEmpty
|
|
38
|
+
// The empty-state line: no history yet, or an empty trash.
|
|
39
|
+
function overlayEmpty(mode) {
|
|
40
|
+
return overlayMode(mode) === "trash" ? " trash is empty" : " no history yet"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Function: createBehaviors
|
|
44
|
+
// Builds the OverlayList behavior map for the given renderer environment.
|
|
45
|
+
function createBehaviors(env) {
|
|
46
|
+
const behaviors = {
|
|
47
|
+
bg: () => env.overlayBg,
|
|
48
|
+
borderFg: () => env.borderPrimaryActiveFg,
|
|
49
|
+
titleColor: () => env.textHeadingFg,
|
|
50
|
+
title: (_self, data) => overlayTitle(data.mode),
|
|
51
|
+
footer: (_self, data) => overlayFooter(data.mode),
|
|
52
|
+
footerFg: () => env.textSecondaryFg,
|
|
53
|
+
empty: (_self, data) => overlayEmpty(data.mode),
|
|
54
|
+
emptyFg: () => env.textSecondaryFg,
|
|
55
|
+
emptyVisible: (_self, data) => (unwrap(data.items)?.length ?? 0) === 0,
|
|
56
|
+
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
57
|
+
}
|
|
58
|
+
if (env?.rows) {
|
|
59
|
+
behaviors.rows = (self, data) => env.rows(self, data)
|
|
60
|
+
}
|
|
61
|
+
if (env?.scrollTop) {
|
|
62
|
+
behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
|
|
63
|
+
}
|
|
64
|
+
return behaviors
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { createBehaviors, overlayEmpty, overlayFooter, overlayMode, overlayTitle }
|
|
68
|
+
|
|
69
|
+
// EOF
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<box when="open" width="100%" height="100%" alignItems="center" justifyContent="center">
|
|
2
|
+
<box width="72%" height="60%" border borderStyle="rounded"
|
|
3
|
+
out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
|
|
4
|
+
out:backgroundColor="bg" flexDirection="column">
|
|
5
|
+
<text when="emptyVisible" out="empty" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
|
|
6
|
+
<scrollbox when="!emptyVisible" ref="list" flexGrow="1">
|
|
7
|
+
<box out="rows" flexDirection="column"></box>
|
|
8
|
+
</scrollbox>
|
|
9
|
+
<box flexDirection="row" height="1" paddingLeft="1">
|
|
10
|
+
<text out="footer" out:fg="footerFg"></text>
|
|
11
|
+
</box>
|
|
12
|
+
</box>
|
|
13
|
+
</box>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<li class="overlay-row" out:class="rowClass" out:data-current="rowCurrent" out:data-dim="rowDim" on:click="item.key!overlay:select">
|
|
2
|
+
<span class="overlay-marker" out="rowMarker"></span>
|
|
3
|
+
<span class="overlay-text" out="rowText"></span>
|
|
4
|
+
<span class="overlay-meta" out="rowMeta"></span>
|
|
5
|
+
</li>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-17
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/OverlayRow
|
|
7
|
+
// One row of the shared list overlay (`u` history / `⇧T` trash). Reads
|
|
8
|
+
// `{ item: { text, meta, dim }, index, currentRow }`; the builder snapshots the
|
|
9
|
+
// overlay cursor into a plain `currentRow` boolean, with the shared `cursor`
|
|
10
|
+
// cell as fallback for hand-built bags.
|
|
11
|
+
//
|
|
12
|
+
// Environment: `accentSecondaryFg`, `textFg`, `textSecondaryFg`,
|
|
13
|
+
// `textHeadingFg`, `cursorBg` (current row background), `currentClass` (web).
|
|
14
|
+
|
|
15
|
+
import { unwrap } from "select/state/cells"
|
|
16
|
+
|
|
17
|
+
// Function: rowCurrent
|
|
18
|
+
// Whether the row is the overlay cursor: the plain `currentRow` flag wins over
|
|
19
|
+
// the shared `cursor` cell.
|
|
20
|
+
function rowCurrent(data) {
|
|
21
|
+
if (data.currentRow !== undefined) return data.currentRow === true
|
|
22
|
+
return data.index === unwrap(data.cursor)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Function: createBehaviors
|
|
26
|
+
// Builds the OverlayRow behavior map for the given renderer environment.
|
|
27
|
+
function createBehaviors(env) {
|
|
28
|
+
return {
|
|
29
|
+
rowBg: (_self, data) => (rowCurrent(data) ? env.cursorBg : undefined),
|
|
30
|
+
rowClass: (_self, data) => (rowCurrent(data) ? env.currentClass : null),
|
|
31
|
+
rowCurrent: (_self, data) => `${rowCurrent(data)}`,
|
|
32
|
+
rowDim: (_self, data) => `${Boolean(data.item?.dim)}`,
|
|
33
|
+
rowMarker: (_self, data) => (rowCurrent(data) ? "> " : " "),
|
|
34
|
+
rowMarkerFg: () => env.accentSecondaryFg,
|
|
35
|
+
rowText: (_self, data) => data.item?.text ?? "",
|
|
36
|
+
// The cursor row reads bright; an undone/dim row (a superseded history
|
|
37
|
+
// entry) takes the secondary colour, like a hint.
|
|
38
|
+
rowTextFg: (_self, data) =>
|
|
39
|
+
rowCurrent(data) ? env.textHeadingFg : data.item?.dim ? env.textSecondaryFg : env.textFg,
|
|
40
|
+
rowMeta: (_self, data) => data.item?.meta ?? "",
|
|
41
|
+
rowMetaFg: () => env.textSecondaryFg,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { createBehaviors, rowCurrent }
|
|
46
|
+
|
|
47
|
+
// EOF
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="item.key!overlay:select">
|
|
2
|
+
<text out="rowMarker" width="2" out:fg="rowMarkerFg"></text>
|
|
3
|
+
<box flexGrow="1" minWidth="0" overflow="hidden">
|
|
4
|
+
<text out="rowText" out:fg="rowTextFg" wrapMode="none" truncate="true"></text>
|
|
5
|
+
</box>
|
|
6
|
+
<box flexShrink="0" overflow="hidden">
|
|
7
|
+
<text out="rowMeta" out:fg="rowMetaFg" wrapMode="none" truncate="true"></text>
|
|
8
|
+
</box>
|
|
9
|
+
</box>
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/format
|
|
7
7
|
// Presentation-only formatting helpers (sizes, counts, header title) shared by
|
|
8
|
-
// every component for every renderer.
|
|
8
|
+
// every component for every renderer. Size and disk formatting come from
|
|
9
|
+
// `core/format` because operations report the same values.
|
|
10
|
+
|
|
11
|
+
import { formatDisk, formatSize } from "../core/format.js"
|
|
9
12
|
|
|
10
13
|
// Function: formatTitle
|
|
11
14
|
// Header line `TinyCmd - HOST DIR/BASE` for the given directory. When it
|
|
@@ -78,28 +81,6 @@ function elideCrumbs(segments, width) {
|
|
|
78
81
|
return [marker, ...rest]
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
// Function: formatSize
|
|
82
|
-
// Human-readable byte size; directories pass through their own label.
|
|
83
|
-
function formatSize(bytes) {
|
|
84
|
-
const value = Number(bytes ?? 0)
|
|
85
|
-
if (value < 1024) return `${value}B`
|
|
86
|
-
const units = ["K", "M", "G", "T"]
|
|
87
|
-
let size = value / 1024
|
|
88
|
-
let unit = 0
|
|
89
|
-
while (size >= 1024 && unit < units.length - 1) {
|
|
90
|
-
size /= 1024
|
|
91
|
-
unit++
|
|
92
|
-
}
|
|
93
|
-
return `${size >= 100 ? Math.round(size) : size.toFixed(1)}${units[unit]}`
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Function: formatDisk
|
|
97
|
-
// Compact free/total disk line, e.g. "12G free of 256G".
|
|
98
|
-
function formatDisk(disk) {
|
|
99
|
-
if (!disk) return ""
|
|
100
|
-
return `${formatSize(disk.free)} free of ${formatSize(disk.total)}`
|
|
101
|
-
}
|
|
102
|
-
|
|
103
84
|
// Function: formatTime
|
|
104
85
|
// Local `YYYY-MM-DD HH:MM` stamp for a millisecond epoch; empty when absent.
|
|
105
86
|
function formatTime(ms) {
|