@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
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
// their leading content; other kinds show a metadata card. Renderer-agnostic
|
|
9
9
|
// logic; `FilePreview.xml` / `FilePreview.html` are the presentations.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
11
|
+
// A directory is the exception: it renders the *directory column* — its own
|
|
12
|
+
// breadcrumbs over a cursorable listing — so a previewed directory looks and
|
|
13
|
+
// behaves like a pane in every view mode (click a row to focus it, Enter to
|
|
14
|
+
// descend). The listing is the inspector pane's (`session.panes.preview`),
|
|
15
|
+
// threaded down as the `dir*` inputs; `paneId` is what rows, crumbs and the
|
|
16
|
+
// column body publish back to the App.
|
|
17
|
+
//
|
|
18
|
+
// Environment: `textFg`, `textSecondaryFg`, `textHeadingFg`, and the optional
|
|
19
|
+
// `actions` footer text (default: the TUI's `[Open O] [Edit E]` hints; the web
|
|
20
|
+
// substitutes a note that those keys do not exist in a browser).
|
|
12
21
|
|
|
13
22
|
import { unwrap } from "select/state/cells"
|
|
14
23
|
import { formatSize } from "./format.js"
|
|
24
|
+
import { previewSource } from "../core/preview.js"
|
|
15
25
|
|
|
16
26
|
const KIND_LABELS = {
|
|
17
27
|
directory: "directory",
|
|
@@ -48,16 +58,20 @@ function hasText(preview) {
|
|
|
48
58
|
|
|
49
59
|
// Function: previewTitle
|
|
50
60
|
// "Filename <EXT>" style title; falls back to the kind label when the file has
|
|
51
|
-
// no extension.
|
|
61
|
+
// no extension. A decompressed preview adds the inner label, so
|
|
62
|
+
// `notes.txt.gz` reads `notes.txt.gz <GZ · TXT>`.
|
|
52
63
|
function previewTitle(preview) {
|
|
53
64
|
if (!preview) return "preview"
|
|
54
65
|
const badge = (preview.ext || KIND_LABELS[preview.kind] || "file").toUpperCase()
|
|
55
|
-
|
|
66
|
+
const inner = preview.compression && preview.innerExt ? ` · ${preview.innerExt.toUpperCase()}` : ""
|
|
67
|
+
return `${preview.title} <${badge}${inner}>`
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
// Function: entryCard
|
|
59
71
|
// Metadata card for the entry the session did not preview: a directory, or a
|
|
60
|
-
// file whose kind has no inline preview.
|
|
72
|
+
// file whose kind has no inline preview. Directories normally take the
|
|
73
|
+
// directory column instead (see `isDirectoryPane`); the branch stays for a
|
|
74
|
+
// `stat` payload and for hosts that render no listing.
|
|
61
75
|
function entryCard(entry) {
|
|
62
76
|
if (!entry) return ""
|
|
63
77
|
const lines = []
|
|
@@ -69,42 +83,127 @@ function entryCard(entry) {
|
|
|
69
83
|
return lines.join("\n")
|
|
70
84
|
}
|
|
71
85
|
|
|
86
|
+
// Function: isDirectoryPane
|
|
87
|
+
// True when the inspector previews a plain directory: no `stat` card was
|
|
88
|
+
// pinned on it, so the directory column takes the body.
|
|
89
|
+
function isDirectoryPane(preview, entry) {
|
|
90
|
+
return Boolean(entry?.isDir) && !preview
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
// Function: headerTitle
|
|
73
|
-
// The inspector title: the preview's own title, else the cursor entry
|
|
94
|
+
// The inspector title: the preview's own title, else the cursor entry — whose
|
|
95
|
+
// symlink target and resolved extension `previewSource` supplies, so the
|
|
96
|
+
// header names the traversal before the async read lands.
|
|
74
97
|
function headerTitle(preview, entry) {
|
|
75
98
|
if (preview) return previewTitle(preview)
|
|
76
99
|
if (entry) {
|
|
77
|
-
const
|
|
78
|
-
|
|
100
|
+
const source = previewSource(entry)
|
|
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
|
+
: ""
|
|
106
|
+
const name = entry.isDir ? `${entry.name}/` : entry.name
|
|
107
|
+
const target = entry.isSymlink && entry.target ? ` -> ${entry.target}` : ""
|
|
108
|
+
return `${name}${target} <${badge}${inner}>`
|
|
79
109
|
}
|
|
80
110
|
return "inspector"
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
// Function: createBehaviors
|
|
84
114
|
// Builds the FilePreview behavior map for the given renderer environment.
|
|
115
|
+
// `env.actions` overrides the footer line so a presentation can carry its own
|
|
116
|
+
// copy (the browser has no Open/Edit); the default advertises the TUI keys.
|
|
85
117
|
function createBehaviors(env) {
|
|
118
|
+
const actions = env.actions ?? (() => " [Open O] [Edit E]")
|
|
119
|
+
const directory = (_self, data) =>
|
|
120
|
+
isDirectoryPane(unwrap(data.preview), unwrap(data.entry))
|
|
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
|
+
}
|
|
86
134
|
return {
|
|
87
|
-
actions
|
|
135
|
+
actions,
|
|
88
136
|
actionsFg: () => env.textSecondaryFg,
|
|
89
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
|
+
},
|
|
148
|
+
// The directory column: breadcrumbs over the inspector pane's listing,
|
|
149
|
+
// shaped like a pane header (a rule separates it from the body).
|
|
150
|
+
dirCrumbsView: (_self, data) => ({ segments: data.dirSegments, paneId: data.paneId }),
|
|
151
|
+
dirListView: (_self, data) => ({
|
|
152
|
+
entries: data.dirEntries,
|
|
153
|
+
cursor: data.dirCursor,
|
|
154
|
+
marked: data.dirMarked,
|
|
155
|
+
active: data.dirActive,
|
|
156
|
+
loading: data.dirLoading,
|
|
157
|
+
paneId: data.paneId,
|
|
158
|
+
}),
|
|
90
159
|
dimFg: () => env.textSecondaryFg,
|
|
91
160
|
emptyText: () => " No file selected\n Move the cursor or press v",
|
|
92
161
|
entryText: (_self, data) => entryCard(unwrap(data.entry)),
|
|
162
|
+
// The rule between the contents and the shortcut footer.
|
|
163
|
+
footRule: () => ["top"],
|
|
164
|
+
footRuleFg: () => env.textSecondaryFg,
|
|
165
|
+
headRule: () => ["top"],
|
|
166
|
+
headRuleFg: () => env.textSecondaryFg,
|
|
93
167
|
hasCard: (_self, data) => hasCard(unwrap(data.preview)),
|
|
94
|
-
// The metadata card fills in whenever there is a cursor entry but no
|
|
95
|
-
// preview payload (a directory, or a non-text file).
|
|
96
|
-
hasEntryCard: (_self, data) => !unwrap(data.preview) && Boolean(unwrap(data.entry)),
|
|
97
168
|
hasText: (_self, data) => hasText(unwrap(data.preview)),
|
|
98
169
|
headerTitle: (_self, data) => headerTitle(unwrap(data.preview), unwrap(data.entry)),
|
|
99
170
|
isEmpty: (_self, data) => !unwrap(data.preview) && !unwrap(data.entry),
|
|
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,
|
|
100
176
|
previewText: (_self, data) => unwrap(data.preview)?.text ?? "",
|
|
101
177
|
previewTitle: (_self, data) => previewTitle(unwrap(data.preview)),
|
|
102
178
|
previewTitleColor: () => env.textHeadingFg,
|
|
103
179
|
show: (_self, data) => Boolean(unwrap(data.show)),
|
|
180
|
+
// One body at a time: the directory column, else the card/text/empty
|
|
181
|
+
// states, with the title row yielding to the breadcrumb header.
|
|
182
|
+
// `showEntryCard` fills in whenever there is a cursor entry but no
|
|
183
|
+
// preview payload — except a directory, which gets the listing.
|
|
184
|
+
showBody: body,
|
|
185
|
+
showCard: (_self, data) => body(_self, data) && hasCard(unwrap(data.preview)),
|
|
186
|
+
showEmpty: (_self, data) => body(_self, data) && !unwrap(data.preview) && !unwrap(data.entry),
|
|
187
|
+
showEntryCard: (_self, data) =>
|
|
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),
|
|
192
|
+
showText: (_self, data) => body(_self, data) && hasText(unwrap(data.preview)),
|
|
193
|
+
showTextOrCard: (_self, data) => body(_self, data) && !isImage(_self, data),
|
|
104
194
|
textFg: () => env.textFg,
|
|
105
195
|
}
|
|
106
196
|
}
|
|
107
197
|
|
|
108
|
-
export {
|
|
198
|
+
export {
|
|
199
|
+
cardText,
|
|
200
|
+
createBehaviors,
|
|
201
|
+
entryCard,
|
|
202
|
+
hasCard,
|
|
203
|
+
hasText,
|
|
204
|
+
headerTitle,
|
|
205
|
+
isDirectoryPane,
|
|
206
|
+
previewTitle,
|
|
207
|
+
}
|
|
109
208
|
|
|
110
209
|
// EOF
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
+
<!-- The inspector: a card/text body for a file, and a directory column
|
|
2
|
+
(breadcrumbs + cursorable listing) for a directory, so a previewed
|
|
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. -->
|
|
1
7
|
<box when="show" out:title="headerTitle" out:titleColor="previewTitleColor" border borderStyle="rounded"
|
|
2
|
-
flexDirection="column" flexGrow="1" overflow="hidden">
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
8
|
+
flexDirection="column" flexGrow="1" overflow="hidden" on:click="paneId!pane:activate">
|
|
9
|
+
<box when="isDirPane" flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
10
|
+
<box flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
11
|
+
<breadcrumbs in:segments="dirSegments" in:paneId="paneId" height="1"></breadcrumbs>
|
|
12
|
+
</box>
|
|
13
|
+
</box>
|
|
14
|
+
<box when="isDirPane" height="1" out:border="headRule" out:borderColor="headRuleFg"></box>
|
|
15
|
+
<filelist when="isDirPane" in:entries="dirEntries" in:cursor="dirCursor" in:marked="dirMarked"
|
|
16
|
+
in:active="dirActive" in:paneId="paneId" in:loading="dirLoading"></filelist>
|
|
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>
|
|
10
30
|
</box>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<li class="file-row" out:class="rowClass" on:click="paneId:paneId,index:index!list:select">
|
|
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>
|
|
@@ -8,22 +8,27 @@
|
|
|
8
8
|
// markedRow, active, paneId }`, the same bag shape in both engines.
|
|
9
9
|
// `currentRow`/`markedRow` are plain per-row booleans snapshotted by the
|
|
10
10
|
// `fileRows` builder (see `rows.js`); `active` is the pane-focus cell, read
|
|
11
|
-
// with `unwrap` at the point of use.
|
|
11
|
+
// with `unwrap` at the point of use. A symlink row carries its target in the
|
|
12
|
+
// name (`notes -> ../real/notes`, see `rowName`).
|
|
12
13
|
//
|
|
13
14
|
// Environment: `cursorBg`, `cursorInactiveBg`, `currentClass`,
|
|
14
|
-
// `accentSecondaryFg`, `accentFg`, `linkFg`, `dirFg`, `devFg`, `
|
|
15
|
-
// `textSecondaryFg`, `dirClass`, `
|
|
15
|
+
// `accentSecondaryFg`, `accentFg`, `linkFg`, `dirFg`, `devFg`, `execFg`,
|
|
16
|
+
// `readonlyFg`, `fileFg`, `textSecondaryFg`, `dirClass`, `execClass`,
|
|
17
|
+
// `readonlyClass`, `icons` (show Nerd Font icons).
|
|
16
18
|
|
|
17
19
|
import { unwrap } from "select/state/cells"
|
|
18
20
|
import { formatSize } from "./format.js"
|
|
19
|
-
import { iconFor } from "./icons.js"
|
|
21
|
+
import { iconFor, lucideIconFor } from "./icons.js"
|
|
20
22
|
|
|
21
23
|
// Function: rowName
|
|
22
24
|
// The entry name with the directory slash (the icon is its own slot, so it
|
|
23
|
-
// can be dimmer than the name — see `rowIcon`)
|
|
25
|
+
// can be dimmer than the name — see `rowIcon`), and, for a symlink, the
|
|
26
|
+
// `-> target` tail `ls -l` prints. The name cell truncates, so a narrow pane
|
|
27
|
+
// drops the target before the name itself.
|
|
24
28
|
function rowName(entry) {
|
|
25
29
|
if (!entry) return ""
|
|
26
|
-
|
|
30
|
+
const name = entry.isDir ? `${entry.name}/` : entry.name
|
|
31
|
+
return entry.target ? `${name} -> ${entry.target}` : name
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
// Function: rowIcon
|
|
@@ -33,6 +38,12 @@ function rowIcon(entry, icons) {
|
|
|
33
38
|
return icons ? iconFor(entry) : ""
|
|
34
39
|
}
|
|
35
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
|
+
|
|
36
47
|
// Function: rowSize
|
|
37
48
|
function rowSize(entry) {
|
|
38
49
|
if (!entry) return ""
|
|
@@ -76,21 +87,64 @@ function rowIsMarked(data) {
|
|
|
76
87
|
return isMarked(data.item, unwrap(data.marked))
|
|
77
88
|
}
|
|
78
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
|
+
|
|
79
116
|
// Function: nameFg
|
|
80
117
|
// The listing color for an entry: a symlink reads as a link, a directory as a
|
|
81
|
-
// 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.
|
|
82
120
|
// Symlinks win over directories, matching `dircolors` (a link to a directory
|
|
83
|
-
// 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).
|
|
84
123
|
function nameFg(item, env) {
|
|
85
124
|
if (item?.isSymlink) return env.linkFg
|
|
86
125
|
if (item?.isDir) return env.dirFg
|
|
87
126
|
if (item?.isDevice) return env.devFg
|
|
127
|
+
if (isExecutable(item)) return env.execFg
|
|
128
|
+
if (isReadonly(item)) return env.readonlyFg
|
|
88
129
|
return env.fileFg
|
|
89
130
|
}
|
|
90
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
|
+
|
|
91
144
|
// Function: rowNameFg
|
|
92
145
|
// The name color for a row: a marked row reads orange (the selection signal),
|
|
93
|
-
// 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`).
|
|
94
148
|
// Marks win over the cursor so a marked-and-current row still reads as
|
|
95
149
|
// marked, the same precedence the row marker follows.
|
|
96
150
|
function rowNameFg(data, env) {
|
|
@@ -115,21 +169,25 @@ function createBehaviors(env) {
|
|
|
115
169
|
return undefined
|
|
116
170
|
},
|
|
117
171
|
rowClass: (_self, data) => (rowCurrent(data) ? env.currentClass : null),
|
|
172
|
+
rowMarked: (_self, data) => `${rowIsMarked(data)}`,
|
|
173
|
+
rowActive: (_self, data) => `${unwrap(data.active) !== false}`,
|
|
174
|
+
rowKind: (_self, data) => data.item?.isSymlink ? "link" : data.item?.isDir ? "dir" : data.item?.isDevice ? "dev" : "file",
|
|
118
175
|
// The icon is its own slot so it can sit dimmer than the name; it
|
|
119
176
|
// collapses when the environment keeps rows icon-free.
|
|
120
|
-
hasIcon: () => Boolean(env.icons),
|
|
177
|
+
hasIcon: () => Boolean(env.icons || env.lucideIcons),
|
|
121
178
|
rowIcon: (_self, data) => rowIcon(data.item, Boolean(env.icons)),
|
|
179
|
+
rowIconHref: (_self, data) => rowIconHref(data.item, Boolean(env.lucideIcons)),
|
|
122
180
|
rowIconFg: () => env.textSecondaryFg,
|
|
123
181
|
rowMarker: (_self, data) => rowMarker(data),
|
|
124
182
|
rowMarkerFg: (_self, data) => (rowIsMarked(data) ? env.accentSecondaryFg : env.accentFg),
|
|
125
183
|
rowName: (_self, data) => rowName(data.item),
|
|
126
|
-
rowNameClass: (_self, data) => (data.item
|
|
184
|
+
rowNameClass: (_self, data) => nameClass(data.item, env),
|
|
127
185
|
rowNameFg: (_self, data) => rowNameFg(data, env),
|
|
128
186
|
rowSize: (_self, data) => rowSize(data.item),
|
|
129
187
|
rowSizeFg: () => env.textSecondaryFg,
|
|
130
188
|
}
|
|
131
189
|
}
|
|
132
190
|
|
|
133
|
-
export { createBehaviors, isMarked, nameFg, rowIcon, rowName, rowNameFg, rowSize }
|
|
191
|
+
export { createBehaviors, isExecutable, isMarked, isReadonly, nameClass, nameFg, rowIcon, rowIconHref, rowName, rowNameFg, rowSize }
|
|
134
192
|
|
|
135
193
|
// EOF
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="filter-bar" when="visible">
|
|
2
2
|
<span class="filter-prompt" out="prompt"></span>
|
|
3
|
-
<input when="editing" ref="query" inout="query" class="filter-input" />
|
|
3
|
+
<input when="editing" ref="query" inout="query" on:input="onFilter" class="filter-input" />
|
|
4
4
|
<span when="applied" class="filter-label" out="label"></span>
|
|
5
5
|
<span class="filter-alert" out="alert"></span>
|
|
6
6
|
</div>
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
// FilterBar: the fuzzy filter indicator, folded into the pane header so it
|
|
8
8
|
// never shifts the listing. While the pane owns the filter input
|
|
9
9
|
// (`filtering === paneId`) it shows a compact focused input bound to the pane's
|
|
10
|
-
// `query`;
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the
|
|
10
|
+
// `query`; each change also routes through the pane's `setFilter` (passed as
|
|
11
|
+
// `setFilter`), so the cursor re-anchors with no directory watcher. Once
|
|
12
|
+
// accepted it degrades to the same `/ query` indication, so the pane is
|
|
13
|
+
// navigable again and the filter can still be cleared with Esc. When the query
|
|
14
|
+
// matches no entry it shows a `no match` alert. The key hints live in the
|
|
15
|
+
// command bar, not here.
|
|
14
16
|
//
|
|
15
17
|
// Environment: `accentFg` (the applied label), `textSecondaryFg` (the `/`
|
|
16
18
|
// prompt), `warningFg` (the no-match alert).
|
|
@@ -33,6 +35,14 @@ function createBehaviors(env) {
|
|
|
33
35
|
applied: (self, data) => !editing(self, data) && activeQuery(self, data).length > 0,
|
|
34
36
|
editing,
|
|
35
37
|
focused: editing,
|
|
38
|
+
// The input both displays and captures through `inout`; `on:input`
|
|
39
|
+
// additionally routes the change through the pane's `setFilter` so the
|
|
40
|
+
// cursor re-anchors on the first match. The TUI delivers the input
|
|
41
|
+
// value directly, the DOM an event carrying it.
|
|
42
|
+
onFilter: (_self, data, event) => {
|
|
43
|
+
const value = typeof event === "string" ? event : `${event?.target?.value ?? ""}`
|
|
44
|
+
data.setFilter?.(value)
|
|
45
|
+
},
|
|
36
46
|
// The bare query (shared with the web presentation).
|
|
37
47
|
label: (self, data) => queryText(self, data),
|
|
38
48
|
labelFg: () => env.accentFg,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<box when="visible" flexDirection="row" height="1" flexShrink="0">
|
|
2
2
|
<text out="prompt" out:fg="promptFg"></text>
|
|
3
|
-
<input when="editing" ref="query" inout="query" out:focused="focused" width="14" height="1"></input>
|
|
3
|
+
<input when="editing" ref="query" inout="query" on:input="onFilter" out:focused="focused" width="14" height="1"></input>
|
|
4
4
|
<text when="applied" out="labelText" out:fg="labelFg" wrapMode="none" truncate="true"></text>
|
|
5
5
|
<text out="alert" out:fg="alertFg" wrapMode="none"></text>
|
|
6
6
|
</box>
|