@ssebastien/tcmd 0.0.0-stage → 0.1.0

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 (95) hide show
  1. package/README.md +234 -2
  2. package/bin/tcmd +7 -0
  3. package/package.json +37 -5
  4. package/src/js/tcmd/components/App.html +28 -0
  5. package/src/js/tcmd/components/App.js +355 -0
  6. package/src/js/tcmd/components/App.xml +98 -0
  7. package/src/js/tcmd/components/BookmarkList.html +12 -0
  8. package/src/js/tcmd/components/BookmarkList.js +73 -0
  9. package/src/js/tcmd/components/BookmarkList.xml +18 -0
  10. package/src/js/tcmd/components/BookmarkRow.html +5 -0
  11. package/src/js/tcmd/components/BookmarkRow.js +40 -0
  12. package/src/js/tcmd/components/BookmarkRow.xml +10 -0
  13. package/src/js/tcmd/components/Breadcrumbs.js +27 -0
  14. package/src/js/tcmd/components/CommandArgRow.js +90 -0
  15. package/src/js/tcmd/components/CommandArgRow.xml +5 -0
  16. package/src/js/tcmd/components/CommandBar.html +4 -0
  17. package/src/js/tcmd/components/CommandBar.js +126 -0
  18. package/src/js/tcmd/components/CommandBar.xml +4 -0
  19. package/src/js/tcmd/components/CommandItem.html +3 -0
  20. package/src/js/tcmd/components/CommandItem.js +78 -0
  21. package/src/js/tcmd/components/CommandItem.xml +6 -0
  22. package/src/js/tcmd/components/CommandPalette.html +7 -0
  23. package/src/js/tcmd/components/CommandPalette.js +58 -0
  24. package/src/js/tcmd/components/CommandPalette.xml +16 -0
  25. package/src/js/tcmd/components/CommandPanel.js +98 -0
  26. package/src/js/tcmd/components/CommandPanel.xml +35 -0
  27. package/src/js/tcmd/components/Confirm.html +3 -0
  28. package/src/js/tcmd/components/Confirm.js +36 -0
  29. package/src/js/tcmd/components/Confirm.xml +7 -0
  30. package/src/js/tcmd/components/Crumb.html +4 -0
  31. package/src/js/tcmd/components/Crumb.js +48 -0
  32. package/src/js/tcmd/components/Crumb.xml +4 -0
  33. package/src/js/tcmd/components/EntryDetail.html +15 -0
  34. package/src/js/tcmd/components/EntryDetail.js +194 -0
  35. package/src/js/tcmd/components/EntryDetail.xml +16 -0
  36. package/src/js/tcmd/components/FileList.html +1 -0
  37. package/src/js/tcmd/components/FileList.js +41 -0
  38. package/src/js/tcmd/components/FileList.xml +7 -0
  39. package/src/js/tcmd/components/FilePreview.html +5 -0
  40. package/src/js/tcmd/components/FilePreview.js +110 -0
  41. package/src/js/tcmd/components/FilePreview.xml +10 -0
  42. package/src/js/tcmd/components/FileRow.html +5 -0
  43. package/src/js/tcmd/components/FileRow.js +135 -0
  44. package/src/js/tcmd/components/FileRow.xml +10 -0
  45. package/src/js/tcmd/components/FilterBar.html +6 -0
  46. package/src/js/tcmd/components/FilterBar.js +50 -0
  47. package/src/js/tcmd/components/FilterBar.xml +6 -0
  48. package/src/js/tcmd/components/Help.xml +11 -0
  49. package/src/js/tcmd/components/PaletteRow.html +3 -0
  50. package/src/js/tcmd/components/PaletteRow.js +43 -0
  51. package/src/js/tcmd/components/PaletteRow.xml +4 -0
  52. package/src/js/tcmd/components/RowList.html +1 -0
  53. package/src/js/tcmd/components/RowList.xml +1 -0
  54. package/src/js/tcmd/components/Summary.html +3 -0
  55. package/src/js/tcmd/components/Summary.js +77 -0
  56. package/src/js/tcmd/components/Summary.xml +3 -0
  57. package/src/js/tcmd/components/bind.js +20 -0
  58. package/src/js/tcmd/components/format.js +155 -0
  59. package/src/js/tcmd/components/icons.js +185 -0
  60. package/src/js/tcmd/components/index.js +41 -0
  61. package/src/js/tcmd/components/rows.js +212 -0
  62. package/src/js/tcmd/core/bookmarks.js +175 -0
  63. package/src/js/tcmd/core/commands.js +930 -0
  64. package/src/js/tcmd/core/directory.js +194 -0
  65. package/src/js/tcmd/core/entries.js +88 -0
  66. package/src/js/tcmd/core/fs.js +266 -0
  67. package/src/js/tcmd/core/index.js +39 -0
  68. package/src/js/tcmd/core/keymap.js +60 -0
  69. package/src/js/tcmd/core/operations.js +453 -0
  70. package/src/js/tcmd/core/os.js +128 -0
  71. package/src/js/tcmd/core/pane.js +74 -0
  72. package/src/js/tcmd/core/panel.js +383 -0
  73. package/src/js/tcmd/core/path.js +57 -0
  74. package/src/js/tcmd/core/script.js +186 -0
  75. package/src/js/tcmd/core/selection.js +43 -0
  76. package/src/js/tcmd/core/session.js +405 -0
  77. package/src/js/tcmd/index.js +53 -0
  78. package/src/js/tcmd/tui/app.js +140 -0
  79. package/src/js/tcmd/tui/catalogue.js +300 -0
  80. package/src/js/tcmd/tui/frame.js +269 -0
  81. package/src/js/tcmd/tui/help.js +32 -0
  82. package/src/js/tcmd/tui/index.js +104 -0
  83. package/src/js/tcmd/tui/theme.js +177 -0
  84. package/src/js/tcmd/vendor/fzy.js +243 -0
  85. package/src/js/tcmd/web/app.js +154 -0
  86. package/src/js/tcmd/web/catalogue.js +159 -0
  87. package/src/js/tcmd/web/header.html +1 -0
  88. package/src/js/tcmd/web/header.js +38 -0
  89. package/src/js/tcmd/web/http.js +79 -0
  90. package/src/js/tcmd/web/index.js +22 -0
  91. package/src/js/tcmd/web/main.js +151 -0
  92. package/src/js/tcmd/web/opfs.js +282 -0
  93. package/src/js/tcmd/web/seed.js +90 -0
  94. package/src/js/tcmd/web/server.js +191 -0
  95. package/src/js/tcmd/web/theme.js +23 -0
