@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,156 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-22
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/core/picker
|
|
7
|
+
// The command panel's file picker: the private pane the panel browses while a
|
|
8
|
+
// `dest`/`path` argument is being filled. The picker owns its editable path
|
|
9
|
+
// text, the directory that text resolves against, the visible listing and the
|
|
10
|
+
// queue that keeps fast typing from landing an older navigation after a newer
|
|
11
|
+
// one. State cells live on the session (see `core/session`); this module holds
|
|
12
|
+
// the path rules and the row shaping so `core/panel` stays about argument
|
|
13
|
+
// binding.
|
|
14
|
+
|
|
15
|
+
import { basename, join, normalize, parent } from "./path.js"
|
|
16
|
+
|
|
17
|
+
// Function: pickerAbsolutePath
|
|
18
|
+
// Resolves the picker's editable path text: `~`, an absolute path, or a path
|
|
19
|
+
// relative to `base` (the directory the picker was opened from).
|
|
20
|
+
function pickerAbsolutePath(text, base = "/", home = "/") {
|
|
21
|
+
const raw = `${text ?? ""}`
|
|
22
|
+
if (!raw) return normalize(base || "/")
|
|
23
|
+
if (raw === "~") return normalize(home || "/")
|
|
24
|
+
if (raw.startsWith("~/")) return join(home, raw.slice(2))
|
|
25
|
+
if (raw.startsWith("/")) return normalize(raw)
|
|
26
|
+
return join(base || "/", raw)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Function: pickerDirectoryText
|
|
30
|
+
// The path input's display form: a trailing slash marks a directory, so typing
|
|
31
|
+
// after it starts a child name (the same affordance a shell prompt gives).
|
|
32
|
+
function pickerDirectoryText(path) {
|
|
33
|
+
const normalized = normalize(path)
|
|
34
|
+
return normalized === "/" ? "/" : `${normalized}/`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Function: resolvePickerPath
|
|
38
|
+
// Interprets the editable path text as `{ directory, query }`: an existing
|
|
39
|
+
// directory lists its contents, an existing file keeps its parent listing with
|
|
40
|
+
// the file as the filter, and a missing path walks up to the closest existing
|
|
41
|
+
// directory while the dropped segment becomes the filter. This is what makes
|
|
42
|
+
// typing `/home/do` show the matching entries under `/home`.
|
|
43
|
+
async function resolvePickerPath(fs, text, base = "/", home = "/") {
|
|
44
|
+
const raw = `${text ?? ""}`
|
|
45
|
+
const absolute = pickerAbsolutePath(raw, base, home)
|
|
46
|
+
const stats = await fs.stat(absolute)
|
|
47
|
+
if (stats?.isDir) return { directory: absolute, query: "" }
|
|
48
|
+
if (stats) return { directory: parent(absolute) ?? "/", query: basename(absolute) }
|
|
49
|
+
|
|
50
|
+
const trailing = raw.endsWith("/")
|
|
51
|
+
let directory = trailing ? absolute : parent(absolute) ?? "/"
|
|
52
|
+
const rest = trailing ? [] : [basename(absolute)]
|
|
53
|
+
while (directory !== "/") {
|
|
54
|
+
if ((await fs.stat(directory))?.isDir) break
|
|
55
|
+
rest.unshift(basename(directory))
|
|
56
|
+
directory = parent(directory) ?? "/"
|
|
57
|
+
}
|
|
58
|
+
return { directory, query: rest[0] ?? "" }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Function: pickerVisibleEntries
|
|
62
|
+
// The entries the picker lists for a mode: a destination picker lists
|
|
63
|
+
// directories only, so every row is a valid choice; a file/any picker keeps the
|
|
64
|
+
// listing intact, where a directory row descends.
|
|
65
|
+
function pickerVisibleEntries(entries, mode) {
|
|
66
|
+
return mode === "dir" ? (entries ?? []).filter((entry) => entry.isDir) : entries ?? []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Function: pickerRowList
|
|
70
|
+
// Flattens the picker's visible listing into the rows the picker renders. The
|
|
71
|
+
// `..` parent navigation row stays at the top when filtering and is omitted at
|
|
72
|
+
// the filesystem root; entry rows keep the listing details (size, mode, link
|
|
73
|
+
// target) so they render exactly like a file panel.
|
|
74
|
+
function pickerRowList(entries, path) {
|
|
75
|
+
const rows = []
|
|
76
|
+
const parentPath = parent(path)
|
|
77
|
+
if (parentPath) {
|
|
78
|
+
rows.push({
|
|
79
|
+
type: "parent",
|
|
80
|
+
key: `parent:${path}`,
|
|
81
|
+
name: "..",
|
|
82
|
+
path: parentPath,
|
|
83
|
+
isDir: true,
|
|
84
|
+
isSymlink: false,
|
|
85
|
+
isDevice: false,
|
|
86
|
+
isParent: true,
|
|
87
|
+
target: null,
|
|
88
|
+
size: 0,
|
|
89
|
+
mode: 0,
|
|
90
|
+
ext: null,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
for (const entry of entries ?? []) {
|
|
94
|
+
rows.push({ ...entry, type: "entry", key: entry.path })
|
|
95
|
+
}
|
|
96
|
+
return rows
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Function: createPickerPathQueue
|
|
100
|
+
// Creates the picker's path-resolution queue for one session. `invalidate`
|
|
101
|
+
// drops any queued work before an explicit navigation or close; `schedule`
|
|
102
|
+
// resolves one path text and applies the resulting directory and filter. Work
|
|
103
|
+
// is serialized so fast typing cannot land an older navigation after a newer
|
|
104
|
+
// one, and a token drops every intermediate keystroke.
|
|
105
|
+
function createPickerPathQueue(session) {
|
|
106
|
+
let version = 0
|
|
107
|
+
let queue = Promise.resolve()
|
|
108
|
+
return {
|
|
109
|
+
invalidate() {
|
|
110
|
+
version += 1
|
|
111
|
+
},
|
|
112
|
+
schedule(text) {
|
|
113
|
+
if (!session.pickerOpen.value) return
|
|
114
|
+
const token = ++version
|
|
115
|
+
queue = queue
|
|
116
|
+
.then(async () => {
|
|
117
|
+
if (token !== version || !session.pickerOpen.value) return
|
|
118
|
+
const base = session.pickerBasePath.value || session.pickerPane.path.value || "/"
|
|
119
|
+
const { directory, query } = await resolvePickerPath(
|
|
120
|
+
session.fs,
|
|
121
|
+
text,
|
|
122
|
+
base,
|
|
123
|
+
session.fs.home?.() ?? "/",
|
|
124
|
+
)
|
|
125
|
+
if (token !== version || session.pickerPathText.value !== text) return
|
|
126
|
+
if (session.pickerPane.path.value !== directory) {
|
|
127
|
+
await session.pickerPane.navigate(directory, { record: false })
|
|
128
|
+
}
|
|
129
|
+
if (token !== version || session.pickerPathText.value !== text) return
|
|
130
|
+
session.pickerQuery.set(query)
|
|
131
|
+
const rows = session.pickerRows.value
|
|
132
|
+
const firstMatch = query ? rows.findIndex((row) => row.type === "entry") : 0
|
|
133
|
+
session.panelRowIndex.set(firstMatch >= 0 ? firstMatch : 0)
|
|
134
|
+
})
|
|
135
|
+
.catch((error) => {
|
|
136
|
+
// Keep the queue alive after an unreadable path; the picker
|
|
137
|
+
// keeps the last listing and the command bar reports what
|
|
138
|
+
// failed.
|
|
139
|
+
if (token === version) {
|
|
140
|
+
session.notice?.set({ kind: "warning", text: `${error?.message ?? error}` })
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
createPickerPathQueue,
|
|
149
|
+
pickerAbsolutePath,
|
|
150
|
+
pickerDirectoryText,
|
|
151
|
+
pickerRowList,
|
|
152
|
+
pickerVisibleEntries,
|
|
153
|
+
resolvePickerPath,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// EOF
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-16
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/core/preview
|
|
7
|
+
// Preview classification and payload helpers, shared by the session's preview
|
|
8
|
+
// effect and by the commands that need to know how an entry previews — Enter
|
|
9
|
+
// opens a text file in `$EDITOR` and everything else in the fullscreen
|
|
10
|
+
// inspector (see `core/commands`). Pure and renderer-free: `previewKind` maps
|
|
11
|
+
// a resolved extension to a preview kind, `previewSource` resolves what a
|
|
12
|
+
// symlink previews *through* (and what a compressed file decompresses *to*),
|
|
13
|
+
// and `hasBinaryByte` is the content sniff that turns a text head into a
|
|
14
|
+
// metadata card.
|
|
15
|
+
|
|
16
|
+
import { basename, extname } from "./path.js"
|
|
17
|
+
|
|
18
|
+
// Constant: PREVIEW_BYTES / SNIFF_BYTES
|
|
19
|
+
// Bytes of a text file the inspector reads for its inline body — the head
|
|
20
|
+
// budget every filesystem adapter defaults to — and the shorter opening run
|
|
21
|
+
// `hasBinaryByte` sniffs to call a body binary.
|
|
22
|
+
const PREVIEW_BYTES = 65536
|
|
23
|
+
const SNIFF_BYTES = 2048
|
|
24
|
+
|
|
25
|
+
// Constant: IMAGE_EXTS / MEDIA_EXTS
|
|
26
|
+
// Extensions with a dedicated (non-text) preview: they render as a metadata
|
|
27
|
+
// card, never as an editable text body.
|
|
28
|
+
const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"]
|
|
29
|
+
const MEDIA_EXTS = ["mp3", "wav", "ogg", "flac", "mp4", "webm", "mkv", "mov", "pdf"]
|
|
30
|
+
|
|
31
|
+
// Constant: COMPRESSION_EXTS
|
|
32
|
+
// Single-file compression formats the inspector decompresses before sniffing
|
|
33
|
+
// the body. The compressed entry keeps its own extension in the header; the
|
|
34
|
+
// name inside (`notes.txt` in `notes.txt.gz`) supplies the kind, the syntax
|
|
35
|
+
// filetype and the badge's inner label.
|
|
36
|
+
const COMPRESSION_EXTS = new Map([
|
|
37
|
+
["gz", "gzip"],
|
|
38
|
+
["bz2", "bzip2"],
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
// Function: compressionOf
|
|
42
|
+
// Decompressor for a resolved extension ("gzip"/"bzip2"), or null when the
|
|
43
|
+
// entry is not compressed.
|
|
44
|
+
function compressionOf(ext) {
|
|
45
|
+
return ext ? COMPRESSION_EXTS.get(ext) ?? null : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Function: previewKind
|
|
49
|
+
// Preview classification for a resolved file extension; directories are not
|
|
50
|
+
// previewed (the caller passes their `null` ext). Takes the extension rather
|
|
51
|
+
// than the entry so a symlink classifies as what it points at.
|
|
52
|
+
function previewKind(ext) {
|
|
53
|
+
if (ext && IMAGE_EXTS.includes(ext)) return "image"
|
|
54
|
+
if (ext && MEDIA_EXTS.includes(ext)) return "media"
|
|
55
|
+
return "text"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Function: hasBinaryByte
|
|
59
|
+
// Content sniff: a text file's head carries no control byte in its opening
|
|
60
|
+
// run, so a hit here means the body is binary and previews as a card.
|
|
61
|
+
function hasBinaryByte(text) {
|
|
62
|
+
for (let i = 0; i < SNIFF_BYTES && i < text.length; i++) {
|
|
63
|
+
const code = text.charCodeAt(i)
|
|
64
|
+
if (code < 9 || (code > 13 && code < 32)) return true
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Function: previewSource
|
|
70
|
+
// What the preview renders *through* a symlink, and what a compressed file
|
|
71
|
+
// decompresses to: `title` names the traversal (`notes -> ../real/notes`) so
|
|
72
|
+
// it is visible in the header; `ext` is the entry's (target's) own extension;
|
|
73
|
+
// `innerName`/`innerExt` strip a `.gz`/`.bz2` suffix so `notes.txt.gz`
|
|
74
|
+
// previews with the kind, filetype and badge of `notes.txt`; `compression` is
|
|
75
|
+
// the decompressor the session applies, or null. Plain entries pass through
|
|
76
|
+
// with `innerName === name` and `innerExt === ext`.
|
|
77
|
+
function previewSource(entry) {
|
|
78
|
+
const target = entry?.isSymlink && entry.target ? `${entry.target}` : null
|
|
79
|
+
const name = target ? basename(target) : basename(entry?.path ?? "")
|
|
80
|
+
const ext = extname(name) ?? entry?.ext ?? null
|
|
81
|
+
const compression = compressionOf(ext)
|
|
82
|
+
const innerName = compression ? name.slice(0, name.lastIndexOf(".")) : name
|
|
83
|
+
const innerExt = compression ? extname(innerName) : ext
|
|
84
|
+
return {
|
|
85
|
+
title: target ? `${basename(entry.path)} -> ${target}` : name,
|
|
86
|
+
ext,
|
|
87
|
+
innerName,
|
|
88
|
+
innerExt,
|
|
89
|
+
compression,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
compressionOf,
|
|
95
|
+
hasBinaryByte,
|
|
96
|
+
PREVIEW_BYTES,
|
|
97
|
+
previewKind,
|
|
98
|
+
previewSource,
|
|
99
|
+
SNIFF_BYTES,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// EOF
|