@ssebastien/tcmd 0.1.3 → 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 +10 -2
- package/bin/tcmd.js +13 -2
- 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,61 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-27
|
|
5
|
+
|
|
6
|
+
// Module: scripts/serve-web
|
|
7
|
+
// CLI for the web view server: serves `public/` plus the `/api/*` filesystem
|
|
8
|
+
// API (see `src/js/tcmd/web/server.js`). The page uses OPFS when the browser
|
|
9
|
+
// has it and falls back to the API otherwise, so both backends come from this
|
|
10
|
+
// one command.
|
|
11
|
+
//
|
|
12
|
+
// Environment: HOST (default 127.0.0.1, an IP literal: 0.0.0.0 or :: serves
|
|
13
|
+
// every interface), PORT (default 8787), TCMD_WEB_ROOT (default "/", like the
|
|
14
|
+
// TUI).
|
|
15
|
+
|
|
16
|
+
import { createWebServer } from "../src/js/tcmd/web/server.js"
|
|
17
|
+
import { isIP } from "node:net"
|
|
18
|
+
import { fileURLToPath } from "node:url"
|
|
19
|
+
|
|
20
|
+
// Function: hostUrl
|
|
21
|
+
// The URL a client should open. IPv6 literals (possibly zone-qualified) are
|
|
22
|
+
// bracketed so the result is a valid URL.
|
|
23
|
+
function hostUrl(hostname, port) {
|
|
24
|
+
const host = isIP(hostname) === 6 ? `[${hostname}]` : hostname
|
|
25
|
+
return `http://${host}:${port}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Function: main
|
|
29
|
+
// Starts the existing loopback server. Published packages include the bundle;
|
|
30
|
+
// a source checkout builds it on demand without depending on the caller's cwd.
|
|
31
|
+
async function main(options = {}) {
|
|
32
|
+
const publicDir = options.publicDir ?? new URL("../public/", import.meta.url)
|
|
33
|
+
if (!(await Bun.file(new URL("main.js", publicDir)).exists())) {
|
|
34
|
+
const build = await Bun.build({
|
|
35
|
+
entrypoints: [fileURLToPath(new URL("../src/js/tcmd/web/main.js", import.meta.url))],
|
|
36
|
+
outdir: fileURLToPath(publicDir),
|
|
37
|
+
target: "browser",
|
|
38
|
+
})
|
|
39
|
+
if (!build.success) throw new AggregateError(build.logs, "Could not build the web UI")
|
|
40
|
+
}
|
|
41
|
+
const result = createWebServer({ ...options, publicDir })
|
|
42
|
+
const host = result.server.hostname
|
|
43
|
+
// A machine name resolves to several addresses (often including link-local
|
|
44
|
+
// IPv6), but the server binds only one of them, so prefer an IP literal.
|
|
45
|
+
if (host !== "127.0.0.1" && host !== "localhost" && isIP(host) === 0) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`tcmd web: warning: ${host} is a name; bound to one of its addresses only.\n` +
|
|
48
|
+
` Use an IP literal instead (0.0.0.0 or :: for every interface).`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
console.log(`tcmd web: ${hostUrl(host, result.server.port)}`)
|
|
52
|
+
console.log(`filesystem API: /api/* rooted at ${result.root}`)
|
|
53
|
+
console.log("use ?fs=http for your files, or ?fs=opfs for browser storage")
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (import.meta.main) await main()
|
|
58
|
+
|
|
59
|
+
export { hostUrl, main }
|
|
60
|
+
|
|
61
|
+
// EOF
|
|
@@ -1,28 +1,44 @@
|
|
|
1
|
-
<div class="app" tabindex="0" on:keydown="keys">
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
1
|
+
<div class="app" ref="root" tabindex="0" on:keydown="keys">
|
|
2
|
+
<div out="headerView|Header"></div>
|
|
3
|
+
<section class="resize-hint" when="tooSmall">
|
|
4
|
+
<h2 out="minSizeTitle"></h2>
|
|
5
|
+
<p out="minSizeHint"></p>
|
|
6
|
+
</section>
|
|
7
|
+
<main class="panes" when="!tooSmall">
|
|
8
|
+
<section class="pane pane-parent" when="showParentPane" out:class="parentPaneClass" on:click="parentId!pane:activate">
|
|
5
9
|
<div class="pane-header">
|
|
6
|
-
<div out="
|
|
10
|
+
<div when="!parentFilterOn" out="parentCrumbsView|Breadcrumbs"></div>
|
|
11
|
+
<div out="parentFilterView|FilterBar"></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div data-slot="list" out="parentListView|FileList"></div>
|
|
14
|
+
<div out="parentDetailView|EntryDetail"></div>
|
|
15
|
+
<div out="parentSummaryView|Summary"></div>
|
|
16
|
+
</section>
|
|
17
|
+
<section class="pane pane-left" when="showLeftPane" out:class="leftPaneClass" on:click="leftId!pane:activate">
|
|
18
|
+
<div class="pane-header">
|
|
19
|
+
<div when="!leftFilterOn" out="leftCrumbsView|Breadcrumbs"></div>
|
|
7
20
|
<div out="leftFilterView|FilterBar"></div>
|
|
8
21
|
</div>
|
|
9
|
-
<div out="leftListView|FileList"></div>
|
|
22
|
+
<div data-slot="list" out="leftListView|FileList"></div>
|
|
10
23
|
<div out="leftDetailView|EntryDetail"></div>
|
|
11
24
|
<div out="leftSummaryView|Summary"></div>
|
|
12
25
|
</section>
|
|
13
|
-
<section class="pane pane-right" out:class="rightPaneClass" on:click="rightId!pane:activate">
|
|
26
|
+
<section class="pane pane-right" when="showRightPane" out:class="rightPaneClass" on:click="rightId!pane:activate">
|
|
14
27
|
<div class="pane-header">
|
|
15
|
-
<div out="rightCrumbsView|Breadcrumbs"></div>
|
|
28
|
+
<div when="!rightFilterOn" out="rightCrumbsView|Breadcrumbs"></div>
|
|
16
29
|
<div out="rightFilterView|FilterBar"></div>
|
|
17
30
|
</div>
|
|
18
|
-
<div out="rightListView|FileList"></div>
|
|
31
|
+
<div data-slot="list" out="rightListView|FileList"></div>
|
|
19
32
|
<div out="rightDetailView|EntryDetail"></div>
|
|
20
33
|
<div out="rightSummaryView|Summary"></div>
|
|
21
34
|
</section>
|
|
22
|
-
<div when="
|
|
35
|
+
<div data-slot="inspector" when="showInspectorInline" out="previewView|FilePreview"></div>
|
|
36
|
+
<div class="preview-max" when="showPreviewFull" out="previewView|FilePreview"></div>
|
|
23
37
|
</main>
|
|
24
|
-
<
|
|
25
|
-
<div when="
|
|
38
|
+
<div out="commandBarView|CommandBar"></div>
|
|
39
|
+
<div class="viewer" when="showViewer" role="dialog" aria-modal="true" aria-label="File inspector" tabindex="-1" out="previewView|FilePreview"></div>
|
|
40
|
+
<div when="panelOpen" out="panelView|CommandPanel"></div>
|
|
26
41
|
<div when="bookmarkOpen" out="bookmarkView|BookmarkList"></div>
|
|
27
42
|
<div out="confirmView|Confirm"></div>
|
|
43
|
+
<div when="helpOpen" out="helpView|Help"></div>
|
|
28
44
|
</div>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/components/App
|
|
7
7
|
// Root component: a header, two panes (breadcrumbs + FileList + summary each),
|
|
8
|
-
// an optional preview column, the command bar and the command
|
|
8
|
+
// an optional preview column, the command bar and the command panel overlay.
|
|
9
9
|
// App is the controller seam: component events (published bubbles) and key
|
|
10
10
|
// chords are translated into `session.dispatch(commandId, payload)` calls; it
|
|
11
11
|
// never mutates model state directly.
|
|
@@ -23,14 +23,18 @@ import { PANEL_FOCUS } from "../core/panel.js"
|
|
|
23
23
|
function createInit(session) {
|
|
24
24
|
const left = session.panes.left
|
|
25
25
|
const right = session.panes.right
|
|
26
|
+
const parent = session.panes.parent ?? {}
|
|
27
|
+
const preview = session.panes.preview ?? {}
|
|
26
28
|
// Pane-focus flags so the listing can render the cursor differently in the
|
|
27
29
|
// inactive pane. Shared by both engines.
|
|
28
30
|
const leftActive = derived([session.active], ([active]) => active === "left")
|
|
29
31
|
const rightActive = derived([session.active], ([active]) => active === "right")
|
|
32
|
+
const parentActive = derived([session.active], ([active]) => active === "parent")
|
|
33
|
+
const previewActive = derived([session.active], ([active]) => active === "preview")
|
|
30
34
|
return () => ({
|
|
31
35
|
active: session.active,
|
|
32
|
-
// The preview target entry, so the inspector can show a directory
|
|
33
|
-
// even when there is no text/card preview.
|
|
36
|
+
// The preview target entry, so the inspector can show a directory
|
|
37
|
+
// column even when there is no text/card preview.
|
|
34
38
|
appEntry: session.previewEntry,
|
|
35
39
|
appHost: session.host,
|
|
36
40
|
appPath: session.activePath,
|
|
@@ -42,6 +46,7 @@ function createInit(session) {
|
|
|
42
46
|
bookmarkTarget: session.bookmarkTarget,
|
|
43
47
|
commandBarItems: session.commandBarItems,
|
|
44
48
|
confirm: session.confirm,
|
|
49
|
+
currentId: session.currentId,
|
|
45
50
|
dispatch: session.dispatch,
|
|
46
51
|
filtering: session.filtering,
|
|
47
52
|
leftActive,
|
|
@@ -55,28 +60,49 @@ function createInit(session) {
|
|
|
55
60
|
leftLoading: left.loading,
|
|
56
61
|
leftMarked: left.marked,
|
|
57
62
|
leftSegments: left.segments,
|
|
63
|
+
leftSetFilter: (value) => left.setFilter(value),
|
|
58
64
|
notice: session.notice,
|
|
59
|
-
paletteIndex: session.paletteIndex,
|
|
60
|
-
paletteItems: session.paletteItems,
|
|
61
|
-
paletteMode: session.paletteMode,
|
|
62
|
-
paletteOpen: session.paletteOpen,
|
|
63
|
-
paletteQuery: session.paletteQuery,
|
|
64
65
|
panelCommand: session.panelCommand,
|
|
65
66
|
panelDraft: session.panelDraft,
|
|
66
67
|
panelCursorRow: session.panelCursorRow,
|
|
68
|
+
panelEntry: session.panelEntry,
|
|
67
69
|
panelFiltered: session.panelFiltered,
|
|
68
70
|
panelFocus: session.panelFocus,
|
|
69
71
|
panelOpIndex: session.panelOpIndex,
|
|
70
72
|
panelItems: session.panelItems,
|
|
73
|
+
panelLabelWidth: session.panelLabelWidth,
|
|
71
74
|
panelOpen: session.panelOpen,
|
|
72
|
-
|
|
75
|
+
panelPicking: session.panelPicking,
|
|
73
76
|
panelQuery: session.panelQuery,
|
|
74
77
|
panelRowIndex: session.panelRowIndex,
|
|
75
78
|
panelRows: session.panelRows,
|
|
76
79
|
panelRunnable: session.panelRunnable,
|
|
77
80
|
panelTextual: session.panelTextual,
|
|
78
81
|
panelValue: session.panelValue,
|
|
82
|
+
// The explorer's first column: the parent of the current directory.
|
|
83
|
+
parentActive,
|
|
84
|
+
parentCursor: parent.cursor,
|
|
85
|
+
parentDisk: parent.disk,
|
|
86
|
+
parentEntries: parent.entries,
|
|
87
|
+
parentEntry: session.parentCursorEntry,
|
|
88
|
+
parentError: parent.error,
|
|
89
|
+
parentFilter: parent.filter,
|
|
90
|
+
parentId: "parent",
|
|
91
|
+
parentLoading: parent.loading,
|
|
92
|
+
parentMarked: parent.marked,
|
|
93
|
+
parentSegments: parent.segments,
|
|
94
|
+
parentSetFilter: (value) => parent.setFilter(value),
|
|
79
95
|
preview: session.preview,
|
|
96
|
+
previewFull: session.previewFull,
|
|
97
|
+
// The third column: the directory the inspector previews, so it reads
|
|
98
|
+
// as a pane. Over a file the inspector stays a card and these idle.
|
|
99
|
+
previewActive,
|
|
100
|
+
previewCursor: preview.cursor,
|
|
101
|
+
previewEntries: preview.entries,
|
|
102
|
+
previewId: "preview",
|
|
103
|
+
previewLoading: preview.loading,
|
|
104
|
+
previewMarked: preview.marked,
|
|
105
|
+
previewSegments: preview.segments,
|
|
80
106
|
rightActive,
|
|
81
107
|
rightCursor: right.cursor,
|
|
82
108
|
rightDisk: right.disk,
|
|
@@ -88,17 +114,33 @@ function createInit(session) {
|
|
|
88
114
|
rightLoading: right.loading,
|
|
89
115
|
rightMarked: right.marked,
|
|
90
116
|
rightSegments: right.segments,
|
|
117
|
+
rightSetFilter: (value) => right.setFilter(value),
|
|
91
118
|
showPreview: session.showPreview,
|
|
119
|
+
viewMode: session.viewMode,
|
|
120
|
+
// The panel's file picker: its filter input and the directory it
|
|
121
|
+
// currently browses, so the panel can render the picker view.
|
|
122
|
+
pickerPath: session.pickerPane?.path,
|
|
123
|
+
pickerQuery: session.pickerQuery,
|
|
92
124
|
})
|
|
93
125
|
}
|
|
94
126
|
|
|
127
|
+
// Function: fieldOwnsKeys
|
|
128
|
+
// True when one of the panel's inputs owns the keyboard: the command filter
|
|
129
|
+
// (`PANEL_FOCUS.OPS`) or a textual argument's value field (`panelTextual`).
|
|
130
|
+
// While an input owns the keyboard the shared handler leaves text editing to
|
|
131
|
+
// it — typing, Backspace, Delete, caret movement. A plain argument row or an
|
|
132
|
+
// item row has no input pressed, so the handler handles the key itself.
|
|
133
|
+
function fieldOwnsKeys(data, focus) {
|
|
134
|
+
return focus === PANEL_FOCUS.OPS || Boolean(unwrap(data.panelTextual))
|
|
135
|
+
}
|
|
136
|
+
|
|
95
137
|
// Function: createBehaviors
|
|
96
138
|
// Builds the App data/key behavior map for the given renderer environment.
|
|
97
139
|
// Published events are registered separately via `createEvents()` (`.sub`).
|
|
98
140
|
function createBehaviors(env) {
|
|
99
141
|
return {
|
|
100
|
-
// Global key handling: confirm dialogs first,
|
|
101
|
-
// keymap.
|
|
142
|
+
// Global key handling: confirm dialogs first, the command panel, then
|
|
143
|
+
// the keymap.
|
|
102
144
|
keys: (self, data, event) => {
|
|
103
145
|
const key = env.normalizeKey ? env.normalizeKey(event) : event
|
|
104
146
|
if (unwrap(data.confirm)) {
|
|
@@ -107,33 +149,46 @@ function createBehaviors(env) {
|
|
|
107
149
|
else if (name === "n" || name === "escape") data.dispatch("confirm.no")
|
|
108
150
|
return
|
|
109
151
|
}
|
|
110
|
-
if (unwrap(data.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
if (unwrap(data.panelOpen)) {
|
|
153
|
+
// The picker is a sub-mode of the panel: it owns navigation and
|
|
154
|
+
// choice, while typing filters through its own input.
|
|
155
|
+
if (unwrap(data.panelPicking)) {
|
|
156
|
+
if (key?.name === "escape") {
|
|
157
|
+
data.dispatch("panel.pick.cancel")
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (key?.name === "tab") {
|
|
161
|
+
data.dispatch("panel.pick.cancel")
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (key?.name === "up") {
|
|
165
|
+
data.dispatch("panel.pick.up")
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (key?.name === "down") {
|
|
169
|
+
data.dispatch("panel.pick.down")
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
if (key?.name === "right") {
|
|
173
|
+
data.dispatch("panel.pick.descend")
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
if (key?.name === "backspace") {
|
|
177
|
+
// An empty filter steps up; otherwise the filter input
|
|
178
|
+
// owns Backspace and edits itself.
|
|
179
|
+
if (`${unwrap(data.pickerQuery) ?? ""}` === "") data.dispatch("panel.pick.parent")
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
183
|
+
data.dispatch(key.ctrl ? "panel.pick.chooseDir" : "panel.pick.enter")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
// Everything else (typing, caret movement) is the filter
|
|
187
|
+
// input's to handle.
|
|
130
188
|
return
|
|
131
189
|
}
|
|
132
|
-
// Let the focused input handle typing.
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
if (unwrap(data.panelOpen)) {
|
|
136
190
|
const focus = unwrap(data.panelFocus)
|
|
191
|
+
const fieldOwns = fieldOwnsKeys(data, focus)
|
|
137
192
|
// Escape closes; Tab cycles the focus ring (command ⇄ args);
|
|
138
193
|
// arrows move within the focused column; Enter runs the command.
|
|
139
194
|
if (key?.name === "escape") {
|
|
@@ -152,29 +207,35 @@ function createBehaviors(env) {
|
|
|
152
207
|
data.dispatch(focus === PANEL_FOCUS.ARGS ? "panel.nextArg" : "panel.down")
|
|
153
208
|
return
|
|
154
209
|
}
|
|
210
|
+
if (key?.name === "o" && key.ctrl) {
|
|
211
|
+
// Browse a path/destination argument in the file picker.
|
|
212
|
+
data.dispatch("panel.browse")
|
|
213
|
+
return
|
|
214
|
+
}
|
|
155
215
|
if (key?.name === "return" || key?.name === "enter") {
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
// Enter runs the highlighted command with the current draft.
|
|
216
|
+
// Enter runs a complete command, or walks to the next
|
|
217
|
+
// missing field (see `panel.run`); the root routes it for
|
|
218
|
+
// every renderer, so no per-input `on:enter` is needed.
|
|
160
219
|
data.dispatch("panel.run")
|
|
161
220
|
return
|
|
162
221
|
}
|
|
163
|
-
//
|
|
164
|
-
|
|
222
|
+
// While an input owns the keyboard (the command filter, or a
|
|
223
|
+
// textual argument's value field) it also owns text editing:
|
|
224
|
+
// typing, Backspace, Delete and caret movement pass through to
|
|
225
|
+
// it. Intercepting Backspace here used to bounce the cursor
|
|
226
|
+
// back to the command filter mid-edit.
|
|
227
|
+
if (fieldOwns) return
|
|
228
|
+
// A focused non-textual argument row: Space toggles its
|
|
229
|
+
// selection, any other printable key jumps back to the command
|
|
230
|
+
// filter and appends the character; Backspace edits the filter.
|
|
231
|
+
if (key?.name === "space") {
|
|
165
232
|
data.dispatch("panel.toggleItem")
|
|
166
233
|
return
|
|
167
234
|
}
|
|
168
|
-
if (key?.name === "backspace"
|
|
169
|
-
// Editing the command filter from the argument column.
|
|
235
|
+
if (key?.name === "backspace") {
|
|
170
236
|
data.dispatch("panel.type", { backspace: true })
|
|
171
237
|
return
|
|
172
238
|
}
|
|
173
|
-
// While the command column is focused, or a textual argument
|
|
174
|
-
// owns the cursor, the focused input handles typing.
|
|
175
|
-
if (focus !== PANEL_FOCUS.ARGS || unwrap(data.panelTextual)) return
|
|
176
|
-
// Typing on a non-textual argument jumps back to the command
|
|
177
|
-
// filter and appends the character.
|
|
178
239
|
if (key?.name && key.name.length === 1 && !key.ctrl && !key.meta) {
|
|
179
240
|
data.dispatch("panel.type", { char: key.name })
|
|
180
241
|
return
|
|
@@ -244,7 +305,12 @@ function createBehaviors(env) {
|
|
|
244
305
|
// filter (or on Shift-Esc) the keymap decides — Esc steps back
|
|
245
306
|
// through the pane's directory history.
|
|
246
307
|
if (key?.name === "escape" && !key.shift) {
|
|
247
|
-
const
|
|
308
|
+
const filters = {
|
|
309
|
+
left: data.leftFilter,
|
|
310
|
+
parent: data.parentFilter,
|
|
311
|
+
right: data.rightFilter,
|
|
312
|
+
}
|
|
313
|
+
const filter = unwrap(filters[active])
|
|
248
314
|
if (filter) {
|
|
249
315
|
data.dispatch("filter.clear")
|
|
250
316
|
return
|
|
@@ -256,9 +322,6 @@ function createBehaviors(env) {
|
|
|
256
322
|
env.quit(self)
|
|
257
323
|
return
|
|
258
324
|
}
|
|
259
|
-
// The command panel is a TUI-only view; the web renderer omits the
|
|
260
|
-
// capability so `m` never opens an invisible modal there.
|
|
261
|
-
if (id === "panel.toggle" && !env.panel) return
|
|
262
325
|
data.dispatch(id, { paneId: unwrap(data.active) })
|
|
263
326
|
},
|
|
264
327
|
}
|
|
@@ -279,24 +342,38 @@ function createEvents(env = {}) {
|
|
|
279
342
|
if (event.data) data.dispatch(event.data)
|
|
280
343
|
},
|
|
281
344
|
"panel:select": (_self, data, event) => {
|
|
282
|
-
// Clicking
|
|
283
|
-
// argument/item row focuses the
|
|
345
|
+
// Clicking a command row moves the cursor to it (a group header is
|
|
346
|
+
// inert); clicking an argument/item row focuses the right pane
|
|
347
|
+
// (toggling an item); clicking a picker row chooses it.
|
|
284
348
|
const items = unwrap(data.panelItems) ?? []
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
287
|
-
data.dispatch("panel.setOp", { index })
|
|
349
|
+
const item = items.find((row) => row.id === event.data)
|
|
350
|
+
if (item?.type === "op") {
|
|
351
|
+
data.dispatch("panel.setOp", { index: item.entryIndex })
|
|
288
352
|
return
|
|
289
353
|
}
|
|
354
|
+
if (item) return
|
|
290
355
|
const rows = unwrap(data.panelRows) ?? []
|
|
291
356
|
const rowIndex = rows.findIndex((row) => row.key === event.data)
|
|
292
357
|
if (rowIndex < 0) return
|
|
358
|
+
const row = rows[rowIndex]
|
|
359
|
+
if (row?.type === "entry") {
|
|
360
|
+
data.dispatch("panel.focusArg")
|
|
361
|
+
data.dispatch("panel.pick.setCursor", { index: rowIndex })
|
|
362
|
+
data.dispatch("panel.pick.enter")
|
|
363
|
+
return
|
|
364
|
+
}
|
|
293
365
|
data.dispatch("panel.focusArg")
|
|
294
366
|
data.dispatch("panel.setRow", { index: rowIndex })
|
|
295
|
-
const row = rows[rowIndex]
|
|
296
367
|
if (row?.type === "item") data.dispatch("panel.toggleItem")
|
|
297
368
|
},
|
|
298
369
|
"panel:confirm": (_self, data) => {
|
|
370
|
+
// Published by a focused panel input's `on:enter`; the key handler
|
|
371
|
+
// leaves Enter to it (see `fieldOwnsKeys`).
|
|
299
372
|
if (!unwrap(data.panelOpen)) return
|
|
373
|
+
if (unwrap(data.panelPicking)) {
|
|
374
|
+
data.dispatch("panel.pick.enter")
|
|
375
|
+
return
|
|
376
|
+
}
|
|
300
377
|
data.dispatch("panel.run")
|
|
301
378
|
},
|
|
302
379
|
"crumbs:go": (_self, data, event) => {
|
|
@@ -320,33 +397,6 @@ function createEvents(env = {}) {
|
|
|
320
397
|
if (unwrap(data.bookmarkMode) === "name") data.dispatch("bookmark.save")
|
|
321
398
|
else data.dispatch("bookmark.select")
|
|
322
399
|
},
|
|
323
|
-
"palette:confirm": (_self, data, event) => {
|
|
324
|
-
// The global key handler and the input's on:enter can both fire;
|
|
325
|
-
// only the first one (while the palette is open) acts.
|
|
326
|
-
if (!unwrap(data.paletteOpen)) return
|
|
327
|
-
if (unwrap(data.paletteMode) === "script") {
|
|
328
|
-
// Submit the typed line: run it, or complete the highlighted row
|
|
329
|
-
// when the input is empty.
|
|
330
|
-
data.dispatch("palette.submit")
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
const items = unwrap(data.paletteItems) ?? []
|
|
334
|
-
const index = Math.max(0, Math.min(unwrap(data.paletteIndex) ?? 0, items.length - 1))
|
|
335
|
-
const match = items[index] ?? items[0]
|
|
336
|
-
if (match?.id) data.dispatch(match.id)
|
|
337
|
-
data.dispatch("palette.toggle")
|
|
338
|
-
},
|
|
339
|
-
"palette:run": (_self, data, event) => {
|
|
340
|
-
if (!event.data) return
|
|
341
|
-
if (unwrap(data.paletteMode) === "script") {
|
|
342
|
-
// Clicking a script row completes it; running stays on Enter.
|
|
343
|
-
const item = (unwrap(data.paletteItems) ?? []).find((row) => row.id === event.data)
|
|
344
|
-
if (item?.completion) data.dispatch("palette.setQuery", { query: item.completion })
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
|
-
data.dispatch(event.data)
|
|
348
|
-
data.dispatch("palette.toggle")
|
|
349
|
-
},
|
|
350
400
|
}
|
|
351
401
|
}
|
|
352
402
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<!-- Identity bar: the one visible trace of TinyCmd itself (the host
|
|
11
11
|
aside, everything else on screen is the user's files). "Tiny"
|
|
12
12
|
shimmers blue letter by letter, "Cmd" shimmers orange. -->
|
|
13
|
-
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
13
|
+
<box flexDirection="row" height="1" paddingLeft="1" paddingRight="1" overflow="hidden">
|
|
14
14
|
<text out="brandT" out:fg="brandTFg" wrapMode="none"></text>
|
|
15
15
|
<text out="brandI" out:fg="brandIFg" wrapMode="none"></text>
|
|
16
16
|
<text out="brandN" out:fg="brandNFg" wrapMode="none"></text>
|
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
<text out="brandM" out:fg="brandMFg" wrapMode="none"></text>
|
|
20
20
|
<text out="brandD" out:fg="brandDFg" wrapMode="none"></text>
|
|
21
21
|
<text out="topBar" out:fg="topBarFg" wrapMode="none"></text>
|
|
22
|
+
<box flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden"></box>
|
|
23
|
+
<!-- The view mode, letter by letter so each carries its gradient:
|
|
24
|
+
explorer shimmers along the Tiny blues, commander along the
|
|
25
|
+
Cmd oranges (see `modeGradient` in Frame.js). The trailing
|
|
26
|
+
slots of the shorter word are empty and take no column. -->
|
|
27
|
+
<box flexDirection="row" flexShrink="0" overflow="hidden">
|
|
28
|
+
<text when="modeHas0" out="modeL0" out:fg="modeFg0" wrapMode="none"></text>
|
|
29
|
+
<text when="modeHas1" out="modeL1" out:fg="modeFg1" wrapMode="none"></text>
|
|
30
|
+
<text when="modeHas2" out="modeL2" out:fg="modeFg2" wrapMode="none"></text>
|
|
31
|
+
<text when="modeHas3" out="modeL3" out:fg="modeFg3" wrapMode="none"></text>
|
|
32
|
+
<text when="modeHas4" out="modeL4" out:fg="modeFg4" wrapMode="none"></text>
|
|
33
|
+
<text when="modeHas5" out="modeL5" out:fg="modeFg5" wrapMode="none"></text>
|
|
34
|
+
<text when="modeHas6" out="modeL6" out:fg="modeFg6" wrapMode="none"></text>
|
|
35
|
+
<text when="modeHas7" out="modeL7" out:fg="modeFg7" wrapMode="none"></text>
|
|
36
|
+
<text when="modeHas8" out="modeL8" out:fg="modeFg8" wrapMode="none"></text>
|
|
37
|
+
<text out="topBarStatus" out:fg="topBarStatusFg" wrapMode="none"></text>
|
|
38
|
+
</box>
|
|
22
39
|
</box>
|
|
23
40
|
|
|
24
41
|
<!-- Minimum-size gate: below the breakpoint the workspace is unusable. -->
|
|
@@ -27,17 +44,36 @@
|
|
|
27
44
|
<text out="minSizeHint" out:fg="dimFg"></text>
|
|
28
45
|
</box>
|
|
29
46
|
|
|
30
|
-
<!--
|
|
47
|
+
<!-- Columns: commander tiles source and destination; explorer leads
|
|
48
|
+
with the parent mirror so the current pane sits in the middle,
|
|
49
|
+
slightly wider than its neighbours. The inspector completes the
|
|
50
|
+
three columns in either mode. -->
|
|
31
51
|
<box when="!tooSmall" flexDirection="column" flexGrow="1" overflow="hidden">
|
|
32
|
-
<box flexDirection="row" flexGrow="1" overflow="hidden">
|
|
33
|
-
<box flexDirection="column" flexGrow="1" border borderStyle="single"
|
|
52
|
+
<box when="!showPreviewFull" flexDirection="row" flexGrow="1" overflow="hidden">
|
|
53
|
+
<box when="showParentPane" flexDirection="column" flexGrow="1" flexBasis="0" border borderStyle="single"
|
|
54
|
+
out:border="leftBorder" out:borderColor="parentBorderFg"
|
|
55
|
+
on:click="parentId!pane:activate">
|
|
56
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
57
|
+
<box when="!parentFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
58
|
+
<breadcrumbs in:segments="parentSegments" in:paneId="parentId" height="1"></breadcrumbs>
|
|
59
|
+
</box>
|
|
60
|
+
<filterbar in:query="parentFilter" in:paneId="parentId" in:filtering="filtering" in:entries="parentEntries" in:setFilter="parentSetFilter"></filterbar>
|
|
61
|
+
</box>
|
|
62
|
+
<box height="1" out:border="headRule" out:borderColor="parentHeadRuleFg"></box>
|
|
63
|
+
<filelist in:entries="parentEntries" in:cursor="parentCursor" in:marked="parentMarked"
|
|
64
|
+
in:active="parentActive" in:paneId="parentId" in:loading="parentLoading"
|
|
65
|
+
in:viewport="listViewport"></filelist>
|
|
66
|
+
<entrydetail in:entry="parentEntry" in:marked="parentMarked" height="2"></entrydetail>
|
|
67
|
+
<summary in:entries="parentEntries" in:disk="parentDisk" height="1"></summary>
|
|
68
|
+
</box>
|
|
69
|
+
<box when="showLeftPane" flexDirection="column" flexBasis="0" out:flexGrow="leftGrow" border borderStyle="single"
|
|
34
70
|
out:border="leftBorder" out:borderColor="leftBorderFg"
|
|
35
71
|
on:click="leftId!pane:activate">
|
|
36
72
|
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
37
73
|
<box when="!leftFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
38
74
|
<breadcrumbs in:segments="leftSegments" in:paneId="leftId" height="1"></breadcrumbs>
|
|
39
75
|
</box>
|
|
40
|
-
<filterbar in:query="leftFilter" in:paneId="leftId" in:filtering="filtering" in:entries="leftEntries"></filterbar>
|
|
76
|
+
<filterbar in:query="leftFilter" in:paneId="leftId" in:filtering="filtering" in:entries="leftEntries" in:setFilter="leftSetFilter"></filterbar>
|
|
41
77
|
</box>
|
|
42
78
|
<box height="1" out:border="headRule" out:borderColor="leftHeadRuleFg"></box>
|
|
43
79
|
<filelist in:entries="leftEntries" in:cursor="leftCursor" in:marked="leftMarked"
|
|
@@ -46,14 +82,14 @@
|
|
|
46
82
|
<entrydetail in:entry="leftEntry" in:marked="leftMarked" height="2"></entrydetail>
|
|
47
83
|
<summary in:entries="leftEntries" in:disk="leftDisk" height="1"></summary>
|
|
48
84
|
</box>
|
|
49
|
-
<box flexDirection="column" flexGrow="
|
|
85
|
+
<box when="showRightPane" flexDirection="column" flexBasis="0" out:flexGrow="rightGrow" border borderStyle="single"
|
|
50
86
|
out:border="rightBorder" out:borderColor="rightBorderFg"
|
|
51
87
|
on:click="rightId!pane:activate">
|
|
52
88
|
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
53
89
|
<box when="!rightFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
54
90
|
<breadcrumbs in:segments="rightSegments" in:paneId="rightId" height="1"></breadcrumbs>
|
|
55
91
|
</box>
|
|
56
|
-
<filterbar in:query="rightFilter" in:paneId="rightId" in:filtering="filtering" in:entries="rightEntries"></filterbar>
|
|
92
|
+
<filterbar in:query="rightFilter" in:paneId="rightId" in:filtering="filtering" in:entries="rightEntries" in:setFilter="rightSetFilter"></filterbar>
|
|
57
93
|
</box>
|
|
58
94
|
<box height="1" out:border="headRule" out:borderColor="rightHeadRuleFg"></box>
|
|
59
95
|
<filelist in:entries="rightEntries" in:cursor="rightCursor" in:marked="rightMarked"
|
|
@@ -63,8 +99,20 @@
|
|
|
63
99
|
<summary in:entries="rightEntries" in:disk="rightDisk" height="1"></summary>
|
|
64
100
|
</box>
|
|
65
101
|
<filepreview when="showInspectorInline" in:preview="preview" in:show="showInspectorInline"
|
|
66
|
-
in:entry="appEntry"
|
|
102
|
+
in:entry="appEntry" in:paneId="previewId"
|
|
103
|
+
in:dirSegments="previewSegments" in:dirEntries="previewEntries"
|
|
104
|
+
in:dirCursor="previewCursor" in:dirMarked="previewMarked"
|
|
105
|
+
in:dirActive="previewActive" in:dirLoading="previewLoading"
|
|
106
|
+
flexGrow="1" flexBasis="0"></filepreview>
|
|
67
107
|
</box>
|
|
108
|
+
<!-- Maximized inspector (`⇧V`): the preview takes the whole pane
|
|
109
|
+
row, the identity and command bars staying in place. -->
|
|
110
|
+
<filepreview when="showPreviewFull" in:preview="preview" in:show="showPreviewFull"
|
|
111
|
+
in:entry="appEntry" in:paneId="previewId"
|
|
112
|
+
in:dirSegments="previewSegments" in:dirEntries="previewEntries"
|
|
113
|
+
in:dirCursor="previewCursor" in:dirMarked="previewMarked"
|
|
114
|
+
in:dirActive="previewActive" in:dirLoading="previewLoading"
|
|
115
|
+
flexGrow="1"></filepreview>
|
|
68
116
|
<commandbar in:items="commandBarItems" in:notice="notice" in:leftError="leftError"
|
|
69
117
|
in:rightError="rightError" in:active="active"
|
|
70
118
|
in:leftMarked="leftMarked" in:rightMarked="rightMarked" out:height="barHeight"></commandbar>
|
|
@@ -75,18 +123,19 @@
|
|
|
75
123
|
<box when="showViewer" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="10"
|
|
76
124
|
alignItems="center" justifyContent="center">
|
|
77
125
|
<box width="80%" height="90%">
|
|
78
|
-
<filepreview in:preview="preview" in:show="showViewer" in:entry="appEntry"
|
|
126
|
+
<filepreview in:preview="preview" in:show="showViewer" in:entry="appEntry" in:paneId="previewId"
|
|
127
|
+
in:dirSegments="previewSegments" in:dirEntries="previewEntries"
|
|
128
|
+
in:dirCursor="previewCursor" in:dirMarked="previewMarked"
|
|
129
|
+
in:dirActive="previewActive" in:dirLoading="previewLoading"></filepreview>
|
|
79
130
|
</box>
|
|
80
131
|
</box>
|
|
81
132
|
|
|
82
|
-
<commandpalette when="paletteOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="20"
|
|
83
|
-
in:open="paletteOpen" in:mode="paletteMode" in:query="paletteQuery"
|
|
84
|
-
in:items="paletteItems" in:selected="paletteIndex"></commandpalette>
|
|
85
133
|
<commandpanel when="panelOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="22"
|
|
86
|
-
in:open="panelOpen" in:
|
|
134
|
+
in:open="panelOpen" in:entry="panelEntry" in:items="panelItems"
|
|
87
135
|
in:opCursor="panelOpIndex" in:rows="panelRows" in:argCursor="panelRowIndex"
|
|
88
|
-
in:
|
|
89
|
-
in:command="panelCommand" in:runnable="panelRunnable"
|
|
136
|
+
in:labelWidth="panelLabelWidth"
|
|
137
|
+
in:focus="panelFocus" in:command="panelCommand" in:runnable="panelRunnable"
|
|
138
|
+
in:picking="panelPicking" in:pickerQuery="pickerQuery" in:pickerPath="pickerPath"
|
|
90
139
|
in:query="panelQuery" in:value="panelValue"></commandpanel>
|
|
91
140
|
<bookmarklist when="bookmarkOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="25"
|
|
92
141
|
in:open="bookmarkOpen" in:mode="bookmarkMode" in:query="bookmarkQuery"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
<div class="bookmark-list" when="open">
|
|
1
|
+
<div class="bookmark-list" when="open" role="dialog" aria-modal="true" aria-label="Bookmarks">
|
|
2
2
|
<div class="bookmark-dialog">
|
|
3
|
+
<h2 out="title"></h2>
|
|
3
4
|
<header>
|
|
4
5
|
<span class="bookmark-sign" out="sign"></span>
|
|
5
|
-
<input class="bookmark-query" type="text" inout="query" on:enter="confirm">
|
|
6
|
+
<input class="bookmark-query" type="text" inout="query" aria-label="Bookmark name or query" on:enter="confirm">
|
|
6
7
|
</header>
|
|
7
8
|
<p class="bookmark-target" when="inputVisible" out="target"></p>
|
|
8
9
|
<p class="bookmark-empty" when="emptyList" out="empty"></p>
|
|
@@ -62,7 +62,7 @@ function createBehaviors(env) {
|
|
|
62
62
|
emptyFg: () => env.textSecondaryFg,
|
|
63
63
|
}
|
|
64
64
|
if (env?.rows) {
|
|
65
|
-
behaviors.rows = (
|
|
65
|
+
behaviors.rows = (self, data) => env.rows(self, data)
|
|
66
66
|
}
|
|
67
67
|
if (env?.scrollTop) {
|
|
68
68
|
behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
|