@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
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, paneColumnWidth } from "../components/Frame.js"
|
|
18
|
+
import { connectEffects, isToolbarTarget } 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,108 +48,289 @@ 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
|
-
|
|
62
|
+
// The view mode as an app-level class (`is-explorer` / `is-commander`),
|
|
63
|
+
// so the CSS can tint the active pane per mode (blue/gold).
|
|
64
|
+
appModeClass: (_self, data) => [
|
|
65
|
+
`is-${unwrap(data.viewMode) === "explorer" ? "explorer" : "commander"}`,
|
|
66
|
+
unwrap(data.desktopMode) ? "is-desktop" : null,
|
|
67
|
+
].filter(Boolean).join(" "),
|
|
68
|
+
headerView: (_self, data) => ({ host: data.appHost, path: data.appPath, mode: data.viewMode }),
|
|
69
|
+
desktopViewLabel: (_self, data) => unwrap(data.viewMode) === "explorer" ? "Explorer" : "Commander",
|
|
70
|
+
desktopPreviewLabel: (_self, data) => unwrap(data.showPreview) ? "Hide preview" : "Show preview",
|
|
51
71
|
bookmarkView: (_self, data) => ({
|
|
52
72
|
items: data.bookmarkItems,
|
|
53
|
-
mode: data.bookmarkMode,
|
|
54
73
|
open: data.bookmarkOpen,
|
|
55
74
|
query: data.bookmarkQuery,
|
|
56
75
|
selected: data.bookmarkIndex,
|
|
57
|
-
target: data.bookmarkTarget,
|
|
58
76
|
}),
|
|
59
77
|
commandBarView: (_self, data) => ({
|
|
60
78
|
items: data.commandBarItems,
|
|
79
|
+
desktopMode: data.desktopMode,
|
|
61
80
|
leftError: data.leftError,
|
|
62
81
|
notice: data.notice,
|
|
63
82
|
rightError: data.rightError,
|
|
83
|
+
active: data.active,
|
|
84
|
+
leftMarked: data.leftMarked,
|
|
85
|
+
rightMarked: data.rightMarked,
|
|
86
|
+
uiSize: data.uiSize,
|
|
64
87
|
}),
|
|
65
88
|
confirmView: (_self, data) => ({ confirm: data.confirm }),
|
|
66
89
|
leftCrumbsView: (_self, data) => ({
|
|
67
90
|
segments: data.leftSegments,
|
|
68
91
|
paneId: data.leftId,
|
|
69
92
|
}),
|
|
70
|
-
leftDetailView: (_self, data) => ({ entry: data.leftEntry, marked: data.leftMarked }),
|
|
93
|
+
leftDetailView: (_self, data) => ({ entry: data.leftEntry, marked: data.leftMarked, width: data.paneWidth }),
|
|
71
94
|
leftFilterView: (_self, data) => ({
|
|
72
95
|
query: data.leftFilter,
|
|
73
96
|
paneId: data.leftId,
|
|
74
97
|
filtering: data.filtering,
|
|
75
98
|
entries: data.leftEntries,
|
|
99
|
+
setFilter: data.leftSetFilter,
|
|
76
100
|
}),
|
|
77
|
-
leftLabelView: (_self, data) => ({ paneId: data.leftId, active: data.active }),
|
|
78
101
|
leftListView: (_self, data) => ({
|
|
79
102
|
entries: data.leftEntries,
|
|
80
103
|
cursor: data.leftCursor,
|
|
81
104
|
marked: data.leftMarked,
|
|
82
105
|
paneId: data.leftId,
|
|
106
|
+
active: data.leftActive,
|
|
107
|
+
loading: data.leftLoading,
|
|
83
108
|
}),
|
|
84
|
-
leftPaneClass: (_self, data) => (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
leftPaneClass: (_self, data) => paneClass(data, "left"),
|
|
110
|
+
// The explorer's first column: the parent of the current directory,
|
|
111
|
+
// a full pane of its own (header, filter, listing, detail).
|
|
112
|
+
parentCrumbsView: (_self, data) => ({
|
|
113
|
+
segments: data.parentSegments,
|
|
114
|
+
paneId: data.parentId,
|
|
115
|
+
}),
|
|
116
|
+
parentDetailView: (_self, data) => ({
|
|
117
|
+
entry: data.parentEntry,
|
|
118
|
+
marked: data.parentMarked,
|
|
119
|
+
width: data.paneWidth,
|
|
120
|
+
}),
|
|
121
|
+
parentFilterView: (_self, data) => ({
|
|
122
|
+
query: data.parentFilter,
|
|
123
|
+
paneId: data.parentId,
|
|
124
|
+
filtering: data.filtering,
|
|
125
|
+
entries: data.parentEntries,
|
|
126
|
+
setFilter: data.parentSetFilter,
|
|
127
|
+
}),
|
|
128
|
+
parentListView: (_self, data) => ({
|
|
129
|
+
entries: data.parentEntries,
|
|
130
|
+
cursor: data.parentCursor,
|
|
131
|
+
marked: data.parentMarked,
|
|
132
|
+
paneId: data.parentId,
|
|
133
|
+
active: data.parentActive,
|
|
134
|
+
loading: data.parentLoading,
|
|
88
135
|
}),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
parentPaneClass: (_self, data) => paneClass(data, "parent"),
|
|
137
|
+
panelView: (_self, data) => ({
|
|
138
|
+
// Cells pass through (as `in:` slots do in the TUI); `query`/`value`
|
|
139
|
+
// are written back by the inputs' `inout` two-way bindings.
|
|
140
|
+
open: data.panelOpen,
|
|
141
|
+
entry: data.panelEntry,
|
|
142
|
+
items: data.panelItems,
|
|
143
|
+
opCursor: data.panelOpIndex,
|
|
144
|
+
rows: data.panelRows,
|
|
145
|
+
argCursor: data.panelRowIndex,
|
|
146
|
+
focus: data.panelFocus,
|
|
147
|
+
preview: data.panelPreview,
|
|
148
|
+
picking: data.pickerOpen,
|
|
149
|
+
pickerPathText: data.pickerPathText,
|
|
150
|
+
pickerFocus: data.pickerFocus,
|
|
151
|
+
pickerRows: data.pickerRows,
|
|
152
|
+
query: data.panelQuery,
|
|
153
|
+
value: data.panelValue,
|
|
154
|
+
}),
|
|
155
|
+
helpView: (_self, data) => ({ open: data.helpOpen }),
|
|
156
|
+
overlayView: (_self, data) => ({
|
|
157
|
+
open: data.overlay,
|
|
158
|
+
mode: data.overlay,
|
|
159
|
+
items: data.overlayItems,
|
|
160
|
+
cursor: data.overlayIndex,
|
|
97
161
|
}),
|
|
98
162
|
previewView: (_self, data) => ({
|
|
99
163
|
preview: data.preview,
|
|
100
|
-
show:
|
|
164
|
+
show: true,
|
|
165
|
+
entry: data.appEntry,
|
|
166
|
+
paneId: data.previewId,
|
|
167
|
+
// The directory column: the inspector pane's own listing, so a
|
|
168
|
+
// previewed directory reads and behaves like a pane.
|
|
169
|
+
dirSegments: data.previewSegments,
|
|
170
|
+
dirEntries: data.previewEntries,
|
|
171
|
+
dirCursor: data.previewCursor,
|
|
172
|
+
dirMarked: data.previewMarked,
|
|
173
|
+
dirActive: data.previewActive,
|
|
174
|
+
dirLoading: data.previewLoading,
|
|
101
175
|
}),
|
|
102
176
|
rightCrumbsView: (_self, data) => ({
|
|
103
177
|
segments: data.rightSegments,
|
|
104
178
|
paneId: data.rightId,
|
|
105
179
|
}),
|
|
106
|
-
rightDetailView: (_self, data) => ({ entry: data.rightEntry, marked: data.rightMarked }),
|
|
180
|
+
rightDetailView: (_self, data) => ({ entry: data.rightEntry, marked: data.rightMarked, width: data.paneWidth }),
|
|
107
181
|
rightFilterView: (_self, data) => ({
|
|
108
182
|
query: data.rightFilter,
|
|
109
183
|
paneId: data.rightId,
|
|
110
184
|
filtering: data.filtering,
|
|
111
185
|
entries: data.rightEntries,
|
|
186
|
+
setFilter: data.rightSetFilter,
|
|
112
187
|
}),
|
|
113
|
-
rightLabelView: (_self, data) => ({ paneId: data.rightId, active: data.active }),
|
|
114
188
|
rightListView: (_self, data) => ({
|
|
115
189
|
entries: data.rightEntries,
|
|
116
190
|
cursor: data.rightCursor,
|
|
117
191
|
marked: data.rightMarked,
|
|
118
192
|
paneId: data.rightId,
|
|
193
|
+
active: data.rightActive,
|
|
194
|
+
loading: data.rightLoading,
|
|
119
195
|
}),
|
|
120
|
-
rightPaneClass: (_self, data) => (
|
|
121
|
-
rightSummaryView: (_self, data) => ({
|
|
122
|
-
entries: data.rightEntries,
|
|
123
|
-
disk: data.rightDisk,
|
|
124
|
-
}),
|
|
196
|
+
rightPaneClass: (_self, data) => paneClass(data, "right"),
|
|
125
197
|
}
|
|
126
198
|
}
|
|
127
199
|
|
|
128
200
|
// Function: createWebApp
|
|
129
201
|
// Builds the root App component for `session` on the web presentation.
|
|
130
|
-
function createWebApp(session) {
|
|
202
|
+
function createWebApp(session, options = {}) {
|
|
131
203
|
registerWebComponents()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
204
|
+
const size = options.size ?? cell({ width: 0, height: 0 })
|
|
205
|
+
const help = cell(false)
|
|
206
|
+
// The fullscreen inspector overlay lives in the session (core), so a
|
|
207
|
+
// command — Enter on a file that cannot be edited — can open it; the shared
|
|
208
|
+
// controller routes its keys (`components/App#createBehaviors`).
|
|
209
|
+
const viewer = session.viewerOpen
|
|
210
|
+
const paneWidth = derived([size, session.showPreview], ([value, show]) => {
|
|
211
|
+
if (!value.width) return 0
|
|
212
|
+
const columns = show && value.width >= INSPECTOR_INLINE_WIDTH ? 3 : 2
|
|
213
|
+
return Math.max(1, Math.floor(value.width / columns) - 4)
|
|
214
|
+
})
|
|
215
|
+
// Breadcrumb budgets follow each pane's OWN share of the row (explorer
|
|
216
|
+
// grows the current column, so its mirrors are narrower than an even
|
|
217
|
+
// split), or the crumbs overflow the narrower columns.
|
|
218
|
+
const crumbWidth = (paneId) =>
|
|
219
|
+
derived([size, session.showPreview, session.viewMode, session.currentId], ([value, showPreview, viewMode, currentId]) => {
|
|
220
|
+
const columns = paneColumnWidth(value?.width, paneId, { viewMode, currentId, showPreview })
|
|
221
|
+
return columns > 0 ? Math.max(1, columns - 4) : 0
|
|
222
|
+
})
|
|
223
|
+
const crumbs = (paneId, pane) =>
|
|
224
|
+
derived([pane.segments, crumbWidth(paneId)], ([segments, width]) => elideCrumbs(segments, width))
|
|
225
|
+
const quit = options.onQuit ?? ((self) => self.unmount?.())
|
|
226
|
+
// DOM inputs have no native `enter` event; the root routes Enter for them.
|
|
227
|
+
const baseKeys = appBehaviors({ normalizeKey, quit }).keys
|
|
228
|
+
const events = appEvents()
|
|
140
229
|
return ui(appHtml)
|
|
141
|
-
.init(
|
|
230
|
+
.init((self) => {
|
|
231
|
+
// The panel's Quit command reaches the host through this hook.
|
|
232
|
+
session.quit = () => quit(self)
|
|
233
|
+
return {
|
|
234
|
+
...appInit(session)(),
|
|
235
|
+
desktopMode: Boolean(options.desktopMode),
|
|
236
|
+
uiSize: size,
|
|
237
|
+
helpOpen: help,
|
|
238
|
+
viewerOpen: viewer,
|
|
239
|
+
paneWidth,
|
|
240
|
+
leftSegments: crumbs("left", session.panes.left),
|
|
241
|
+
parentSegments: crumbs("parent", session.panes.parent),
|
|
242
|
+
rightSegments: crumbs("right", session.panes.right),
|
|
243
|
+
previewSegments: crumbs("preview", session.panes.preview),
|
|
244
|
+
}
|
|
245
|
+
})
|
|
142
246
|
.does({
|
|
143
|
-
...
|
|
144
|
-
normalizeKey,
|
|
145
|
-
quit: (self) => self.unmount?.(),
|
|
146
|
-
}),
|
|
247
|
+
...createFrameBehaviors({ theme: webTheme }),
|
|
147
248
|
...webViewBehaviors(),
|
|
249
|
+
minSizeTitle: () => "Window too small",
|
|
250
|
+
"effects!": (self) => {
|
|
251
|
+
if (!self._effects) self.effect(() => {
|
|
252
|
+
self._effects = connectEffects(session, self.ref.root, { size, help, viewer })
|
|
253
|
+
return () => self._effects.dispose()
|
|
254
|
+
})
|
|
255
|
+
self._effects.schedule()
|
|
256
|
+
},
|
|
257
|
+
keys: (self, data, event) => {
|
|
258
|
+
if (event.isComposing) return
|
|
259
|
+
const busy = unwrap(data.confirm) || unwrap(data.bookmarkOpen) || unwrap(data.panelOpen) || unwrap(data.filtering) || unwrap(data.overlay)
|
|
260
|
+
if (event.key === "F6" && unwrap(data.desktopMode) && !busy && !help.value && !viewer.value) {
|
|
261
|
+
event.preventDefault()
|
|
262
|
+
const target = isToolbarTarget(event.target) ? self.ref.root : self.ref.root.querySelector(".desktop-toolbar button")
|
|
263
|
+
target?.focus({ preventScroll: true })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
// Native controls own their activation and traversal keys, not the
|
|
267
|
+
// pane underneath them. Escape hands focus back to the workspace.
|
|
268
|
+
if (isToolbarTarget(event.target)) {
|
|
269
|
+
if (event.key === "Escape") {
|
|
270
|
+
event.preventDefault()
|
|
271
|
+
self.ref.root.focus({ preventScroll: true })
|
|
272
|
+
}
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
const key = normalizeKey(event)
|
|
276
|
+
if (help.value) {
|
|
277
|
+
if (["escape", "q", "?"].includes(key.name)) help.set(false)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
if (!busy && key.name === "?") {
|
|
281
|
+
help.set(true)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
// The viewer overlay and the narrow-`v` shortcut are shared
|
|
285
|
+
// modal routing, handled by `components/App#createBehaviors`.
|
|
286
|
+
return baseKeys(self, data, event)
|
|
287
|
+
},
|
|
288
|
+
dialogStop: (_self, _data, event) => event.stopPropagation(),
|
|
289
|
+
})
|
|
290
|
+
.sub({
|
|
291
|
+
...events,
|
|
292
|
+
"dialog:panel-cancel": (_self, data) => {
|
|
293
|
+
if (!unwrap(data.desktopMode)) return
|
|
294
|
+
data.dispatch(unwrap(data.pickerOpen) ? "picker.cancel" : "panel.cancel")
|
|
295
|
+
},
|
|
296
|
+
"dialog:bookmark-cancel": (_self, data) => {
|
|
297
|
+
if (unwrap(data.desktopMode)) data.dispatch("bookmark.cancel")
|
|
298
|
+
},
|
|
299
|
+
"dialog:overlay-cancel": (_self, data) => {
|
|
300
|
+
if (unwrap(data.desktopMode)) data.dispatch("overlay.cancel")
|
|
301
|
+
},
|
|
302
|
+
"dialog:confirm-cancel": (_self, data) => {
|
|
303
|
+
if (unwrap(data.desktopMode)) data.dispatch("confirm.no")
|
|
304
|
+
},
|
|
305
|
+
"dialog:help-cancel": (_self, data) => {
|
|
306
|
+
if (unwrap(data.desktopMode)) help.set(false)
|
|
307
|
+
},
|
|
308
|
+
"dialog:viewer-cancel": (_self, data) => {
|
|
309
|
+
if (unwrap(data.desktopMode)) data.dispatch("view.toggleViewer")
|
|
310
|
+
},
|
|
311
|
+
"toolbar:open-folder": async () => {
|
|
312
|
+
try {
|
|
313
|
+
await options.onOpenFolder?.()
|
|
314
|
+
} catch (error) {
|
|
315
|
+
await options.onError?.(error)
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
"toolbar:back": (_self, data) => data.dispatch("pane.back"),
|
|
319
|
+
"toolbar:forward": (_self, data) => data.dispatch("pane.forward"),
|
|
320
|
+
"toolbar:up": (_self, data) => data.dispatch("pane.up"),
|
|
321
|
+
"toolbar:refresh": (_self, data) => data.dispatch("pane.reload"),
|
|
322
|
+
"toolbar:toggle-mode": (_self, data) => data.dispatch("view.toggleMode"),
|
|
323
|
+
"toolbar:toggle-preview": (_self, data) => data.dispatch("view.togglePreview"),
|
|
324
|
+
"toolbar:commands": (_self, data) => data.dispatch("panel.open"),
|
|
325
|
+
"toolbar:help": () => help.set(true),
|
|
326
|
+
"commandbar:run": (self, data, event) => {
|
|
327
|
+
if (event.data === "app.help") {
|
|
328
|
+
help.set(!help.value)
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
events["commandbar:run"](self, data, event)
|
|
332
|
+
},
|
|
148
333
|
})
|
|
149
|
-
.sub(appEvents({ quit: (self) => self.unmount?.() }))
|
|
150
334
|
}
|
|
151
335
|
|
|
152
336
|
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
|
|
@@ -13,20 +13,27 @@
|
|
|
13
13
|
// presentation bind the same source (`Breadcrumbs` uses `RowList.html`).
|
|
14
14
|
|
|
15
15
|
import { ui } from "select/ui"
|
|
16
|
+
import { unwrap } from "select/state/cells"
|
|
16
17
|
import bookmarkListHtml from "../components/BookmarkList.html" with { type: "text" }
|
|
17
18
|
import bookmarkRowHtml from "../components/BookmarkRow.html" with { type: "text" }
|
|
18
19
|
import commandBarHtml from "../components/CommandBar.html" with { type: "text" }
|
|
20
|
+
import commandArgRowHtml from "../components/CommandArgRow.html" with { type: "text" }
|
|
21
|
+
import commandPanelHtml from "../components/CommandPanel.html" with { type: "text" }
|
|
19
22
|
import commandItemHtml from "../components/CommandItem.html" with { type: "text" }
|
|
20
|
-
import commandPaletteHtml from "../components/CommandPalette.html" with { type: "text" }
|
|
21
23
|
import confirmHtml from "../components/Confirm.html" with { type: "text" }
|
|
22
24
|
import crumbHtml from "../components/Crumb.html" with { type: "text" }
|
|
23
25
|
import entryDetailHtml from "../components/EntryDetail.html" with { type: "text" }
|
|
24
26
|
import fileListHtml from "../components/FileList.html" with { type: "text" }
|
|
27
|
+
import filePickerHtml from "../components/FilePicker.html" with { type: "text" }
|
|
28
|
+
import filePickerRowHtml from "../components/FilePickerRow.html" with { type: "text" }
|
|
25
29
|
import filePreviewHtml from "../components/FilePreview.html" with { type: "text" }
|
|
26
30
|
import fileRowHtml from "../components/FileRow.html" with { type: "text" }
|
|
27
31
|
import filterBarHtml from "../components/FilterBar.html" with { type: "text" }
|
|
32
|
+
import helpHtml from "../components/Help.html" with { type: "text" }
|
|
33
|
+
import helpRowHtml from "../components/HelpRow.html" with { type: "text" }
|
|
28
34
|
import headerHtml from "./header.html" with { type: "text" }
|
|
29
|
-
import
|
|
35
|
+
import overlayListHtml from "../components/OverlayList.html" with { type: "text" }
|
|
36
|
+
import overlayRowHtml from "../components/OverlayRow.html" with { type: "text" }
|
|
30
37
|
import rowListHtml from "../components/RowList.html" with { type: "text" }
|
|
31
38
|
import summaryHtml from "../components/Summary.html" with { type: "text" }
|
|
32
39
|
|
|
@@ -37,67 +44,75 @@ import {
|
|
|
37
44
|
bookmarkRows,
|
|
38
45
|
breadcrumbsBehaviors,
|
|
39
46
|
commandBarBehaviors,
|
|
47
|
+
commandBarFit,
|
|
48
|
+
commandBarMarksItem,
|
|
49
|
+
commandArgRowBehaviors,
|
|
40
50
|
commandItemBehaviors,
|
|
41
51
|
commandItemRows,
|
|
42
|
-
|
|
52
|
+
commandPanelBehaviors,
|
|
43
53
|
confirmBehaviors,
|
|
44
54
|
crumbBehaviors,
|
|
45
55
|
crumbRows,
|
|
46
56
|
entryDetailBehaviors,
|
|
47
57
|
fileListBehaviors,
|
|
58
|
+
filePickerBehaviors,
|
|
59
|
+
filePickerRowBehaviors,
|
|
60
|
+
filePickerRows,
|
|
48
61
|
filePreviewBehaviors,
|
|
49
62
|
fileRowBehaviors,
|
|
50
63
|
fileRows,
|
|
51
64
|
filterBarBehaviors,
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
helpBehaviors,
|
|
66
|
+
helpRowBehaviors,
|
|
67
|
+
helpRows,
|
|
68
|
+
overlayListBehaviors,
|
|
69
|
+
overlayRowBehaviors,
|
|
70
|
+
overlayRows,
|
|
71
|
+
panelArgRows,
|
|
72
|
+
panelOpRows,
|
|
73
|
+
panelRunRows,
|
|
54
74
|
summaryBehaviors,
|
|
55
75
|
} from "../components/index.js"
|
|
56
76
|
import { createBehaviors as headerBehaviors } from "./header.js"
|
|
77
|
+
import { columnWidth } from "./effects.js"
|
|
57
78
|
import { webTheme } from "./theme.js"
|
|
58
79
|
|
|
59
80
|
let registered = false
|
|
60
81
|
|
|
61
|
-
//
|
|
62
|
-
|
|
82
|
+
// Function: paneWidth
|
|
83
|
+
// Reading the injected width cell keeps fitting reactive on pure resizes.
|
|
84
|
+
function paneWidth(self, data) {
|
|
85
|
+
const width = unwrap(data.width) ?? 0
|
|
86
|
+
return columnWidth(self.ref.box) || width
|
|
87
|
+
}
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// 0 when the DOM cannot measure (happy-dom, no layout), disabling compression.
|
|
67
|
-
// ponytail: measured per render; a pure viewport resize waits for the next one.
|
|
68
|
-
function columnWidth(el) {
|
|
69
|
-
const px = el?.clientWidth || el?.ownerDocument?.defaultView?.innerWidth || 0
|
|
70
|
-
if (!px) return 0
|
|
71
|
-
const view = el.ownerDocument?.defaultView
|
|
72
|
-
if (!view) return 0
|
|
73
|
-
if (!charWidth) {
|
|
74
|
-
try {
|
|
75
|
-
const ctx = el.ownerDocument.createElement("canvas").getContext("2d")
|
|
76
|
-
const style = view.getComputedStyle(el)
|
|
77
|
-
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`
|
|
78
|
-
charWidth = ctx.measureText("0").width || 0
|
|
79
|
-
} catch {
|
|
80
|
-
return 0
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return charWidth ? Math.floor(px / charWidth) : 0
|
|
89
|
+
function elide(text, width) {
|
|
90
|
+
return width > 0 && text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// Function: createComponents
|
|
87
94
|
// Binds every shared component to its `.html` presentation.
|
|
88
95
|
function createComponents() {
|
|
89
96
|
const commandItem = bind(ui, "CommandItem", commandItemHtml, commandItemBehaviors(webTheme))
|
|
97
|
+
// Keep command namespace markers textual; the bundled Nerd face is reserved
|
|
98
|
+
// for filesystem names and paths.
|
|
99
|
+
const commandArgRow = bind(ui, "CommandArgRow", commandArgRowHtml, commandArgRowBehaviors({
|
|
100
|
+
...webTheme,
|
|
101
|
+
icons: false,
|
|
102
|
+
}))
|
|
90
103
|
const crumb = bind(ui, "Crumb", crumbHtml, crumbBehaviors(webTheme))
|
|
91
104
|
const fileRow = bind(ui, "FileRow", fileRowHtml, fileRowBehaviors(webTheme))
|
|
105
|
+
const filePickerRow = bind(ui, "FilePickerRow", filePickerRowHtml, filePickerRowBehaviors(webTheme))
|
|
92
106
|
const filterBar = bind(ui, "FilterBar", filterBarHtml, filterBarBehaviors(webTheme))
|
|
93
|
-
const paletteRow = bind(ui, "PaletteRow", paletteRowHtml, paletteRowBehaviors(webTheme))
|
|
94
107
|
const bookmarkRow = bind(ui, "BookmarkRow", bookmarkRowHtml, bookmarkRowBehaviors(webTheme))
|
|
108
|
+
const overlayRow = bind(ui, "OverlayRow", overlayRowHtml, overlayRowBehaviors(webTheme))
|
|
109
|
+
const helpRow = bind(ui, "HelpRow", helpRowHtml, helpRowBehaviors({ theme: webTheme }))
|
|
95
110
|
return {
|
|
96
111
|
BookmarkRow: bookmarkRow,
|
|
97
112
|
CommandItem: commandItem,
|
|
113
|
+
CommandArgRow: commandArgRow,
|
|
98
114
|
Crumb: crumb,
|
|
99
115
|
FileRow: fileRow,
|
|
100
|
-
PaletteRow: paletteRow,
|
|
101
116
|
Breadcrumbs: bind(ui, "Breadcrumbs", rowListHtml, breadcrumbsBehaviors({
|
|
102
117
|
...webTheme,
|
|
103
118
|
listClass: "breadcrumbs",
|
|
@@ -110,25 +125,50 @@ function createComponents() {
|
|
|
110
125
|
CommandBar: bind(ui, "CommandBar", commandBarHtml, commandBarBehaviors({
|
|
111
126
|
...webTheme,
|
|
112
127
|
listClass: "command-bar-keys",
|
|
113
|
-
rows:
|
|
128
|
+
rows: (self, data) => {
|
|
129
|
+
const viewport = unwrap(data.uiSize)
|
|
130
|
+
const viewportWidth = Math.floor(viewport?.width ?? 0)
|
|
131
|
+
// The measured box is exact but still shows the previous width on
|
|
132
|
+
// the frame a resize arrives; prefer the injected viewport then.
|
|
133
|
+
const width = viewportWidth > 0 ? viewportWidth - 1 : columnWidth(self.ref.box)
|
|
134
|
+
const marks = commandBarMarksItem(data)
|
|
135
|
+
const items = unwrap(data.items) ?? []
|
|
136
|
+
return commandItemRows(commandItem)(self, {
|
|
137
|
+
...data,
|
|
138
|
+
items: commandBarFit(marks ? [...items, marks] : items, width),
|
|
139
|
+
})
|
|
140
|
+
},
|
|
114
141
|
})),
|
|
115
|
-
|
|
142
|
+
CommandPanel: bind(ui, "CommandPanel", commandPanelHtml, commandPanelBehaviors({
|
|
116
143
|
...webTheme,
|
|
117
|
-
|
|
144
|
+
opRows: panelOpRows(commandArgRow),
|
|
145
|
+
argRows: panelArgRows(commandArgRow),
|
|
146
|
+
runRows: panelRunRows(commandArgRow),
|
|
118
147
|
})),
|
|
119
148
|
Confirm: bind(ui, "Confirm", confirmHtml, confirmBehaviors(webTheme)),
|
|
120
|
-
EntryDetail: bind(ui, "EntryDetail", entryDetailHtml, entryDetailBehaviors(webTheme)),
|
|
149
|
+
EntryDetail: bind(ui, "EntryDetail", entryDetailHtml, entryDetailBehaviors({ ...webTheme, width: paneWidth, elide })),
|
|
121
150
|
FileList: bind(ui, "FileList", fileListHtml, fileListBehaviors({
|
|
122
151
|
...webTheme,
|
|
123
152
|
rows: fileRows(fileRow),
|
|
124
153
|
})),
|
|
125
|
-
|
|
154
|
+
FilePicker: bind(ui, "FilePicker", filePickerHtml, filePickerBehaviors({
|
|
155
|
+
...webTheme,
|
|
156
|
+
rows: filePickerRows(filePickerRow),
|
|
157
|
+
})),
|
|
158
|
+
FilePreview: bind(ui, "FilePreview", filePreviewHtml, filePreviewBehaviors({
|
|
159
|
+
...webTheme,
|
|
160
|
+
// Open/edit/run are TUI-only: the browser says so instead of
|
|
161
|
+
// advertising keys it cannot honour.
|
|
162
|
+
actions: () => " Open / edit are unavailable in the browser",
|
|
163
|
+
})),
|
|
126
164
|
FilterBar: filterBar,
|
|
127
|
-
|
|
165
|
+
Help: bind(ui, "Help", helpHtml, helpBehaviors({ theme: webTheme, rows: helpRows(helpRow) })),
|
|
166
|
+
Header: bind(ui, "Header", headerHtml, headerBehaviors(webTheme)),
|
|
167
|
+
OverlayList: bind(ui, "OverlayList", overlayListHtml, overlayListBehaviors({
|
|
128
168
|
...webTheme,
|
|
129
|
-
|
|
169
|
+
rows: overlayRows(overlayRow),
|
|
130
170
|
})),
|
|
131
|
-
Summary: bind(ui, "Summary", summaryHtml, summaryBehaviors(webTheme)),
|
|
171
|
+
Summary: bind(ui, "Summary", summaryHtml, summaryBehaviors({ ...webTheme, width: paneWidth, elide })),
|
|
132
172
|
}
|
|
133
173
|
}
|
|
134
174
|
|
|
@@ -143,13 +183,16 @@ function registerWebComponents() {
|
|
|
143
183
|
ui.format("Breadcrumbs", components.Breadcrumbs)
|
|
144
184
|
ui.format("BookmarkList", components.BookmarkList)
|
|
145
185
|
ui.format("CommandBar", components.CommandBar)
|
|
146
|
-
ui.format("
|
|
186
|
+
ui.format("CommandPanel", components.CommandPanel)
|
|
147
187
|
ui.format("Confirm", components.Confirm)
|
|
148
188
|
ui.format("EntryDetail", components.EntryDetail)
|
|
149
189
|
ui.format("FileList", components.FileList)
|
|
190
|
+
ui.format("FilePicker", components.FilePicker)
|
|
150
191
|
ui.format("FilePreview", components.FilePreview)
|
|
151
192
|
ui.format("FilterBar", components.FilterBar)
|
|
152
193
|
ui.format("Header", components.Header)
|
|
194
|
+
ui.format("Help", components.Help)
|
|
195
|
+
ui.format("OverlayList", components.OverlayList)
|
|
153
196
|
ui.format("Summary", components.Summary)
|
|
154
197
|
return components
|
|
155
198
|
}
|