@ssebastien/tcmd 0.1.2 → 0.1.4
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 +121 -38
- package/bin/tcmd +11 -3
- package/bin/tcmd.js +15 -0
- package/package.json +4 -3
- package/public/index.html +14 -0
- package/public/main.js +17501 -0
- package/public/style.css +901 -0
- package/scripts/serve-web.js +61 -0
- package/src/js/tcmd/components/App.html +28 -12
- package/src/js/tcmd/components/App.js +135 -85
- package/src/js/tcmd/components/App.xml +64 -15
- package/src/js/tcmd/components/BookmarkList.html +3 -2
- package/src/js/tcmd/components/BookmarkList.js +1 -1
- package/src/js/tcmd/components/Breadcrumbs.js +1 -1
- package/src/js/tcmd/components/CommandArgRow.html +17 -0
- package/src/js/tcmd/components/CommandArgRow.js +86 -26
- package/src/js/tcmd/components/CommandArgRow.xml +31 -4
- package/src/js/tcmd/components/CommandBar.html +1 -1
- package/src/js/tcmd/components/CommandBar.js +3 -3
- package/src/js/tcmd/components/CommandItem.html +1 -1
- package/src/js/tcmd/components/CommandItem.js +1 -0
- package/src/js/tcmd/components/CommandPanel.html +29 -0
- package/src/js/tcmd/components/CommandPanel.js +54 -52
- package/src/js/tcmd/components/CommandPanel.xml +38 -24
- package/src/js/tcmd/components/Confirm.html +1 -1
- package/src/js/tcmd/components/EntryDetail.html +7 -7
- package/src/js/tcmd/components/EntryDetail.js +1 -1
- package/src/js/tcmd/components/FileList.html +5 -1
- package/src/js/tcmd/components/FileList.js +2 -1
- package/src/js/tcmd/components/FilePreview.html +14 -3
- package/src/js/tcmd/components/FilePreview.js +73 -10
- package/src/js/tcmd/components/FilePreview.xml +19 -6
- package/src/js/tcmd/components/FileRow.html +1 -1
- package/src/js/tcmd/components/FileRow.js +10 -3
- package/src/js/tcmd/components/FilterBar.html +1 -1
- package/src/js/tcmd/components/FilterBar.js +14 -4
- package/src/js/tcmd/components/FilterBar.xml +1 -1
- package/src/js/tcmd/{tui/frame.js → components/Frame.js} +120 -91
- package/src/js/tcmd/components/Help.html +7 -0
- package/src/js/tcmd/components/Help.js +109 -0
- package/src/js/tcmd/components/Summary.html +1 -1
- package/src/js/tcmd/components/Summary.js +1 -6
- package/src/js/tcmd/components/icons.js +39 -5
- package/src/js/tcmd/components/index.js +3 -4
- package/src/js/tcmd/components/rows.js +37 -33
- package/src/js/tcmd/core/commands.js +422 -180
- package/src/js/tcmd/core/directory.js +145 -95
- package/src/js/tcmd/core/fs.js +29 -2
- package/src/js/tcmd/core/history.js +58 -0
- package/src/js/tcmd/core/index.js +3 -9
- package/src/js/tcmd/core/keymap.js +3 -3
- package/src/js/tcmd/core/operations.js +108 -15
- package/src/js/tcmd/core/pane.js +31 -8
- package/src/js/tcmd/core/panel.js +172 -82
- package/src/js/tcmd/core/script.js +7 -95
- package/src/js/tcmd/core/selection.js +9 -1
- package/src/js/tcmd/core/session.js +280 -152
- package/src/js/tcmd/index.js +0 -1
- package/src/js/tcmd/tui/app.js +26 -5
- package/src/js/tcmd/tui/catalogue.js +6 -19
- package/src/js/tcmd/tui/index.js +1 -16
- package/src/js/tcmd/tui/theme.js +5 -0
- package/src/js/tcmd/web/app.js +158 -32
- package/src/js/tcmd/web/bookmarks.js +36 -0
- package/src/js/tcmd/web/catalogue.js +45 -39
- package/src/js/tcmd/web/effects.js +143 -0
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/header.js +15 -11
- package/src/js/tcmd/web/main.js +6 -66
- package/src/js/tcmd/web/seed.js +3 -3
- package/src/js/tcmd/web/server.js +6 -4
- package/src/js/tcmd/web/theme.js +1 -0
- package/src/js/tcmd/components/CommandPalette.html +0 -7
- package/src/js/tcmd/components/CommandPalette.js +0 -58
- package/src/js/tcmd/components/CommandPalette.xml +0 -16
- package/src/js/tcmd/components/PaletteRow.html +0 -3
- package/src/js/tcmd/components/PaletteRow.js +0 -43
- package/src/js/tcmd/components/PaletteRow.xml +0 -4
- package/src/js/tcmd/tui/help.js +0 -32
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<li class="panel-row" out:class="rowClass" out:data-risk="row.risk" out:data-type="row.type" out:data-selected="rowSelected" on:click="row.key!panel:select">
|
|
2
|
+
<span class="panel-list-indent" when="isList" out="listSpacer"></span>
|
|
3
|
+
<span class="panel-list-box" when="isList">
|
|
4
|
+
<span class="panel-marker" out="marker"></span>
|
|
5
|
+
<span class="panel-name" out="name"></span>
|
|
6
|
+
<span class="panel-summary" out="summary"></span>
|
|
7
|
+
</span>
|
|
8
|
+
<span class="panel-marker" when="!isList" out="marker"></span>
|
|
9
|
+
<span class="panel-name" when="!isList" out="name"></span>
|
|
10
|
+
<span class="panel-field" when="isArg">
|
|
11
|
+
<span class="panel-field-bracket" out="fieldOpen"></span>
|
|
12
|
+
<input class="panel-value" when="editing" type="text" inout="value" aria-label="Argument value" />
|
|
13
|
+
<span class="panel-field-value" when="!editing" out="summary"></span>
|
|
14
|
+
<span class="panel-field-bracket" out="fieldClose"></span>
|
|
15
|
+
</span>
|
|
16
|
+
<span class="panel-summary" when="isPlain" out="summary"></span>
|
|
17
|
+
</li>
|
|
@@ -4,19 +4,23 @@
|
|
|
4
4
|
// Created: 2026-10-09
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandArgRow
|
|
7
|
-
// CommandArgRow: one row shared by
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// `
|
|
7
|
+
// CommandArgRow: one row shared by the command panel's two panes. The left pane
|
|
8
|
+
// passes `{ type: "op" }` rows (name = entry name, summary = signature or key
|
|
9
|
+
// shortcut, `risk` tints destructive/caution operations, `source` marks the
|
|
10
|
+
// namespace) and `{ type: "group" }` header rows ("System" / "Commands"). The
|
|
11
|
+
// right pane passes `{ type: "arg" | "item" | "frame" | "entry" }` rows: an
|
|
12
|
+
// argument header shows an aligned label and its value — a bracketed field with
|
|
13
|
+
// a distinct background for a textual argument, a summary otherwise; an item
|
|
14
|
+
// shows a `[x]`/`[ ]` (or radio) marker with its basename and parent; and a
|
|
15
|
+
// `frame` row draws the top/bottom of the bordered list a selection's items sit
|
|
16
|
+
// in. Reads the row bag `{ item, index, row, cursor, focus, value, labelWidth }`
|
|
17
|
+
// built by `rows.js`.
|
|
15
18
|
//
|
|
16
19
|
// Environment: `accentFg` (focused row), `textFg`, `textSecondaryFg` (summary),
|
|
17
|
-
// `
|
|
18
|
-
// `
|
|
19
|
-
// (
|
|
20
|
+
// `textHeadingFg` (a group header), `accentSecondaryFg` (a selected item),
|
|
21
|
+
// `errorFg` (destructive op), `warningFg` (caution op), `selectionBg` (row
|
|
22
|
+
// background), `fieldBg`/`fieldFg` (the value field), `borderSecondaryFg` (the
|
|
23
|
+
// list border), `focusedClass` (web), `icons` (namespace marker).
|
|
20
24
|
|
|
21
25
|
import { unwrap } from "select/state/cells"
|
|
22
26
|
|
|
@@ -30,35 +34,44 @@ function riskFg(row, env) {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
// Function: rowName
|
|
33
|
-
// The row's left cell:
|
|
34
|
-
// `[x] basename
|
|
37
|
+
// The row's left cell: a group header's label, an operation's name, an
|
|
38
|
+
// argument's label, an item's `[x] basename`, or a picker entry (directories
|
|
39
|
+
// carry a trailing slash).
|
|
35
40
|
function rowName(row) {
|
|
36
41
|
if (!row) return ""
|
|
42
|
+
if (row.type === "group") return row.name ?? ""
|
|
37
43
|
if (row.type === "item") return `${row.marker ?? "[ ]"} ${row.name ?? ""}`
|
|
44
|
+
if (row.type === "entry") return `${row.name ?? ""}${row.isDir ? "/" : ""}`
|
|
38
45
|
return row.name ?? ""
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
// Function: rowSummary
|
|
42
|
-
// The row's right cell:
|
|
43
|
-
// summary
|
|
49
|
+
// The row's right cell: an entry's argument list or key shortcut, an argument's
|
|
50
|
+
// binding summary (or its value), an item's parent directory, or a picker
|
|
51
|
+
// entry's kind.
|
|
44
52
|
function rowSummary(row) {
|
|
45
53
|
if (!row) return ""
|
|
46
54
|
if (row.type === "item") return row.parent ?? ""
|
|
47
|
-
if (row.type === "
|
|
55
|
+
if (row.type === "entry") {
|
|
56
|
+
if (row.isSymlink && row.target) return `${row.target}`
|
|
57
|
+
return row.isDir ? "dir" : ""
|
|
58
|
+
}
|
|
48
59
|
return row.summary ?? ""
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
// Function: wantsFocus
|
|
52
|
-
// The focus token a row belongs to: the
|
|
53
|
-
//
|
|
63
|
+
// The focus token a row belongs to: the command list ("ops"), the argument
|
|
64
|
+
// pane ("args"), or none (a group header or a list frame never takes the
|
|
65
|
+
// cursor).
|
|
54
66
|
function wantsFocus(row) {
|
|
67
|
+
if (row?.type === "group" || row?.type === "frame") return "none"
|
|
55
68
|
return row?.type === "op" ? "ops" : "args"
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
// Function: isFocused
|
|
59
|
-
// True when this row owns the cursor in the focused
|
|
72
|
+
// True when this row owns the cursor in the focused region.
|
|
60
73
|
function isFocused(row, index, cursor, focus) {
|
|
61
|
-
if (!row || !focus) return false
|
|
74
|
+
if (!row || !focus || row.type === "group" || row.type === "frame") return false
|
|
62
75
|
return focus === wantsFocus(row) && index === cursor
|
|
63
76
|
}
|
|
64
77
|
|
|
@@ -66,24 +79,71 @@ function isFocused(row, index, cursor, focus) {
|
|
|
66
79
|
// Builds the CommandArgRow behavior map for the given renderer environment.
|
|
67
80
|
function createBehaviors(env) {
|
|
68
81
|
const focused = (data) => isFocused(data.row, data.index, unwrap(data.cursor), unwrap(data.focus))
|
|
82
|
+
const type = (data) => data.row?.type
|
|
83
|
+
const isList = (data) => type(data) === "item" || type(data) === "frame"
|
|
84
|
+
// The web classes that draw a selection's list box: the item rows carry the
|
|
85
|
+
// sides, the frame rows the top/bottom.
|
|
86
|
+
const listClass = (data) => {
|
|
87
|
+
if (!isList(data)) return null
|
|
88
|
+
if (type(data) === "frame") return data.row?.variant === "top" ? "is-list-top" : "is-list-bottom"
|
|
89
|
+
return "is-list-item"
|
|
90
|
+
}
|
|
91
|
+
// A textual argument header edits in place while it owns the cursor: its
|
|
92
|
+
// summary yields to a value input bound to the panel's edit cell.
|
|
93
|
+
const editing = (_self, data) => focused(data) && type(data) === "arg" && data.row?.textual === true
|
|
69
94
|
return {
|
|
95
|
+
editing,
|
|
96
|
+
// --- shared row content ---
|
|
70
97
|
marker: (_self, data) => (focused(data) ? ">" : " "),
|
|
71
98
|
markerFg: () => env.accentFg,
|
|
72
|
-
|
|
99
|
+
// An argument label is padded to the column width so values line up
|
|
100
|
+
// across arguments (both renderers honor the spaces).
|
|
101
|
+
name: (_self, data) => {
|
|
102
|
+
const text = rowName(data.row)
|
|
103
|
+
return type(data) === "arg" ? text.padEnd(data.labelWidth ?? 8) : text
|
|
104
|
+
},
|
|
73
105
|
rowBg: (_self, data) => (focused(data) ? env.selectionBg : undefined),
|
|
74
|
-
rowClass: (_self, data) =>
|
|
106
|
+
rowClass: (_self, data) =>
|
|
107
|
+
[focused(data) ? env.focusedClass : null, listClass(data)].filter(Boolean).join(" ") || null,
|
|
108
|
+
rowSelected: (_self, data) => `${data.row?.selected === true}`,
|
|
75
109
|
rowFg: (_self, data) => {
|
|
76
|
-
|
|
77
|
-
// focused — so danger reads at a glance; focus is carried by the
|
|
78
|
-
// `>` marker and the row background instead.
|
|
110
|
+
if (type(data) === "group") return env.textHeadingFg
|
|
79
111
|
const risk = riskFg(data.row, env)
|
|
80
112
|
if (risk) return risk
|
|
81
113
|
if (focused(data)) return env.accentFg
|
|
82
|
-
if (data
|
|
114
|
+
if (type(data) === "item" && data.row?.selected) return env.accentSecondaryFg
|
|
83
115
|
return env.textFg
|
|
84
116
|
},
|
|
85
117
|
summary: (_self, data) => rowSummary(data.row),
|
|
86
118
|
summaryFg: () => env.textSecondaryFg,
|
|
119
|
+
// The list branch repeats the marker/name/summary nodes, so it carries
|
|
120
|
+
// its own out-names (duplicate `out` names in one template collide).
|
|
121
|
+
listMarker: (_self, data) => (focused(data) ? ">" : " "),
|
|
122
|
+
listName: (_self, data) => rowName(data.row, env.icons),
|
|
123
|
+
listSummary: (_self, data) => rowSummary(data.row),
|
|
124
|
+
// --- layout branches ---
|
|
125
|
+
isArg: (_self, data) => type(data) === "arg",
|
|
126
|
+
isItem: (_self, data) => type(data) === "item",
|
|
127
|
+
isFrame: (_self, data) => type(data) === "frame",
|
|
128
|
+
isList: (_self, data) => isList(data),
|
|
129
|
+
isPlain: (_self, data) => !isList(data) && type(data) !== "arg",
|
|
130
|
+
labelWidth: (_self, data) => data.labelWidth ?? 8,
|
|
131
|
+
// The value field: a distinct background and a `[ ]` frame.
|
|
132
|
+
fieldBg: () => env.fieldBg,
|
|
133
|
+
fieldFg: () => env.fieldFg ?? env.textFg,
|
|
134
|
+
fieldOpen: () => "[",
|
|
135
|
+
fieldClose: () => "]",
|
|
136
|
+
// The list indents under the value column: the marker (2) plus the gap
|
|
137
|
+
// (1) plus the label column plus the gap (1). The spacer is NBSP so the
|
|
138
|
+
// renderers do not collapse the leading run.
|
|
139
|
+
rowIndent: (_self, data) => (isList(data) ? 4 + (data.labelWidth ?? 8) : 0),
|
|
140
|
+
listIndent: (_self, data) => 4 + (data.labelWidth ?? 8),
|
|
141
|
+
listSpacer: (_self, data) => "\u00a0".repeat(4 + (data.labelWidth ?? 8)),
|
|
142
|
+
listBorder: (_self, data) => {
|
|
143
|
+
if (type(data) !== "frame") return ["left", "right"]
|
|
144
|
+
return data.row?.variant === "top" ? ["top", "left", "right"] : ["bottom", "left", "right"]
|
|
145
|
+
},
|
|
146
|
+
borderFg: () => env.borderSecondaryFg,
|
|
87
147
|
}
|
|
88
148
|
}
|
|
89
149
|
|
|
@@ -1,5 +1,32 @@
|
|
|
1
|
-
<box flexDirection="row" height="1"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<text
|
|
1
|
+
<box flexDirection="row" height="1" out:backgroundColor="rowBg" on:click="row.key!panel:select">
|
|
2
|
+
<!-- A selection's item list: indented under the value column and framed
|
|
3
|
+
by the top/bottom border rows. -->
|
|
4
|
+
<text when="isList" out="listSpacer" wrapMode="none"></text>
|
|
5
|
+
<box when="isList" flexGrow="1" flexShrink="1" minWidth="0" flexDirection="row" columnGap="1"
|
|
6
|
+
out:border="listBorder" out:borderColor="borderFg">
|
|
7
|
+
<text when="isItem" out="listMarker" width="2" out:fg="markerFg"></text>
|
|
8
|
+
<text when="isItem" out="listName" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
9
|
+
<text when="isItem" out="listSummary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
10
|
+
</box>
|
|
11
|
+
<!-- An argument header: an aligned label and a bracketed value field. -->
|
|
12
|
+
<box when="isArg" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
|
|
13
|
+
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
14
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
15
|
+
<box when="editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
|
|
16
|
+
<text out="fieldOpen" out:fg="fieldFg"></text>
|
|
17
|
+
<input inout="value" flexGrow="1" minWidth="6" height="1" out:focused="editing"></input>
|
|
18
|
+
<text out="fieldClose" out:fg="fieldFg"></text>
|
|
19
|
+
</box>
|
|
20
|
+
<box when="!editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
|
|
21
|
+
<text out="fieldOpen" out:fg="fieldFg"></text>
|
|
22
|
+
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
23
|
+
<text out="fieldClose" out:fg="fieldFg"></text>
|
|
24
|
+
</box>
|
|
25
|
+
</box>
|
|
26
|
+
<!-- Operation, group and picker-entry rows: marker + name + summary. -->
|
|
27
|
+
<box when="isPlain" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
|
|
28
|
+
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
29
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
30
|
+
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
31
|
+
</box>
|
|
5
32
|
</box>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<footer class="command-bar">
|
|
2
2
|
<nav when="showKeys" out:class="listClass" out="rows"></nav>
|
|
3
|
-
<span when="showStatus" out="status" out:class="statusClass"></span>
|
|
3
|
+
<span when="showStatus" out="status" out:class="statusClass" out:data-kind="notice.kind" role="status"></span>
|
|
4
4
|
</footer>
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandBar
|
|
7
7
|
// CommandBar: the bottom bar. By default it shows the keys available in the
|
|
8
|
-
// current context (the command registry's `bar` commands, or the
|
|
9
|
-
//
|
|
8
|
+
// current context (the command registry's `bar` commands, or the hints of the
|
|
9
|
+
// overlay that owns the keyboard); a pane error or a session notice takes
|
|
10
10
|
// its place. Entries are bracket shorthands (see `CommandItem.js`) that wrap
|
|
11
11
|
// onto as many rows as the width demands; the active pane's marked count
|
|
12
12
|
// joins the list as a dimmed, non-clickable entry. The row logic lives in
|
|
@@ -116,7 +116,7 @@ function createBehaviors(env) {
|
|
|
116
116
|
},
|
|
117
117
|
}
|
|
118
118
|
if (env?.rows) {
|
|
119
|
-
behaviors.rows = (
|
|
119
|
+
behaviors.rows = (self, data) => env.rows(self, data)
|
|
120
120
|
}
|
|
121
121
|
return behaviors
|
|
122
122
|
}
|
|
@@ -61,6 +61,7 @@ function createBehaviors(env) {
|
|
|
61
61
|
return {
|
|
62
62
|
// Web: the whole shorthand as one label.
|
|
63
63
|
itemLabel: (_self, data) => commandLabel(data.item),
|
|
64
|
+
itemHint: (_self, data) => !data.item?.id,
|
|
64
65
|
// TUI: the dim pipe divider, then the bracket + title head, the
|
|
65
66
|
// accent key glyph, the closer.
|
|
66
67
|
itemSep: (_self, data) => (data.index > 0 ? COMMAND_SEP : ""),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<section class="command-panel" when="open" role="dialog" aria-modal="true" aria-label="Command panel" tabindex="-1">
|
|
2
|
+
<div class="command-dialog">
|
|
3
|
+
<header out="title"></header>
|
|
4
|
+
<div class="panel-columns">
|
|
5
|
+
<div class="panel-command-column">
|
|
6
|
+
<label class="panel-query-line">
|
|
7
|
+
<span out="sign"></span>
|
|
8
|
+
<input class="panel-query" type="text" inout="query" aria-label="Filter commands" />
|
|
9
|
+
</label>
|
|
10
|
+
<ul class="panel-ops" aria-label="Commands" out="opRows"></ul>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="panel-args-column">
|
|
13
|
+
<p class="panel-command-name" out="commandName"></p>
|
|
14
|
+
<div class="panel-picker" when="picking">
|
|
15
|
+
<p class="panel-picker-path" out="pickerPath"></p>
|
|
16
|
+
<label class="panel-picker-line">
|
|
17
|
+
<span out="pickerSign"></span>
|
|
18
|
+
<input class="panel-picker-query" type="text" inout="pickerQuery" aria-label="Filter entries" />
|
|
19
|
+
</label>
|
|
20
|
+
</div>
|
|
21
|
+
<p class="panel-args-empty" when="argsEmpty" out="argsEmptyText"></p>
|
|
22
|
+
<ul class="panel-args" aria-label="Arguments" out="argRows"></ul>
|
|
23
|
+
<p class="panel-args-empty" when="pickerEmpty" out="pickerEmptyText"></p>
|
|
24
|
+
<div class="panel-runs" out:data-runnable="runnableState"><pre out="preview"></pre></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<footer out="footer"></footer>
|
|
28
|
+
</div>
|
|
29
|
+
</section>
|
|
@@ -4,95 +4,97 @@
|
|
|
4
4
|
// Created: 2026-10-09
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandPanel
|
|
7
|
-
// CommandPanel: the
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// panel keys. The row logic lives in `CommandArgRow.js`; this parent maps both
|
|
15
|
-
// the operation list and the flattened argument rows.
|
|
7
|
+
// CommandPanel: the one entry point (`:`), a two-pane palette. The left pane
|
|
8
|
+
// holds a `:` filter above the command list — the CLI operations, flat. The
|
|
9
|
+
// right pane lists the selected operation's arguments — one row per argument,
|
|
10
|
+
// its label and its value (a bracketed field, or a checkbox/radio list of the
|
|
11
|
+
// selected paths) — under the operation's name, with the resolved command at
|
|
12
|
+
// the pane's foot. The row logic lives in `CommandArgRow.js`; this parent maps
|
|
13
|
+
// the command list and the argument rows.
|
|
16
14
|
//
|
|
17
15
|
// Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
|
|
18
|
-
// (footer), `textHeadingFg` (title/sign), `
|
|
19
|
-
// `
|
|
20
|
-
//
|
|
21
|
-
// tracking).
|
|
16
|
+
// (footer), `textHeadingFg` (title/sign/command name), `opRows(self, data)` /
|
|
17
|
+
// `argRows(self, data)` returning the keyed row lists, and
|
|
18
|
+
// `argScrollTop(self, data)` (TUI viewport tracking).
|
|
22
19
|
|
|
23
20
|
import { unwrap } from "select/state/cells"
|
|
24
21
|
import { PANEL_FOCUS } from "../core/panel.js"
|
|
25
22
|
|
|
26
23
|
// Function: panelTitle
|
|
27
|
-
// " Command · copy " while an
|
|
24
|
+
// " Command · copy " while an entry is selected (spaces so the title sits
|
|
28
25
|
// clear of the border corners).
|
|
29
|
-
function panelTitle(
|
|
30
|
-
const name = unwrap(
|
|
26
|
+
function panelTitle(entry) {
|
|
27
|
+
const name = unwrap(entry)?.name
|
|
31
28
|
return name ? ` Command · ${name} ` : " Command "
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
// Function: panelFooter
|
|
35
|
-
const PANEL_FOOTER = "type to filter ⇥ args Spc toggle ↵ run Esc close"
|
|
32
|
+
const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run Esc close"
|
|
33
|
+
|
|
34
|
+
// Function: pickerFooter
|
|
35
|
+
const PICKER_FOOTER = "type to filter ↑↓ row → open ↵ choose ⌃↵ this dir ⌫ up Esc back"
|
|
36
36
|
|
|
37
37
|
// Function: panelSign
|
|
38
|
-
// The prompt before the command filter input
|
|
39
|
-
// text doesn't crowd the arrow; overridable per renderer).
|
|
38
|
+
// The filter prompt (`:`) before the command filter input.
|
|
40
39
|
function panelSign() {
|
|
41
|
-
return "
|
|
40
|
+
return ":"
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
// Function: createBehaviors
|
|
45
44
|
// Builds the CommandPanel behavior map for the given renderer environment.
|
|
46
45
|
function createBehaviors(env) {
|
|
47
|
-
|
|
48
|
-
// label: the preview reads as an error until every required argument is
|
|
49
|
-
// filled.
|
|
50
|
-
const previewColor = (data) => (unwrap(data.runnable) === false ? env.errorFg ?? env.accentFg : env.accentFg)
|
|
46
|
+
const panelOpen = (_self, data) => Boolean(unwrap(data.open))
|
|
51
47
|
const behaviors = {
|
|
52
48
|
bg: () => env.overlayBg,
|
|
53
49
|
borderFg: () => env.borderPrimaryActiveFg,
|
|
54
|
-
//
|
|
55
|
-
|
|
50
|
+
// The right pane's header: the selected command's name.
|
|
51
|
+
commandName: (_self, data) => unwrap(data.entry)?.name ?? "",
|
|
52
|
+
commandNameFg: () => env.textHeadingFg,
|
|
56
53
|
confirm: (self) => self.pub("panel:confirm"),
|
|
57
|
-
// The argument
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
// The argument pane's empty state: an operation with no bound rows has
|
|
55
|
+
// nothing to fill. While picking the picker's own empty state stands in.
|
|
56
|
+
argsEmpty: (_self, data) =>
|
|
57
|
+
!unwrap(data.picking) && (unwrap(data.rows)?.length ?? 0) === 0,
|
|
58
|
+
argsEmptyText: () => "no arguments",
|
|
59
|
+
// The file picker sub-mode: a path line, its own filter input, and the
|
|
60
|
+
// entry rows (rendered through the shared arg-row map).
|
|
61
|
+
picking: (_self, data) => Boolean(unwrap(data.picking)),
|
|
62
|
+
pickerPath: (_self, data) => unwrap(data.pickerPath) ?? "",
|
|
63
|
+
pickerSign: () => "⌕ ",
|
|
64
|
+
pickerFocused: (self, data) => panelOpen(self, data) && Boolean(unwrap(data.picking)),
|
|
65
|
+
pickerEmpty: (_self, data) =>
|
|
66
|
+
Boolean(unwrap(data.picking)) && (unwrap(data.rows)?.length ?? 0) === 0,
|
|
67
|
+
pickerEmptyText: () => " empty",
|
|
68
|
+
footer: (_self, data) => (unwrap(data?.picking) ? PICKER_FOOTER : PANEL_FOOTER),
|
|
66
69
|
footerFg: () => env.textSecondaryFg,
|
|
67
|
-
// Dividers: a vertical rule between the
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
+
// Dividers: a vertical rule between the panes, horizontal ones under the
|
|
71
|
+
// filter and above the resolved command — all in the secondary border
|
|
72
|
+
// colour, like the pane header rules.
|
|
70
73
|
ruleFg: () => env.borderSecondaryFg,
|
|
71
74
|
sideRule: () => ["left"],
|
|
72
75
|
headRule: () => ["top"],
|
|
73
76
|
labelFg: () => env.textSecondaryFg,
|
|
74
77
|
focusArgs: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.ARGS,
|
|
75
|
-
focusOps: (_self, data) => unwrap(data.focus)
|
|
78
|
+
focusOps: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.OPS,
|
|
76
79
|
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
77
|
-
// The
|
|
78
|
-
//
|
|
79
|
-
// read as one block.
|
|
80
|
+
// The resolved command the draft expands to, dim, at the foot of the
|
|
81
|
+
// argument pane; a `«NAME»` marks a required argument still to fill.
|
|
80
82
|
preview: (_self, data) => unwrap(data.command) ?? "",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
runnableState: (_self, data) => `${unwrap(data.runnable) !== false}`,
|
|
84
|
+
previewFg: () => env.textSecondaryFg,
|
|
85
|
+
// The filter input owns the keyboard while the panel is open, the
|
|
86
|
+
// command list is focused and the picker is not browsing.
|
|
87
|
+
queryFocused: (self, data) =>
|
|
88
|
+
panelOpen(self, data) && !unwrap(data.picking) && unwrap(data.focus) !== PANEL_FOCUS.ARGS,
|
|
87
89
|
sign: () => panelSign(),
|
|
88
90
|
signFg: () => env.textHeadingFg,
|
|
89
|
-
title: (_self, data) => panelTitle(data.
|
|
91
|
+
title: (_self, data) => panelTitle(data.entry),
|
|
90
92
|
titleColor: () => env.textHeadingFg,
|
|
91
93
|
}
|
|
92
|
-
if (env?.opRows) behaviors.opRows = (
|
|
93
|
-
if (env?.argRows) behaviors.argRows = (
|
|
94
|
+
if (env?.opRows) behaviors.opRows = (self, data) => env.opRows(self, data)
|
|
95
|
+
if (env?.argRows) behaviors.argRows = (self, data) => env.argRows(self, data)
|
|
94
96
|
if (env?.argScrollTop) behaviors.argScrollTop = (self, data) => env.argScrollTop(self, data)
|
|
95
97
|
return behaviors
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
export { PANEL_FOOTER, createBehaviors, panelSign, panelTitle }
|
|
100
|
+
export { PANEL_FOOTER, PICKER_FOOTER, createBehaviors, panelSign, panelTitle }
|
|
@@ -2,31 +2,45 @@
|
|
|
2
2
|
<box width="80%" height="72%" border borderStyle="rounded"
|
|
3
3
|
out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
|
|
4
4
|
out:backgroundColor="bg" flexDirection="column">
|
|
5
|
-
<box flexDirection="row"
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
<box flexDirection="row" flexGrow="1" overflow="hidden">
|
|
6
|
+
<!-- Left pane: the `:` filter over the grouped command list. -->
|
|
7
|
+
<box flexDirection="column" flexGrow="2" flexBasis="0" overflow="hidden">
|
|
8
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
9
|
+
<text out="sign" out:fg="signFg" wrapMode="none"></text>
|
|
10
|
+
<input ref="query" inout="query" flexGrow="1" height="1" out:focused="queryFocused"></input>
|
|
11
|
+
</box>
|
|
12
|
+
<box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
|
|
13
|
+
<scrollbox flexGrow="1">
|
|
14
|
+
<box out="opRows" flexDirection="column"></box>
|
|
15
|
+
</scrollbox>
|
|
16
|
+
</box>
|
|
17
|
+
<!-- Vertical rule between the panes. -->
|
|
16
18
|
<box width="1" out:border="sideRule" out:borderColor="ruleFg"></box>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
<!-- Right pane: the command name, its arguments, the resolved command. -->
|
|
20
|
+
<box flexDirection="column" flexGrow="3" flexBasis="0" overflow="hidden">
|
|
21
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
22
|
+
<text out="commandName" out:fg="commandNameFg" wrapMode="none"></text>
|
|
23
|
+
</box>
|
|
24
|
+
<box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
|
|
25
|
+
<scrollbox ref="args" flexGrow="1" out:scrollTop="argScrollTop">
|
|
26
|
+
<box flexDirection="column">
|
|
27
|
+
<!-- File picker sub-mode: the browsed path, its own filter,
|
|
28
|
+
then the entry rows (which arrive through `argRows`). -->
|
|
29
|
+
<text when="picking" out="pickerPath" out:fg="labelFg" paddingLeft="1" wrapMode="none" truncate="true"></text>
|
|
30
|
+
<box when="picking" flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
31
|
+
<text out="pickerSign" out:fg="signFg" wrapMode="none"></text>
|
|
32
|
+
<input ref="pickQuery" inout="pickerQuery" flexGrow="1" height="1" out:focused="pickerFocused"></input>
|
|
33
|
+
</box>
|
|
34
|
+
<text when="argsEmpty" out="argsEmptyText" out:fg="labelFg" paddingLeft="1" wrapMode="none"></text>
|
|
35
|
+
<box out="argRows" flexDirection="column"></box>
|
|
36
|
+
<text when="pickerEmpty" out="pickerEmptyText" out:fg="labelFg" paddingLeft="1" wrapMode="none"></text>
|
|
37
|
+
</box>
|
|
38
|
+
</scrollbox>
|
|
39
|
+
<box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
|
|
40
|
+
<box flexDirection="column" flexShrink="0" paddingLeft="1" paddingRight="1" overflow="hidden">
|
|
41
|
+
<text out="preview" out:fg="previewFg" wrapMode="word"></text>
|
|
42
|
+
</box>
|
|
43
|
+
</box>
|
|
30
44
|
</box>
|
|
31
45
|
<box flexDirection="row" height="1" paddingLeft="1">
|
|
32
46
|
<text out="footer" out:fg="footerFg"></text>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
<div class="entry-detail">
|
|
1
|
+
<div class="entry-detail" ref="box">
|
|
2
2
|
<div class="entry-detail-line">
|
|
3
|
-
<span class="entry-detail-indent" out="detailIndent"></span>
|
|
4
|
-
<span class="entry-detail-marked" out="detailMarked"></span>
|
|
3
|
+
<span class="entry-detail-indent" when="detailHasIndent" out="detailIndent"></span>
|
|
4
|
+
<span class="entry-detail-marked" when="detailHasMarked" out="detailMarked"></span>
|
|
5
5
|
<span class="entry-detail-sep" when="detailSepTime" out="detailSep"></span>
|
|
6
|
-
<span class="entry-detail-time" out="detailTime"></span>
|
|
6
|
+
<span class="entry-detail-time" when="detailHasTime" out="detailTime"></span>
|
|
7
7
|
<span class="entry-detail-sep" when="detailSepType" out="detailSep"></span>
|
|
8
|
-
<span class="entry-detail-type" out:class="detailTypeClass" out="detailType"></span>
|
|
8
|
+
<span class="entry-detail-type" when="detailHasType" out:class="detailTypeClass" out="detailType"></span>
|
|
9
9
|
<span class="entry-detail-sep" when="detailSepSize" out="detailSep"></span>
|
|
10
|
-
<span class="entry-detail-size" out="detailSize"></span>
|
|
10
|
+
<span class="entry-detail-size" when="detailHasSize" out="detailSize"></span>
|
|
11
11
|
<span class="entry-detail-spacer" when="detailHasMode"></span>
|
|
12
12
|
<span class="entry-detail-sep" when="detailSepMode" out="detailSep"></span>
|
|
13
|
-
<span class="entry-detail-mode" out="detailMode"></span>
|
|
13
|
+
<span class="entry-detail-mode" when="detailHasMode" out="detailMode"></span>
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
@@ -135,7 +135,7 @@ function createBehaviors(env = {}) {
|
|
|
135
135
|
const entry = unwrap(data.entry)
|
|
136
136
|
const marked = unwrap(data.marked)
|
|
137
137
|
const markedCount = marked?.size ?? 0
|
|
138
|
-
const width = env.width ? env.width(self) : 0
|
|
138
|
+
const width = env.width ? env.width(self, data) : 0
|
|
139
139
|
const kept = fitBlocks(detailBlocks(entry, markedCount), width, measure, env.elide)
|
|
140
140
|
const keys = new Set(kept.map((block) => block.key))
|
|
141
141
|
const texts = {}
|
|
@@ -25,7 +25,8 @@ function createBehaviors(env) {
|
|
|
25
25
|
loadingFg: () => env?.textSecondaryFg,
|
|
26
26
|
showEmpty: (_self, data) =>
|
|
27
27
|
!unwrap(data.loading) && (unwrap(data.entries)?.length ?? 0) === 0,
|
|
28
|
-
showLoading: (_self, data) =>
|
|
28
|
+
showLoading: (_self, data) =>
|
|
29
|
+
Boolean(unwrap(data.loading)) && (unwrap(data.entries)?.length ?? 0) === 0,
|
|
29
30
|
}
|
|
30
31
|
if (env?.rows) {
|
|
31
32
|
behaviors.rows = (self, data) => env.rows(self, data)
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<!-- The inspector: a card/text body for a file, and a directory column
|
|
2
|
+
(breadcrumbs + cursorable listing) for a directory, so a previewed
|
|
3
|
+
directory reads as a pane. The body and the column share the frame, and
|
|
4
|
+
the whole column is clickable to take focus. -->
|
|
5
|
+
<aside class="preview" when="show" out:title="headerTitle" on:click="paneId!pane:activate">
|
|
6
|
+
<header when="showBody" out="headerTitle"></header>
|
|
7
|
+
<div class="pane-header" when="isDirPane">
|
|
8
|
+
<div out="dirCrumbsView|Breadcrumbs"></div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="preview-list" when="isDirPane" data-slot="list" out="dirListView|FileList"></div>
|
|
11
|
+
<pre when="showText" out="previewText" class="preview-text"></pre>
|
|
12
|
+
<pre when="showCard" out="cardText" class="preview-card"></pre>
|
|
13
|
+
<pre when="showEntryCard" out="entryText" class="preview-card"></pre>
|
|
14
|
+
<pre when="showEmpty" out="emptyText" class="preview-card"></pre>
|
|
4
15
|
<footer class="preview-actions" out="actions"></footer>
|
|
5
16
|
</aside>
|