@@ -0,0 +1,98 @@
1
+ <!-- Root wrapper: the workspace, plus overlays positioned against the whole
2
+ viewport. The overlays stay INSIDE this root box so keypress routing (which
3
+ binds to it once) sees them as its focused subtree. -->
4
+ <box flexDirection="column" width="100%" height="100%" on:keypress="keys">
5
+
6
+ <!-- Workspace wrapper: always mounted, so the root box has one stable
7
+ child whether the workspace or the min-size gate shows. -->
8
+ <box flexGrow="1" flexDirection="column" overflow="hidden">
9
+
10
+ <!-- Identity bar: the one visible trace of TinyCmd itself (the host
11
+ aside, everything else on screen is the user's files). "Tiny"
12
+ shimmers blue letter by letter, "Cmd" shimmers orange. -->
13
+ <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
14
+ <text out="brandT" out:fg="brandTFg" wrapMode="none"></text>
15
+ <text out="brandI" out:fg="brandIFg" wrapMode="none"></text>
16
+ <text out="brandN" out:fg="brandNFg" wrapMode="none"></text>
17
+ <text out="brandY" out:fg="brandYFg" wrapMode="none"></text>
18
+ <text out="brandC" out:fg="brandCFg" wrapMode="none"></text>
19
+ <text out="brandM" out:fg="brandMFg" wrapMode="none"></text>
20
+ <text out="brandD" out:fg="brandDFg" wrapMode="none"></text>
21
+ <text out="topBar" out:fg="topBarFg" wrapMode="none"></text>
22
+ </box>
23
+
24
+ <!-- Minimum-size gate: below the breakpoint the workspace is unusable. -->
25
+ <box when="tooSmall" flexGrow="1" alignItems="center" justifyContent="center" flexDirection="column" rowGap="1">
26
+ <text out="minSizeTitle" out:fg="titleFg"></text>
27
+ <text out="minSizeHint" out:fg="dimFg"></text>
28
+ </box>
29
+
30
+ <!-- Two boxed panes plus the inline inspector. -->
31
+ <box when="!tooSmall" flexDirection="column" flexGrow="1" overflow="hidden">
32
+ <box flexDirection="row" flexGrow="1" overflow="hidden">
33
+ <box flexDirection="column" flexGrow="1" border borderStyle="single"
34
+ out:border="leftBorder" out:borderColor="leftBorderFg"
35
+ on:click="leftId!pane:activate">
36
+ <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
37
+ <box when="!leftFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
38
+ <breadcrumbs in:segments="leftSegments" in:paneId="leftId" height="1"></breadcrumbs>
39
+ </box>
40
+ <filterbar in:query="leftFilter" in:paneId="leftId" in:filtering="filtering" in:entries="leftEntries"></filterbar>
41
+ </box>
42
+ <box height="1" out:border="headRule" out:borderColor="leftHeadRuleFg"></box>
43
+ <filelist in:entries="leftEntries" in:cursor="leftCursor" in:marked="leftMarked"
44
+ in:active="leftActive" in:paneId="leftId" in:loading="leftLoading"
45
+ in:viewport="listViewport"></filelist>
46
+ <entrydetail in:entry="leftEntry" in:marked="leftMarked" height="2"></entrydetail>
47
+ <summary in:entries="leftEntries" in:disk="leftDisk" height="1"></summary>
48
+ </box>
49
+ <box flexDirection="column" flexGrow="1" border borderStyle="single"
50
+ out:border="rightBorder" out:borderColor="rightBorderFg"
51
+ on:click="rightId!pane:activate">
52
+ <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
53
+ <box when="!rightFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
54
+ <breadcrumbs in:segments="rightSegments" in:paneId="rightId" height="1"></breadcrumbs>
55
+ </box>
56
+ <filterbar in:query="rightFilter" in:paneId="rightId" in:filtering="filtering" in:entries="rightEntries"></filterbar>
57
+ </box>
58
+ <box height="1" out:border="headRule" out:borderColor="rightHeadRuleFg"></box>
59
+ <filelist in:entries="rightEntries" in:cursor="rightCursor" in:marked="rightMarked"
60
+ in:active="rightActive" in:paneId="rightId" in:loading="rightLoading"
61
+ in:viewport="listViewport"></filelist>
62
+ <entrydetail in:entry="rightEntry" in:marked="rightMarked" height="2"></entrydetail>
63
+ <summary in:entries="rightEntries" in:disk="rightDisk" height="1"></summary>
64
+ </box>
65
+ <filepreview when="showInspectorInline" in:preview="preview" in:show="showInspectorInline"
66
+ in:entry="appEntry" flexGrow="1"></filepreview>
67
+ </box>
68
+ <commandbar in:items="commandBarItems" in:notice="notice" in:leftError="leftError"
69
+ in:rightError="rightError" in:active="active"
70
+ in:leftMarked="leftMarked" in:rightMarked="rightMarked" out:height="barHeight"></commandbar>
71
+ </box>
72
+ </box>
73
+
74
+ <!-- Fullscreen inspector for narrow viewports. -->
75
+ <box when="showViewer" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="10"
76
+ alignItems="center" justifyContent="center">
77
+ <box width="80%" height="90%">
78
+ <filepreview in:preview="preview" in:show="showViewer" in:entry="appEntry"></filepreview>
79
+ </box>
80
+ </box>
81
+
82
+ <commandpalette when="paletteOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="20"
83
+ in:open="paletteOpen" in:mode="paletteMode" in:query="paletteQuery"
84
+ in:items="paletteItems" in:selected="paletteIndex"></commandpalette>
85
+ <commandpanel when="panelOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="22"
86
+ in:open="panelOpen" in:operation="panelOperation" in:items="panelItems"
87
+ in:opCursor="panelOpIndex" in:rows="panelRows" in:argCursor="panelRowIndex"
88
+ in:focus="panelFocus" in:textual="panelTextual" in:argRow="panelCursorRow"
89
+ in:command="panelCommand" in:runnable="panelRunnable"
90
+ in:query="panelQuery" in:value="panelValue"></commandpanel>
91
+ <bookmarklist when="bookmarkOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="25"
92
+ in:open="bookmarkOpen" in:mode="bookmarkMode" in:query="bookmarkQuery"
93
+ in:target="bookmarkTarget" in:items="bookmarkItems" in:selected="bookmarkIndex"></bookmarklist>
94
+ <confirm when="confirm" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="30"
95
+ in:confirm="confirm"></confirm>
96
+ <help when="helpOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="40"
97
+ in:open="helpOpen"></help>
98
+ </box>
@@ -0,0 +1,12 @@
1
+ <div class="bookmark-list" when="open">
2
+ <div class="bookmark-dialog">
3
+ <header>
4
+ <span class="bookmark-sign" out="sign"></span>
5
+ <input class="bookmark-query" type="text" inout="query" on:enter="confirm">
6
+ </header>
7
+ <p class="bookmark-target" when="inputVisible" out="target"></p>
8
+ <p class="bookmark-empty" when="emptyList" out="empty"></p>
9
+ <ul class="bookmark-rows" when="listVisible" out="rows"></ul>
10
+ <footer out="footer"></footer>
11
+ </div>
12
+ </div>
@@ -0,0 +1,73 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-09
5
+
6
+ // Module: tcmd/components/BookmarkList
7
+ // BookmarkList: the `g`/`b` overlay. In "name" mode it shows an input to name
8
+ // the path being recorded; in "list" mode it shows the fuzzy picker over the
9
+ // stored bookmarks (matched by name or path). The row logic lives in
10
+ // `BookmarkRow.js`; this parent maps its `items` to rows.
11
+ //
12
+ // Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
13
+ // (footer/path), `textHeadingFg` (title), `rows(data)` returning the keyed rows
14
+ // and `scrollTop(self, data)` (TUI viewport tracking).
15
+
16
+ import { unwrap } from "select/state/cells"
17
+
18
+ // Function: title
19
+ // The overlay title: "bookmark" while naming, "bookmarks" while picking.
20
+ function bookmarkTitle(mode) {
21
+ return unwrap(mode) === "name" ? "bookmark" : "bookmarks"
22
+ }
23
+
24
+ // Function: footer
25
+ function bookmarkFooter(mode) {
26
+ return unwrap(mode) === "name"
27
+ ? "name the bookmark ↵ save Esc cancel"
28
+ : "↑↓ move ↵ jump Esc close"
29
+ }
30
+
31
+ // Function: targetLine
32
+ // The path being recorded (name mode). `target` is `{ path, isDir }` or null.
33
+ function targetLine(target) {
34
+ const value = unwrap(target)
35
+ if (!value) return ""
36
+ if (typeof value === "string") return value
37
+ return `${value.path ?? ""}${value.isDir === true ? "/" : ""}`
38
+ }
39
+
40
+ // Function: createBehaviors
41
+ // Builds the BookmarkList behavior map for the given renderer environment.
42
+ function createBehaviors(env) {
43
+ const behaviors = {
44
+ bg: () => env.overlayBg,
45
+ borderFg: () => env.borderPrimaryActiveFg,
46
+ confirm: (self, data) => self.pub("bookmark:confirm"),
47
+ footer: (_self, data) => bookmarkFooter(data.mode),
48
+ footerFg: () => env.textSecondaryFg,
49
+ // The input is focused/typing in name mode; the list navigates otherwise.
50
+ focused: (_self, data) => Boolean(unwrap(data.open)),
51
+ inputVisible: (_self, data) => unwrap(data.mode) === "name",
52
+ listVisible: (_self, data) => unwrap(data.mode) === "list",
53
+ open: (_self, data) => Boolean(unwrap(data.open)),
54
+ pathFg: () => env.textSecondaryFg,
55
+ sign: (_self, data) => (unwrap(data.mode) === "name" ? "+" : "›"),
56
+ target: (_self, data) => targetLine(data.target),
57
+ title: (_self, data) => bookmarkTitle(data.mode),
58
+ titleColor: () => env.textHeadingFg,
59
+ emptyList: (_self, data) =>
60
+ unwrap(data.mode) === "list" && (unwrap(data.items)?.length ?? 0) === 0,
61
+ empty: () => " no bookmarks",
62
+ emptyFg: () => env.textSecondaryFg,
63
+ }
64
+ if (env?.rows) {
65
+ behaviors.rows = (_self, data) => env.rows(self, data)
66
+ }
67
+ if (env?.scrollTop) {
68
+ behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
69
+ }
70
+ return behaviors
71
+ }
72
+
73
+ export { bookmarkTitle, createBehaviors }
@@ -0,0 +1,18 @@
1
+ <box when="open" width="100%" height="100%" alignItems="center" justifyContent="center">
2
+ <box width="72%" height="60%" border borderStyle="rounded"
3
+ out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
4
+ out:backgroundColor="bg" flexDirection="column">
5
+ <box flexDirection="row" height="1" paddingLeft="1">
6
+ <text out="sign" out:fg="titleColor"></text>
7
+ <input ref="query" inout="query" flexGrow="1" height="1" out:focused="focused" on:enter="confirm"></input>
8
+ </box>
9
+ <text when="inputVisible" out="target" out:fg="pathFg" paddingLeft="1" wrapMode="none" truncate="true"></text>
10
+ <text when="emptyList" out="empty" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
11
+ <scrollbox when="listVisible" ref="list" flexGrow="1">
12
+ <box out="rows" flexDirection="column"></box>
13
+ </scrollbox>
14
+ <box flexDirection="row" height="1" paddingLeft="1">
15
+ <text out="footer" out:fg="footerFg"></text>
16
+ </box>
17
+ </box>
18
+ </box>
@@ -0,0 +1,5 @@
1
+ <li class="bookmark-row" out:class="rowClass" on:click="item.name!bookmark:select">
2
+ <span class="bookmark-marker" out="rowMarker"></span>
3
+ <span class="bookmark-name" out="rowName"></span>
4
+ <span class="bookmark-path" out="rowPath"></span>
5
+ </li>
@@ -0,0 +1,40 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-09
5
+
6
+ // Module: tcmd/components/BookmarkRow
7
+ // One row of the bookmark picker. Reads `{ item: { name, path }, index,
8
+ // selected }`, the same bag shape in both engines. Selection is snapshotted by
9
+ // the row builder into a plain `selectedRow` boolean and read through the
10
+ // shared `isSelected` predicate, with the `selected` cell as fallback for
11
+ // hand-built bags.
12
+ //
13
+ // Environment: `accentFg`, `textFg`, `textSecondaryFg`, `selectionBg` (TUI row
14
+ // background), `selectedClass` (web), `icons` (show Nerd Font icons).
15
+
16
+ import { isSelected } from "./rows.js"
17
+ import { iconFor } from "./icons.js"
18
+
19
+ // Function: createBehaviors
20
+ // Builds the BookmarkRow behavior map for the given renderer environment.
21
+ function createBehaviors(env) {
22
+ return {
23
+ rowBg: (_self, data) => (isSelected(data) ? env.selectionBg : undefined),
24
+ rowClass: (_self, data) => (isSelected(data) ? env.selectedClass : null),
25
+ rowFg: (_self, data) => (isSelected(data) ? env.accentFg : env.textFg),
26
+ // The icon is its own slot, dimmer than the name; it collapses when
27
+ // the environment keeps rows icon-free.
28
+ hasIcon: () => Boolean(env.icons),
29
+ rowIcon: (_self, data) => (env.icons ? iconFor(data.item) : ""),
30
+ rowIconFg: () => env.textSecondaryFg,
31
+ rowMarker: (_self, data) => (isSelected(data) ? ">" : " "),
32
+ // A directory bookmark reads as `name/`, mirroring the listing.
33
+ rowName: (_self, data) =>
34
+ `${data.item?.name ?? ""}${data.item?.isDir === true ? "/" : ""}`,
35
+ rowPath: (_self, data) => `${data.item?.path ?? ""}`,
36
+ rowPathFg: () => env.textSecondaryFg,
37
+ }
38
+ }
39
+
40
+ export { createBehaviors }
@@ -0,0 +1,10 @@
1
+ <box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="item.name!bookmark:select">
2
+ <text out="rowMarker" width="2" out:fg="rowFg"></text>
3
+ <text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
4
+ <box minWidth="12" flexShrink="0">
5
+ <text out="rowName" out:fg="rowFg" wrapMode="none" truncate="true"></text>
6
+ </box>
7
+ <box flexGrow="1" minWidth="0" overflow="hidden">
8
+ <text out="rowPath" out:fg="rowPathFg" wrapMode="none" truncate="true"></text>
9
+ </box>
10
+ </box>
@@ -0,0 +1,27 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-09-26
5
+
6
+ // Module: tcmd/components/Breadcrumbs
7
+ // PathBreadcrumbs: clickable path elements for one pane. The row logic lives
8
+ // in `Crumb.js`; this parent only maps its `segments` to `Crumb` rows and
9
+ // exposes the web root class.
10
+ //
11
+ // Environment: `rows(data)` returning the keyed row list and `listClass`
12
+ // (web root class); both are injected by the renderer binding. The TUI binds
13
+ // this component to the shared `RowList.xml`, the web to `RowList.html`.
14
+
15
+ // Function: createBehaviors
16
+ // Builds the Breadcrumbs behavior map for the given renderer environment.
17
+ function createBehaviors(env) {
18
+ const behaviors = { listClass: () => env?.listClass }
19
+ if (env?.rows) {
20
+ behaviors.rows = (_self, data) => env.rows(self, data)
21
+ }
22
+ return behaviors
23
+ }
24
+
25
+ export { createBehaviors }
26
+
27
+ // EOF
@@ -0,0 +1,90 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-09
5
+
6
+ // Module: tcmd/components/CommandArgRow
7
+ // CommandArgRow: one row shared by both command-panel columns. The operation
8
+ // column passes `{ type: "op" }` rows (name = operation title, summary =
9
+ // signature, `risk` tints destructive/caution operations); the argument column
10
+ // passes `{ type: "arg" | "item" }` rows from `core/panel.panelRowList`. An
11
+ // item shows a `[x]`/`[ ]` toggle with its basename and, dimmed after it, the
12
+ // parent directory; a focused row carries a `>` marker so focus is not
13
+ // colour-only. Reads the row bag `{ item, index, row, cursor, focus }` built by
14
+ // `rows.js`.
15
+ //
16
+ // Environment: `accentFg` (focused row), `textFg`, `textSecondaryFg` (summary),
17
+ // `accentSecondaryFg` (a selected item), `errorFg` (destructive op),
18
+ // `warningFg` (caution op), `selectionBg` (row background), `focusedClass`
19
+ // (web).
20
+
21
+ import { unwrap } from "select/state/cells"
22
+
23
+ // Function: riskFg
24
+ // The colour a row's risk maps to: red for destructive operations, amber for
25
+ // caution (a move), null when the row is not risky.
26
+ function riskFg(row, env) {
27
+ if (row?.type === "op" && row.risk === "destructive") return env.errorFg
28
+ if (row?.type === "op" && row.risk === "caution") return env.warningFg
29
+ return null
30
+ }
31
+
32
+ // Function: rowName
33
+ // The row's left cell: the operation title, an argument's name, or an item's
34
+ // `[x] basename`.
35
+ function rowName(row) {
36
+ if (!row) return ""
37
+ if (row.type === "item") return `${row.marker ?? "[ ]"} ${row.name ?? ""}`
38
+ return row.name ?? ""
39
+ }
40
+
41
+ // Function: rowSummary
42
+ // The row's right cell: the operation signature, an argument's binding
43
+ // summary, or an item's parent directory.
44
+ function rowSummary(row) {
45
+ if (!row) return ""
46
+ if (row.type === "item") return row.parent ?? ""
47
+ if (row.type === "op") return row.summary ?? ""
48
+ return row.summary ?? ""
49
+ }
50
+
51
+ // Function: wantsFocus
52
+ // The focus token a row belongs to: the operation column ("ops") or the
53
+ // argument column ("args").
54
+ function wantsFocus(row) {
55
+ return row?.type === "op" ? "ops" : "args"
56
+ }
57
+
58
+ // Function: isFocused
59
+ // True when this row owns the cursor in the focused column.
60
+ function isFocused(row, index, cursor, focus) {
61
+ if (!row || !focus) return false
62
+ return focus === wantsFocus(row) && index === cursor
63
+ }
64
+
65
+ // Function: createBehaviors
66
+ // Builds the CommandArgRow behavior map for the given renderer environment.
67
+ function createBehaviors(env) {
68
+ const focused = (data) => isFocused(data.row, data.index, unwrap(data.cursor), unwrap(data.focus))
69
+ return {
70
+ marker: (_self, data) => (focused(data) ? ">" : " "),
71
+ markerFg: () => env.accentFg,
72
+ name: (_self, data) => rowName(data.row),
73
+ rowBg: (_self, data) => (focused(data) ? env.selectionBg : undefined),
74
+ rowClass: (_self, data) => (focused(data) ? env.focusedClass : null),
75
+ rowFg: (_self, data) => {
76
+ // A risky operation (delete/erase/move) is always tinted — even
77
+ // focused — so danger reads at a glance; focus is carried by the
78
+ // `>` marker and the row background instead.
79
+ const risk = riskFg(data.row, env)
80
+ if (risk) return risk
81
+ if (focused(data)) return env.accentFg
82
+ if (data.row?.type === "item" && data.row?.selected) return env.accentSecondaryFg
83
+ return env.textFg
84
+ },
85
+ summary: (_self, data) => rowSummary(data.row),
86
+ summaryFg: () => env.textSecondaryFg,
87
+ }
88
+ }
89
+
90
+ export { createBehaviors, isFocused, riskFg, rowName, rowSummary, wantsFocus }
@@ -0,0 +1,5 @@
1
+ <box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="row.key!panel:select">
2
+ <text out="marker" width="2" out:fg="markerFg"></text>
3
+ <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
4
+ <text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
5
+ </box>
@@ -0,0 +1,4 @@
1
+ <footer class="command-bar">
2
+ <nav when="showKeys" out:class="listClass" out="rows"></nav>
3
+ <span when="showStatus" out="status" out:class="statusClass"></span>
4
+ </footer>
@@ -0,0 +1,126 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-09-27
5
+
6
+ // Module: tcmd/components/CommandBar
7
+ // CommandBar: the bottom bar. By default it shows the keys available in the
8
+ // current context (the command registry's `bar` commands, or the palette
9
+ // hints while the palette is open); a pane error or a session notice takes
10
+ // its place. Entries are bracket shorthands (see `CommandItem.js`) that wrap
11
+ // onto as many rows as the width demands; the active pane's marked count
12
+ // joins the list as a dimmed, non-clickable entry. The row logic lives in
13
+ // `CommandItem.js`; this parent maps its `items` to rows.
14
+ //
15
+ // The web presentation swaps the key list for a single status line
16
+ // (`showKeys`/`showStatus`/`status`/`statusClass`); the TUI presentation
17
+ // keeps the wrapped key list and swaps it for the status when there is one.
18
+ //
19
+ // Environment: `rows(data)` returning the keyed row list, `listClass` (web
20
+ // root class), `errorClass` (web), and the TUI colors plus the
21
+ // error/warning/tip foreground/background pairs for the status badge.
22
+
23
+ import { unwrap } from "select/state/cells"
24
+ import { commandWidth } from "./CommandItem.js"
25
+
26
+ // Function: commandBarStatus
27
+ // The one-line status the bar shows when something needs attention: the
28
+ // session notice first, then a pane error. Empty when everything is fine.
29
+ function commandBarStatus(data) {
30
+ const notice = unwrap(data?.notice)
31
+ if (notice) {
32
+ const text = notice.text ?? `${notice}`
33
+ return notice.kind === "error" ? `error: ${text}` : text
34
+ }
35
+ const error = unwrap(data?.leftError) ?? unwrap(data?.rightError)
36
+ return error ? `error: ${error}` : ""
37
+ }
38
+
39
+ // Function: commandBarMarks
40
+ // Count of marked entries in the active pane, for the status bar's right zone.
41
+ function commandBarMarks(data) {
42
+ const active = unwrap(data?.active)
43
+ const marked = active === "left" ? unwrap(data?.leftMarked) : unwrap(data?.rightMarked)
44
+ const count = marked?.size ?? 0
45
+ return count > 0 ? `${count} marked` : ""
46
+ }
47
+
48
+ // Function: commandBarMarksItem
49
+ // The marked count as a trailing, non-clickable bar entry (null when none).
50
+ function commandBarMarksItem(data) {
51
+ const marks = commandBarMarks(data)
52
+ return marks ? { id: null, title: marks, dim: true } : null
53
+ }
54
+
55
+ // Function: commandBarRowCount
56
+ // How many rows the wrapped entries occupy at `width` columns (greedy fill,
57
+ // matching the flex layout, separators included). `width <= 0` (unknown)
58
+ // assumes a single row.
59
+ function commandBarRowCount(items, width) {
60
+ if (!width || width <= 0) return 1
61
+ let rows = 1
62
+ let cols = 0
63
+ let index = 0
64
+ for (const item of items ?? []) {
65
+ const w = commandWidth(item, index)
66
+ if (cols > 0 && cols + w > width) {
67
+ rows++
68
+ cols = 0
69
+ }
70
+ cols += w
71
+ index++
72
+ }
73
+ return rows
74
+ }
75
+
76
+ // Function: isInfo
77
+ // True when the bar status comes from a non-error notice.
78
+ function isInfo(data) {
79
+ const notice = unwrap(data?.notice)
80
+ return Boolean(notice && notice.kind !== "error")
81
+ }
82
+
83
+ // Function: statusKind
84
+ // The notice kind driving the status (null when there is no notice).
85
+ function statusKind(data) {
86
+ return unwrap(data?.notice)?.kind ?? null
87
+ }
88
+
89
+ // Function: createBehaviors
90
+ // Builds the CommandBar behavior map for the given renderer environment.
91
+ function createBehaviors(env) {
92
+ const behaviors = {
93
+ listClass: () => env?.listClass,
94
+ showKeys: (_self, data) => !commandBarStatus(data),
95
+ showStatus: (_self, data) => Boolean(commandBarStatus(data)),
96
+ status: (_self, data) => commandBarStatus(data),
97
+ statusBg: (_self, data) => {
98
+ const kind = statusKind(data)
99
+ if (kind === "warning") return env?.warningBg
100
+ if (kind === "tip") return env?.tipBg
101
+ if (kind === "error" || unwrap(data?.leftError) || unwrap(data?.rightError)) return env?.errorBg
102
+ return undefined
103
+ },
104
+ statusClass: (_self, data) => (commandBarStatus(data) && !isInfo(data) ? env?.errorClass : null),
105
+ statusFg: (_self, data) => {
106
+ const kind = statusKind(data)
107
+ if (kind === "warning") return env?.warningFg
108
+ if (kind === "tip") return env?.tipFg
109
+ if (kind === "error" || unwrap(data?.leftError) || unwrap(data?.rightError)) return env?.errorFg
110
+ return env?.accentFg
111
+ },
112
+ // TUI: a padded status for the right zone, empty when all is well.
113
+ statusPad: (_self, data) => {
114
+ const text = commandBarStatus(data)
115
+ return text ? ` ${text} ` : ""
116
+ },
117
+ }
118
+ if (env?.rows) {
119
+ behaviors.rows = (_self, data) => env.rows(self, data)
120
+ }
121
+ return behaviors
122
+ }
123
+
124
+ export { commandBarMarks, commandBarMarksItem, commandBarRowCount, commandBarStatus, createBehaviors }
125
+
126
+ // EOF
@@ -0,0 +1,4 @@
1
+ <box flexDirection="column" overflow="hidden">
2
+ <box when="showKeys" out="rows" flexDirection="row" flexWrap="wrap" flexGrow="1" overflow="hidden"></box>
3
+ <text when="showStatus" out="status" out:fg="statusFg" out:bg="statusBg" flexShrink="0"></text>
4
+ </box>
@@ -0,0 +1,3 @@
1
+ <button class="command-key" type="button" on:click="item.id!commandbar:run">
2
+ <span out="itemLabel"></span>
3
+ </button>
@@ -0,0 +1,78 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-09-27
5
+
6
+ // Module: tcmd/components/CommandItem
7
+ // Command-bar entry behaviors. Read `{ item, index }`, the same bag shape in
8
+ // both engines. `item.id` may be null for pure key hints, in which case a
9
+ // click dispatches nothing.
10
+ //
11
+ // Each entry renders as a plain bracket shorthand, preceded by a dim `|`
12
+ // divider (except the first entry):
13
+ //
14
+ // [Open ⏎] — the bracket and title in the text colour, the key glyph in
15
+ // the accent (primary) or secondary accent, and a trailing
16
+ // space separating it from the next entry.
17
+ //
18
+ // Entries without a key (the marked count) render as a dimmed plain word
19
+ // with no brackets. The web renders the same label as a single string (its
20
+ // buttons are separated by their gap, not the pipe).
21
+ //
22
+ // Environment: `accentFg` (primary), `accentSecondaryFg` (secondary),
23
+ // `textFg`, `textSecondaryFg`.
24
+
25
+ // Function: keyGlyph
26
+ // The shorthand glyph for a shortcut label: ⏎ for Enter, ⇥ for Tab.
27
+ function keyGlyph(keys) {
28
+ const GLYPHS = { Enter: "⏎", "↵": "⏎", Tab: "⇥" }
29
+ return GLYPHS[keys] ?? keys
30
+ }
31
+
32
+ // Function: commandLabel
33
+ // "[Open ⏎] " style label; entries without a key are just a plain word.
34
+ function commandLabel(item) {
35
+ if (!item) return ""
36
+ return item.keys ? `[${item.title} ${keyGlyph(item.keys)}] ` : `${item.title} `
37
+ }
38
+
39
+ // Constant: COMMAND_SEP
40
+ // The divider drawn before every entry but the first: a dim pipe and the
41
+ // space that keeps it clear of the button.
42
+ const COMMAND_SEP = "| "
43
+
44
+ // Function: commandWidth
45
+ // Columns the entry occupies: its label plus the leading separator when it
46
+ // is not the first entry.
47
+ function commandWidth(item, index) {
48
+ return commandLabel(item).length + (index > 0 ? COMMAND_SEP.length : 0)
49
+ }
50
+
51
+ // Function: isPrimary
52
+ // True when the item is its context's most important action.
53
+ function isPrimary(item) {
54
+ return Boolean(item?.primary)
55
+ }
56
+
57
+ // Function: createBehaviors
58
+ // Builds the CommandItem behavior map for the given renderer environment.
59
+ function createBehaviors(env) {
60
+ const head = (_self, data) => (data.item?.keys ? `[${data.item.title} ` : `${data.item?.title ?? ""} `)
61
+ return {
62
+ // Web: the whole shorthand as one label.
63
+ itemLabel: (_self, data) => commandLabel(data.item),
64
+ // TUI: the dim pipe divider, then the bracket + title head, the
65
+ // accent key glyph, the closer.
66
+ itemSep: (_self, data) => (data.index > 0 ? COMMAND_SEP : ""),
67
+ itemSepFg: () => env?.textSecondaryFg,
68
+ itemHead: head,
69
+ itemFg: (_self, data) => (data.item?.keys ? env?.textFg : env?.textSecondaryFg),
70
+ keyText: (_self, data) => (data.item?.keys ? keyGlyph(data.item.keys) : ""),
71
+ keyFg: (_self, data) => (isPrimary(data.item) ? env?.accentFg : env?.accentSecondaryFg),
72
+ endText: (_self, data) => (data.item?.keys ? "] " : ""),
73
+ }
74
+ }
75
+
76
+ export { COMMAND_SEP, commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
77
+
78
+ // EOF
@@ -0,0 +1,6 @@
1
+ <box flexDirection="row" height="1" flexShrink="0" on:click="item.id!commandbar:run">
2
+ <text out="itemSep" out:fg="itemSepFg" wrapMode="none"></text>
3
+ <text out="itemHead" out:fg="itemFg" wrapMode="none"></text>
4
+ <text out="keyText" out:fg="keyFg" wrapMode="none"></text>
5
+ <text out="endText" out:fg="itemFg" wrapMode="none"></text>
6
+ </box>
@@ -0,0 +1,7 @@
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>