@ssebastien/tcmd 0.1.4 → 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 +169 -58
- 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 +17 -5
- package/public/fonts/OFL.txt +93 -0
- package/public/licenses/LUCIDE.txt +43 -0
- package/public/main.js +5066 -1888
- package/public/style.css +660 -46
- package/scripts/build-desktop.js +31 -0
- package/src/js/tcmd/components/App.html +56 -6
- package/src/js/tcmd/components/App.js +233 -58
- package/src/js/tcmd/components/App.xml +27 -22
- package/src/js/tcmd/components/BookmarkList.html +4 -5
- package/src/js/tcmd/components/BookmarkList.js +14 -40
- 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/CommandArgRow.html +18 -7
- package/src/js/tcmd/components/CommandArgRow.js +100 -44
- package/src/js/tcmd/components/CommandArgRow.xml +39 -15
- package/src/js/tcmd/components/CommandBar.html +1 -1
- package/src/js/tcmd/components/CommandBar.js +31 -25
- package/src/js/tcmd/components/CommandBar.xml +2 -2
- package/src/js/tcmd/components/CommandItem.js +24 -25
- package/src/js/tcmd/components/CommandItem.xml +2 -2
- package/src/js/tcmd/components/CommandPanel.html +13 -13
- package/src/js/tcmd/components/CommandPanel.js +57 -33
- package/src/js/tcmd/components/CommandPanel.xml +30 -20
- package/src/js/tcmd/components/Confirm.html +2 -2
- package/src/js/tcmd/components/EntryDetail.js +21 -7
- 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 +1 -2
- package/src/js/tcmd/components/FilePreview.js +40 -4
- package/src/js/tcmd/components/FilePreview.xml +17 -10
- package/src/js/tcmd/components/FileRow.html +1 -0
- package/src/js/tcmd/components/FileRow.js +60 -9
- package/src/js/tcmd/components/Frame.js +57 -31
- package/src/js/tcmd/components/Help.html +12 -4
- package/src/js/tcmd/components/Help.js +198 -47
- 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/OverlayList.xml +13 -0
- 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/format.js +4 -23
- package/src/js/tcmd/components/icons.js +137 -0
- package/src/js/tcmd/components/index.js +8 -3
- package/src/js/tcmd/components/rows.js +149 -25
- 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 +612 -136
- 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 +81 -5
- package/src/js/tcmd/core/format.js +36 -0
- package/src/js/tcmd/core/fs.js +410 -53
- package/src/js/tcmd/core/journal.js +93 -0
- package/src/js/tcmd/core/keymap.js +39 -3
- package/src/js/tcmd/core/operations.js +735 -63
- package/src/js/tcmd/core/os.js +29 -3
- package/src/js/tcmd/core/pane.js +4 -2
- package/src/js/tcmd/core/panel.js +179 -93
- 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/session.js +286 -116
- 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/tui/app.js +111 -74
- package/src/js/tcmd/tui/catalogue.js +232 -23
- package/src/js/tcmd/tui/index.js +18 -4
- 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 +27 -2
- package/src/js/tcmd/vendor/bzip2.js +276 -0
- package/src/js/tcmd/web/app.js +114 -56
- package/src/js/tcmd/web/catalogue.js +41 -4
- package/src/js/tcmd/web/effects.js +94 -14
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/http.js +13 -0
- package/src/js/tcmd/web/index.js +2 -2
- package/src/js/tcmd/web/opfs.js +297 -30
- package/src/js/tcmd/web/seed.js +1 -1
- package/src/js/tcmd/web/server.js +118 -17
- package/src/js/tcmd/web/theme.js +3 -0
- package/tinyjs.json +16 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="panel-picker-pane" when="open" aria-label="File picker">
|
|
2
|
+
<div class="panel-picker">
|
|
3
|
+
<label class="panel-picker-line">
|
|
4
|
+
<svg class="panel-picker-icon ui-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use href="#lucide-folder-open"></use></svg>
|
|
5
|
+
<input class="panel-picker-path" type="text" inout="path" aria-label="Path" spellcheck="false" autocomplete="off" />
|
|
6
|
+
</label>
|
|
7
|
+
</div>
|
|
8
|
+
<ul class="panel-args panel-picker-rows" aria-label="Picker entries" out="rows"></ul>
|
|
9
|
+
<p class="panel-args-empty" when="empty" out="emptyText"></p>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-20
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FilePicker
|
|
7
|
+
// The command panel's file picker: an editable path line over a file-panel
|
|
8
|
+
// listing (`FilePickerRow`), with the `..` parent row kept at the top. Typing
|
|
9
|
+
// in the path narrows the listing (or enters a directory) through the session's
|
|
10
|
+
// path resolution; the row logic lives in `FilePickerRow.js`.
|
|
11
|
+
//
|
|
12
|
+
// Environment: `rows(self, data)` returning keyed `FilePickerRow`s and
|
|
13
|
+
// `scrollTop(self, data)` (TUI viewport tracking).
|
|
14
|
+
|
|
15
|
+
import { unwrap } from "select/state/cells"
|
|
16
|
+
|
|
17
|
+
// Function: createBehaviors
|
|
18
|
+
// Builds the FilePicker behavior map for the given renderer environment.
|
|
19
|
+
function createBehaviors(env) {
|
|
20
|
+
const open = (_self, data) => Boolean(unwrap(data.open))
|
|
21
|
+
const behaviors = {
|
|
22
|
+
open,
|
|
23
|
+
// The editable path owns the keyboard by default; Down/Tab hands it to
|
|
24
|
+
// the entry list.
|
|
25
|
+
pathFocused: (_self, data) => open(_self, data) && (unwrap(data.focus) ?? "path") === "path",
|
|
26
|
+
pathSign: () => "› ",
|
|
27
|
+
pathSignFg: () => env.accentFg,
|
|
28
|
+
// The parent row is a navigation row, not a match: an empty listing is
|
|
29
|
+
// an empty result even while `..` stays visible.
|
|
30
|
+
empty: (_self, data) =>
|
|
31
|
+
open(_self, data) && !(unwrap(data.rows) ?? []).some((row) => row.type === "entry"),
|
|
32
|
+
emptyText: () => " no matches",
|
|
33
|
+
emptyFg: () => env.textSecondaryFg,
|
|
34
|
+
rule: () => ["bottom"],
|
|
35
|
+
ruleFg: () => env.borderSecondaryFg,
|
|
36
|
+
borderFg: () => env.borderSecondaryFg,
|
|
37
|
+
}
|
|
38
|
+
if (env?.rows) behaviors.rows = (self, data) => env.rows(self, data)
|
|
39
|
+
if (env?.scrollTop) behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
|
|
40
|
+
return behaviors
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { createBehaviors }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<box when="open" flexDirection="column" flexGrow="1" flexShrink="1" minHeight="0" paddingLeft="1" paddingRight="1" paddingBottom="1">
|
|
2
|
+
<box flexDirection="column" flexGrow="1" flexShrink="1" minHeight="0" border borderStyle="single" out:borderColor="borderFg">
|
|
3
|
+
<box flexDirection="row" height="1" paddingLeft="1" paddingRight="1" overflow="hidden">
|
|
4
|
+
<text out="pathSign" out:fg="pathSignFg" wrapMode="none"></text>
|
|
5
|
+
<input ref="path" inout="path" flexGrow="1" minWidth="0" height="1" out:focused="pathFocused"></input>
|
|
6
|
+
</box>
|
|
7
|
+
<box height="1" out:border="rule" out:borderColor="ruleFg"></box>
|
|
8
|
+
<scrollbox ref="list" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="scrollTop">
|
|
9
|
+
<box out="rows" flexDirection="column"></box>
|
|
10
|
+
</scrollbox>
|
|
11
|
+
<text when="empty" out="emptyText" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
|
|
12
|
+
</box>
|
|
13
|
+
</box>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<li class="file-row picker-row" out:class="rowClass" out:data-marked="rowMarked" out:data-active="rowActive" out:data-kind="rowKind" out:data-type="rowType" out:data-key="rowHref" on:click="key!panel:select">
|
|
2
|
+
<span class="marker" out="rowMarker"></span>
|
|
3
|
+
<svg class="file-row-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="rowIconHref"></use></svg>
|
|
4
|
+
<span class="name" out:class="rowNameClass" out="rowName"></span>
|
|
5
|
+
<span class="size" out="rowSize"></span>
|
|
6
|
+
</li>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-20
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/FilePickerRow
|
|
7
|
+
// One entry in the file picker. The row presentation deliberately mirrors the
|
|
8
|
+
// file panes (`FileRow`): marker, kind icon, name and right-aligned size. It
|
|
9
|
+
// only overrides the `..` parent row (no trailing slash) and its click event,
|
|
10
|
+
// which the command panel routes back through `panel:select`.
|
|
11
|
+
|
|
12
|
+
import { createBehaviors as fileRowBehaviors, rowName as fileRowName } from "./FileRow.js"
|
|
13
|
+
|
|
14
|
+
// Function: createBehaviors
|
|
15
|
+
// Builds the FilePickerRow behavior map from the shared FileRow behaviors.
|
|
16
|
+
function createBehaviors(env) {
|
|
17
|
+
const behaviors = fileRowBehaviors(env)
|
|
18
|
+
return {
|
|
19
|
+
...behaviors,
|
|
20
|
+
rowName: (_self, data) => (data.item?.isParent ? ".." : fileRowName(data.item)),
|
|
21
|
+
rowSize: (_self, data) => (data.item?.isParent ? "" : behaviors.rowSize?.(_self, data) ?? ""),
|
|
22
|
+
rowType: (_self, data) => (data.item?.isParent ? "parent" : data.item?.type ?? "entry"),
|
|
23
|
+
rowHref: (_self, data) => data.key ?? data.item?.path ?? "",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { createBehaviors }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="key!panel:select">
|
|
2
|
+
<text out="rowMarker" width="2" out:fg="rowMarkerFg"></text>
|
|
3
|
+
<box flexGrow="1" minWidth="0" overflow="hidden" flexDirection="row" columnGap="1">
|
|
4
|
+
<text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
|
|
5
|
+
<text out="rowName" out:fg="rowNameFg" wrapMode="none" truncate="true"></text>
|
|
6
|
+
</box>
|
|
7
|
+
<box width="9" justifyContent="flex-end">
|
|
8
|
+
<text out="rowSize" out:fg="rowSizeFg" wrapMode="none"></text>
|
|
9
|
+
</box>
|
|
10
|
+
</box>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
(breadcrumbs + cursorable listing) for a directory, so a previewed
|
|
3
3
|
directory reads as a pane. The body and the column share the frame, and
|
|
4
4
|
the whole column is clickable to take focus. -->
|
|
5
|
-
<aside class="preview" when="show" out:title="headerTitle" on:click="paneId!pane:activate">
|
|
5
|
+
<aside class="preview" when="show" out:title="headerTitle" on:click="paneId!pane:activate.">
|
|
6
6
|
<header when="showBody" out="headerTitle"></header>
|
|
7
7
|
<div class="pane-header" when="isDirPane">
|
|
8
8
|
<div out="dirCrumbsView|Breadcrumbs"></div>
|
|
@@ -12,5 +12,4 @@
|
|
|
12
12
|
<pre when="showCard" out="cardText" class="preview-card"></pre>
|
|
13
13
|
<pre when="showEntryCard" out="entryText" class="preview-card"></pre>
|
|
14
14
|
<pre when="showEmpty" out="emptyText" class="preview-card"></pre>
|
|
15
|
-
<footer class="preview-actions" out="actions"></footer>
|
|
16
15
|
</aside>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { unwrap } from "select/state/cells"
|
|
23
23
|
import { formatSize } from "./format.js"
|
|
24
|
-
import { previewSource } from "../core/
|
|
24
|
+
import { previewSource } from "../core/preview.js"
|
|
25
25
|
|
|
26
26
|
const KIND_LABELS = {
|
|
27
27
|
directory: "directory",
|
|
@@ -58,11 +58,13 @@ function hasText(preview) {
|
|
|
58
58
|
|
|
59
59
|
// Function: previewTitle
|
|
60
60
|
// "Filename <EXT>" style title; falls back to the kind label when the file has
|
|
61
|
-
// no extension.
|
|
61
|
+
// no extension. A decompressed preview adds the inner label, so
|
|
62
|
+
// `notes.txt.gz` reads `notes.txt.gz <GZ · TXT>`.
|
|
62
63
|
function previewTitle(preview) {
|
|
63
64
|
if (!preview) return "preview"
|
|
64
65
|
const badge = (preview.ext || KIND_LABELS[preview.kind] || "file").toUpperCase()
|
|
65
|
-
|
|
66
|
+
const inner = preview.compression && preview.innerExt ? ` · ${preview.innerExt.toUpperCase()}` : ""
|
|
67
|
+
return `${preview.title} <${badge}${inner}>`
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
// Function: entryCard
|
|
@@ -97,9 +99,13 @@ function headerTitle(preview, entry) {
|
|
|
97
99
|
if (entry) {
|
|
98
100
|
const source = previewSource(entry)
|
|
99
101
|
const badge = entry.isDir ? "DIR" : (source.ext || "file").toUpperCase()
|
|
102
|
+
const inner =
|
|
103
|
+
!entry.isDir && source.compression && source.innerExt
|
|
104
|
+
? ` · ${source.innerExt.toUpperCase()}`
|
|
105
|
+
: ""
|
|
100
106
|
const name = entry.isDir ? `${entry.name}/` : entry.name
|
|
101
107
|
const target = entry.isSymlink && entry.target ? ` -> ${entry.target}` : ""
|
|
102
|
-
return `${name}${target} <${badge}>`
|
|
108
|
+
return `${name}${target} <${badge}${inner}>`
|
|
103
109
|
}
|
|
104
110
|
return "inspector"
|
|
105
111
|
}
|
|
@@ -113,10 +119,32 @@ function createBehaviors(env) {
|
|
|
113
119
|
const directory = (_self, data) =>
|
|
114
120
|
isDirectoryPane(unwrap(data.preview), unwrap(data.entry))
|
|
115
121
|
const body = (_self, data) => !directory(_self, data)
|
|
122
|
+
// An image the renderer can actually decode: the card stays whenever the
|
|
123
|
+
// preview carries `stat` details, or the entry sits behind the cursor as a
|
|
124
|
+
// directory. `imageExts` is renderer knowledge (the terminal image path
|
|
125
|
+
// decodes the raster formats only), so it comes from the environment and
|
|
126
|
+
// the web — which has no image renderable — simply never matches.
|
|
127
|
+
const isImage = (_self, data) => {
|
|
128
|
+
const preview = unwrap(data.preview)
|
|
129
|
+
const entry = unwrap(data.entry)
|
|
130
|
+
if (preview?.kind !== "image" || preview.details || !entry?.path) return false
|
|
131
|
+
const ext = `${preview.ext ?? entry.ext ?? ""}`.toLowerCase()
|
|
132
|
+
return (env.imageExts ?? []).includes(ext)
|
|
133
|
+
}
|
|
116
134
|
return {
|
|
117
135
|
actions,
|
|
118
136
|
actionsFg: () => env.textSecondaryFg,
|
|
119
137
|
cardText: (_self, data) => cardText(unwrap(data.preview)),
|
|
138
|
+
// Tree-sitter filetype for the previewed text, resolved by the
|
|
139
|
+
// renderer (the web has no highlighter and supplies no `filetype`).
|
|
140
|
+
// A compressed preview highlights the name inside, so `.txt.gz`
|
|
141
|
+
// still lands on the text parser.
|
|
142
|
+
codeFiletype: (_self, data) => {
|
|
143
|
+
const entry = unwrap(data.entry)
|
|
144
|
+
if (!entry?.path) return undefined
|
|
145
|
+
const source = previewSource(entry)
|
|
146
|
+
return env.filetype?.(source.compression ? source.innerName : entry.path)
|
|
147
|
+
},
|
|
120
148
|
// The directory column: breadcrumbs over the inspector pane's listing,
|
|
121
149
|
// shaped like a pane header (a rule separates it from the body).
|
|
122
150
|
dirCrumbsView: (_self, data) => ({ segments: data.dirSegments, paneId: data.paneId }),
|
|
@@ -141,6 +169,10 @@ function createBehaviors(env) {
|
|
|
141
169
|
headerTitle: (_self, data) => headerTitle(unwrap(data.preview), unwrap(data.entry)),
|
|
142
170
|
isEmpty: (_self, data) => !unwrap(data.preview) && !unwrap(data.entry),
|
|
143
171
|
isDirPane: directory,
|
|
172
|
+
// The path the image renderable loads (symlinks read through, like the
|
|
173
|
+
// text body).
|
|
174
|
+
previewImageSource: (_self, data) =>
|
|
175
|
+
isImage(_self, data) ? unwrap(data.entry)?.path : undefined,
|
|
144
176
|
previewText: (_self, data) => unwrap(data.preview)?.text ?? "",
|
|
145
177
|
previewTitle: (_self, data) => previewTitle(unwrap(data.preview)),
|
|
146
178
|
previewTitleColor: () => env.textHeadingFg,
|
|
@@ -154,7 +186,11 @@ function createBehaviors(env) {
|
|
|
154
186
|
showEmpty: (_self, data) => body(_self, data) && !unwrap(data.preview) && !unwrap(data.entry),
|
|
155
187
|
showEntryCard: (_self, data) =>
|
|
156
188
|
body(_self, data) && !unwrap(data.preview) && Boolean(unwrap(data.entry)),
|
|
189
|
+
// The image takes the body on its own; when the format has no decoder
|
|
190
|
+
// (or the preview is a `stat` card) the scrollbox carries the body.
|
|
191
|
+
showImage: (_self, data) => body(_self, data) && isImage(_self, data),
|
|
157
192
|
showText: (_self, data) => body(_self, data) && hasText(unwrap(data.preview)),
|
|
193
|
+
showTextOrCard: (_self, data) => body(_self, data) && !isImage(_self, data),
|
|
158
194
|
textFg: () => env.textFg,
|
|
159
195
|
}
|
|
160
196
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<!-- The inspector: a card/text body for a file, and a directory column
|
|
2
2
|
(breadcrumbs + cursorable listing) for a directory, so a previewed
|
|
3
|
-
directory reads as a pane.
|
|
4
|
-
|
|
3
|
+
directory reads as a pane. A text body highlights through the `code`
|
|
4
|
+
tree-sitter renderable; an image draws through the `image` renderable's
|
|
5
|
+
terminal graphics protocol. The body and the column share the border, and
|
|
6
|
+
the whole column is clickable to take focus. -->
|
|
5
7
|
<box when="show" out:title="headerTitle" out:titleColor="previewTitleColor" border borderStyle="rounded"
|
|
6
8
|
flexDirection="column" flexGrow="1" overflow="hidden" on:click="paneId!pane:activate">
|
|
7
9
|
<box when="isDirPane" flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
@@ -12,12 +14,17 @@
|
|
|
12
14
|
<box when="isDirPane" height="1" out:border="headRule" out:borderColor="headRuleFg"></box>
|
|
13
15
|
<filelist when="isDirPane" in:entries="dirEntries" in:cursor="dirCursor" in:marked="dirMarked"
|
|
14
16
|
in:active="dirActive" in:paneId="paneId" in:loading="dirLoading"></filelist>
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
<box when="showBody" flexGrow="1" position="relative" overflow="hidden">
|
|
18
|
+
<scrollbox when="showTextOrCard" ref="body" flexGrow="1">
|
|
19
|
+
<code when="showText" out="previewText" out:fg="textFg" out:filetype="codeFiletype"></code>
|
|
20
|
+
<text when="showCard" out="cardText" out:fg="dimFg"></text>
|
|
21
|
+
<text when="showEntryCard" out="entryText" out:fg="dimFg"></text>
|
|
22
|
+
<text when="showEmpty" out="emptyText" out:fg="dimFg"></text>
|
|
23
|
+
</scrollbox>
|
|
24
|
+
<!-- Image files draw through the terminal's graphics protocol
|
|
25
|
+
(Kitty/sixel, half-block fallback). Formats with no decoder
|
|
26
|
+
(`bmp`, `svg`) keep the metadata card instead. -->
|
|
27
|
+
<image when="showImage" position="absolute" top="0" left="0" width="100%" height="100%"
|
|
28
|
+
fit="fit" protocol="auto" out:source="previewImageSource"></image>
|
|
29
|
+
</box>
|
|
23
30
|
</box>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<li class="file-row" out:class="rowClass" out:data-marked="rowMarked" out:data-active="rowActive" out:data-kind="rowKind" on:click="paneId:paneId,index:index!list:select">
|
|
2
2
|
<span class="marker" out="rowMarker"></span>
|
|
3
|
+
<svg class="file-row-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="rowIconHref"></use></svg>
|
|
3
4
|
<span class="name" out:class="rowNameClass" out="rowName"></span>
|
|
4
5
|
<span class="size" out="rowSize"></span>
|
|
5
6
|
</li>
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
// name (`notes -> ../real/notes`, see `rowName`).
|
|
13
13
|
//
|
|
14
14
|
// Environment: `cursorBg`, `cursorInactiveBg`, `currentClass`,
|
|
15
|
-
// `accentSecondaryFg`, `accentFg`, `linkFg`, `dirFg`, `devFg`, `
|
|
16
|
-
// `textSecondaryFg`, `dirClass`, `
|
|
15
|
+
// `accentSecondaryFg`, `accentFg`, `linkFg`, `dirFg`, `devFg`, `execFg`,
|
|
16
|
+
// `readonlyFg`, `fileFg`, `textSecondaryFg`, `dirClass`, `execClass`,
|
|
17
|
+
// `readonlyClass`, `icons` (show Nerd Font icons).
|
|
17
18
|
|
|
18
19
|
import { unwrap } from "select/state/cells"
|
|
19
20
|
import { formatSize } from "./format.js"
|
|
20
|
-
import { iconFor } from "./icons.js"
|
|
21
|
+
import { iconFor, lucideIconFor } from "./icons.js"
|
|
21
22
|
|
|
22
23
|
// Function: rowName
|
|
23
24
|
// The entry name with the directory slash (the icon is its own slot, so it
|
|
@@ -37,6 +38,12 @@ function rowIcon(entry, icons) {
|
|
|
37
38
|
return icons ? iconFor(entry) : ""
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Function: rowIconHref
|
|
42
|
+
// The current-file SVG reference used by the browser presentation.
|
|
43
|
+
function rowIconHref(entry, icons) {
|
|
44
|
+
return icons ? `#lucide-${lucideIconFor(entry)}` : ""
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
// Function: rowSize
|
|
41
48
|
function rowSize(entry) {
|
|
42
49
|
if (!entry) return ""
|
|
@@ -80,21 +87,64 @@ function rowIsMarked(data) {
|
|
|
80
87
|
return isMarked(data.item, unwrap(data.marked))
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
// Function: hasMode
|
|
91
|
+
// Whether the entry carries POSIX mode bits. Backends that report no mode
|
|
92
|
+
// (OPFS, some HTTP hosts) leave it 0, so such entries never read as executable
|
|
93
|
+
// or read-only.
|
|
94
|
+
function hasMode(entry) {
|
|
95
|
+
return Number.isFinite(entry?.mode) && entry.mode > 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Function: isExecutable
|
|
99
|
+
// Whether the entry is a regular file with an execute bit set (any of the
|
|
100
|
+
// owner/group/other slots). Directories, symlinks and device nodes have their
|
|
101
|
+
// own colours and never read as executable here.
|
|
102
|
+
function isExecutable(entry) {
|
|
103
|
+
if (!entry || entry.isDir || entry.isSymlink || entry.isDevice) return false
|
|
104
|
+
return hasMode(entry) && (entry.mode & 0o111) !== 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Function: isReadonly
|
|
108
|
+
// Whether the entry is a regular file its owner cannot write (`mode` owner
|
|
109
|
+
// write bit clear). Directories, symlinks and device nodes are excluded, and
|
|
110
|
+
// the web backends, which report no mode, stay unaffected.
|
|
111
|
+
function isReadonly(entry) {
|
|
112
|
+
if (!entry || entry.isDir || entry.isSymlink || entry.isDevice) return false
|
|
113
|
+
return hasMode(entry) && (entry.mode & 0o200) === 0
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
// Function: nameFg
|
|
84
117
|
// The listing color for an entry: a symlink reads as a link, a directory as a
|
|
85
|
-
// directory, a device node as a device,
|
|
118
|
+
// directory, a device node as a device, an executable file green, a read-only
|
|
119
|
+
// file dimmer than a plain file, otherwise the plain file color.
|
|
86
120
|
// Symlinks win over directories, matching `dircolors` (a link to a directory
|
|
87
|
-
// renders as a link)
|
|
121
|
+
// renders as a link), and an executable wins over a read-only file, matching
|
|
122
|
+
// `ls --color` (an executable stays green even when it cannot be written).
|
|
88
123
|
function nameFg(item, env) {
|
|
89
124
|
if (item?.isSymlink) return env.linkFg
|
|
90
125
|
if (item?.isDir) return env.dirFg
|
|
91
126
|
if (item?.isDevice) return env.devFg
|
|
127
|
+
if (isExecutable(item)) return env.execFg
|
|
128
|
+
if (isReadonly(item)) return env.readonlyFg
|
|
92
129
|
return env.fileFg
|
|
93
130
|
}
|
|
94
131
|
|
|
132
|
+
// Function: nameClass
|
|
133
|
+
// The web state classes for an entry's name, mirroring `nameFg`: `dirClass`
|
|
134
|
+
// for a directory, else the executable/read-only class for a file, else null.
|
|
135
|
+
// The class tokens are absent on the TUI env, which colours through
|
|
136
|
+
// `rowNameFg` instead.
|
|
137
|
+
function nameClass(item, env) {
|
|
138
|
+
if (item?.isDir) return env.dirClass
|
|
139
|
+
if (isExecutable(item)) return env.execClass
|
|
140
|
+
if (isReadonly(item)) return env.readonlyClass
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
95
144
|
// Function: rowNameFg
|
|
96
145
|
// The name color for a row: a marked row reads orange (the selection signal),
|
|
97
|
-
// the cursor row the bright heading color, otherwise the entry's
|
|
146
|
+
// the cursor row the bright heading color, otherwise the entry's own color
|
|
147
|
+
// (kind plus executable/read-only state, see `nameFg`).
|
|
98
148
|
// Marks win over the cursor so a marked-and-current row still reads as
|
|
99
149
|
// marked, the same precedence the row marker follows.
|
|
100
150
|
function rowNameFg(data, env) {
|
|
@@ -124,19 +174,20 @@ function createBehaviors(env) {
|
|
|
124
174
|
rowKind: (_self, data) => data.item?.isSymlink ? "link" : data.item?.isDir ? "dir" : data.item?.isDevice ? "dev" : "file",
|
|
125
175
|
// The icon is its own slot so it can sit dimmer than the name; it
|
|
126
176
|
// collapses when the environment keeps rows icon-free.
|
|
127
|
-
hasIcon: () => Boolean(env.icons),
|
|
177
|
+
hasIcon: () => Boolean(env.icons || env.lucideIcons),
|
|
128
178
|
rowIcon: (_self, data) => rowIcon(data.item, Boolean(env.icons)),
|
|
179
|
+
rowIconHref: (_self, data) => rowIconHref(data.item, Boolean(env.lucideIcons)),
|
|
129
180
|
rowIconFg: () => env.textSecondaryFg,
|
|
130
181
|
rowMarker: (_self, data) => rowMarker(data),
|
|
131
182
|
rowMarkerFg: (_self, data) => (rowIsMarked(data) ? env.accentSecondaryFg : env.accentFg),
|
|
132
183
|
rowName: (_self, data) => rowName(data.item),
|
|
133
|
-
rowNameClass: (_self, data) => (data.item
|
|
184
|
+
rowNameClass: (_self, data) => nameClass(data.item, env),
|
|
134
185
|
rowNameFg: (_self, data) => rowNameFg(data, env),
|
|
135
186
|
rowSize: (_self, data) => rowSize(data.item),
|
|
136
187
|
rowSizeFg: () => env.textSecondaryFg,
|
|
137
188
|
}
|
|
138
189
|
}
|
|
139
190
|
|
|
140
|
-
export { createBehaviors, isMarked, nameFg, rowIcon, rowName, rowNameFg, rowSize }
|
|
191
|
+
export { createBehaviors, isExecutable, isMarked, isReadonly, nameClass, nameFg, rowIcon, rowIconHref, rowName, rowNameFg, rowSize }
|
|
141
192
|
|
|
142
193
|
// EOF
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// reference's text lives with its overlay in `Help.js`.
|
|
13
13
|
|
|
14
14
|
import { unwrap } from "select/state/cells"
|
|
15
|
-
import {
|
|
15
|
+
import { commandBarStatus } from "./CommandBar.js"
|
|
16
16
|
import pkg from "../../../../package.json" with { type: "json" }
|
|
17
17
|
|
|
18
18
|
// Constant: VERSION
|
|
@@ -92,36 +92,56 @@ function currentColumnId(data) {
|
|
|
92
92
|
return `${unwrap(data?.currentId) ?? unwrap(data?.active) ?? "left"}`
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
//
|
|
96
|
-
// The
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
95
|
+
// Constant: PANE_GROW
|
|
96
|
+
// The explorer's current column is this much wider than the parent/preview
|
|
97
|
+
// mirrors — enough to read as the working pane, not enough to starve its
|
|
98
|
+
// neighbours. Both renderers apply the same ratio (the TUI's pane row growers
|
|
99
|
+
// and the web CSS `.pane.is-major`), so the geometry has one source of truth
|
|
100
|
+
// here.
|
|
101
|
+
const PANE_GROW = 1.35
|
|
103
102
|
|
|
104
|
-
// Function:
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
// Function: paneColumnWidth
|
|
104
|
+
// The columns a pane's column occupies at viewport `width` for the current
|
|
105
|
+
// view. Explorer leads with the parent mirror and grows the current column by
|
|
106
|
+
// PANE_GROW, so its mirrors are narrower than an even split; commander tiles
|
|
107
|
+
// its two panes evenly. The inline inspector adds an equal column to either.
|
|
108
|
+
// Panes hidden in the view, and an unknown viewport (`width <= 0`), are 0.
|
|
109
|
+
//
|
|
110
|
+
// The caller subtracts its own chrome (border, inset, safety) to get a content
|
|
111
|
+
// budget: feeding every pane an even `width / columns` makes the narrower
|
|
112
|
+
// mirror columns overflow, and the renderer then mangles their breadcrumbs.
|
|
113
|
+
function paneColumnWidth(width, paneId, { viewMode, currentId, showPreview } = {}) {
|
|
114
|
+
const total = Math.floor(width ?? 0)
|
|
115
|
+
if (total <= 0) return 0
|
|
116
|
+
const weights = {}
|
|
117
|
+
if (viewMode === "explorer") {
|
|
118
|
+
weights.parent = 1
|
|
119
|
+
weights[`${currentId}` === "right" ? "right" : "left"] = PANE_GROW
|
|
120
|
+
} else {
|
|
121
|
+
weights.left = 1
|
|
122
|
+
weights.right = 1
|
|
123
|
+
}
|
|
124
|
+
if (Boolean(showPreview) && total >= INSPECTOR_INLINE_WIDTH) weights.preview = 1
|
|
125
|
+
const weight = weights[paneId] ?? 0
|
|
126
|
+
if (weight <= 0) return 0
|
|
127
|
+
const sum = Object.values(weights).reduce((acc, value) => acc + value, 0)
|
|
128
|
+
return Math.floor((total * weight) / sum)
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
// Function: paneListHeight
|
|
114
|
-
// Rows available to a pane's listing: the viewport height minus the
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
132
|
+
// Rows available to a pane's listing: the viewport height minus the app header
|
|
133
|
+
// (1 row in the web UI, 2 for the desktop toolbar), the pane border (2), pane
|
|
134
|
+
// header + rule (2), entry detail (2), and the command bar when visible. Fed to
|
|
135
|
+
// the windowed `filelist` so a large directory mounts only the visible slice.
|
|
136
|
+
// `0` means "unknown — render everything to measure".
|
|
119
137
|
function paneListHeight(data, self) {
|
|
120
138
|
const height = Math.floor(sizeOf(data, self)?.height ?? 0)
|
|
121
139
|
if (height <= 0) return 0
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
const desktop = Boolean(unwrap(data?.desktopMode))
|
|
141
|
+
// A notice or error keeps a one-line status in desktop mode; otherwise that
|
|
142
|
+
// renderer has no bottom command bar at all. The bar is always one line.
|
|
143
|
+
const bar = commandBarStatus(data) || !desktop ? 1 : 0
|
|
144
|
+
return Math.max(1, height - (desktop ? 8 : 7) - bar)
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
// Function: mixHex
|
|
@@ -256,12 +276,14 @@ function createFrameBehaviors(env) {
|
|
|
256
276
|
(!isExplorer(data) || currentColumnId(data) === "right") && !unwrap(data?.previewFull),
|
|
257
277
|
|
|
258
278
|
// The inline inspector shows when the preview is on and the viewport is
|
|
259
|
-
// wide enough;
|
|
260
|
-
// (
|
|
279
|
+
// wide enough; the viewer overlay shows whenever it is open, at any
|
|
280
|
+
// width (`v` on a narrow terminal, or Enter on a file that cannot be
|
|
281
|
+
// edited). Maximizing (`⇧V`) replaces the column with
|
|
282
|
+
// `showPreviewFull` below.
|
|
261
283
|
inspectorNarrow: (self, data) => isNarrow(data, self),
|
|
262
284
|
showInspectorInline: (self, data) =>
|
|
263
285
|
Boolean(unwrap(data.showPreview)) && !isNarrow(data, self) && !unwrap(data?.previewFull),
|
|
264
|
-
showViewer: (
|
|
286
|
+
showViewer: (_self, data) => Boolean(unwrap(data.viewerOpen)),
|
|
265
287
|
// The maximized inspector: the preview fills the pane row in either
|
|
266
288
|
// view mode, pushed by `view.maximize` (`⇧V`).
|
|
267
289
|
showPreviewFull: (_self, data) => Boolean(unwrap(data?.previewFull)),
|
|
@@ -270,17 +292,19 @@ function createFrameBehaviors(env) {
|
|
|
270
292
|
// overlay has a single source of truth.
|
|
271
293
|
helpOpen: (_self, data) => Boolean(unwrap(data.helpOpen)),
|
|
272
294
|
|
|
295
|
+
// The shared list overlay (history / trash) is mounted whenever a mode
|
|
296
|
+
// is set; the component itself gates on `open`.
|
|
297
|
+
overlayOpen: (_self, data) => Boolean(unwrap(data.overlay)),
|
|
298
|
+
|
|
273
299
|
// The windowed listing height for both panes (they share the row).
|
|
274
300
|
listViewport: (self, data) => paneListHeight(data, self),
|
|
275
301
|
|
|
276
|
-
// The command bar's height:
|
|
277
|
-
|
|
278
|
-
barHeight: (self, data) => (commandBarStatus(data) ? 1 : commandBarLines(data, self)),
|
|
302
|
+
// The command bar's height: always a single line, status or key hints.
|
|
303
|
+
barHeight: () => 1,
|
|
279
304
|
}
|
|
280
305
|
}
|
|
281
306
|
|
|
282
307
|
export {
|
|
283
|
-
commandBarLines,
|
|
284
308
|
createFrameBehaviors,
|
|
285
309
|
currentColumnId,
|
|
286
310
|
INSPECTOR_INLINE_WIDTH,
|
|
@@ -291,6 +315,8 @@ export {
|
|
|
291
315
|
MODE_SLOTS,
|
|
292
316
|
modeGradient,
|
|
293
317
|
modeWord,
|
|
318
|
+
PANE_GROW,
|
|
319
|
+
paneColumnWidth,
|
|
294
320
|
sizeOf,
|
|
295
321
|
versionTrail,
|
|
296
322
|
VERSION,
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
<section class="help" when="open" role="dialog" aria-modal="true" aria-label="Key reference" tabindex="-1">
|
|
2
|
-
<div class="help-dialog">
|
|
1
|
+
<section class="help" when="open" role="dialog" aria-modal="true" aria-label="Key reference" tabindex="-1" on:click="!dialog:help-cancel">
|
|
2
|
+
<div class="help-dialog" on:click="dialogStop">
|
|
3
3
|
<header out="title"></header>
|
|
4
|
-
<
|
|
5
|
-
|
|
4
|
+
<div class="help-columns">
|
|
5
|
+
<div class="help-column" out="leftRows"></div>
|
|
6
|
+
<div class="help-column" out="rightRows"></div>
|
|
7
|
+
</div>
|
|
8
|
+
<footer class="help-footer">
|
|
9
|
+
<span class="help-bracket" out="hintOpen"></span>
|
|
10
|
+
<span class="help-title" out="hintHead"></span>
|
|
11
|
+
<span class="help-key" out="hintKeys"></span>
|
|
12
|
+
<span class="help-bracket" out="hintClose"></span>
|
|
13
|
+
</footer>
|
|
6
14
|
</div>
|
|
7
15
|
</section>
|