@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.
Files changed (134) hide show
  1. package/README.md +256 -62
  2. package/bin/tcmd +10 -2
  3. package/bin/tcmd.js +13 -2
  4. package/desktop/dist/index.html +13 -0
  5. package/desktop/dist/main.js +20298 -0
  6. package/desktop/dist/style.css +1515 -0
  7. package/desktop/frontend/index.html +13 -0
  8. package/package.json +19 -6
  9. package/public/fonts/OFL.txt +93 -0
  10. package/public/index.html +14 -0
  11. package/public/licenses/LUCIDE.txt +43 -0
  12. package/public/main.js +20679 -0
  13. package/public/style.css +1515 -0
  14. package/scripts/build-desktop.js +31 -0
  15. package/scripts/serve-web.js +61 -0
  16. package/src/js/tcmd/components/App.html +80 -14
  17. package/src/js/tcmd/components/App.js +339 -114
  18. package/src/js/tcmd/components/App.xml +76 -22
  19. package/src/js/tcmd/components/BookmarkList.html +5 -5
  20. package/src/js/tcmd/components/BookmarkList.js +15 -41
  21. package/src/js/tcmd/components/BookmarkList.xml +1 -2
  22. package/src/js/tcmd/components/BookmarkRow.html +2 -1
  23. package/src/js/tcmd/components/BookmarkRow.js +14 -9
  24. package/src/js/tcmd/components/BookmarkRow.xml +1 -1
  25. package/src/js/tcmd/components/Breadcrumbs.js +1 -1
  26. package/src/js/tcmd/components/CommandArgRow.html +28 -0
  27. package/src/js/tcmd/components/CommandArgRow.js +146 -30
  28. package/src/js/tcmd/components/CommandArgRow.xml +55 -4
  29. package/src/js/tcmd/components/CommandBar.html +2 -2
  30. package/src/js/tcmd/components/CommandBar.js +33 -27
  31. package/src/js/tcmd/components/CommandBar.xml +2 -2
  32. package/src/js/tcmd/components/CommandItem.html +1 -1
  33. package/src/js/tcmd/components/CommandItem.js +25 -25
  34. package/src/js/tcmd/components/CommandItem.xml +2 -2
  35. package/src/js/tcmd/components/CommandPanel.html +29 -0
  36. package/src/js/tcmd/components/CommandPanel.js +78 -52
  37. package/src/js/tcmd/components/CommandPanel.xml +49 -25
  38. package/src/js/tcmd/components/Confirm.html +2 -2
  39. package/src/js/tcmd/components/EntryDetail.html +7 -7
  40. package/src/js/tcmd/components/EntryDetail.js +22 -8
  41. package/src/js/tcmd/components/FileList.html +5 -1
  42. package/src/js/tcmd/components/FileList.js +2 -1
  43. package/src/js/tcmd/components/FilePicker.html +10 -0
  44. package/src/js/tcmd/components/FilePicker.js +43 -0
  45. package/src/js/tcmd/components/FilePicker.xml +13 -0
  46. package/src/js/tcmd/components/FilePickerRow.html +6 -0
  47. package/src/js/tcmd/components/FilePickerRow.js +27 -0
  48. package/src/js/tcmd/components/FilePickerRow.xml +10 -0
  49. package/src/js/tcmd/components/FilePreview.html +14 -4
  50. package/src/js/tcmd/components/FilePreview.js +111 -12
  51. package/src/js/tcmd/components/FilePreview.xml +28 -8
  52. package/src/js/tcmd/components/FileRow.html +2 -1
  53. package/src/js/tcmd/components/FileRow.js +70 -12
  54. package/src/js/tcmd/components/FilterBar.html +1 -1
  55. package/src/js/tcmd/components/FilterBar.js +14 -4
  56. package/src/js/tcmd/components/FilterBar.xml +1 -1
  57. package/src/js/tcmd/components/Frame.js +324 -0
  58. package/src/js/tcmd/components/Help.html +15 -0
  59. package/src/js/tcmd/components/Help.js +260 -0
  60. package/src/js/tcmd/components/Help.xml +13 -4
  61. package/src/js/tcmd/components/HelpRow.html +8 -0
  62. package/src/js/tcmd/components/HelpRow.xml +7 -0
  63. package/src/js/tcmd/components/OverlayList.html +8 -0
  64. package/src/js/tcmd/components/OverlayList.js +69 -0
  65. package/src/js/tcmd/components/{CommandPalette.xml → OverlayList.xml} +2 -5
  66. package/src/js/tcmd/components/OverlayRow.html +5 -0
  67. package/src/js/tcmd/components/OverlayRow.js +47 -0
  68. package/src/js/tcmd/components/OverlayRow.xml +9 -0
  69. package/src/js/tcmd/components/Summary.html +1 -1
  70. package/src/js/tcmd/components/Summary.js +1 -6
  71. package/src/js/tcmd/components/format.js +4 -23
  72. package/src/js/tcmd/components/icons.js +176 -5
  73. package/src/js/tcmd/components/index.js +9 -5
  74. package/src/js/tcmd/components/rows.js +176 -48
  75. package/src/js/tcmd/core/base64.js +64 -0
  76. package/src/js/tcmd/core/bookmarks.js +45 -8
  77. package/src/js/tcmd/core/commands.js +978 -260
  78. package/src/js/tcmd/core/concurrency.js +59 -0
  79. package/src/js/tcmd/core/decompress.js +117 -0
  80. package/src/js/tcmd/core/directory.js +219 -93
  81. package/src/js/tcmd/core/format.js +36 -0
  82. package/src/js/tcmd/core/fs.js +438 -54
  83. package/src/js/tcmd/core/history.js +58 -0
  84. package/src/js/tcmd/core/index.js +3 -9
  85. package/src/js/tcmd/core/journal.js +93 -0
  86. package/src/js/tcmd/core/keymap.js +42 -6
  87. package/src/js/tcmd/core/operations.js +813 -48
  88. package/src/js/tcmd/core/os.js +29 -3
  89. package/src/js/tcmd/core/pane.js +33 -8
  90. package/src/js/tcmd/core/panel.js +280 -104
  91. package/src/js/tcmd/core/path.js +30 -3
  92. package/src/js/tcmd/core/permissions.js +107 -0
  93. package/src/js/tcmd/core/picker.js +156 -0
  94. package/src/js/tcmd/core/preview.js +102 -0
  95. package/src/js/tcmd/core/script.js +7 -95
  96. package/src/js/tcmd/core/selection.js +9 -1
  97. package/src/js/tcmd/core/session.js +498 -200
  98. package/src/js/tcmd/core/watch.js +153 -0
  99. package/src/js/tcmd/desktop/backend/api.js +165 -0
  100. package/src/js/tcmd/desktop/backend/fs.js +626 -0
  101. package/src/js/tcmd/desktop/backend/main.ts +9 -0
  102. package/src/js/tcmd/desktop/bookmarks.js +23 -0
  103. package/src/js/tcmd/desktop/fs.js +91 -0
  104. package/src/js/tcmd/desktop/main.js +194 -0
  105. package/src/js/tcmd/desktop/os.js +33 -0
  106. package/src/js/tcmd/index.js +0 -1
  107. package/src/js/tcmd/tui/app.js +118 -60
  108. package/src/js/tcmd/tui/catalogue.js +238 -42
  109. package/src/js/tcmd/tui/index.js +18 -19
  110. package/src/js/tcmd/tui/opentui.js +33 -0
  111. package/src/js/tcmd/tui/syntax.js +91 -0
  112. package/src/js/tcmd/tui/theme.js +32 -2
  113. package/src/js/tcmd/vendor/bzip2.js +276 -0
  114. package/src/js/tcmd/web/app.js +225 -41
  115. package/src/js/tcmd/web/bookmarks.js +36 -0
  116. package/src/js/tcmd/web/catalogue.js +81 -38
  117. package/src/js/tcmd/web/effects.js +223 -0
  118. package/src/js/tcmd/web/header.html +1 -1
  119. package/src/js/tcmd/web/header.js +15 -11
  120. package/src/js/tcmd/web/http.js +13 -0
  121. package/src/js/tcmd/web/index.js +2 -2
  122. package/src/js/tcmd/web/main.js +6 -66
  123. package/src/js/tcmd/web/opfs.js +297 -30
  124. package/src/js/tcmd/web/seed.js +4 -4
  125. package/src/js/tcmd/web/server.js +124 -21
  126. package/src/js/tcmd/web/theme.js +4 -0
  127. package/tinyjs.json +16 -0
  128. package/src/js/tcmd/components/CommandPalette.html +0 -7
  129. package/src/js/tcmd/components/CommandPalette.js +0 -58
  130. package/src/js/tcmd/components/PaletteRow.html +0 -3
  131. package/src/js/tcmd/components/PaletteRow.js +0 -43
  132. package/src/js/tcmd/components/PaletteRow.xml +0 -4
  133. package/src/js/tcmd/tui/frame.js +0 -269
  134. package/src/js/tcmd/tui/help.js +0 -32
