@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
@@ -14,6 +14,10 @@ const webTheme = {
14
14
  currentClass: "is-current",
15
15
  dirClass: "is-dir",
16
16
  errorClass: "is-error",
17
+ execClass: "is-exec",
18
+ focusedClass: "is-focused",
19
+ lucideIcons: true,
20
+ readonlyClass: "is-readonly",
17
21
  rootClass: "is-root",
18
22
  selectedClass: "is-selected",
19
23
  }
package/tinyjs.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "tcmd",
3
+ "title": "TinyCommand",
4
+ "id": "io.github.sebastien.tcmd",
5
+ "version": "0.1.4",
6
+ "minTinyjsVersion": "0.57.0",
7
+ "size": "1280x800",
8
+ "chrome": { "menu": false },
9
+ "backend": "src/js/tcmd/desktop/backend/main.ts",
10
+ "openFolders": true,
11
+ "frontend": {
12
+ "build": "bun run desktop:bundle",
13
+ "dir": "desktop/dist",
14
+ "dist": "desktop/dist"
15
+ }
16
+ }
@@ -1,7 +0,0 @@
1
- <section class="palette" when="open">
2
- <div class="palette-line">
3
- <span class="palette-sign" out="sign"></span>
4
- <input ref="query" inout="query" class="palette-input" placeholder="Command..." on:enter="confirm" />
5
- </div>
6
- <ul class="palette-list" out="rows"></ul>
7
- </section>
@@ -1,58 +0,0 @@
1
- // Project: TinyCommand
2
- // Author: Sebastien Pierre
3
- // License: BSD
4
- // Created: 2026-09-26
5
-
6
- // Module: tcmd/components/CommandPalette
7
- // CommandPalette: popup listing commands filtered by the typed query. In
8
- // scripting mode it lists operations and filesets and prefixes the input with
9
- // `:`. The row logic lives in `PaletteRow.js`; this parent maps its `items` to
10
- // rows.
11
- //
12
- // Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
13
- // (footer), `textHeadingFg` (exposed as the `titleColor` behavior),
14
- // `rows(data)` returning the keyed rows and `scrollTop(self, data)` (TUI
15
- // viewport tracking).
16
-
17
- import { unwrap } from "select/state/cells"
18
-
19
- // Function: paletteSign
20
- // The prompt shown before the input; overridable per renderer.
21
- function paletteSign(mode) {
22
- return unwrap(mode) === "script" ? ":" : "›"
23
- }
24
-
25
- // Function: paletteTitle
26
- function paletteTitle(mode) {
27
- return unwrap(mode) === "script" ? "script" : "palette"
28
- }
29
-
30
- // Function: createBehaviors
31
- // Builds the CommandPalette behavior map for the given renderer environment.
32
- function createBehaviors(env) {
33
- const behaviors = {
34
- bg: () => env.overlayBg,
35
- borderFg: () => env.borderPrimaryActiveFg,
36
- confirm: (self, data) => self.pub("palette:confirm", unwrap(data.query) ?? ""),
37
- focused: (_self, data) => Boolean(unwrap(data.open)),
38
- footer: (_self, data) =>
39
- unwrap(data.mode) === "script"
40
- ? "Tab complete ↵ run Esc close"
41
- : "↑↓ move Tab complete ↵ run Esc close",
42
- footerFg: () => env.textSecondaryFg,
43
- sign: (_self, data) => paletteSign(data.mode),
44
- title: (_self, data) => paletteTitle(data.mode),
45
- titleColor: () => env.textHeadingFg,
46
- }
47
- if (env?.rows) {
48
- behaviors.rows = (_self, data) => env.rows(self, data)
49
- }
50
- if (env?.scrollTop) {
51
- behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
52
- }
53
- return behaviors
54
- }
55
-
56
- export { createBehaviors, paletteSign, paletteTitle }
57
-
58
- // EOF
@@ -1,3 +0,0 @@
1
- <li class="palette-item" out:class="rowClass" on:click="item.id!palette:run">
2
- <span out="paletteLabel"></span>
3
- </li>
@@ -1,43 +0,0 @@
1
- // Project: TinyCommand
2
- // Author: Sebastien Pierre
3
- // License: BSD
4
- // Created: 2026-09-26
5
-
6
- // Module: tcmd/components/PaletteRow
7
- // Command-palette row behaviors. Read `{ item: command, index, selected }`, the
8
- // same bag shape in both engines. Selection is snapshotted by the row builder
9
- // into a plain `selectedRow` boolean and read through the shared `isSelected`
10
- // predicate, with the `selected` cell as fallback for hand-built bags. Script
11
- // rows carry a `detail` (operation signature, fileset count) appended to the
12
- // label.
13
- //
14
- // Environment: `accentFg`, `textFg`, `selectionBg` (TUI row background),
15
- // `selectedClass` (web).
16
-
17
- import { isSelected } from "./rows.js"
18
-
19
- // Function: paletteLabel
20
- // "Switch [Tab]" for commands; script rows append their detail.
21
- function paletteLabel(command) {
22
- if (!command) return ""
23
- const keys = command.keys ? ` [${command.keys}]` : ""
24
- const detail = command.detail ? ` ${command.detail}` : ""
25
- return `${command.title}${keys}${detail}`
26
- }
27
-
28
- // Function: createBehaviors
29
- // Builds the PaletteRow behavior map for the given renderer environment.
30
- function createBehaviors(env) {
31
- return {
32
- paletteLabel: (_self, data) => paletteLabel(data.item),
33
- rowBg: (_self, data) => (isSelected(data) ? env.selectionBg : undefined),
34
- rowClass: (_self, data) => (isSelected(data) ? env.selectedClass : null),
35
- rowFg: (_self, data) => (isSelected(data) ? env.accentFg : env.textFg),
36
- // TUI: a marker so the selection is not color-only.
37
- rowMarker: (_self, data) => (isSelected(data) ? ">" : " "),
38
- }
39
- }
40
-
41
- export { createBehaviors, paletteLabel }
42
-
43
- // EOF
@@ -1,4 +0,0 @@
1
- <box flexDirection="row" height="1" out:backgroundColor="rowBg" on:click="item.id!palette:run">
2
- <text out="rowMarker" width="2" out:fg="rowFg"></text>
3
- <text out="paletteLabel" flexGrow="1" out:fg="rowFg" wrapMode="none" truncate="true"></text>
4
- </box>
@@ -1,269 +0,0 @@
1
- // Project: TinyCommand
2
- // Author: Sebastien Pierre
3
- // License: BSD
4
- // Created: 2026-10-02
5
-
6
- // Module: tcmd/tui/frame
7
- // TUI-only presentation behaviors for the workspace chrome: the responsive
8
- // breakpoints driven by the `uiSize` cell, the pane borders, header rules,
9
- // the command-bar geometry and the help overlay state. Everything here is
10
- // injected into the App binding; none of it lives in the shared
11
- // `components/` logic.
12
-
13
- import { unwrap } from "select/state/cells"
14
- import { DEFAULT_KEYMAP } from "../core/keymap.js"
15
- import { getCommands } from "../core/commands.js"
16
- import { commandBarMarksItem, commandBarRowCount, commandBarStatus } from "../components/CommandBar.js"
17
-
18
- // Below this the two-pane workspace is replaced by a resize hint.
19
- const MIN_WIDTH = 64
20
- const MIN_HEIGHT = 16
21
- // At or above this the inspector can sit beside the panes; below it becomes a
22
- // fullscreen overlay.
23
- const INSPECTOR_INLINE_WIDTH = 100
24
-
25
- // Function: sizeOf
26
- // Reads the viewport size from the App data, falling back to the renderer's
27
- // own dimensions so the gates work even when no `uiSize` cell is injected
28
- // (tests, web). Returns `null` when neither is known.
29
- function sizeOf(data, self) {
30
- const size = unwrap(data?.uiSize)
31
- if (size && (size.width || size.height)) return size
32
- const width = Math.floor(self?.ctx?.width ?? 0)
33
- const height = Math.floor(self?.ctx?.height ?? 0)
34
- if (!width && !height) return null
35
- return { width, height }
36
- }
37
-
38
- // Function: isNarrow
39
- // True when the viewport is known and too narrow for the inline inspector.
40
- function isNarrow(data, self) {
41
- const size = sizeOf(data, self)
42
- return Boolean(size && (size.width ?? 0) > 0 && size.width < INSPECTOR_INLINE_WIDTH)
43
- }
44
-
45
- // Function: commandBarRowsFor
46
- // The entries the bar currently shows: the context hints or the `bar`
47
- // commands, plus the active pane's marked count as a trailing entry.
48
- function commandBarRowsFor(data) {
49
- const items = unwrap(data?.commandBarItems) ?? []
50
- const marks = commandBarMarksItem(data)
51
- return marks ? [...items, marks] : items
52
- }
53
-
54
- // Function: commandBarLines
55
- // Rows the wrapped command bar occupies at the current viewport (the bar
56
- // is inset one column from the viewport edge).
57
- function commandBarLines(data, self) {
58
- const width = Math.floor(sizeOf(data, self)?.width ?? 0)
59
- if (width <= 0) return 1
60
- return commandBarRowCount(commandBarRowsFor(data), width - 1)
61
- }
62
-
63
- // Function: paneListHeight
64
- // Rows available to a pane's listing: the viewport height minus the identity
65
- // bar (1), the pane's own border (2), the pane header + rule (2), the entry
66
- // detail (2), the summary (1) and the wrapped command bar — eight chrome rows
67
- // plus the bar. Fed to the windowed `filelist` so a large directory mounts
68
- // only the visible slice. `0` means "unknown — render everything to measure".
69
- function paneListHeight(data, self) {
70
- const height = Math.floor(sizeOf(data, self)?.height ?? 0)
71
- if (height <= 0) return 0
72
- // A notice or error swaps the keys for a one-line status.
73
- const bar = commandBarStatus(data) ? 1 : commandBarLines(data, self)
74
- return Math.max(1, height - 8 - bar)
75
- }
76
-
77
- // Function: mixHex
78
- // Linear interpolation between two `#rrggbb` colours at `t` in [0, 1]. A
79
- // missing colour (the mono theme) yields `undefined` so the whole run falls
80
- // back to the terminal default.
81
- function mixHex(from, to, t) {
82
- if (!from || !to) return undefined
83
- const channel = (index) => {
84
- const a = Number.parseInt(from.slice(index, index + 2), 16)
85
- const b = Number.parseInt(to.slice(index, index + 2), 16)
86
- return Math.round(a + (b - a) * t)
87
- .toString(16)
88
- .padStart(2, "0")
89
- }
90
- return `#${channel(1)}${channel(3)}${channel(5)}`
91
- }
92
-
93
- // Function: shineOf
94
- // One `{ letter, fg }` per letter of a brand word, interpolating `from` → `to`
95
- // across it so the word reads as a gradient; a missing colour (mono theme)
96
- // leaves every `fg` undefined and the run falls back to the terminal default.
97
- function shineOf(letters, from, to) {
98
- return `${letters}`.split("").map((letter, index, all) => ({
99
- letter,
100
- fg: mixHex(from, to, index / (all.length - 1)),
101
- }))
102
- }
103
-
104
- // Function: createFrameBehaviors
105
- // Behaviors for the App root and its chrome.
106
- function createFrameBehaviors(env) {
107
- const tiny = shineOf("Tiny", env.theme.brandTinyFrom, env.theme.brandTinyTo)
108
- const cmd = shineOf("Cmd", env.theme.brandCmdFrom, env.theme.brandCmdTo)
109
- return {
110
- // Identity bar: "Tiny" shimmers along the brand blues, "Cmd" along a
111
- // brighter orange than the mark accent, and the host trail follows in
112
- // the heading colour. Everything else on screen belongs to the user's
113
- // files; this one row is the app's own.
114
- brandT: () => tiny[0].letter,
115
- brandTFg: () => tiny[0].fg,
116
- brandI: () => tiny[1].letter,
117
- brandIFg: () => tiny[1].fg,
118
- brandN: () => tiny[2].letter,
119
- brandNFg: () => tiny[2].fg,
120
- brandY: () => tiny[3].letter,
121
- brandYFg: () => tiny[3].fg,
122
- brandC: () => cmd[0].letter,
123
- brandCFg: () => cmd[0].fg,
124
- brandM: () => cmd[1].letter,
125
- brandMFg: () => cmd[1].fg,
126
- brandD: () => cmd[2].letter,
127
- brandDFg: () => cmd[2].fg,
128
- topBar: (_self, data) => {
129
- const host = `${unwrap(data.appHost) ?? ""}`.trim()
130
- return host ? ` - ${host} ` : " "
131
- },
132
- topBarFg: () => env.theme.textHeadingFg,
133
-
134
- // Responsive gates. `uiSize` is absent in tests and the web, so the
135
- // workspace renders normally there.
136
- tooSmall: (self, data) => {
137
- const size = sizeOf(data, self)
138
- if (!size) return false
139
- return (size.width ?? MIN_WIDTH) < MIN_WIDTH || (size.height ?? MIN_HEIGHT) < MIN_HEIGHT
140
- },
141
- minSizeTitle: () => "Terminal too small",
142
- minSizeHint: (self, data) => {
143
- const size = sizeOf(data, self) ?? { width: 0, height: 0 }
144
- return `need ${MIN_WIDTH}x${MIN_HEIGHT}, have ${size.width ?? 0}x${size.height ?? 0}`
145
- },
146
- // The min-size gate text uses the heading/secondary text tokens.
147
- titleFg: () => env.theme.textHeadingFg,
148
- dimFg: () => env.theme.textSecondaryFg,
149
-
150
- // Each pane is a complete box. Both draw all four edges (the middle
151
- // shows two adjacent rules) so each pane's border colour can track its
152
- // own focus independently — a shared edge would be owned by one pane
153
- // and never highlight when the other is active.
154
- leftBorder: () => ["top", "left", "bottom", "right"],
155
- rightBorder: () => ["top", "left", "bottom", "right"],
156
- // The header rule separates the pane header row from the listing body.
157
- headRule: () => ["top"],
158
-
159
- // The breadcrumb yields the pane header row to the filter while a
160
- // filter is being edited or applied.
161
- leftFilterOn: (_self, data) => Boolean(unwrap(data.leftFilter)),
162
- rightFilterOn: (_self, data) => Boolean(unwrap(data.rightFilter)),
163
-
164
- // The inline inspector shows when the preview is on and the viewport is
165
- // wide enough; otherwise `v` opens the fullscreen viewer.
166
- inspectorNarrow: (self, data) => isNarrow(data, self),
167
- showInspectorInline: (self, data) => Boolean(unwrap(data.showPreview)) && !isNarrow(data, self),
168
- showViewer: (self, data) => Boolean(unwrap(data.viewerOpen)) && isNarrow(data, self),
169
-
170
- // Help overlay visibility; the content lives in `tui/Help.js` so the
171
- // overlay has a single source of truth.
172
- helpOpen: (_self, data) => Boolean(unwrap(data.helpOpen)),
173
-
174
- // The windowed listing height for both panes (they share the row).
175
- listViewport: (self, data) => paneListHeight(data, self),
176
-
177
- // The command bar's height: one line for a status, otherwise as many
178
- // rows as the wrapped bracket shorthands need.
179
- barHeight: (self, data) => (commandBarStatus(data) ? 1 : commandBarLines(data, self)),
180
- }
181
- }
182
-
183
- // Function: keyGlyph
184
- // Human-readable glyph for a keymap chord ("↑", "⇧Tab", "^W", "PgUp").
185
- function keyGlyph(chord) {
186
- const name = chord.key
187
- const named = {
188
- up: "↑",
189
- down: "↓",
190
- left: "←",
191
- right: "→",
192
- return: "↵",
193
- enter: "↵",
194
- space: "Space",
195
- tab: "Tab",
196
- pageup: "PgUp",
197
- pagedown: "PgDn",
198
- escape: "Esc",
199
- backspace: "Bksp",
200
- home: "Home",
201
- end: "End",
202
- }
203
- let base = named[name] ?? (name.length === 1 ? name.toUpperCase() : `${name}`)
204
- if (chord.ctrl) base = `^${base}`
205
- if (chord.meta) base = `⌘${base}`
206
- if (chord.shift && name !== "tab") base = `⇧${base}`
207
- return base
208
- }
209
-
210
- // Constant: GROUP_ORDER / GROUP_TITLES
211
- // Help sections, in display order.
212
- const GROUP_ORDER = ["nav", "pane", "list", "bookmark", "view", "file", "panel", "palette", "filter", "app"]
213
- const GROUP_TITLES = {
214
- app: "Session",
215
- bookmark: "Bookmarks",
216
- file: "Files",
217
- filter: "Filter",
218
- list: "Selection",
219
- nav: "Navigation",
220
- palette: "Commands",
221
- panel: "Command panel",
222
- pane: "Panes",
223
- view: "View",
224
- }
225
-
226
- // Function: helpLines
227
- // Builds the categorized key reference from the keymap and the command
228
- // registry, so it can never drift from the actual bindings.
229
- function helpLines() {
230
- const byId = new Map(getCommands().map((command) => [command.id, command]))
231
- const groups = new Map()
232
- const seen = new Set()
233
- for (const binding of DEFAULT_KEYMAP) {
234
- const command = byId.get(binding.command)
235
- if (!command) continue
236
- const group = binding.command.split(".")[0]
237
- if (!groups.has(group)) groups.set(group, new Map())
238
- const rows = groups.get(group)
239
- if (!rows.has(binding.command)) rows.set(binding.command, { keys: [], title: command.title })
240
- const row = rows.get(binding.command)
241
- const glyph = keyGlyph(binding)
242
- if (!row.keys.includes(glyph) && !seen.has(`${binding.command}:${glyph}`)) {
243
- row.keys.push(glyph)
244
- seen.add(`${binding.command}:${glyph}`)
245
- }
246
- }
247
- const lines = []
248
- for (const group of GROUP_ORDER) {
249
- const rows = groups.get(group)
250
- if (!rows) continue
251
- lines.push(GROUP_TITLES[group] ?? group)
252
- for (const row of rows.values()) {
253
- lines.push(` ${row.keys.join("/").padEnd(9)} ${row.title}`)
254
- }
255
- lines.push("")
256
- }
257
- return lines
258
- }
259
-
260
- export {
261
- commandBarLines,
262
- createFrameBehaviors,
263
- helpLines,
264
- INSPECTOR_INLINE_WIDTH,
265
- isNarrow,
266
- keyGlyph,
267
- sizeOf,
268
- }
269
- // EOF
@@ -1,32 +0,0 @@
1
- // Project: TinyCommand
2
- // Author: Sebastien Pierre
3
- // License: BSD
4
- // Created: 2026-10-02
5
-
6
- // Module: tcmd/tui/help
7
- // TUI-only behavior for the `?` key reference overlay. The content is built
8
- // from the live keymap and command registry (see `tui/frame.js`), so it
9
- // cannot drift from the actual bindings.
10
-
11
- import { unwrap } from "select/state/cells"
12
- import { helpLines } from "./frame.js"
13
-
14
- // Function: createBehaviors
15
- // Builds the Help behavior map for the given TUI environment.
16
- function createBehaviors(env) {
17
- return {
18
- bg: () => env.theme.surfaceBg,
19
- borderFg: () => env.theme.borderPrimaryActiveFg,
20
- dimFg: () => env.theme.textSecondaryFg,
21
- hint: () => " Esc / ? close ",
22
- open: (_self, data) => Boolean(unwrap(data.open)),
23
- text: () => helpLines().join("\n"),
24
- textFg: () => env.theme.textFg,
25
- title: () => " Keys ",
26
- titleFg: () => env.theme.textHeadingFg,
27
- }
28
- }
29
-
30
- export { createBehaviors }
31
-
32
- // EOF