@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<box flexDirection="row" height="
|
|
1
|
+
<box flexDirection="row" out:height="rowHeight" out:marginTop="runMarginTop" out:backgroundColor="rowBg" on:click="row.key!panel:select">
|
|
2
2
|
<!-- A selection's item list: indented under the value column and framed
|
|
3
3
|
by the top/bottom border rows. -->
|
|
4
4
|
<text when="isList" out="listSpacer" wrapMode="none"></text>
|
|
@@ -8,23 +8,47 @@
|
|
|
8
8
|
<text when="isItem" out="listName" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
9
9
|
<text when="isItem" out="listSummary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
10
10
|
</box>
|
|
11
|
-
<!-- An argument header:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
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>
|
|
11
|
+
<!-- An argument header: its label on one line, the value field under it.
|
|
12
|
+
Both start one cell in from the pane border. -->
|
|
13
|
+
<box when="isArg" flexDirection="column" flexGrow="1" flexShrink="1" minWidth="0" paddingLeft="1">
|
|
14
|
+
<box flexDirection="row" height="1" columnGap="1" minWidth="0">
|
|
15
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
19
16
|
</box>
|
|
20
|
-
<box
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
<box flexDirection="row" height="1" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
18
|
+
<box when="editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
|
|
19
|
+
<text when="textual" out="fieldOpen" out:fg="fieldFg"></text>
|
|
20
|
+
<input inout="value" flexGrow="1" minWidth="6" height="1" out:focused="editing"></input>
|
|
21
|
+
<text when="textual" out="fieldClose" out:fg="fieldFg"></text>
|
|
22
|
+
</box>
|
|
23
|
+
<box when="!editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
|
|
24
|
+
<text when="textual" out="fieldOpen" out:fg="fieldFg"></text>
|
|
25
|
+
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
26
|
+
<text when="textual" out="fieldClose" out:fg="fieldFg"></text>
|
|
27
|
+
</box>
|
|
24
28
|
</box>
|
|
25
29
|
</box>
|
|
26
|
-
<!--
|
|
27
|
-
|
|
30
|
+
<!-- A permission class: its label and three clickable r/w/x cells. The bit
|
|
31
|
+
cursor marks the cell Space toggles. -->
|
|
32
|
+
<box when="isPerm" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" height="1" columnGap="1" paddingLeft="1">
|
|
33
|
+
<text out="permClass" width="7" out:fg="rowFg" wrapMode="none"></text>
|
|
34
|
+
<box height="1" on:click="row.keyR!panel:perm." out:backgroundColor="permRBg"><text out="permR" width="1" out:fg="permRFg"></text></box>
|
|
35
|
+
<box height="1" on:click="row.keyW!panel:perm." out:backgroundColor="permWBg"><text out="permW" width="1" out:fg="permWFg"></text></box>
|
|
36
|
+
<box height="1" on:click="row.keyX!panel:perm." out:backgroundColor="permXBg"><text out="permX" width="1" out:fg="permXFg"></text></box>
|
|
37
|
+
</box>
|
|
38
|
+
<box when="isRun" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1" justifyContent="flex-start">
|
|
39
|
+
<text out="runLabel" out:fg="runFg" wrapMode="none"></text>
|
|
40
|
+
</box>
|
|
41
|
+
<!-- Bold color-group title; operations below it are indented. -->
|
|
42
|
+
<box when="isGroup" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" paddingLeft="1">
|
|
43
|
+
<text out="name" out:fg="rowFg" out:attributes="groupAttributes" wrapMode="none"></text>
|
|
44
|
+
</box>
|
|
45
|
+
<!-- Name-only operation row, indented beneath its group title. -->
|
|
46
|
+
<box when="isOp" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1" paddingLeft="2">
|
|
47
|
+
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
48
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
49
|
+
</box>
|
|
50
|
+
<!-- Picker entries retain their cursor marker and trailing kind summary. -->
|
|
51
|
+
<box when="isEntry" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
|
|
28
52
|
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
29
53
|
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
30
54
|
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<footer class="command-bar">
|
|
1
|
+
<footer class="command-bar" ref="box" when="showBar">
|
|
2
2
|
<nav when="showKeys" out:class="listClass" out="rows"></nav>
|
|
3
3
|
<span when="showStatus" out="status" out:class="statusClass" out:data-kind="notice.kind" role="status"></span>
|
|
4
4
|
</footer>
|
|
@@ -5,16 +5,17 @@
|
|
|
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 `
|
|
8
|
+
// current context (the command registry's `BAR_COMMANDS`, or the hints of the
|
|
9
9
|
// overlay that owns the keyboard); a pane error or a session notice takes
|
|
10
|
-
// its place. Entries are bracket shorthands (see `CommandItem.js`) that
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// its place. Entries are bracket shorthands (see `CommandItem.js`) that stay
|
|
11
|
+
// on one line: entries that do not fit are dropped and a trailing ellipsis
|
|
12
|
+
// takes their place, while the active pane's marked count joins the list as a
|
|
13
|
+
// dimmed, non-clickable entry. The row logic lives in `CommandItem.js`; this
|
|
14
|
+
// parent maps its `items` to rows.
|
|
14
15
|
//
|
|
15
16
|
// The web presentation swaps the key list for a single status line
|
|
16
17
|
// (`showKeys`/`showStatus`/`status`/`statusClass`); the TUI presentation
|
|
17
|
-
// keeps the
|
|
18
|
+
// keeps the single-line key list and swaps it for the status when there is one.
|
|
18
19
|
//
|
|
19
20
|
// Environment: `rows(data)` returning the keyed row list, `listClass` (web
|
|
20
21
|
// root class), `errorClass` (web), and the TUI colors plus the
|
|
@@ -23,6 +24,10 @@
|
|
|
23
24
|
import { unwrap } from "select/state/cells"
|
|
24
25
|
import { commandWidth } from "./CommandItem.js"
|
|
25
26
|
|
|
27
|
+
// Constant: ELLIPSIS
|
|
28
|
+
// The trailing marker shown when the bar cannot fit every entry.
|
|
29
|
+
const ELLIPSIS = { id: null, title: "…", dim: true }
|
|
30
|
+
|
|
26
31
|
// Function: commandBarStatus
|
|
27
32
|
// The one-line status the bar shows when something needs attention: the
|
|
28
33
|
// session notice first, then a pane error. Empty when everything is fine.
|
|
@@ -52,25 +57,25 @@ function commandBarMarksItem(data) {
|
|
|
52
57
|
return marks ? { id: null, title: marks, dim: true } : null
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
// Function:
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
// Function: commandBarFit
|
|
61
|
+
// Truncates entries to `width` columns, appending a dim ellipsis when an entry
|
|
62
|
+
// had to be dropped. `width <= 0` (not measured yet) keeps every entry and
|
|
63
|
+
// lets the renderer clip. The bar is always one line: entries never wrap.
|
|
64
|
+
function commandBarFit(items, width) {
|
|
65
|
+
const list = items ?? []
|
|
66
|
+
if (!width || width <= 0) return list
|
|
67
|
+
let used = 0
|
|
68
|
+
for (let i = 0; i < list.length; i++) {
|
|
69
|
+
const next = used + commandWidth(list[i])
|
|
70
|
+
if (next > width) {
|
|
71
|
+
const kept = list.slice(0, i)
|
|
72
|
+
// The ellipsis needs its own column plus the separating space.
|
|
73
|
+
while (kept.length && used + 2 > width) used -= commandWidth(kept.pop())
|
|
74
|
+
return [...kept, ELLIPSIS]
|
|
69
75
|
}
|
|
70
|
-
|
|
71
|
-
index++
|
|
76
|
+
used = next
|
|
72
77
|
}
|
|
73
|
-
return
|
|
78
|
+
return list
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
// Function: isInfo
|
|
@@ -91,7 +96,8 @@ function statusKind(data) {
|
|
|
91
96
|
function createBehaviors(env) {
|
|
92
97
|
const behaviors = {
|
|
93
98
|
listClass: () => env?.listClass,
|
|
94
|
-
|
|
99
|
+
showBar: (_self, data) => !unwrap(data?.desktopMode) || Boolean(commandBarStatus(data)),
|
|
100
|
+
showKeys: (_self, data) => !unwrap(data?.desktopMode) && !commandBarStatus(data),
|
|
95
101
|
showStatus: (_self, data) => Boolean(commandBarStatus(data)),
|
|
96
102
|
status: (_self, data) => commandBarStatus(data),
|
|
97
103
|
statusBg: (_self, data) => {
|
|
@@ -121,6 +127,6 @@ function createBehaviors(env) {
|
|
|
121
127
|
return behaviors
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
export { commandBarMarks, commandBarMarksItem,
|
|
130
|
+
export { commandBarFit, commandBarMarks, commandBarMarksItem, commandBarStatus, createBehaviors }
|
|
125
131
|
|
|
126
132
|
// EOF
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<box flexDirection="column" overflow="hidden">
|
|
2
|
-
<box when="showKeys" out="rows" flexDirection="row" flexWrap="
|
|
1
|
+
<box ref="box" flexDirection="column" overflow="hidden">
|
|
2
|
+
<box when="showKeys" out="rows" flexDirection="row" flexWrap="nowrap" flexGrow="1" overflow="hidden"></box>
|
|
3
3
|
<text when="showStatus" out="status" out:fg="statusFg" out:bg="statusBg" flexShrink="0"></text>
|
|
4
4
|
</box>
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
// both engines. `item.id` may be null for pure key hints, in which case a
|
|
9
9
|
// click dispatches nothing.
|
|
10
10
|
//
|
|
11
|
-
// Each entry renders as a plain bracket shorthand
|
|
12
|
-
// divider (except the first entry):
|
|
11
|
+
// Each entry renders as a plain bracket shorthand:
|
|
13
12
|
//
|
|
14
|
-
// [Open ⏎] — the
|
|
15
|
-
// the accent (primary) or secondary
|
|
16
|
-
// space separating it from the next
|
|
13
|
+
// [Open ⏎] — the brackets in the dim text colour, the title in the text
|
|
14
|
+
// colour, the key glyph in the accent (primary) or secondary
|
|
15
|
+
// accent, and a trailing space separating it from the next
|
|
16
|
+
// entry. Entries are separated by that space alone — no
|
|
17
|
+
// divider glyph.
|
|
17
18
|
//
|
|
18
19
|
// Entries without a key (the marked count) render as a dimmed plain word
|
|
19
|
-
// with no brackets. The web renders the same label as a single string
|
|
20
|
-
// buttons are separated by their gap, not the pipe).
|
|
20
|
+
// with no brackets. The web renders the same label as a single string.
|
|
21
21
|
//
|
|
22
22
|
// Environment: `accentFg` (primary), `accentSecondaryFg` (secondary),
|
|
23
23
|
// `textFg`, `textSecondaryFg`.
|
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
// Function: keyGlyph
|
|
26
26
|
// The shorthand glyph for a shortcut label: ⏎ for Enter, ⇥ for Tab.
|
|
27
27
|
function keyGlyph(keys) {
|
|
28
|
+
if (!keys) return keys
|
|
28
29
|
const GLYPHS = { Enter: "⏎", "↵": "⏎", Tab: "⇥" }
|
|
29
|
-
return
|
|
30
|
+
return keys
|
|
31
|
+
.split("/")
|
|
32
|
+
.map((key) => GLYPHS[key] ?? (key.endsWith("Tab") ? `${key.slice(0, -3)}⇥` : key))
|
|
33
|
+
.join("/")
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
// Function: commandLabel
|
|
@@ -36,16 +40,10 @@ function commandLabel(item) {
|
|
|
36
40
|
return item.keys ? `[${item.title} ${keyGlyph(item.keys)}] ` : `${item.title} `
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
// Constant: COMMAND_SEP
|
|
40
|
-
// The divider drawn before every entry but the first: a dim pipe and the
|
|
41
|
-
// space that keeps it clear of the button.
|
|
42
|
-
const COMMAND_SEP = "| "
|
|
43
|
-
|
|
44
43
|
// Function: commandWidth
|
|
45
|
-
// Columns the entry occupies: its label
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return commandLabel(item).length + (index > 0 ? COMMAND_SEP.length : 0)
|
|
44
|
+
// Columns the entry occupies: its label, brackets and trailing space included.
|
|
45
|
+
function commandWidth(item) {
|
|
46
|
+
return commandLabel(item).length
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
// Function: isPrimary
|
|
@@ -57,23 +55,24 @@ function isPrimary(item) {
|
|
|
57
55
|
// Function: createBehaviors
|
|
58
56
|
// Builds the CommandItem behavior map for the given renderer environment.
|
|
59
57
|
function createBehaviors(env) {
|
|
60
|
-
const head = (_self, data) => (data.item?.keys ? `[${data.item.title} ` : `${data.item?.title ?? ""} `)
|
|
61
58
|
return {
|
|
62
59
|
// Web: the whole shorthand as one label.
|
|
63
60
|
itemLabel: (_self, data) => commandLabel(data.item),
|
|
64
61
|
itemHint: (_self, data) => !data.item?.id,
|
|
65
|
-
// TUI: the dim
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
// TUI: the dim brackets around the bright title, then the accent key
|
|
63
|
+
// glyph and the closer. Keyless entries (the marked count) render as a
|
|
64
|
+
// plain dim word, with no brackets.
|
|
65
|
+
itemOpen: (_self, data) => (data.item?.keys ? "[" : ""),
|
|
66
|
+
itemOpenFg: () => env?.textSecondaryFg,
|
|
67
|
+
itemHead: (_self, data) => `${data.item?.title ?? ""} `,
|
|
70
68
|
itemFg: (_self, data) => (data.item?.keys ? env?.textFg : env?.textSecondaryFg),
|
|
71
69
|
keyText: (_self, data) => (data.item?.keys ? keyGlyph(data.item.keys) : ""),
|
|
72
70
|
keyFg: (_self, data) => (isPrimary(data.item) ? env?.accentFg : env?.accentSecondaryFg),
|
|
73
|
-
|
|
71
|
+
itemClose: (_self, data) => (data.item?.keys ? "] " : ""),
|
|
72
|
+
itemCloseFg: () => env?.textSecondaryFg,
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
export {
|
|
76
|
+
export { commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
|
|
78
77
|
|
|
79
78
|
// EOF
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<box flexDirection="row" height="1" flexShrink="0" on:click="item.id!commandbar:run">
|
|
2
|
-
<text out="
|
|
2
|
+
<text out="itemOpen" out:fg="itemOpenFg" wrapMode="none"></text>
|
|
3
3
|
<text out="itemHead" out:fg="itemFg" wrapMode="none"></text>
|
|
4
4
|
<text out="keyText" out:fg="keyFg" wrapMode="none"></text>
|
|
5
|
-
<text out="
|
|
5
|
+
<text out="itemClose" out:fg="itemCloseFg" wrapMode="none"></text>
|
|
6
6
|
</box>
|
|
@@ -1,6 +1,7 @@
|
|
|
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>
|
|
1
|
+
<section class="command-panel" when="open" role="dialog" aria-modal="true" aria-label="Command panel" tabindex="-1" on:click="!dialog:panel-cancel">
|
|
2
|
+
<div class="command-dialog" on:click="dialogStop">
|
|
3
|
+
<header class="panel-title" out="title"></header>
|
|
4
|
+
<button class="panel-close" type="button" tabindex="-1" aria-label="Close command panel" on:click="!panel:close." out="closeHint"></button>
|
|
4
5
|
<div class="panel-columns">
|
|
5
6
|
<div class="panel-command-column">
|
|
6
7
|
<label class="panel-query-line">
|
|
@@ -11,17 +12,16 @@
|
|
|
11
12
|
</div>
|
|
12
13
|
<div class="panel-args-column">
|
|
13
14
|
<p class="panel-command-name" out="commandName"></p>
|
|
14
|
-
<div
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
<div when="picking" out="pickerView|FilePicker"></div>
|
|
16
|
+
<div class="panel-args-scroll" when="!picking">
|
|
17
|
+
<ul class="panel-args" aria-label="Arguments" out="argRows"></ul>
|
|
18
|
+
<pre class="panel-preview" when="hasPreview" out="preview"></pre>
|
|
19
|
+
<ul class="panel-run-action" aria-label="Run command" out="runRows"></ul>
|
|
20
|
+
<div class="panel-run-error-block" when="hasRunError">
|
|
21
|
+
<p out="runError"></p>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="panel-confirm-command" when="hasConfirmCommand" out="confirmCommand" out:data-risk="confirmRisk"></p>
|
|
20
24
|
</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
25
|
</div>
|
|
26
26
|
</div>
|
|
27
27
|
<footer out="footer"></footer>
|
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandPanel
|
|
7
7
|
// CommandPanel: the one entry point (`:`), a two-pane palette. The left pane
|
|
8
|
-
// holds a `:` filter above
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the command list and the argument rows.
|
|
8
|
+
// holds a `:` filter above categorized commands. The right pane lists the
|
|
9
|
+
// selected operation's arguments followed by its [Run]/[Retry]
|
|
10
|
+
// row.
|
|
11
|
+
// The row logic lives in `CommandArgRow.js`; this parent maps the operation
|
|
12
|
+
// list, scrolling argument rows, custom preview and pinned action row.
|
|
14
13
|
//
|
|
15
14
|
// Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
|
|
16
15
|
// (footer), `textHeadingFg` (title/sign/command name), `opRows(self, data)` /
|
|
17
|
-
// `argRows(self, data)` returning
|
|
18
|
-
//
|
|
16
|
+
// `argRows(self, data)` returning keyed row lists, and
|
|
17
|
+
// the TUI viewport hooks `opScrollTop` / `argScrollTop`.
|
|
19
18
|
|
|
20
19
|
import { unwrap } from "select/state/cells"
|
|
21
20
|
import { PANEL_FOCUS } from "../core/panel.js"
|
|
@@ -29,10 +28,10 @@ function panelTitle(entry) {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
// Function: panelFooter
|
|
32
|
-
const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run
|
|
31
|
+
const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run"
|
|
33
32
|
|
|
34
33
|
// Function: pickerFooter
|
|
35
|
-
const PICKER_FOOTER = "type
|
|
34
|
+
const PICKER_FOOTER = "type path ↑↓ rows → open ↵ go ⌃↵ here ⇥ focus Esc back"
|
|
36
35
|
|
|
37
36
|
// Function: panelSign
|
|
38
37
|
// The filter prompt (`:`) before the command filter input.
|
|
@@ -47,29 +46,51 @@ function createBehaviors(env) {
|
|
|
47
46
|
const behaviors = {
|
|
48
47
|
bg: () => env.overlayBg,
|
|
49
48
|
borderFg: () => env.borderPrimaryActiveFg,
|
|
50
|
-
// The right pane's header: the selected command's name.
|
|
49
|
+
// The right pane's header: the selected command's name. The close
|
|
50
|
+
// shorthand lives in the dialog's top border.
|
|
51
51
|
commandName: (_self, data) => unwrap(data.entry)?.name ?? "",
|
|
52
52
|
commandNameFg: () => env.textHeadingFg,
|
|
53
|
+
closeHint: () => "[Close Esc]",
|
|
54
|
+
closeHintFg: () => env.textSecondaryFg,
|
|
53
55
|
confirm: (self) => self.pub("panel:confirm"),
|
|
54
|
-
// The
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
// The armed action prints the command it will execute below `[Confirm]`,
|
|
57
|
+
// tinted with the action's risk colour.
|
|
58
|
+
confirmCommand: (_self, data) =>
|
|
59
|
+
(unwrap(data?.rows) ?? []).find((row) => row.type === "run")?.command ?? "",
|
|
60
|
+
hasConfirmCommand: (_self, data) =>
|
|
61
|
+
Boolean((unwrap(data?.rows) ?? []).find((row) => row.type === "run")?.command),
|
|
62
|
+
confirmCommandFg: (_self, data) =>
|
|
63
|
+
unwrap(data?.entry)?.risk === "destructive" ? env.errorFg : env.warningFg,
|
|
64
|
+
confirmRisk: (_self, data) => unwrap(data?.entry)?.risk ?? null,
|
|
65
|
+
// A failed run moves its error out of the action row into a framed
|
|
66
|
+
// block below the button.
|
|
67
|
+
hasRunError: (_self, data) =>
|
|
68
|
+
Boolean((unwrap(data?.rows) ?? []).find((row) => row.type === "run" && row.error)?.error),
|
|
69
|
+
runError: (_self, data) =>
|
|
70
|
+
(unwrap(data?.rows) ?? []).find((row) => row.type === "run" && row.error)?.error ?? "",
|
|
71
|
+
runErrorFg: () => env.errorFg,
|
|
72
|
+
// The file picker sub-mode: the nested FilePicker owns its editable path,
|
|
73
|
+
// file-panel rows and scrollbar.
|
|
61
74
|
picking: (_self, data) => Boolean(unwrap(data.picking)),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
pickerView: (_self, data) => ({
|
|
76
|
+
open: data.picking,
|
|
77
|
+
path: data.pickerPathText,
|
|
78
|
+
focus: data.pickerFocus,
|
|
79
|
+
rows: data.pickerRows,
|
|
80
|
+
cursor: data.argCursor,
|
|
81
|
+
}),
|
|
82
|
+
footer: (_self, data) => {
|
|
83
|
+
if (unwrap(data?.picking)) return PICKER_FOOTER
|
|
84
|
+
// An armed risky action turns Enter into the confirmation step. The
|
|
85
|
+
// run row carries the flag (the rows input is already reactive).
|
|
86
|
+
const armed = (unwrap(data?.rows) ?? []).some((row) => row.type === "run" && row.armed)
|
|
87
|
+
const base = armed ? PANEL_FOOTER.replace("↵ run", "↵ confirm") : PANEL_FOOTER
|
|
88
|
+
// The permission grid adds a bit cursor to the usual keys.
|
|
89
|
+
return unwrap(data?.entry)?.name === "chmod" ? `${base} ←→ bits` : base
|
|
90
|
+
},
|
|
69
91
|
footerFg: () => env.textSecondaryFg,
|
|
70
|
-
// Dividers: a vertical rule between the panes
|
|
71
|
-
//
|
|
72
|
-
// colour, like the pane header rules.
|
|
92
|
+
// Dividers: a vertical rule between the panes and a horizontal rule under
|
|
93
|
+
// the filter, in the secondary border colour.
|
|
73
94
|
ruleFg: () => env.borderSecondaryFg,
|
|
74
95
|
sideRule: () => ["left"],
|
|
75
96
|
headRule: () => ["top"],
|
|
@@ -77,15 +98,16 @@ function createBehaviors(env) {
|
|
|
77
98
|
focusArgs: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.ARGS,
|
|
78
99
|
focusOps: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.OPS,
|
|
79
100
|
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
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.
|
|
82
|
-
preview: (_self, data) => unwrap(data.command) ?? "",
|
|
83
|
-
runnableState: (_self, data) => `${unwrap(data.runnable) !== false}`,
|
|
84
|
-
previewFg: () => env.textSecondaryFg,
|
|
85
101
|
// The filter input owns the keyboard while the panel is open, the
|
|
86
102
|
// command list is focused and the picker is not browsing.
|
|
87
103
|
queryFocused: (self, data) =>
|
|
88
104
|
panelOpen(self, data) && !unwrap(data.picking) && unwrap(data.focus) !== PANEL_FOCUS.ARGS,
|
|
105
|
+
// Only operation-specific preflight output is rendered; the assembled
|
|
106
|
+
// shell command remains available from the session but is not echoed.
|
|
107
|
+
hasPreview: (_self, data) => !unwrap(data.picking) && Boolean(unwrap(data.preview)),
|
|
108
|
+
preview: (_self, data) => unwrap(data.preview) ?? "",
|
|
109
|
+
previewFg: () => env.textSecondaryFg,
|
|
110
|
+
panelHeight: (self, data) => env.panelHeight?.(self, data) ?? "72%",
|
|
89
111
|
sign: () => panelSign(),
|
|
90
112
|
signFg: () => env.textHeadingFg,
|
|
91
113
|
title: (_self, data) => panelTitle(data.entry),
|
|
@@ -93,6 +115,8 @@ function createBehaviors(env) {
|
|
|
93
115
|
}
|
|
94
116
|
if (env?.opRows) behaviors.opRows = (self, data) => env.opRows(self, data)
|
|
95
117
|
if (env?.argRows) behaviors.argRows = (self, data) => env.argRows(self, data)
|
|
118
|
+
if (env?.runRows) behaviors.runRows = (self, data) => env.runRows(self, data)
|
|
119
|
+
if (env?.opScrollTop) behaviors.opScrollTop = (self, data) => env.opScrollTop(self, data)
|
|
96
120
|
if (env?.argScrollTop) behaviors.argScrollTop = (self, data) => env.argScrollTop(self, data)
|
|
97
121
|
return behaviors
|
|
98
122
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<box when="open" width="100%" height="100%" alignItems="center" justifyContent="center">
|
|
2
|
-
<box width="80%" height="
|
|
2
|
+
<box width="80%" out:height="panelHeight" border borderStyle="rounded"
|
|
3
3
|
out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
|
|
4
4
|
out:backgroundColor="bg" flexDirection="column">
|
|
5
5
|
<box flexDirection="row" flexGrow="1" overflow="hidden">
|
|
@@ -10,40 +10,50 @@
|
|
|
10
10
|
<input ref="query" inout="query" flexGrow="1" height="1" out:focused="queryFocused"></input>
|
|
11
11
|
</box>
|
|
12
12
|
<box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
|
|
13
|
-
<scrollbox flexGrow="1">
|
|
13
|
+
<scrollbox ref="ops" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="opScrollTop">
|
|
14
14
|
<box out="opRows" flexDirection="column"></box>
|
|
15
15
|
</scrollbox>
|
|
16
16
|
</box>
|
|
17
17
|
<!-- Vertical rule between the panes. -->
|
|
18
18
|
<box width="1" out:border="sideRule" out:borderColor="ruleFg"></box>
|
|
19
|
-
<!-- Right pane: the command name,
|
|
19
|
+
<!-- Right pane: the command name, arguments and trailing Run action. -->
|
|
20
20
|
<box flexDirection="column" flexGrow="3" flexBasis="0" overflow="hidden">
|
|
21
|
+
<!-- The command name heads the pane. -->
|
|
21
22
|
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
22
|
-
<text out="commandName" out:fg="commandNameFg" wrapMode="none"></text>
|
|
23
|
+
<text out="commandName" out:fg="commandNameFg" flexGrow="1" flexShrink="1" minWidth="0" wrapMode="none" truncate="true"></text>
|
|
23
24
|
</box>
|
|
24
25
|
<box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
<!-- Arguments, preflight preview and the Run action share one
|
|
27
|
+
scroller, so Run lands one line below the last field rather
|
|
28
|
+
than pinned to the bottom of the pane. -->
|
|
29
|
+
<scrollbox when="!picking" ref="args" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="argScrollTop">
|
|
30
|
+
<box out="argRows" flexDirection="column"></box>
|
|
31
|
+
<box ref="previewBox" when="hasPreview" flexDirection="column" flexShrink="0" maxHeight="5" paddingLeft="1" overflow="hidden">
|
|
32
|
+
<text out="preview" out:fg="previewFg" wrapMode="word"></text>
|
|
33
|
+
</box>
|
|
34
|
+
<box out="runRows" flexDirection="column" flexShrink="0" paddingLeft="1" paddingRight="1"></box>
|
|
35
|
+
<box when="hasRunError" border borderStyle="single" out:borderColor="runErrorFg"
|
|
36
|
+
flexDirection="column" flexShrink="0" marginTop="1" marginLeft="1" marginRight="1"
|
|
37
|
+
paddingLeft="1" paddingRight="1">
|
|
38
|
+
<text out="runError" out:fg="runErrorFg" wrapMode="word"></text>
|
|
39
|
+
</box>
|
|
40
|
+
<box when="hasConfirmCommand" paddingLeft="1">
|
|
41
|
+
<text out="confirmCommand" out:fg="confirmCommandFg" wrapMode="word"></text>
|
|
37
42
|
</box>
|
|
38
43
|
</scrollbox>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</box>
|
|
44
|
+
<filepicker when="picking" flexGrow="1" flexShrink="1" minHeight="0"
|
|
45
|
+
in:open="picking" in:path="pickerPathText" in:focus="pickerFocus"
|
|
46
|
+
in:rows="pickerRows" in:cursor="argCursor"></filepicker>
|
|
43
47
|
</box>
|
|
44
48
|
</box>
|
|
45
49
|
<box flexDirection="row" height="1" paddingLeft="1">
|
|
46
50
|
<text out="footer" out:fg="footerFg"></text>
|
|
47
51
|
</box>
|
|
52
|
+
<!-- The close action sits in the top border, upper-right, like a close
|
|
53
|
+
button; the background masks the border run underneath. -->
|
|
54
|
+
<box position="absolute" top="-1" right="2" height="1" zIndex="3"
|
|
55
|
+
out:backgroundColor="bg" on:click="!panel:close.">
|
|
56
|
+
<text out="closeHint" out:fg="closeHintFg" wrapMode="none"></text>
|
|
57
|
+
</box>
|
|
48
58
|
</box>
|
|
49
59
|
</box>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<div class="confirm" when="visible" role="dialog" aria-modal="true" aria-label="Confirm operation" tabindex="-1">
|
|
2
|
-
<p out="text"></p>
|
|
1
|
+
<div class="confirm" when="visible" role="dialog" aria-modal="true" aria-label="Confirm operation" tabindex="-1" on:click="!dialog:confirm-cancel">
|
|
2
|
+
<p out="text" on:click="dialogStop"></p>
|
|
3
3
|
</div>
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
//
|
|
11
11
|
// - `selected` (the pane's marked count, above one) leads in the mark colour;
|
|
12
12
|
// - `date` (mtime) and `size` are the numeric fields, drawn bright;
|
|
13
|
-
// - `type` (file/dir/link/dev) is coloured like the listing
|
|
13
|
+
// - `type` (file/dir/link/dev) is coloured like the listing, a plain file
|
|
14
|
+
// taking its executable/read-only colour too;
|
|
14
15
|
// - `rights` (permission mode) is accented.
|
|
15
16
|
//
|
|
16
17
|
// When the pane is too narrow the trailing blocks give way first (`rights`,
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
// out.
|
|
22
23
|
|
|
23
24
|
import { unwrap } from "select/state/cells"
|
|
25
|
+
import { isExecutable, isReadonly, nameFg } from "./FileRow.js"
|
|
24
26
|
import { formatMode, formatSize, formatTime } from "./format.js"
|
|
25
27
|
|
|
26
28
|
// Constant: KIND_LABELS
|
|
@@ -49,13 +51,24 @@ function entryKind(entry) {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// Function: typeColor
|
|
52
|
-
// The listing colour for an entry's kind,
|
|
54
|
+
// The listing colour for an entry's kind, delegating to `FileRow.nameFg` so
|
|
55
|
+
// the detail's type reads exactly like the row's name — directory, symlink and
|
|
56
|
+
// device kinds, plus the executable/read-only states of a plain file.
|
|
53
57
|
function typeColor(entry, env) {
|
|
58
|
+
return nameFg(entry, env)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Function: typeClass
|
|
62
|
+
// The web state classes for the detail's type span: the kind's `is-<kind>`
|
|
63
|
+
// class, plus the executable/read-only class when the entry is a plain file
|
|
64
|
+
// (mirroring `FileRow.nameClass`, but on the type-specific `is-<kind>` base).
|
|
65
|
+
function typeClass(entry, env) {
|
|
54
66
|
const kind = entryKind(entry)
|
|
55
|
-
|
|
56
|
-
if (kind
|
|
57
|
-
if (
|
|
58
|
-
return env.
|
|
67
|
+
const base = `is-${kind}`
|
|
68
|
+
if (kind !== "file") return base
|
|
69
|
+
if (isExecutable(entry)) return `${base} ${env.execClass}`
|
|
70
|
+
if (isReadonly(entry)) return `${base} ${env.readonlyClass}`
|
|
71
|
+
return base
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
// Function: detailBlocks
|
|
@@ -168,7 +181,7 @@ function createBehaviors(env = {}) {
|
|
|
168
181
|
detailTime: (self, data) => text(self, data, "time"),
|
|
169
182
|
detailHasType: (self, data) => has(self, data, "type"),
|
|
170
183
|
detailType: (self, data) => text(self, data, "type"),
|
|
171
|
-
detailTypeClass: (_self, data) =>
|
|
184
|
+
detailTypeClass: (_self, data) => typeClass(unwrap(data.entry), env),
|
|
172
185
|
detailTypeFg: (_self, data) => typeColor(unwrap(data.entry), env),
|
|
173
186
|
detailHasSize: (self, data) => has(self, data, "size"),
|
|
174
187
|
detailSize: (self, data) => text(self, data, "size"),
|
|
@@ -190,5 +203,6 @@ export {
|
|
|
190
203
|
detailBlocks,
|
|
191
204
|
detailLine,
|
|
192
205
|
entryKind,
|
|
206
|
+
typeClass,
|
|
193
207
|
typeColor,
|
|
194
208
|
}
|