@ssebastien/tcmd 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -38
- package/bin/tcmd +11 -3
- package/bin/tcmd.js +15 -0
- package/package.json +4 -3
- package/public/index.html +14 -0
- package/public/main.js +17501 -0
- package/public/style.css +901 -0
- package/scripts/serve-web.js +61 -0
- package/src/js/tcmd/components/App.html +28 -12
- package/src/js/tcmd/components/App.js +135 -85
- package/src/js/tcmd/components/App.xml +64 -15
- package/src/js/tcmd/components/BookmarkList.html +3 -2
- package/src/js/tcmd/components/BookmarkList.js +1 -1
- package/src/js/tcmd/components/Breadcrumbs.js +1 -1
- package/src/js/tcmd/components/CommandArgRow.html +17 -0
- package/src/js/tcmd/components/CommandArgRow.js +86 -26
- package/src/js/tcmd/components/CommandArgRow.xml +31 -4
- package/src/js/tcmd/components/CommandBar.html +1 -1
- package/src/js/tcmd/components/CommandBar.js +3 -3
- package/src/js/tcmd/components/CommandItem.html +1 -1
- package/src/js/tcmd/components/CommandItem.js +1 -0
- package/src/js/tcmd/components/CommandPanel.html +29 -0
- package/src/js/tcmd/components/CommandPanel.js +54 -52
- package/src/js/tcmd/components/CommandPanel.xml +38 -24
- package/src/js/tcmd/components/Confirm.html +1 -1
- package/src/js/tcmd/components/EntryDetail.html +7 -7
- package/src/js/tcmd/components/EntryDetail.js +1 -1
- package/src/js/tcmd/components/FileList.html +5 -1
- package/src/js/tcmd/components/FileList.js +2 -1
- package/src/js/tcmd/components/FilePreview.html +14 -3
- package/src/js/tcmd/components/FilePreview.js +73 -10
- package/src/js/tcmd/components/FilePreview.xml +19 -6
- package/src/js/tcmd/components/FileRow.html +1 -1
- package/src/js/tcmd/components/FileRow.js +10 -3
- package/src/js/tcmd/components/FilterBar.html +1 -1
- package/src/js/tcmd/components/FilterBar.js +14 -4
- package/src/js/tcmd/components/FilterBar.xml +1 -1
- package/src/js/tcmd/{tui/frame.js → components/Frame.js} +120 -91
- package/src/js/tcmd/components/Help.html +7 -0
- package/src/js/tcmd/components/Help.js +109 -0
- package/src/js/tcmd/components/Summary.html +1 -1
- package/src/js/tcmd/components/Summary.js +1 -6
- package/src/js/tcmd/components/icons.js +39 -5
- package/src/js/tcmd/components/index.js +3 -4
- package/src/js/tcmd/components/rows.js +37 -33
- package/src/js/tcmd/core/commands.js +422 -180
- package/src/js/tcmd/core/directory.js +145 -95
- package/src/js/tcmd/core/fs.js +29 -2
- package/src/js/tcmd/core/history.js +58 -0
- package/src/js/tcmd/core/index.js +3 -9
- package/src/js/tcmd/core/keymap.js +3 -3
- package/src/js/tcmd/core/operations.js +108 -15
- package/src/js/tcmd/core/pane.js +31 -8
- package/src/js/tcmd/core/panel.js +172 -82
- package/src/js/tcmd/core/script.js +7 -95
- package/src/js/tcmd/core/selection.js +9 -1
- package/src/js/tcmd/core/session.js +280 -152
- package/src/js/tcmd/index.js +0 -1
- package/src/js/tcmd/tui/app.js +26 -5
- package/src/js/tcmd/tui/catalogue.js +6 -19
- package/src/js/tcmd/tui/index.js +1 -16
- package/src/js/tcmd/tui/theme.js +5 -0
- package/src/js/tcmd/web/app.js +158 -32
- package/src/js/tcmd/web/bookmarks.js +36 -0
- package/src/js/tcmd/web/catalogue.js +45 -39
- package/src/js/tcmd/web/effects.js +143 -0
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/header.js +15 -11
- package/src/js/tcmd/web/main.js +6 -66
- package/src/js/tcmd/web/seed.js +3 -3
- package/src/js/tcmd/web/server.js +6 -4
- package/src/js/tcmd/web/theme.js +1 -0
- package/src/js/tcmd/components/CommandPalette.html +0 -7
- package/src/js/tcmd/components/CommandPalette.js +0 -58
- package/src/js/tcmd/components/CommandPalette.xml +0 -16
- package/src/js/tcmd/components/PaletteRow.html +0 -3
- package/src/js/tcmd/components/PaletteRow.js +0 -43
- package/src/js/tcmd/components/PaletteRow.xml +0 -4
- package/src/js/tcmd/tui/help.js +0 -32
package/src/js/tcmd/tui/app.js
CHANGED
|
@@ -18,9 +18,15 @@ import appXml from "../components/App.xml" with { type: "text" }
|
|
|
18
18
|
import { elideCrumbs } from "../components/format.js"
|
|
19
19
|
import { appBehaviors, appEvents, appInit } from "../components/index.js"
|
|
20
20
|
import { registerComponents } from "./catalogue.js"
|
|
21
|
-
import { createFrameBehaviors, INSPECTOR_INLINE_WIDTH, isNarrow } from "
|
|
21
|
+
import { createFrameBehaviors, currentColumnId, INSPECTOR_INLINE_WIDTH, isExplorer, isNarrow } from "../components/Frame.js"
|
|
22
22
|
import { tuiTheme } from "./theme.js"
|
|
23
23
|
|
|
24
|
+
// Constant: CURRENT_GROW
|
|
25
|
+
// How much wider the explorer's middle (current) column is than its
|
|
26
|
+
// neighbours — enough to read as the working pane, not enough to starve the
|
|
27
|
+
// parent and preview columns.
|
|
28
|
+
const CURRENT_GROW = 1.35
|
|
29
|
+
|
|
24
30
|
// Function: createAppComponent
|
|
25
31
|
// Builds the root component bound to a `session`.
|
|
26
32
|
//
|
|
@@ -57,6 +63,9 @@ function createAppComponent(session, options = {}) {
|
|
|
57
63
|
const rightCrumbs = derived([session.panes.right.segments, crumbWidth], ([segments, width]) =>
|
|
58
64
|
elideCrumbs(segments, width),
|
|
59
65
|
)
|
|
66
|
+
const parentCrumbs = derived([session.panes.parent.segments, crumbWidth], ([segments, width]) =>
|
|
67
|
+
elideCrumbs(segments, width),
|
|
68
|
+
)
|
|
60
69
|
|
|
61
70
|
const quit = options.quit ?? ((self) => {
|
|
62
71
|
// Destroying the renderer from within a keypress deadlocks the native
|
|
@@ -67,7 +76,7 @@ function createAppComponent(session, options = {}) {
|
|
|
67
76
|
}, 100)
|
|
68
77
|
})
|
|
69
78
|
|
|
70
|
-
const baseKeys = appBehaviors({
|
|
79
|
+
const baseKeys = appBehaviors({ quit }).keys
|
|
71
80
|
const frame = createFrameBehaviors({ theme: tuiTheme })
|
|
72
81
|
|
|
73
82
|
return ui
|
|
@@ -78,6 +87,7 @@ function createAppComponent(session, options = {}) {
|
|
|
78
87
|
helpOpen: help,
|
|
79
88
|
viewerOpen: viewer,
|
|
80
89
|
leftSegments: leftCrumbs,
|
|
90
|
+
parentSegments: parentCrumbs,
|
|
81
91
|
rightSegments: rightCrumbs,
|
|
82
92
|
}))
|
|
83
93
|
.does({
|
|
@@ -87,7 +97,6 @@ function createAppComponent(session, options = {}) {
|
|
|
87
97
|
const name = key?.name
|
|
88
98
|
const isQuestion = name === "?" || (name === "/" && key?.shift)
|
|
89
99
|
const busy =
|
|
90
|
-
unwrap(data.paletteOpen) ||
|
|
91
100
|
unwrap(data.confirm) ||
|
|
92
101
|
unwrap(data.filtering) ||
|
|
93
102
|
unwrap(data.bookmarkOpen) ||
|
|
@@ -113,8 +122,8 @@ function createAppComponent(session, options = {}) {
|
|
|
113
122
|
return
|
|
114
123
|
}
|
|
115
124
|
// Below the inline breakpoint `v` toggles the viewer overlay
|
|
116
|
-
// instead of the side panel.
|
|
117
|
-
if (!busy && name === "v" && isNarrow(data, self)) {
|
|
125
|
+
// instead of the side panel (`⇧V` still maximizes).
|
|
126
|
+
if (!busy && name === "v" && !key.shift && isNarrow(data, self)) {
|
|
118
127
|
viewer.set(!viewer.value)
|
|
119
128
|
return
|
|
120
129
|
}
|
|
@@ -125,11 +134,23 @@ function createAppComponent(session, options = {}) {
|
|
|
125
134
|
unwrap(data.active) === "left" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
|
|
126
135
|
rightBorderFg: (_self, data) =>
|
|
127
136
|
unwrap(data.active) === "right" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
|
|
137
|
+
parentBorderFg: (_self, data) =>
|
|
138
|
+
unwrap(data.active) === "parent" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
|
|
128
139
|
// The header rule carries the same focus tint as the pane border.
|
|
129
140
|
leftHeadRuleFg: (_self, data) =>
|
|
130
141
|
unwrap(data.active) === "left" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
|
|
131
142
|
rightHeadRuleFg: (_self, data) =>
|
|
132
143
|
unwrap(data.active) === "right" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
|
|
144
|
+
parentHeadRuleFg: (_self, data) =>
|
|
145
|
+
unwrap(data.active) === "parent" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
|
|
146
|
+
// In explorer the current column takes the extra width; in
|
|
147
|
+
// commander both panes share the row evenly. The columns carry
|
|
148
|
+
// `flexBasis="0"` so the whole row is distributed by these
|
|
149
|
+
// growers instead of by the content each column would claim.
|
|
150
|
+
leftGrow: (_self, data) =>
|
|
151
|
+
isExplorer(data) && currentColumnId(data) === "left" ? CURRENT_GROW : 1,
|
|
152
|
+
rightGrow: (_self, data) =>
|
|
153
|
+
isExplorer(data) && currentColumnId(data) === "right" ? CURRENT_GROW : 1,
|
|
133
154
|
})
|
|
134
155
|
.sub(appEvents({ quit }))
|
|
135
156
|
.using(appXml)
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
// `rows` environment. Components sharing a presentation bind the same source
|
|
14
14
|
// (`Breadcrumbs` uses `RowList.xml`).
|
|
15
15
|
//
|
|
16
|
-
//
|
|
16
|
+
// The workspace chrome (`Frame.js`) and the key reference (`Help.js`) are
|
|
17
|
+
// shared logic bound here to their `.xml` presentations; the browser view
|
|
18
|
+
// binds the same modules to `.html`.
|
|
17
19
|
|
|
18
20
|
import { extend, ui } from "select.tui"
|
|
19
21
|
import { unwrap } from "select/state/cells"
|
|
@@ -23,7 +25,6 @@ import commandBarXml from "../components/CommandBar.xml" with { type: "text" }
|
|
|
23
25
|
import commandArgRowXml from "../components/CommandArgRow.xml" with { type: "text" }
|
|
24
26
|
import commandItemXml from "../components/CommandItem.xml" with { type: "text" }
|
|
25
27
|
import commandPanelXml from "../components/CommandPanel.xml" with { type: "text" }
|
|
26
|
-
import commandPaletteXml from "../components/CommandPalette.xml" with { type: "text" }
|
|
27
28
|
import confirmXml from "../components/Confirm.xml" with { type: "text" }
|
|
28
29
|
import crumbXml from "../components/Crumb.xml" with { type: "text" }
|
|
29
30
|
import entryDetailXml from "../components/EntryDetail.xml" with { type: "text" }
|
|
@@ -32,7 +33,6 @@ import filePreviewXml from "../components/FilePreview.xml" with { type: "text" }
|
|
|
32
33
|
import fileRowXml from "../components/FileRow.xml" with { type: "text" }
|
|
33
34
|
import filterBarXml from "../components/FilterBar.xml" with { type: "text" }
|
|
34
35
|
import helpXml from "../components/Help.xml" with { type: "text" }
|
|
35
|
-
import paletteRowXml from "../components/PaletteRow.xml" with { type: "text" }
|
|
36
36
|
import rowListXml from "../components/RowList.xml" with { type: "text" }
|
|
37
37
|
import summaryXml from "../components/Summary.xml" with { type: "text" }
|
|
38
38
|
|
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
commandItemRows,
|
|
49
49
|
commandBarMarksItem,
|
|
50
50
|
commandPanelBehaviors,
|
|
51
|
-
commandPaletteBehaviors,
|
|
52
51
|
confirmBehaviors,
|
|
53
52
|
crumbBehaviors,
|
|
54
53
|
crumbRows,
|
|
@@ -60,14 +59,12 @@ import {
|
|
|
60
59
|
fileWindow,
|
|
61
60
|
filterBarBehaviors,
|
|
62
61
|
iconsEnabled,
|
|
63
|
-
paletteRowBehaviors,
|
|
64
|
-
paletteRows,
|
|
65
62
|
panelArgRows,
|
|
66
63
|
panelOpRows,
|
|
67
64
|
summaryBehaviors,
|
|
68
65
|
} from "../components/index.js"
|
|
69
|
-
import { createBehaviors as helpBehaviors } from "
|
|
70
|
-
import { INSPECTOR_INLINE_WIDTH } from "
|
|
66
|
+
import { createBehaviors as helpBehaviors } from "../components/Help.js"
|
|
67
|
+
import { INSPECTOR_INLINE_WIDTH } from "../components/Frame.js"
|
|
71
68
|
import { tuiTheme } from "./theme.js"
|
|
72
69
|
|
|
73
70
|
let registered = false
|
|
@@ -90,6 +87,7 @@ function tuiEnv() {
|
|
|
90
87
|
dirFg: tuiTheme.dirFg,
|
|
91
88
|
errorBg: tuiTheme.errorBg,
|
|
92
89
|
errorFg: tuiTheme.errorFg,
|
|
90
|
+
fieldBg: tuiTheme.fieldBg,
|
|
93
91
|
fileFg: tuiTheme.fileFg,
|
|
94
92
|
// Nerd Font icons in rows, on unless `TCMD_ICONS` opts out.
|
|
95
93
|
icons: iconsEnabled(process.env),
|
|
@@ -177,9 +175,6 @@ function fileListScrollTop(self, data) {
|
|
|
177
175
|
return keepVisible(self.ref.list, local, len)
|
|
178
176
|
}
|
|
179
177
|
return keepVisible(self.ref.list, unwrap(data.cursor) ?? 0, unwrap(data.entries)?.length ?? 0)
|
|
180
|
-
}// Keep the palette selection inside its viewport.
|
|
181
|
-
function paletteScrollTop(self, data) {
|
|
182
|
-
return keepVisible(self.ref.list, unwrap(data.selected) ?? 0, unwrap(data.items)?.length ?? 0)
|
|
183
178
|
}
|
|
184
179
|
|
|
185
180
|
// Keep the bookmark picker selection inside its viewport.
|
|
@@ -214,7 +209,6 @@ function createComponents() {
|
|
|
214
209
|
const crumb = bind(ui, "Crumb", crumbXml, crumbBehaviors(env))
|
|
215
210
|
const fileRow = bind(ui, "FileRow", fileRowXml, fileRowBehaviors(env))
|
|
216
211
|
const filterBar = bind(ui, "FilterBar", filterBarXml, filterBarBehaviors(env))
|
|
217
|
-
const paletteRow = bind(ui, "PaletteRow", paletteRowXml, paletteRowBehaviors(env))
|
|
218
212
|
const bookmarkRow = bind(ui, "BookmarkRow", bookmarkRowXml, bookmarkRowBehaviors(env))
|
|
219
213
|
return {
|
|
220
214
|
BookmarkRow: bookmarkRow,
|
|
@@ -222,7 +216,6 @@ function createComponents() {
|
|
|
222
216
|
CommandItem: commandItem,
|
|
223
217
|
Crumb: crumb,
|
|
224
218
|
FileRow: fileRow,
|
|
225
|
-
PaletteRow: paletteRow,
|
|
226
219
|
Breadcrumbs: bind(ui, "Breadcrumbs", rowListXml, breadcrumbsBehaviors({
|
|
227
220
|
...env,
|
|
228
221
|
rows: crumbRows(crumb),
|
|
@@ -242,11 +235,6 @@ function createComponents() {
|
|
|
242
235
|
argRows: panelArgRows(commandArgRow),
|
|
243
236
|
argScrollTop: panelArgScrollTop,
|
|
244
237
|
})),
|
|
245
|
-
CommandPalette: bind(ui, "CommandPalette", commandPaletteXml, commandPaletteBehaviors({
|
|
246
|
-
...env,
|
|
247
|
-
rows: paletteRows(paletteRow),
|
|
248
|
-
scrollTop: paletteScrollTop,
|
|
249
|
-
})),
|
|
250
238
|
Confirm: bind(ui, "Confirm", confirmXml, confirmBehaviors(env)),
|
|
251
239
|
EntryDetail: bind(ui, "EntryDetail", entryDetailXml, entryDetailBehaviors({
|
|
252
240
|
...env,
|
|
@@ -283,7 +271,6 @@ function registerComponents() {
|
|
|
283
271
|
breadcrumbs: components.Breadcrumbs,
|
|
284
272
|
commandbar: components.CommandBar,
|
|
285
273
|
commandpanel: components.CommandPanel,
|
|
286
|
-
commandpalette: components.CommandPalette,
|
|
287
274
|
confirm: components.Confirm,
|
|
288
275
|
entrydetail: components.EntryDetail,
|
|
289
276
|
filelist: components.FileList,
|
package/src/js/tcmd/tui/index.js
CHANGED
|
@@ -15,24 +15,10 @@ import nodePath from "node:path"
|
|
|
15
15
|
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
16
16
|
import { cell } from "select/state/cells"
|
|
17
17
|
import { render } from "select.tui"
|
|
18
|
-
import { commandRow, getPaletteCommands } from "../core/commands.js"
|
|
19
18
|
import { createBookmarks, createFileBookmarkStore } from "../core/bookmarks.js"
|
|
20
19
|
import { createNodeFs, createNodeOs, createPane, createSession, load } from "../core/index.js"
|
|
21
20
|
import { createAppComponent } from "./app.js"
|
|
22
21
|
|
|
23
|
-
// The TUI palette lists meaningful actions; raw cursor movement is already on
|
|
24
|
-
// the arrow keys and is noise in the list. Hiding it here (rather than in core)
|
|
25
|
-
// keeps the web palette and the shared registry unchanged.
|
|
26
|
-
const PALETTE_NOISE = /^nav\./
|
|
27
|
-
|
|
28
|
-
// Function: tuiPaletteCommands
|
|
29
|
-
// The command rows the TUI palette offers.
|
|
30
|
-
function tuiPaletteCommands() {
|
|
31
|
-
return getPaletteCommands()
|
|
32
|
-
.filter((command) => !PALETTE_NOISE.test(command.id))
|
|
33
|
-
.map(commandRow)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
22
|
// Function: readSize
|
|
37
23
|
// Normalizes a renderer resize payload. OpenTUI's live renderer emits
|
|
38
24
|
// `(width, height)` as two arguments; the test renderer emits a bare width;
|
|
@@ -78,7 +64,6 @@ async function main(paths = []) {
|
|
|
78
64
|
}),
|
|
79
65
|
})
|
|
80
66
|
const session = createSession({ fs, os, left, right, host: nodeOs.hostname(), bookmarks })
|
|
81
|
-
session.paletteCommands.set(tuiPaletteCommands())
|
|
82
67
|
await load(session)
|
|
83
68
|
const size = cell({ width: 0, height: 0 })
|
|
84
69
|
const help = cell(false)
|
|
@@ -99,6 +84,6 @@ async function main(paths = []) {
|
|
|
99
84
|
return session
|
|
100
85
|
}
|
|
101
86
|
|
|
102
|
-
export { main, readSize
|
|
87
|
+
export { main, readSize }
|
|
103
88
|
|
|
104
89
|
// EOF
|
package/src/js/tcmd/tui/theme.js
CHANGED
|
@@ -43,6 +43,8 @@ const DARK = {
|
|
|
43
43
|
cursorBg: "#274a9e",
|
|
44
44
|
cursorInactiveBg: "#1b2a51",
|
|
45
45
|
selectionBg: "#2b3f78",
|
|
46
|
+
// The value fields of the command panel's argument rows.
|
|
47
|
+
fieldBg: "#1b2a51",
|
|
46
48
|
// Text
|
|
47
49
|
textFg: "#e6ecff",
|
|
48
50
|
textSecondaryFg: "#8fa3d9",
|
|
@@ -88,6 +90,8 @@ const LIGHT = {
|
|
|
88
90
|
cursorBg: "#2f56b8",
|
|
89
91
|
cursorInactiveBg: "#c6d1ee",
|
|
90
92
|
selectionBg: "#c9d5f4",
|
|
93
|
+
// The value fields of the command panel's argument rows.
|
|
94
|
+
fieldBg: "#dbe4fb",
|
|
91
95
|
// Text
|
|
92
96
|
textFg: "#101a33",
|
|
93
97
|
textSecondaryFg: "#495785",
|
|
@@ -131,6 +135,7 @@ const MONO = {
|
|
|
131
135
|
cursorBg: undefined,
|
|
132
136
|
cursorInactiveBg: undefined,
|
|
133
137
|
selectionBg: undefined,
|
|
138
|
+
fieldBg: undefined,
|
|
134
139
|
textFg: undefined,
|
|
135
140
|
textSecondaryFg: undefined,
|
|
136
141
|
textHeadingFg: undefined,
|
package/src/js/tcmd/web/app.js
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
// `tcmd/tui/app.js` for the DOM backend.
|
|
10
10
|
|
|
11
11
|
import { ui } from "select/ui"
|
|
12
|
-
import { unwrap } from "select/state/cells"
|
|
12
|
+
import { cell, derived, unwrap } from "select/state/cells"
|
|
13
13
|
import appHtml from "../components/App.html" with { type: "text" }
|
|
14
14
|
import { appBehaviors, appEvents, appInit } from "../components/index.js"
|
|
15
15
|
import { registerWebComponents } from "./catalogue.js"
|
|
16
|
+
import { elideCrumbs } from "../components/format.js"
|
|
17
|
+
import { createFrameBehaviors, INSPECTOR_INLINE_WIDTH, isNarrow } from "../components/Frame.js"
|
|
18
|
+
import { connectEffects } from "./effects.js"
|
|
16
19
|
import { webTheme } from "./theme.js"
|
|
17
20
|
|
|
18
21
|
// Map DOM key names to the core keymap chord names.
|
|
@@ -45,9 +48,18 @@ function normalizeKey(event) {
|
|
|
45
48
|
// Child data keeps the session cells (as `in:` slots do in the TUI), so the
|
|
46
49
|
// child instances subscribe and update granularly; behaviors read cells with
|
|
47
50
|
// `unwrap` at the point of use.
|
|
51
|
+
|
|
52
|
+
// The classes one pane column carries: focus (`is-active`) and, in explorer
|
|
53
|
+
// mode, the extra width of the middle (`is-major`) column.
|
|
54
|
+
function paneClass(data, paneId) {
|
|
55
|
+
const active = unwrap(data.active) === paneId
|
|
56
|
+
const major = unwrap(data.viewMode) === "explorer" && `${unwrap(data.currentId)}` === paneId
|
|
57
|
+
return [active && webTheme.activeClass, major && "is-major"].filter(Boolean).join(" ") || null
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
function webViewBehaviors() {
|
|
49
61
|
return {
|
|
50
|
-
headerView: (_self, data) => ({ host: data.appHost, path: data.appPath }),
|
|
62
|
+
headerView: (_self, data) => ({ host: data.appHost, path: data.appPath, mode: data.viewMode }),
|
|
51
63
|
bookmarkView: (_self, data) => ({
|
|
52
64
|
items: data.bookmarkItems,
|
|
53
65
|
mode: data.bookmarkMode,
|
|
@@ -61,92 +73,206 @@ function webViewBehaviors() {
|
|
|
61
73
|
leftError: data.leftError,
|
|
62
74
|
notice: data.notice,
|
|
63
75
|
rightError: data.rightError,
|
|
76
|
+
active: data.active,
|
|
77
|
+
leftMarked: data.leftMarked,
|
|
78
|
+
rightMarked: data.rightMarked,
|
|
64
79
|
}),
|
|
65
80
|
confirmView: (_self, data) => ({ confirm: data.confirm }),
|
|
66
81
|
leftCrumbsView: (_self, data) => ({
|
|
67
82
|
segments: data.leftSegments,
|
|
68
83
|
paneId: data.leftId,
|
|
69
84
|
}),
|
|
70
|
-
leftDetailView: (_self, data) => ({ entry: data.leftEntry, marked: data.leftMarked }),
|
|
85
|
+
leftDetailView: (_self, data) => ({ entry: data.leftEntry, marked: data.leftMarked, width: data.paneWidth }),
|
|
71
86
|
leftFilterView: (_self, data) => ({
|
|
72
87
|
query: data.leftFilter,
|
|
73
88
|
paneId: data.leftId,
|
|
74
89
|
filtering: data.filtering,
|
|
75
90
|
entries: data.leftEntries,
|
|
91
|
+
setFilter: data.leftSetFilter,
|
|
76
92
|
}),
|
|
77
|
-
leftLabelView: (_self, data) => ({ paneId: data.leftId, active: data.active }),
|
|
78
93
|
leftListView: (_self, data) => ({
|
|
79
94
|
entries: data.leftEntries,
|
|
80
95
|
cursor: data.leftCursor,
|
|
81
96
|
marked: data.leftMarked,
|
|
82
97
|
paneId: data.leftId,
|
|
98
|
+
active: data.leftActive,
|
|
99
|
+
loading: data.leftLoading,
|
|
83
100
|
}),
|
|
84
|
-
leftPaneClass: (_self, data) => (
|
|
101
|
+
leftPaneClass: (_self, data) => paneClass(data, "left"),
|
|
85
102
|
leftSummaryView: (_self, data) => ({
|
|
86
103
|
entries: data.leftEntries,
|
|
87
104
|
disk: data.leftDisk,
|
|
105
|
+
width: data.paneWidth,
|
|
88
106
|
}),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
// The explorer's first column: the parent of the current directory,
|
|
108
|
+
// a full pane of its own (header, filter, listing, detail, summary).
|
|
109
|
+
parentCrumbsView: (_self, data) => ({
|
|
110
|
+
segments: data.parentSegments,
|
|
111
|
+
paneId: data.parentId,
|
|
112
|
+
}),
|
|
113
|
+
parentDetailView: (_self, data) => ({
|
|
114
|
+
entry: data.parentEntry,
|
|
115
|
+
marked: data.parentMarked,
|
|
116
|
+
width: data.paneWidth,
|
|
117
|
+
}),
|
|
118
|
+
parentFilterView: (_self, data) => ({
|
|
119
|
+
query: data.parentFilter,
|
|
120
|
+
paneId: data.parentId,
|
|
121
|
+
filtering: data.filtering,
|
|
122
|
+
entries: data.parentEntries,
|
|
123
|
+
setFilter: data.parentSetFilter,
|
|
97
124
|
}),
|
|
125
|
+
parentListView: (_self, data) => ({
|
|
126
|
+
entries: data.parentEntries,
|
|
127
|
+
cursor: data.parentCursor,
|
|
128
|
+
marked: data.parentMarked,
|
|
129
|
+
paneId: data.parentId,
|
|
130
|
+
active: data.parentActive,
|
|
131
|
+
loading: data.parentLoading,
|
|
132
|
+
}),
|
|
133
|
+
parentPaneClass: (_self, data) => paneClass(data, "parent"),
|
|
134
|
+
parentSummaryView: (_self, data) => ({
|
|
135
|
+
entries: data.parentEntries,
|
|
136
|
+
disk: data.parentDisk,
|
|
137
|
+
width: data.paneWidth,
|
|
138
|
+
}),
|
|
139
|
+
panelView: (_self, data) => ({
|
|
140
|
+
// Cells pass through (as `in:` slots do in the TUI); `query`/`value`
|
|
141
|
+
// are written back by the inputs' `inout` two-way bindings.
|
|
142
|
+
open: data.panelOpen,
|
|
143
|
+
entry: data.panelEntry,
|
|
144
|
+
items: data.panelItems,
|
|
145
|
+
opCursor: data.panelOpIndex,
|
|
146
|
+
rows: data.panelRows,
|
|
147
|
+
argCursor: data.panelRowIndex,
|
|
148
|
+
labelWidth: data.panelLabelWidth,
|
|
149
|
+
focus: data.panelFocus,
|
|
150
|
+
command: data.panelCommand,
|
|
151
|
+
runnable: data.panelRunnable,
|
|
152
|
+
picking: data.panelPicking,
|
|
153
|
+
pickerQuery: data.pickerQuery,
|
|
154
|
+
pickerPath: data.pickerPath,
|
|
155
|
+
query: data.panelQuery,
|
|
156
|
+
value: data.panelValue,
|
|
157
|
+
}),
|
|
158
|
+
helpView: (_self, data) => ({ open: data.helpOpen }),
|
|
98
159
|
previewView: (_self, data) => ({
|
|
99
160
|
preview: data.preview,
|
|
100
|
-
show:
|
|
161
|
+
show: true,
|
|
162
|
+
entry: data.appEntry,
|
|
163
|
+
paneId: data.previewId,
|
|
164
|
+
// The directory column: the inspector pane's own listing, so a
|
|
165
|
+
// previewed directory reads and behaves like a pane.
|
|
166
|
+
dirSegments: data.previewSegments,
|
|
167
|
+
dirEntries: data.previewEntries,
|
|
168
|
+
dirCursor: data.previewCursor,
|
|
169
|
+
dirMarked: data.previewMarked,
|
|
170
|
+
dirActive: data.previewActive,
|
|
171
|
+
dirLoading: data.previewLoading,
|
|
101
172
|
}),
|
|
102
173
|
rightCrumbsView: (_self, data) => ({
|
|
103
174
|
segments: data.rightSegments,
|
|
104
175
|
paneId: data.rightId,
|
|
105
176
|
}),
|
|
106
|
-
rightDetailView: (_self, data) => ({ entry: data.rightEntry, marked: data.rightMarked }),
|
|
177
|
+
rightDetailView: (_self, data) => ({ entry: data.rightEntry, marked: data.rightMarked, width: data.paneWidth }),
|
|
107
178
|
rightFilterView: (_self, data) => ({
|
|
108
179
|
query: data.rightFilter,
|
|
109
180
|
paneId: data.rightId,
|
|
110
181
|
filtering: data.filtering,
|
|
111
182
|
entries: data.rightEntries,
|
|
183
|
+
setFilter: data.rightSetFilter,
|
|
112
184
|
}),
|
|
113
|
-
rightLabelView: (_self, data) => ({ paneId: data.rightId, active: data.active }),
|
|
114
185
|
rightListView: (_self, data) => ({
|
|
115
186
|
entries: data.rightEntries,
|
|
116
187
|
cursor: data.rightCursor,
|
|
117
188
|
marked: data.rightMarked,
|
|
118
189
|
paneId: data.rightId,
|
|
190
|
+
active: data.rightActive,
|
|
191
|
+
loading: data.rightLoading,
|
|
119
192
|
}),
|
|
120
|
-
rightPaneClass: (_self, data) => (
|
|
193
|
+
rightPaneClass: (_self, data) => paneClass(data, "right"),
|
|
121
194
|
rightSummaryView: (_self, data) => ({
|
|
122
195
|
entries: data.rightEntries,
|
|
123
196
|
disk: data.rightDisk,
|
|
197
|
+
width: data.paneWidth,
|
|
124
198
|
}),
|
|
125
199
|
}
|
|
126
200
|
}
|
|
127
201
|
|
|
128
202
|
// Function: createWebApp
|
|
129
203
|
// Builds the root App component for `session` on the web presentation.
|
|
130
|
-
function createWebApp(session) {
|
|
204
|
+
function createWebApp(session, options = {}) {
|
|
131
205
|
registerWebComponents()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
session.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
206
|
+
const size = options.size ?? cell({ width: 0, height: 0 })
|
|
207
|
+
const help = cell(false)
|
|
208
|
+
const viewer = cell(false)
|
|
209
|
+
const paneWidth = derived([size, session.showPreview], ([value, show]) => {
|
|
210
|
+
if (!value.width) return 0
|
|
211
|
+
const columns = show && value.width >= INSPECTOR_INLINE_WIDTH ? 3 : 2
|
|
212
|
+
return Math.max(1, Math.floor(value.width / columns) - 4)
|
|
213
|
+
})
|
|
214
|
+
const crumbs = (pane) => derived([pane.segments, paneWidth], ([segments, width]) => elideCrumbs(segments, width))
|
|
215
|
+
const quit = (self) => self.unmount?.()
|
|
216
|
+
// DOM inputs have no native `enter` event; the root routes Enter for them.
|
|
217
|
+
const baseKeys = appBehaviors({ normalizeKey, quit }).keys
|
|
218
|
+
const events = appEvents({ quit })
|
|
140
219
|
return ui(appHtml)
|
|
141
|
-
.init(
|
|
220
|
+
.init(() => ({
|
|
221
|
+
...appInit(session)(),
|
|
222
|
+
uiSize: size,
|
|
223
|
+
helpOpen: help,
|
|
224
|
+
viewerOpen: viewer,
|
|
225
|
+
paneWidth,
|
|
226
|
+
leftSegments: crumbs(session.panes.left),
|
|
227
|
+
parentSegments: crumbs(session.panes.parent),
|
|
228
|
+
rightSegments: crumbs(session.panes.right),
|
|
229
|
+
}))
|
|
142
230
|
.does({
|
|
143
|
-
...
|
|
144
|
-
normalizeKey,
|
|
145
|
-
quit: (self) => self.unmount?.(),
|
|
146
|
-
}),
|
|
231
|
+
...createFrameBehaviors({ theme: webTheme }),
|
|
147
232
|
...webViewBehaviors(),
|
|
233
|
+
minSizeTitle: () => "Window too small",
|
|
234
|
+
"effects!": (self) => {
|
|
235
|
+
if (!self._effects) self.effect(() => {
|
|
236
|
+
self._effects = connectEffects(session, self.ref.root, { size, help, viewer })
|
|
237
|
+
return () => self._effects.dispose()
|
|
238
|
+
})
|
|
239
|
+
self._effects.schedule()
|
|
240
|
+
},
|
|
241
|
+
keys: (self, data, event) => {
|
|
242
|
+
if (event.isComposing) return
|
|
243
|
+
const key = normalizeKey(event)
|
|
244
|
+
const busy = unwrap(data.confirm) || unwrap(data.bookmarkOpen) || unwrap(data.panelOpen) || unwrap(data.filtering)
|
|
245
|
+
if (help.value) {
|
|
246
|
+
if (["escape", "q", "?"].includes(key.name)) help.set(false)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
if (!busy && key.name === "?") {
|
|
250
|
+
help.set(true)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
// The viewer only exists while narrow: if the viewport widened
|
|
254
|
+
// underneath it, drop the stale state and keep handling the key.
|
|
255
|
+
if (viewer.value && !isNarrow(data, self)) viewer.set(false)
|
|
256
|
+
if (viewer.value) {
|
|
257
|
+
if (key.name === "escape" || key.name === "v") viewer.set(false)
|
|
258
|
+
else if (key.shift && key.name === "o") session.dispatch("file.open")
|
|
259
|
+
else if (key.shift && key.name === "e") session.dispatch("file.edit")
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
if (!busy && key.name === "v" && !key.shift && isNarrow(data, self)) {
|
|
263
|
+
viewer.set(!viewer.value)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
return baseKeys(self, data, event)
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
.sub({
|
|
270
|
+
...events,
|
|
271
|
+
"commandbar:run": (self, data, event) => {
|
|
272
|
+
if (event.data === "view.togglePreview" && isNarrow(data, self)) viewer.set(!viewer.value)
|
|
273
|
+
else events["commandbar:run"](self, data, event)
|
|
274
|
+
},
|
|
148
275
|
})
|
|
149
|
-
.sub(appEvents({ quit: (self) => self.unmount?.() }))
|
|
150
276
|
}
|
|
151
277
|
|
|
152
278
|
export { createWebApp, normalizeKey }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-13
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/web/bookmarks
|
|
7
|
+
// The `core/bookmarks` store contract backed by localStorage
|
|
8
|
+
// (`createWebBookmarkStore`, the web twin of the TUI's
|
|
9
|
+
// `createFileBookmarkStore`). Backend/root namespaces keep browser-private and
|
|
10
|
+
// host-filesystem paths apart. Persistence is best-effort, just like the TUI
|
|
11
|
+
// store (private browsing may deny storage).
|
|
12
|
+
|
|
13
|
+
// Function: createWebBookmarkStore
|
|
14
|
+
function createWebBookmarkStore({ mode, root, storage } = {}) {
|
|
15
|
+
const key = JSON.stringify(["tcmd.bookmarks", mode, root])
|
|
16
|
+
return {
|
|
17
|
+
async load() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse((storage ?? globalThis.localStorage).getItem(key) ?? "{}")
|
|
20
|
+
} catch {
|
|
21
|
+
return {}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
async save(map) {
|
|
25
|
+
try {
|
|
26
|
+
(storage ?? globalThis.localStorage).setItem(key, JSON.stringify(map))
|
|
27
|
+
} catch {
|
|
28
|
+
// Keep in-memory bookmarks usable when persistent storage is denied.
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { createWebBookmarkStore }
|
|
35
|
+
|
|
36
|
+
// EOF
|