@@ -0,0 +1,107 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-22
5
+
6
+ // Module: tcmd/core/permissions
7
+ // POSIX permission bits as the command panel edits them: the nine `u/g/o` ×
8
+ // `r/w/x` toggles `chmod` binds its MODE argument to. The tables are the single
9
+ // source of truth for the display order, the octal values and the `rwx`
10
+ // rendering; the panel projects them into rows and the argument row component
11
+ // renders the three cells of each class.
12
+ //
13
+ // Everything here is pure: the binding is the caller's, and the numeric/octal
14
+ // conversions do not mutate it.
15
+
16
+ // Constant: PERMISSION_CLASSES
17
+ // The permission classes in display order (owner, group, other).
18
+ const PERMISSION_CLASSES = [
19
+ { id: "u", label: "user" },
20
+ { id: "g", label: "group" },
21
+ { id: "o", label: "other" },
22
+ ]
23
+
24
+ // Constant: PERMISSION_BITS
25
+ // The bits of each class in display order (read, write, execute).
26
+ const PERMISSION_BITS = [
27
+ { id: "r", label: "read" },
28
+ { id: "w", label: "write" },
29
+ { id: "x", label: "execute" },
30
+ ]
31
+
32
+ // Constant: PERMISSION_VALUES
33
+ // The octal value of each `class:bit` key.
34
+ const PERMISSION_VALUES = {
35
+ "u:r": 0o400, "u:w": 0o200, "u:x": 0o100,
36
+ "g:r": 0o040, "g:w": 0o020, "g:x": 0o010,
37
+ "o:r": 0o004, "o:w": 0o002, "o:x": 0o001,
38
+ }
39
+
40
+ // Function: permissionItems
41
+ // The nine `u/g/o` × `r/w/x` toggles for a permission argument, seeded from an
42
+ // existing file mode. Each item is a checkbox in the panel's item list: its
43
+ // `key` is the stable `class:bit` key the token builder reads back.
44
+ function permissionItems(mode) {
45
+ const bits = Number(mode) & 0o777
46
+ const items = []
47
+ for (const permissionClass of PERMISSION_CLASSES) {
48
+ for (const bit of PERMISSION_BITS) {
49
+ const key = `${permissionClass.id}:${bit.id}`
50
+ items.push({
51
+ key,
52
+ name: bit.label,
53
+ parent: permissionClass.label,
54
+ isDir: false,
55
+ selected: (bits & PERMISSION_VALUES[key]) !== 0,
56
+ })
57
+ }
58
+ }
59
+ return items
60
+ }
61
+
62
+ // Function: permissionMode
63
+ // The numeric mode a permission binding currently selects.
64
+ function permissionMode(binding) {
65
+ let mode = 0
66
+ for (const item of binding?.items ?? []) {
67
+ if (item.selected) mode |= PERMISSION_VALUES[item.key] ?? 0
68
+ }
69
+ return mode
70
+ }
71
+
72
+ // Function: permissionOctal
73
+ // The three-digit octal token a permission binding contributes to argv.
74
+ function permissionOctal(binding) {
75
+ return permissionMode(binding).toString(8).padStart(3, "0")
76
+ }
77
+
78
+ // Function: permissionSymbolic
79
+ // The `rwx`-style rendering of a numeric mode.
80
+ function permissionSymbolic(mode) {
81
+ return PERMISSION_CLASSES.map((permissionClass) =>
82
+ PERMISSION_BITS.map((bit) =>
83
+ mode & PERMISSION_VALUES[`${permissionClass.id}:${bit.id}`] ? bit.id : "-",
84
+ ).join(""),
85
+ ).join("")
86
+ }
87
+
88
+ // Function: permissionText
89
+ // The binding's value as the panel shows it: the octal mode and its symbolic
90
+ // form (`644 · rw-r--r--`).
91
+ function permissionText(binding) {
92
+ const mode = permissionMode(binding)
93
+ return `${permissionOctal(binding)} · ${permissionSymbolic(mode)}`
94
+ }
95
+
96
+ export {
97
+ PERMISSION_BITS,
98
+ PERMISSION_CLASSES,
99
+ PERMISSION_VALUES,
100
+ permissionItems,
101
+ permissionMode,
102
+ permissionOctal,
103
+ permissionSymbolic,
104
+ permissionText,
105
+ }
106
+
107
+ // EOF
@@ -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
@@ -4,13 +4,13 @@
4
4
  // Created: 2026-09-27
