@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.
Files changed (105) hide show
  1. package/README.md +169 -58
  2. package/desktop/dist/index.html +13 -0
  3. package/desktop/dist/main.js +20298 -0
  4. package/desktop/dist/style.css +1515 -0
  5. package/desktop/frontend/index.html +13 -0
  6. package/package.json +17 -5
  7. package/public/fonts/OFL.txt +93 -0
  8. package/public/licenses/LUCIDE.txt +43 -0
  9. package/public/main.js +5066 -1888
  10. package/public/style.css +660 -46
  11. package/scripts/build-desktop.js +31 -0
  12. package/src/js/tcmd/components/App.html +56 -6
  13. package/src/js/tcmd/components/App.js +233 -58
  14. package/src/js/tcmd/components/App.xml +27 -22
  15. package/src/js/tcmd/components/BookmarkList.html +4 -5
  16. package/src/js/tcmd/components/BookmarkList.js +14 -40
  17. package/src/js/tcmd/components/BookmarkList.xml +1 -2
  18. package/src/js/tcmd/components/BookmarkRow.html +2 -1
  19. package/src/js/tcmd/components/BookmarkRow.js +14 -9
  20. package/src/js/tcmd/components/BookmarkRow.xml +1 -1
  21. package/src/js/tcmd/components/CommandArgRow.html +18 -7
  22. package/src/js/tcmd/components/CommandArgRow.js +100 -44
  23. package/src/js/tcmd/components/CommandArgRow.xml +39 -15
  24. package/src/js/tcmd/components/CommandBar.html +1 -1
  25. package/src/js/tcmd/components/CommandBar.js +31 -25
  26. package/src/js/tcmd/components/CommandBar.xml +2 -2
  27. package/src/js/tcmd/components/CommandItem.js +24 -25
  28. package/src/js/tcmd/components/CommandItem.xml +2 -2
  29. package/src/js/tcmd/components/CommandPanel.html +13 -13
  30. package/src/js/tcmd/components/CommandPanel.js +57 -33
  31. package/src/js/tcmd/components/CommandPanel.xml +30 -20
  32. package/src/js/tcmd/components/Confirm.html +2 -2
  33. package/src/js/tcmd/components/EntryDetail.js +21 -7
  34. package/src/js/tcmd/components/FilePicker.html +10 -0
  35. package/src/js/tcmd/components/FilePicker.js +43 -0
  36. package/src/js/tcmd/components/FilePicker.xml +13 -0
  37. package/src/js/tcmd/components/FilePickerRow.html +6 -0
  38. package/src/js/tcmd/components/FilePickerRow.js +27 -0
  39. package/src/js/tcmd/components/FilePickerRow.xml +10 -0
  40. package/src/js/tcmd/components/FilePreview.html +1 -2
  41. package/src/js/tcmd/components/FilePreview.js +40 -4
  42. package/src/js/tcmd/components/FilePreview.xml +17 -10
  43. package/src/js/tcmd/components/FileRow.html +1 -0
  44. package/src/js/tcmd/components/FileRow.js +60 -9
  45. package/src/js/tcmd/components/Frame.js +57 -31
  46. package/src/js/tcmd/components/Help.html +12 -4
  47. package/src/js/tcmd/components/Help.js +198 -47
  48. package/src/js/tcmd/components/Help.xml +13 -4
  49. package/src/js/tcmd/components/HelpRow.html +8 -0
  50. package/src/js/tcmd/components/HelpRow.xml +7 -0
  51. package/src/js/tcmd/components/OverlayList.html +8 -0
  52. package/src/js/tcmd/components/OverlayList.js +69 -0
  53. package/src/js/tcmd/components/OverlayList.xml +13 -0
  54. package/src/js/tcmd/components/OverlayRow.html +5 -0
  55. package/src/js/tcmd/components/OverlayRow.js +47 -0
  56. package/src/js/tcmd/components/OverlayRow.xml +9 -0
  57. package/src/js/tcmd/components/format.js +4 -23
  58. package/src/js/tcmd/components/icons.js +137 -0
  59. package/src/js/tcmd/components/index.js +8 -3
  60. package/src/js/tcmd/components/rows.js +149 -25
  61. package/src/js/tcmd/core/base64.js +64 -0
  62. package/src/js/tcmd/core/bookmarks.js +45 -8
  63. package/src/js/tcmd/core/commands.js +612 -136
  64. package/src/js/tcmd/core/concurrency.js +59 -0
  65. package/src/js/tcmd/core/decompress.js +117 -0
  66. package/src/js/tcmd/core/directory.js +81 -5
  67. package/src/js/tcmd/core/format.js +36 -0
  68. package/src/js/tcmd/core/fs.js +410 -53
  69. package/src/js/tcmd/core/journal.js +93 -0
  70. package/src/js/tcmd/core/keymap.js +39 -3
  71. package/src/js/tcmd/core/operations.js +735 -63
  72. package/src/js/tcmd/core/os.js +29 -3
  73. package/src/js/tcmd/core/pane.js +4 -2
  74. package/src/js/tcmd/core/panel.js +179 -93
  75. package/src/js/tcmd/core/path.js +30 -3
  76. package/src/js/tcmd/core/permissions.js +107 -0
  77. package/src/js/tcmd/core/picker.js +156 -0
  78. package/src/js/tcmd/core/preview.js +102 -0
  79. package/src/js/tcmd/core/session.js +286 -116
  80. package/src/js/tcmd/core/watch.js +153 -0
  81. package/src/js/tcmd/desktop/backend/api.js +165 -0
  82. package/src/js/tcmd/desktop/backend/fs.js +626 -0
  83. package/src/js/tcmd/desktop/backend/main.ts +9 -0
  84. package/src/js/tcmd/desktop/bookmarks.js +23 -0
  85. package/src/js/tcmd/desktop/fs.js +91 -0
  86. package/src/js/tcmd/desktop/main.js +194 -0
  87. package/src/js/tcmd/desktop/os.js +33 -0
  88. package/src/js/tcmd/tui/app.js +111 -74
  89. package/src/js/tcmd/tui/catalogue.js +232 -23
  90. package/src/js/tcmd/tui/index.js +18 -4
  91. package/src/js/tcmd/tui/opentui.js +33 -0
  92. package/src/js/tcmd/tui/syntax.js +91 -0
  93. package/src/js/tcmd/tui/theme.js +27 -2
  94. package/src/js/tcmd/vendor/bzip2.js +276 -0
  95. package/src/js/tcmd/web/app.js +114 -56
  96. package/src/js/tcmd/web/catalogue.js +41 -4
  97. package/src/js/tcmd/web/effects.js +94 -14
  98. package/src/js/tcmd/web/header.html +1 -1
  99. package/src/js/tcmd/web/http.js +13 -0
  100. package/src/js/tcmd/web/index.js +2 -2
  101. package/src/js/tcmd/web/opfs.js +297 -30
  102. package/src/js/tcmd/web/seed.js +1 -1
  103. package/src/js/tcmd/web/server.js +118 -17
  104. package/src/js/tcmd/web/theme.js +3 -0
  105. 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