@ssebastien/tcmd 0.1.3 → 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 +256 -62
- package/bin/tcmd +10 -2
- package/bin/tcmd.js +13 -2
- 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 +19 -6
- package/public/fonts/OFL.txt +93 -0
- package/public/index.html +14 -0
- package/public/licenses/LUCIDE.txt +43 -0
- package/public/main.js +20679 -0
- package/public/style.css +1515 -0
- package/scripts/build-desktop.js +31 -0
- package/scripts/serve-web.js +61 -0
- package/src/js/tcmd/components/App.html +80 -14
- package/src/js/tcmd/components/App.js +339 -114
- package/src/js/tcmd/components/App.xml +76 -22
- package/src/js/tcmd/components/BookmarkList.html +5 -5
- package/src/js/tcmd/components/BookmarkList.js +15 -41
- 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/Breadcrumbs.js +1 -1
- package/src/js/tcmd/components/CommandArgRow.html +28 -0
- package/src/js/tcmd/components/CommandArgRow.js +146 -30
- package/src/js/tcmd/components/CommandArgRow.xml +55 -4
- package/src/js/tcmd/components/CommandBar.html +2 -2
- package/src/js/tcmd/components/CommandBar.js +33 -27
- package/src/js/tcmd/components/CommandBar.xml +2 -2
- package/src/js/tcmd/components/CommandItem.html +1 -1
- package/src/js/tcmd/components/CommandItem.js +25 -25
- package/src/js/tcmd/components/CommandItem.xml +2 -2
- package/src/js/tcmd/components/CommandPanel.html +29 -0
- package/src/js/tcmd/components/CommandPanel.js +78 -52
- package/src/js/tcmd/components/CommandPanel.xml +49 -25
- package/src/js/tcmd/components/Confirm.html +2 -2
- package/src/js/tcmd/components/EntryDetail.html +7 -7
- package/src/js/tcmd/components/EntryDetail.js +22 -8
- package/src/js/tcmd/components/FileList.html +5 -1
- package/src/js/tcmd/components/FileList.js +2 -1
- 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 +14 -4
- package/src/js/tcmd/components/FilePreview.js +111 -12
- package/src/js/tcmd/components/FilePreview.xml +28 -8
- package/src/js/tcmd/components/FileRow.html +2 -1
- package/src/js/tcmd/components/FileRow.js +70 -12
- 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/components/Frame.js +324 -0
- package/src/js/tcmd/components/Help.html +15 -0
- package/src/js/tcmd/components/Help.js +260 -0
- 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/{CommandPalette.xml → OverlayList.xml} +2 -5
- 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/Summary.html +1 -1
- package/src/js/tcmd/components/Summary.js +1 -6
- package/src/js/tcmd/components/format.js +4 -23
- package/src/js/tcmd/components/icons.js +176 -5
- package/src/js/tcmd/components/index.js +9 -5
- package/src/js/tcmd/components/rows.js +176 -48
- 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 +978 -260
- 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 +219 -93
- package/src/js/tcmd/core/format.js +36 -0
- package/src/js/tcmd/core/fs.js +438 -54
- package/src/js/tcmd/core/history.js +58 -0
- package/src/js/tcmd/core/index.js +3 -9
- package/src/js/tcmd/core/journal.js +93 -0
- package/src/js/tcmd/core/keymap.js +42 -6
- package/src/js/tcmd/core/operations.js +813 -48
- package/src/js/tcmd/core/os.js +29 -3
- package/src/js/tcmd/core/pane.js +33 -8
- package/src/js/tcmd/core/panel.js +280 -104
- 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/script.js +7 -95
- package/src/js/tcmd/core/selection.js +9 -1
- package/src/js/tcmd/core/session.js +498 -200
- 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/index.js +0 -1
- package/src/js/tcmd/tui/app.js +118 -60
- package/src/js/tcmd/tui/catalogue.js +238 -42
- package/src/js/tcmd/tui/index.js +18 -19
- 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 +32 -2
- package/src/js/tcmd/vendor/bzip2.js +276 -0
- package/src/js/tcmd/web/app.js +225 -41
- package/src/js/tcmd/web/bookmarks.js +36 -0
- package/src/js/tcmd/web/catalogue.js +81 -38
- package/src/js/tcmd/web/effects.js +223 -0
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/header.js +15 -11
- package/src/js/tcmd/web/http.js +13 -0
- package/src/js/tcmd/web/index.js +2 -2
- package/src/js/tcmd/web/main.js +6 -66
- package/src/js/tcmd/web/opfs.js +297 -30
- package/src/js/tcmd/web/seed.js +4 -4
- package/src/js/tcmd/web/server.js +124 -21
- package/src/js/tcmd/web/theme.js +4 -0
- package/tinyjs.json +16 -0
- package/src/js/tcmd/components/CommandPalette.html +0 -7
- package/src/js/tcmd/components/CommandPalette.js +0 -58
- 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/frame.js +0 -269
- package/src/js/tcmd/tui/help.js +0 -32
|
@@ -4,95 +4,121 @@
|
|
|
4
4
|
// Created: 2026-10-09
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/CommandPanel
|
|
7
|
-
// CommandPanel: the
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// shows the assembled command the draft maps to. The footer advertises the
|
|
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 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.
|
|
16
13
|
//
|
|
17
14
|
// Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
|
|
18
|
-
// (footer), `textHeadingFg` (title/sign), `
|
|
19
|
-
// `
|
|
20
|
-
//
|
|
21
|
-
// tracking).
|
|
15
|
+
// (footer), `textHeadingFg` (title/sign/command name), `opRows(self, data)` /
|
|
16
|
+
// `argRows(self, data)` returning keyed row lists, and
|
|
17
|
+
// the TUI viewport hooks `opScrollTop` / `argScrollTop`.
|
|
22
18
|
|
|
23
19
|
import { unwrap } from "select/state/cells"
|
|
24
20
|
import { PANEL_FOCUS } from "../core/panel.js"
|
|
25
21
|
|
|
26
22
|
// Function: panelTitle
|
|
27
|
-
// " Command · copy " while an
|
|
23
|
+
// " Command · copy " while an entry is selected (spaces so the title sits
|
|
28
24
|
// clear of the border corners).
|
|
29
|
-
function panelTitle(
|
|
30
|
-
const name = unwrap(
|
|
25
|
+
function panelTitle(entry) {
|
|
26
|
+
const name = unwrap(entry)?.name
|
|
31
27
|
return name ? ` Command · ${name} ` : " Command "
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
// Function: panelFooter
|
|
35
|
-
const PANEL_FOOTER = "type to filter ⇥ args Spc toggle ↵ run
|
|
31
|
+
const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run"
|
|
32
|
+
|
|
33
|
+
// Function: pickerFooter
|
|
34
|
+
const PICKER_FOOTER = "type path ↑↓ rows → open ↵ go ⌃↵ here ⇥ focus Esc back"
|
|
36
35
|
|
|
37
36
|
// Function: panelSign
|
|
38
|
-
// The prompt before the command filter input
|
|
39
|
-
// text doesn't crowd the arrow; overridable per renderer).
|
|
37
|
+
// The filter prompt (`:`) before the command filter input.
|
|
40
38
|
function panelSign() {
|
|
41
|
-
return "
|
|
39
|
+
return ":"
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
// Function: createBehaviors
|
|
45
43
|
// Builds the CommandPanel behavior map for the given renderer environment.
|
|
46
44
|
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)
|
|
45
|
+
const panelOpen = (_self, data) => Boolean(unwrap(data.open))
|
|
51
46
|
const behaviors = {
|
|
52
47
|
bg: () => env.overlayBg,
|
|
53
48
|
borderFg: () => env.borderPrimaryActiveFg,
|
|
54
|
-
//
|
|
55
|
-
//
|
|
49
|
+
// The right pane's header: the selected command's name. The close
|
|
50
|
+
// shorthand lives in the dialog's top border.
|
|
51
|
+
commandName: (_self, data) => unwrap(data.entry)?.name ?? "",
|
|
52
|
+
commandNameFg: () => env.textHeadingFg,
|
|
53
|
+
closeHint: () => "[Close Esc]",
|
|
54
|
+
closeHintFg: () => env.textSecondaryFg,
|
|
56
55
|
confirm: (self) => self.pub("panel:confirm"),
|
|
57
|
-
// The
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
74
|
+
picking: (_self, data) => Boolean(unwrap(data.picking)),
|
|
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
|
|
64
90
|
},
|
|
65
|
-
footer: () => PANEL_FOOTER,
|
|
66
91
|
footerFg: () => env.textSecondaryFg,
|
|
67
|
-
// Dividers: a vertical rule between the
|
|
68
|
-
//
|
|
69
|
-
// secondary border 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.
|
|
70
94
|
ruleFg: () => env.borderSecondaryFg,
|
|
71
95
|
sideRule: () => ["left"],
|
|
72
96
|
headRule: () => ["top"],
|
|
73
97
|
labelFg: () => env.textSecondaryFg,
|
|
74
98
|
focusArgs: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.ARGS,
|
|
75
|
-
focusOps: (_self, data) => unwrap(data.focus)
|
|
99
|
+
focusOps: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.OPS,
|
|
76
100
|
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
77
|
-
// The
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
// The filter input owns the keyboard while the panel is open, the
|
|
102
|
+
// command list is focused and the picker is not browsing.
|
|
103
|
+
queryFocused: (self, data) =>
|
|
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%",
|
|
87
111
|
sign: () => panelSign(),
|
|
88
112
|
signFg: () => env.textHeadingFg,
|
|
89
|
-
title: (_self, data) => panelTitle(data.
|
|
113
|
+
title: (_self, data) => panelTitle(data.entry),
|
|
90
114
|
titleColor: () => env.textHeadingFg,
|
|
91
115
|
}
|
|
92
|
-
if (env?.opRows) behaviors.opRows = (
|
|
93
|
-
if (env?.argRows) behaviors.argRows = (
|
|
116
|
+
if (env?.opRows) behaviors.opRows = (self, data) => env.opRows(self, data)
|
|
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)
|
|
94
120
|
if (env?.argScrollTop) behaviors.argScrollTop = (self, data) => env.argScrollTop(self, data)
|
|
95
121
|
return behaviors
|
|
96
122
|
}
|
|
97
123
|
|
|
98
|
-
export { PANEL_FOOTER, createBehaviors, panelSign, panelTitle }
|
|
124
|
+
export { PANEL_FOOTER, PICKER_FOOTER, createBehaviors, panelSign, panelTitle }
|
|
@@ -1,35 +1,59 @@
|
|
|
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
|
-
<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 ref="ops" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="opScrollTop">
|
|
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, arguments and trailing Run action. -->
|
|
20
|
+
<box flexDirection="column" flexGrow="3" flexBasis="0" overflow="hidden">
|
|
21
|
+
<!-- The command name heads the pane. -->
|
|
22
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
23
|
+
<text out="commandName" out:fg="commandNameFg" flexGrow="1" flexShrink="1" minWidth="0" wrapMode="none" truncate="true"></text>
|
|
24
|
+
</box>
|
|
25
|
+
<box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
|
|
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>
|
|
42
|
+
</box>
|
|
43
|
+
</scrollbox>
|
|
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>
|
|
47
|
+
</box>
|
|
30
48
|
</box>
|
|
31
49
|
<box flexDirection="row" height="1" paddingLeft="1">
|
|
32
50
|
<text out="footer" out:fg="footerFg"></text>
|
|
33
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>
|
|
34
58
|
</box>
|
|
35
59
|
</box>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<div class="confirm" when="visible">
|
|
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>
|
|
@@ -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>
|
|
@@ -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
|
|
@@ -135,7 +148,7 @@ function createBehaviors(env = {}) {
|
|
|
135
148
|
const entry = unwrap(data.entry)
|
|
136
149
|
const marked = unwrap(data.marked)
|
|
137
150
|
const markedCount = marked?.size ?? 0
|
|
138
|
-
const width = env.width ? env.width(self) : 0
|
|
151
|
+
const width = env.width ? env.width(self, data) : 0
|
|
139
152
|
const kept = fitBlocks(detailBlocks(entry, markedCount), width, measure, env.elide)
|
|
140
153
|
const keys = new Set(kept.map((block) => block.key))
|
|
141
154
|
const texts = {}
|
|
@@ -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
|
}
|
|
@@ -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)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="panel-picker-pane" when="open" aria-label="File picker">
|
|
2
|
+
<div class="panel-picker">
|
|
3
|
+
<label class="panel-picker-line">
|
|
4
|
+
<svg class="panel-picker-icon ui-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use href="#lucide-folder-open"></use></svg>
|
|
5
|
+
<input class="panel-picker-path" type="text" inout="path" aria-label="Path" spellcheck="false" autocomplete="off" />
|
|
6
|
+
</label>
|
|
7
|
+
</div>
|
|
8
|
+
<ul class="panel-args panel-picker-rows" aria-label="Picker entries" out="rows"></ul>
|
|
9
|
+
<p class="panel-args-empty" when="empty" out="emptyText"></p>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-20
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FilePicker
|
|
7
|
+
// The command panel's file picker: an editable path line over a file-panel
|
|
8
|
+
// listing (`FilePickerRow`), with the `..` parent row kept at the top. Typing
|
|
9
|
+
// in the path narrows the listing (or enters a directory) through the session's
|
|
10
|
+
// path resolution; the row logic lives in `FilePickerRow.js`.
|
|
11
|
+
//
|
|
12
|
+
// Environment: `rows(self, data)` returning keyed `FilePickerRow`s and
|
|
13
|
+
// `scrollTop(self, data)` (TUI viewport tracking).
|
|
14
|
+
|
|
15
|
+
import { unwrap } from "select/state/cells"
|
|
16
|
+
|
|
17
|
+
// Function: createBehaviors
|
|
18
|
+
// Builds the FilePicker behavior map for the given renderer environment.
|
|
19
|
+
function createBehaviors(env) {
|
|
20
|
+
const open = (_self, data) => Boolean(unwrap(data.open))
|
|
21
|
+
const behaviors = {
|
|
22
|
+
open,
|
|
23
|
+
// The editable path owns the keyboard by default; Down/Tab hands it to
|
|
24
|
+
// the entry list.
|
|
25
|
+
pathFocused: (_self, data) => open(_self, data) && (unwrap(data.focus) ?? "path") === "path",
|
|
26
|
+
pathSign: () => "› ",
|
|
27
|
+
pathSignFg: () => env.accentFg,
|
|
28
|
+
// The parent row is a navigation row, not a match: an empty listing is
|
|
29
|
+
// an empty result even while `..` stays visible.
|
|
30
|
+
empty: (_self, data) =>
|
|
31
|
+
open(_self, data) && !(unwrap(data.rows) ?? []).some((row) => row.type === "entry"),
|
|
32
|
+
emptyText: () => " no matches",
|
|
33
|
+
emptyFg: () => env.textSecondaryFg,
|
|
34
|
+
rule: () => ["bottom"],
|
|
35
|
+
ruleFg: () => env.borderSecondaryFg,
|
|
36
|
+
borderFg: () => env.borderSecondaryFg,
|
|
37
|
+
}
|
|
38
|
+
if (env?.rows) behaviors.rows = (self, data) => env.rows(self, data)
|
|
39
|
+
if (env?.scrollTop) behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
|
|
40
|
+
return behaviors
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { createBehaviors }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<box when="open" flexDirection="column" flexGrow="1" flexShrink="1" minHeight="0" paddingLeft="1" paddingRight="1" paddingBottom="1">
|
|
2
|
+
<box flexDirection="column" flexGrow="1" flexShrink="1" minHeight="0" border borderStyle="single" out:borderColor="borderFg">
|
|
3
|
+
<box flexDirection="row" height="1" paddingLeft="1" paddingRight="1" overflow="hidden">
|
|
4
|
+
<text out="pathSign" out:fg="pathSignFg" wrapMode="none"></text>
|
|
5
|
+
<input ref="path" inout="path" flexGrow="1" minWidth="0" height="1" out:focused="pathFocused"></input>
|
|
6
|
+
</box>
|
|
7
|
+
<box height="1" out:border="rule" out:borderColor="ruleFg"></box>
|
|
8
|
+
<scrollbox ref="list" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="scrollTop">
|
|
9
|
+
<box out="rows" flexDirection="column"></box>
|
|
10
|
+
</scrollbox>
|
|
11
|
+
<text when="empty" out="emptyText" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
|
|
12
|
+
</box>
|
|
13
|
+
</box>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<li class="file-row picker-row" out:class="rowClass" out:data-marked="rowMarked" out:data-active="rowActive" out:data-kind="rowKind" out:data-type="rowType" out:data-key="rowHref" on:click="key!panel:select">
|
|
2
|
+
<span class="marker" out="rowMarker"></span>
|
|
3
|
+
<svg class="file-row-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="rowIconHref"></use></svg>
|
|
4
|
+
<span class="name" out:class="rowNameClass" out="rowName"></span>
|
|
5
|
+
<span class="size" out="rowSize"></span>
|
|
6
|
+
</li>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-20
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FilePickerRow
|
|
7
|
+
// One entry in the file picker. The row presentation deliberately mirrors the
|
|
8
|
+
// file panes (`FileRow`): marker, kind icon, name and right-aligned size. It
|
|
9
|
+
// only overrides the `..` parent row (no trailing slash) and its click event,
|
|
10
|
+
// which the command panel routes back through `panel:select`.
|
|
11
|
+
|
|
12
|
+
import { createBehaviors as fileRowBehaviors, rowName as fileRowName } from "./FileRow.js"
|
|
13
|
+
|
|
14
|
+
// Function: createBehaviors
|
|
15
|
+
// Builds the FilePickerRow behavior map from the shared FileRow behaviors.
|
|
16
|
+
function createBehaviors(env) {
|
|
17
|
+
const behaviors = fileRowBehaviors(env)
|
|
18
|
+
return {
|
|
19
|
+
...behaviors,
|
|
20
|
+
rowName: (_self, data) => (data.item?.isParent ? ".." : fileRowName(data.item)),
|
|
21
|
+
rowSize: (_self, data) => (data.item?.isParent ? "" : behaviors.rowSize?.(_self, data) ?? ""),
|
|
22
|
+
rowType: (_self, data) => (data.item?.isParent ? "parent" : data.item?.type ?? "entry"),
|
|
23
|
+
rowHref: (_self, data) => data.key ?? data.item?.path ?? "",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { createBehaviors }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="key!panel:select">
|
|
2
|
+
<text out="rowMarker" width="2" out:fg="rowMarkerFg"></text>
|
|
3
|
+
<box flexGrow="1" minWidth="0" overflow="hidden" flexDirection="row" columnGap="1">
|
|
4
|
+
<text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
|
|
5
|
+
<text out="rowName" out:fg="rowNameFg" wrapMode="none" truncate="true"></text>
|
|
6
|
+
</box>
|
|
7
|
+
<box width="9" justifyContent="flex-end">
|
|
8
|
+
<text out="rowSize" out:fg="rowSizeFg" wrapMode="none"></text>
|
|
9
|
+
</box>
|
|
10
|
+
</box>
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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>
|
|
5
15
|
</aside>
|