5
5
 
6
6
  // Module: tcmd/core/script
7
- // The scripting language behind the command palette's scripting mode (`:`):
8
- // line tokenizer, operation parser and runner, plus the completion rows the
9
- // palette lists. A line starts with an operation name (or the `!` alias for
10
- // `run`); arguments may be filesets ($A, $B, $FILE), quoted paths, `~` or
11
- // paths relative to the active pane.
7
+ // The TinyCmd scripting language: line tokenizer, operation parser and runner.
8
+ // A line starts with an operation name (or the `!` alias for `run`); arguments
9
+ // may be filesets ($A, $B, $FILE), quoted paths, `~` or paths relative to the
10
+ // active pane. The command panel runs operations structurally and delegates to
11
+ // the same `runLine` through `runOperation`.
12
12
 
13
- import { getOperation, getOperations, humanSize, runOperation } from "./operations.js"
13
+ import { getOperation, runOperation } from "./operations.js"
14
14
 
15
15
  // Function: tokenize
16
16
  // Whitespace tokenizer with single/double quotes and backslash escapes.
@@ -93,94 +93,6 @@ async function runLine(session, line, options = {}) {
93
93
  return runOperation(session, parsed.op, parsed.args, { ...options, line })
94
94
  }
95
95
 
96
- // Function: scriptItems
97
- // Palette rows for scripting mode: the session's available filesets first,
98
- // then every operation, then matching entries of the active pane (path
99
- // completion). Rows carry the full query replacement `completion` for Tab.
100
- function scriptItems(query, filesets, context = {}) {
101
- const text = `${query ?? ""}`
102
- const q = text.trim().toLowerCase()
103
- const head = q.split(/\s+/)[0] ?? ""
104
- const rows = []
105
- const typingFileset = lastToken(q).startsWith("$")
106
- for (const set of filesets ?? []) {
107
- const id = set.id.toLowerCase()
108
- if (head && !id.includes(head) && !q.includes(id) && !typingFileset) continue
109
- rows.push({
110
- id: set.id,
111
- title: set.id,
112
- kind: "fileset",
113
- detail: `${set.count} file${set.count === 1 ? "" : "s"}`,
114
- completion: completeFileset(text, set.id),
115
- })
116
- }
117
- for (const op of getOperations()) {
118
- if (op.hidden) continue
119
- const names = [op.name, ...(op.aliases ?? [])]
120
- const matchesName = !head || names.some((name) => name.toLowerCase().includes(head))
121
- if (!matchesName && !op.signature.toLowerCase().includes(q)) continue
122
- rows.push({
123
- id: `op:${op.name}`,
124
- title: op.name,
125
- kind: "op",
126
- detail: op.signature,
127
- description: op.description,
128
- completion: q.includes(" ") ? text.replace(/^\S+/, op.name) : `${op.name} `,
129
- })
130
- }
131
- const last = lastToken(q)
132
- const token = lastToken(text)
133
- if (token && !token.startsWith("$")) {
134
- const at = token.lastIndexOf("/")
135
- const dirPart = at >= 0 ? token.slice(0, at + 1) : ""
136
- const prefix = (at >= 0 ? token.slice(at + 1) : token).toLowerCase()
137
- for (const entry of context.entries ?? []) {
138
- if (!entry.name.toLowerCase().startsWith(prefix)) continue
139
- const name = entry.isDir ? `${entry.name}/` : entry.name
140
- rows.push({
141
- id: `path:${entry.path}`,
142
- title: name,
143
- kind: "path",
144
- detail: entry.isDir ? "dir" : humanSize(entry.size),
145
- completion: replaceLastToken(text, `${dirPart}${name}`),
146
- })
147
- }
148
- }
149
- return rows
150
- }
151
-
152
- // Function: lastToken
153
- // The last whitespace-separated token, case preserved.
154
- function lastToken(text) {
155
- const match = /(\S+)\s*$/.exec(`${text ?? ""}`)
156
- return match ? match[1] : ""
157
- }
158
-
159
- // Function: completionToken
160
- // For a query whose last token contains "/", the directory part to read and
161
- // the name prefix being completed; null when the token is not a path.
162
- function completionToken(query) {
163
- const token = lastToken(query)
164
- if (!token || token.startsWith("$")) return null
165
- const at = token.lastIndexOf("/")
166
- if (at < 0) return null
167
- return { dir: token.slice(0, at) || "/", prefix: token.slice(at + 1) }
168
- }
169
-
170
- // Function: replaceLastToken
171
- // Replaces the (possibly empty) last whitespace-separated token.
172
- function replaceLastToken(text, replacement) {
173
- const match = /(\S*)\s*$/.exec(text)
174
- return `${text.slice(0, match.index)}${replacement}`
175
- }
176
-
177
- // Function: completeFileset
178
- // Replaces the `$…` token being typed, or appends the fileset.
179
- function completeFileset(text, id) {
180
- if (/\$\w*$/.test(text.trimEnd())) return replaceLastToken(text, id)
181
- return `${text} ${id}`.trim()
182
- }
183
-
184
- export { completionToken, parseLine, runLine, scriptItems, tokenize }
96
+ export { parseLine, runLine, tokenize }
185
97
 
186
98
  // EOF
@@ -26,6 +26,14 @@ function currentEntry(entries, cursor) {
26
26
  return entries[cursor]
27
27
  }
28
28
 
29
+ // Function: indexOfPath
30
+ // The index of the entry whose path is `path`, or -1. Navigations that must
31
+ // land on a known element use it: the directory `up` stepped out of, the
32
+ // current directory in a mirror's listing.
33
+ function indexOfPath(entries, path) {
34
+ return entries.findIndex((entry) => entry.path === path)
35
+ }
36
+
29
37
  // Function: toggleMark
30
38
  // Returns a new Set with `path` toggled in `marks`.
31
39
  function toggleMark(marks, path) {
@@ -38,6 +46,6 @@ function toggleMark(marks, path) {
38
46
  return next
39
47
  }
40
48
 
41
- export { clampCursor, currentEntry, moveCursor, toggleMark }
49
+ export { clampCursor, currentEntry, indexOfPath, moveCursor, toggleMark }
42
50
 
43
51
  // EOF