@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-13
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/web/effects
|
|
7
|
+
// DOM effects owned by the mounted App: viewport measurement, modal focus,
|
|
8
|
+
// default-key trapping and keeping the keyboard cursor visible. select.js
|
|
9
|
+
// renders without any of this, so the web binding owns it explicitly.
|
|
10
|
+
|
|
11
|
+
import { effect } from "select/state/cells"
|
|
12
|
+
|
|
13
|
+
const canvases = new WeakMap()
|
|
14
|
+
|
|
15
|
+
function isToolbarTarget(target) {
|
|
16
|
+
return Boolean(target?.closest?.(".desktop-toolbar"))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Function: columnWidth
|
|
20
|
+
// Available monospace columns, or zero in a DOM without layout (happy-dom).
|
|
21
|
+
function columnWidth(el) {
|
|
22
|
+
if (!el?.clientWidth) return 0
|
|
23
|
+
const doc = el.ownerDocument
|
|
24
|
+
const view = doc.defaultView
|
|
25
|
+
let canvas = canvases.get(doc)
|
|
26
|
+
if (!canvas) {
|
|
27
|
+
canvas = doc.createElement("canvas")
|
|
28
|
+
canvases.set(doc, canvas)
|
|
29
|
+
}
|
|
30
|
+
const ctx = canvas.getContext("2d")
|
|
31
|
+
if (!ctx) return 0
|
|
32
|
+
const style = view.getComputedStyle(el)
|
|
33
|
+
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`
|
|
34
|
+
const width = ctx.measureText("0").width
|
|
35
|
+
const padding = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0)
|
|
36
|
+
return width ? Math.floor((el.clientWidth - padding) / width) : 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Function: keepVisible
|
|
40
|
+
// Scroll only the row's own list, never the page or an ancestor overlay.
|
|
41
|
+
function keepVisible(list, row) {
|
|
42
|
+
if (!list?.clientHeight || !row) return
|
|
43
|
+
const listRect = list.getBoundingClientRect()
|
|
44
|
+
const rowRect = row.getBoundingClientRect()
|
|
45
|
+
if (rowRect.top < listRect.top) list.scrollTop -= listRect.top - rowRect.top
|
|
46
|
+
else if (rowRect.bottom > listRect.bottom) list.scrollTop += rowRect.bottom - listRect.bottom
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Function: previewBodies
|
|
50
|
+
// The scrollable preview bodies in `root`: the inspector, the maximized
|
|
51
|
+
// preview and the viewer each reuse one text/card body.
|
|
52
|
+
function previewBodies(root) {
|
|
53
|
+
return root?.querySelectorAll?.(".preview-text, .preview-card") ?? []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Function: resetPreviewScroll
|
|
57
|
+
// Send the preview body back to the top. The inspector, the maximized preview
|
|
58
|
+
// and the viewer each reuse one scrollable body, so a scroll offset would
|
|
59
|
+
// otherwise carry over to the next file.
|
|
60
|
+
function resetPreviewScroll(root) {
|
|
61
|
+
for (const body of previewBodies(root)) {
|
|
62
|
+
body.scrollTop = 0
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Function: applyPreviewScroll
|
|
67
|
+
// Apply a full-preview scroll request from the App's arrow routing. The DOM
|
|
68
|
+
// clamps `scrollTop` against the visible body, so the edges need no guard.
|
|
69
|
+
function applyPreviewScroll(root, delta) {
|
|
70
|
+
if (!delta) return
|
|
71
|
+
for (const body of previewBodies(root)) {
|
|
72
|
+
body.scrollTop = Math.max(0, body.scrollTop + delta)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Function: connectEffects
|
|
77
|
+
// Returns a frame scheduler and teardown; the App registers teardown with
|
|
78
|
+
// instance.effect(), so remounting leaves no listeners or observers behind.
|
|
79
|
+
function connectEffects(session, root, { size, help, viewer }) {
|
|
80
|
+
// Select initially clones into an inert template document; the live window
|
|
81
|
+
// is available before mount even though ownerDocument.defaultView is not.
|
|
82
|
+
const view = root.ownerDocument.defaultView ?? globalThis.window
|
|
83
|
+
const doc = view.document
|
|
84
|
+
let frame = null
|
|
85
|
+
let retries = 0
|
|
86
|
+
let disposed = false
|
|
87
|
+
// The viewer overlay is a modal at any width (Enter opens it on a file
|
|
88
|
+
// that cannot be edited); it owns focus while open.
|
|
89
|
+
const viewerOpen = () => Boolean(viewer.value)
|
|
90
|
+
const modalOpen = () => Boolean(session.confirm.value || help.value || viewerOpen() || session.panelOpen.value || session.bookmarkOpen.value)
|
|
91
|
+
const focusTarget = () => {
|
|
92
|
+
if (session.confirm.value) return root.querySelector(".confirm")
|
|
93
|
+
if (help.value) return root.querySelector(".help")
|
|
94
|
+
if (viewerOpen()) return root.querySelector(".viewer")
|
|
95
|
+
if (session.panelOpen.value) {
|
|
96
|
+
if (session.pickerOpen.value) {
|
|
97
|
+
return session.pickerFocus.value === "path"
|
|
98
|
+
? root.querySelector(".panel-picker-path")
|
|
99
|
+
: root.querySelector(".command-panel")
|
|
100
|
+
}
|
|
101
|
+
if (session.panelFocus.value === "ops") return root.querySelector(".panel-query")
|
|
102
|
+
return root.querySelector(session.panelTextual.value ? ".panel-value" : ".command-panel")
|
|
103
|
+
}
|
|
104
|
+
if (session.bookmarkOpen.value) return root.querySelector(".bookmark-query")
|
|
105
|
+
if (session.filtering.value) return root.querySelector(`.pane-${session.filtering.value} .filter-input`)
|
|
106
|
+
return root
|
|
107
|
+
}
|
|
108
|
+
const update = () => {
|
|
109
|
+
frame = null
|
|
110
|
+
if (disposed || !root.isConnected) return
|
|
111
|
+
const width = columnWidth(root)
|
|
112
|
+
const lineHeight = Number.parseFloat(view.getComputedStyle(root).lineHeight)
|
|
113
|
+
const height = lineHeight ? Math.floor(root.clientHeight / lineHeight) : 0
|
|
114
|
+
if (width && height && (size.value.width !== width || size.value.height !== height)) size.set({ width, height })
|
|
115
|
+
// A modal's field mounts on a later frame than the flag that opens it;
|
|
116
|
+
// retry a few frames before giving up so focus lands on the overlay
|
|
117
|
+
// instead of the root.
|
|
118
|
+
const target = focusTarget()
|
|
119
|
+
if (modalOpen() && !target && retries < 8) {
|
|
120
|
+
retries += 1
|
|
121
|
+
schedule()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
retries = 0
|
|
125
|
+
// Do not steal focus as Tab moves between native toolbar buttons.
|
|
126
|
+
const toolbarFocused = !modalOpen() && !session.filtering.value &&
|
|
127
|
+
root.contains(doc.activeElement) && isToolbarTarget(doc.activeElement)
|
|
128
|
+
const focusable = toolbarFocused ? doc.activeElement : target ?? root
|
|
129
|
+
if (focusable !== doc.activeElement) focusable.focus({ preventScroll: true })
|
|
130
|
+
for (const list of root.querySelectorAll(".file-list, .bookmark-rows, .panel-ops, .panel-args")) {
|
|
131
|
+
keepVisible(list, list.querySelector(".is-current, .is-selected, .is-focused"))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const schedule = () => {
|
|
135
|
+
// Root renders queue child renders on the next frame. Wait past those
|
|
136
|
+
// before focusing a newly mounted field or measuring a changed layout.
|
|
137
|
+
if (!disposed && frame === null) frame = view.requestAnimationFrame(() => {
|
|
138
|
+
frame = view.requestAnimationFrame(update)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
const trapKeys = (event) => {
|
|
142
|
+
// Reload remains disabled even when native controls own the other keys.
|
|
143
|
+
// F5 now refreshes the listing too, so it must not reload the page.
|
|
144
|
+
if ((event.ctrlKey && event.key.toLowerCase() === "r") || event.key === "F5") {
|
|
145
|
+
event.preventDefault()
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (isToolbarTarget(event.target)) return
|
|
149
|
+
const input = event.target?.tagName === "INPUT" || event.target?.tagName === "TEXTAREA"
|
|
150
|
+
// Editing keeps native text/caret keys except the ones the shared
|
|
151
|
+
// controller explicitly owns (vertical arrows, Enter and Tab).
|
|
152
|
+
if (input && [" ", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"].includes(event.key)) return
|
|
153
|
+
// The bookmark filter edits its query like any input; once it is empty,
|
|
154
|
+
// Backspace removes the selected bookmark instead. Delete always does.
|
|
155
|
+
const bookmarkFilterEmpty = session.bookmarkOpen.value && !`${session.bookmarkQuery?.value ?? ""}`.trim()
|
|
156
|
+
if (input && event.key === "Backspace" && !bookmarkFilterEmpty) return
|
|
157
|
+
if (session.bookmarkOpen.value && event.key === "Delete") event.preventDefault()
|
|
158
|
+
if ([" ", "Backspace", "Enter", "Tab", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", "Escape"].includes(event.key)) event.preventDefault()
|
|
159
|
+
}
|
|
160
|
+
const focusIn = (event) => {
|
|
161
|
+
if (modalOpen() && event.target !== focusTarget()) schedule()
|
|
162
|
+
}
|
|
163
|
+
root.addEventListener("keydown", trapKeys)
|
|
164
|
+
doc.addEventListener("focusin", focusIn)
|
|
165
|
+
root.addEventListener("focusout", schedule)
|
|
166
|
+
view.addEventListener("resize", schedule)
|
|
167
|
+
const observer = view.ResizeObserver ? new view.ResizeObserver(schedule) : null
|
|
168
|
+
observer?.observe(root)
|
|
169
|
+
// Pane proportions also change when the inspector is toggled.
|
|
170
|
+
for (const pane of root.querySelectorAll(".pane")) observer?.observe(pane)
|
|
171
|
+
// Modal state can change before the initial focus frame runs (for example,
|
|
172
|
+
// the user opens the panel immediately after mount). Watch the focus-driving
|
|
173
|
+
// cells directly so that initial layout work cannot swallow that handoff.
|
|
174
|
+
const stopFocusWatch = effect(
|
|
175
|
+
[
|
|
176
|
+
session.panelOpen,
|
|
177
|
+
session.panelFocus,
|
|
178
|
+
session.panelOpIndex,
|
|
179
|
+
session.panelRowIndex,
|
|
180
|
+
session.panelTextual,
|
|
181
|
+
session.pickerOpen,
|
|
182
|
+
session.bookmarkOpen,
|
|
183
|
+
session.filtering,
|
|
184
|
+
session.confirm,
|
|
185
|
+
session.overlay,
|
|
186
|
+
help,
|
|
187
|
+
viewer,
|
|
188
|
+
],
|
|
189
|
+
schedule,
|
|
190
|
+
)
|
|
191
|
+
// The preview target and its payload both change as the cursor moves:
|
|
192
|
+
// reset on either so a new file starts at the top even when its text
|
|
193
|
+
// arrives asynchronously.
|
|
194
|
+
const previewInputs = [session.previewEntry, session.preview].filter((value) => value?.isReactive)
|
|
195
|
+
const stopPreviewWatch = previewInputs.length
|
|
196
|
+
? effect(previewInputs, () => resetPreviewScroll(root))
|
|
197
|
+
: null
|
|
198
|
+
// Full-preview arrow scrolling: the shared controller publishes a delta,
|
|
199
|
+
// the DOM body applies it to its own measured viewport.
|
|
200
|
+
const stopScrollWatch = session.previewScroll?.isReactive
|
|
201
|
+
? effect([session.previewScroll], ([request]) => applyPreviewScroll(root, request?.delta ?? 0))
|
|
202
|
+
: null
|
|
203
|
+
schedule()
|
|
204
|
+
return {
|
|
205
|
+
schedule,
|
|
206
|
+
dispose() {
|
|
207
|
+
disposed = true
|
|
208
|
+
stopFocusWatch?.()
|
|
209
|
+
stopPreviewWatch?.()
|
|
210
|
+
stopScrollWatch?.()
|
|
211
|
+
if (frame !== null) view.cancelAnimationFrame(frame)
|
|
212
|
+
observer?.disconnect()
|
|
213
|
+
root.removeEventListener("keydown", trapKeys)
|
|
214
|
+
root.removeEventListener("focusout", schedule)
|
|
215
|
+
doc.removeEventListener("focusin", focusIn)
|
|
216
|
+
view.removeEventListener("resize", schedule)
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { applyPreviewScroll, columnWidth, connectEffects, isToolbarTarget, keepVisible, resetPreviewScroll }
|
|
222
|
+
|
|
223
|
+
// EOF
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<header class="app-header" ref="box"><span class="brand-tiny">Tiny</span><span class="brand-cmd">Cmd</span><span out="title"></span></header>
|
|
1
|
+
<header class="app-header" ref="box"><span class="brand-tiny">Tiny</span><span class="brand-cmd">Cmd</span><span out="title"></span><span class="app-header-status"><span class="app-header-mode" out:class="modeClass" on:click="!view:toggleMode"><svg class="header-mode-icon ui-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use href="#lucide-columns-2"></use></svg><span out="mode"></span></span><span out="statusSuffix"></span></span></header>
|
|
@@ -4,32 +4,36 @@
|
|
|
4
4
|
// Created: 2026-09-27
|
|
5
5
|
|
|
6
6
|
// Module: tcmd/web/header
|
|
7
|
-
// Header: the application
|
|
8
|
-
//
|
|
7
|
+
// Header: the slim application identity bar, showing `TinyCmd - HOST` and,
|
|
8
|
+
// right-aligned, the view mode and version (`explorer · 0.1.3`).
|
|
9
9
|
// The brand renders as static spans — `Tiny` with a blue CSS gradient, `Cmd`
|
|
10
10
|
// with a brighter orange one (both kept in sync with `tui/theme.js`) — so the
|
|
11
|
-
// `title` behavior carries only the host
|
|
11
|
+
// `title` behavior carries only the host tail (` - HOST`), `mode`/`modeClass`
|
|
12
|
+
// the gradiented mode word and `statusSuffix` the version that follows it.
|
|
12
13
|
//
|
|
13
|
-
// Environment: `textHeadingFg
|
|
14
|
-
// columns (renderer-specific; 0/omitted disables compression).
|
|
14
|
+
// Environment: `textHeadingFg`.
|
|
15
15
|
|
|
16
16
|
import { unwrap } from "select/state/cells"
|
|
17
|
-
import {
|
|
17
|
+
import { modeWord, versionTrail } from "../components/Frame.js"
|
|
18
18
|
|
|
19
19
|
// Constant: BRAND
|
|
20
|
-
// The brand word the static header spans render
|
|
21
|
-
// the tail doesn't repeat it.
|
|
20
|
+
// The brand word the static header spans render.
|
|
22
21
|
const BRAND = "TinyCmd"
|
|
23
22
|
|
|
24
23
|
// Function: createBehaviors
|
|
25
24
|
// Builds the Header behavior map for the given renderer environment.
|
|
26
25
|
function createBehaviors(env) {
|
|
27
26
|
return {
|
|
28
|
-
title: (
|
|
29
|
-
const
|
|
30
|
-
return
|
|
27
|
+
title: (_self, data) => {
|
|
28
|
+
const host = `${unwrap(data.host) ?? ""}`.trim()
|
|
29
|
+
return host ? ` - ${host}` : ""
|
|
31
30
|
},
|
|
32
31
|
titleFg: () => env.textHeadingFg,
|
|
32
|
+
// The mode word picks the brand gradient it shimmers along: explorer
|
|
33
|
+
// the Tiny blues, commander the Cmd oranges (see `.app-header-mode`).
|
|
34
|
+
mode: (_self, data) => modeWord(unwrap(data.mode)),
|
|
35
|
+
modeClass: (_self, data) => `mode-${modeWord(unwrap(data.mode))}`,
|
|
36
|
+
statusSuffix: () => versionTrail(),
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
package/src/js/tcmd/web/http.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// into `index.html`; when the page is not served by `bun run web:serve`, the
|
|
15
15
|
// API is unreachable and the factory fails with a hint.
|
|
16
16
|
|
|
17
|
+
import { decodeBase64 } from "../core/base64.js"
|
|
18
|
+
|
|
17
19
|
// Function: apiToken
|
|
18
20
|
// Token injected into the host page, or null when absent/placeholder.
|
|
19
21
|
function apiToken(doc = globalThis.document) {
|
|
@@ -59,13 +61,24 @@ async function createHttpFs({ base = "/api", token, fetch: doFetch = globalThis.
|
|
|
59
61
|
return {
|
|
60
62
|
readDir: (path) => request("GET", "readDir", { params: { path } }),
|
|
61
63
|
stat: (path) => request("GET", "stat", { params: { path } }),
|
|
64
|
+
realpath: (path) => request("GET", "realpath", { params: { path } }),
|
|
62
65
|
isDir: async (path) => Boolean((await request("GET", "stat", { params: { path } }))?.isDir),
|
|
63
66
|
readTextHead: (path, maxBytes) => request("GET", "readTextHead", { params: { path, maxBytes } }),
|
|
67
|
+
readBytes: async (path, maxBytes) =>
|
|
68
|
+
decodeBase64((await request("GET", "readBytes", { params: { path, maxBytes } })).base64),
|
|
64
69
|
du: (path) => request("GET", "du", { params: { path } }),
|
|
65
70
|
disk: (path) => request("GET", "disk", { params: { path } }),
|
|
66
71
|
copy: (from, to) => request("POST", "copy", { body: { from, to } }),
|
|
67
72
|
move: (from, to) => request("POST", "move", { body: { from, to } }),
|
|
73
|
+
hardlink: (from, to) => request("POST", "hardlink", { body: { from, to } }),
|
|
74
|
+
symlink: (target, link) => request("POST", "symlink", { body: { target, link } }),
|
|
75
|
+
chmod: (path, mode) => request("POST", "chmod", { body: { path, mode } }),
|
|
76
|
+
chown: (path, owner) => request("POST", "chown", { body: { path, owner } }),
|
|
77
|
+
unlink: (path) => request("POST", "unlink", { body: { path } }),
|
|
78
|
+
mkdir: (path) => request("POST", "mkdir", { body: { path } }),
|
|
68
79
|
trash: (paths) => request("POST", "trash", { body: { paths } }),
|
|
80
|
+
trashList: () => request("GET", "trashList"),
|
|
81
|
+
restore: (paths) => request("POST", "restore", { body: { paths } }),
|
|
69
82
|
erase: (paths) => request("POST", "erase", { body: { paths } }),
|
|
70
83
|
touch: (path) => request("POST", "touch", { body: { path } }),
|
|
71
84
|
home: () => info.home,
|
package/src/js/tcmd/web/index.js
CHANGED
|
@@ -12,8 +12,8 @@ import { createWebApp } from "./app.js"
|
|
|
12
12
|
|
|
13
13
|
// Function: main
|
|
14
14
|
// Mounts the web UI for `session` into `selector` (default "#app").
|
|
15
|
-
function main(session, selector = "#app") {
|
|
16
|
-
const App = createWebApp(session)
|
|
15
|
+
function main(session, selector = "#app", options = {}) {
|
|
16
|
+
const App = createWebApp(session, options)
|
|
17
17
|
return App.new().mount(selector)
|
|
18
18
|
}
|
|
19
19
|
|
package/src/js/tcmd/web/main.js
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
// preferred and HTTP is the fallback, so browsers without OPFS (or insecure
|
|
14
14
|
// origins) still work when the page is served by `bun run web:serve`.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// DOM effects (focus, key trapping, resize, scroll) live in `web/effects.js`
|
|
17
|
+
// and are wired by the App binding in `web/app.js`; bookmarks persist through
|
|
18
|
+
// `web/bookmarks.js`, namespaced by backend and API root.
|
|
19
19
|
//
|
|
20
20
|
// Built by `bun run web:build` (see package.json) into `public/main.js`.
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { createBookmarks } from "../core/bookmarks.js"
|
|
23
23
|
import { createNullOs } from "../core/os.js"
|
|
24
24
|
import { createPane } from "../core/pane.js"
|
|
25
25
|
import { createSession, load } from "../core/session.js"
|
|
@@ -27,64 +27,7 @@ import { apiToken, createHttpFs } from "./http.js"
|
|
|
27
27
|
import { main as mount } from "./index.js"
|
|
28
28
|
import { createOpfsFs } from "./opfs.js"
|
|
29
29
|
import { seedOpfs } from "./seed.js"
|
|
30
|
-
|
|
31
|
-
// Constant: APP_KEYS
|
|
32
|
-
// Keys with browser defaults that would fight the app keymap: focus traversal
|
|
33
|
-
// (Tab), caret/scroll arrows, page scroll (Space) and history Backspace.
|
|
34
|
-
const APP_KEYS = new Set([
|
|
35
|
-
" ",
|
|
36
|
-
"ArrowDown",
|
|
37
|
-
"ArrowLeft",
|
|
38
|
-
"ArrowRight",
|
|
39
|
-
"ArrowUp",
|
|
40
|
-
"Backspace",
|
|
41
|
-
"Enter",
|
|
42
|
-
"Tab",
|
|
43
|
-
])
|
|
44
|
-
|
|
45
|
-
// Function: trapKeys
|
|
46
|
-
// Keeps browser defaults out of the way while the app owns the keyboard. Space
|
|
47
|
-
// and Backspace stay untouched in text inputs so the palette query can be
|
|
48
|
-
// typed; the app routes every other listed key itself.
|
|
49
|
-
function trapKeys(root) {
|
|
50
|
-
root?.addEventListener("keydown", (event) => {
|
|
51
|
-
const inInput = event.target instanceof HTMLInputElement
|
|
52
|
-
if (inInput && (event.key === " " || event.key === "Backspace")) return
|
|
53
|
-
if (APP_KEYS.has(event.key)) event.preventDefault()
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Function: trackFocus
|
|
58
|
-
// The key handler lives on the app root, so focus must stay inside it. The
|
|
59
|
-
// palette input is focused while the palette is open (select.js does not), and
|
|
60
|
-
// the active pane's filter input while that pane is filtering; focus returns to
|
|
61
|
-
// the app root otherwise. Focus that leaves the app — a removed input, a click
|
|
62
|
-
// on the page background — is pulled back.
|
|
63
|
-
function trackFocus(session, root) {
|
|
64
|
-
if (!root) return
|
|
65
|
-
const focusInput = () => {
|
|
66
|
-
if (session.paletteOpen.value) return document.querySelector("#app .palette-input")
|
|
67
|
-
if (session.filtering.value) {
|
|
68
|
-
return document.querySelector(`#app .pane-${session.filtering.value} .filter-input`)
|
|
69
|
-
}
|
|
70
|
-
return null
|
|
71
|
-
}
|
|
72
|
-
const applyFocus = () => {
|
|
73
|
-
// select.js swaps the palette DOM on animation frames: wait for it.
|
|
74
|
-
requestAnimationFrame(() => {
|
|
75
|
-
const input = focusInput()
|
|
76
|
-
if (input) input.focus()
|
|
77
|
-
else if (!root.contains(document.activeElement)) root.focus()
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
effect([session.paletteOpen, session.filtering], applyFocus)
|
|
81
|
-
document.addEventListener("focusout", (event) => {
|
|
82
|
-
if (!session.paletteOpen.value && !session.filtering.value && event.relatedTarget && root.contains(event.relatedTarget)) {
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
applyFocus()
|
|
86
|
-
})
|
|
87
|
-
}
|
|
30
|
+
import { createWebBookmarkStore } from "./bookmarks.js"
|
|
88
31
|
|
|
89
32
|
// Function: createOpfsBackend
|
|
90
33
|
// OPFS adapter, probed and seeded. `readDir("/")` resolves the OPFS root, so
|
|
@@ -128,13 +71,10 @@ async function start() {
|
|
|
128
71
|
left,
|
|
129
72
|
right,
|
|
130
73
|
host: globalThis.location?.hostname ?? "",
|
|
74
|
+
bookmarks: createBookmarks({ store: createWebBookmarkStore({ mode, root: mode === "opfs" ? "/" : fs.root() }) }),
|
|
131
75
|
})
|
|
132
76
|
await load(session)
|
|
133
77
|
const instance = mount(session, "#app")
|
|
134
|
-
const root = document.querySelector("#app .app")
|
|
135
|
-
root?.focus()
|
|
136
|
-
trapKeys(root)
|
|
137
|
-
trackFocus(session, root)
|
|
138
78
|
document.title = `TinyCMD web (${mode})`
|
|
139
79
|
globalThis.tcmd = { fs, mode, session, instance }
|
|
140
80
|
} catch (error) {
|