@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
@@ -25,15 +25,18 @@
25
25
  Cmd oranges (see `modeGradient` in Frame.js). The trailing
26
26
  slots of the shorter word are empty and take no column. -->
27
27
  <box flexDirection="row" flexShrink="0" overflow="hidden">
28
- <text when="modeHas0" out="modeL0" out:fg="modeFg0" wrapMode="none"></text>
29
- <text when="modeHas1" out="modeL1" out:fg="modeFg1" wrapMode="none"></text>
30
- <text when="modeHas2" out="modeL2" out:fg="modeFg2" wrapMode="none"></text>
31
- <text when="modeHas3" out="modeL3" out:fg="modeFg3" wrapMode="none"></text>
32
- <text when="modeHas4" out="modeL4" out:fg="modeFg4" wrapMode="none"></text>
33
- <text when="modeHas5" out="modeL5" out:fg="modeFg5" wrapMode="none"></text>
34
- <text when="modeHas6" out="modeL6" out:fg="modeFg6" wrapMode="none"></text>
35
- <text when="modeHas7" out="modeL7" out:fg="modeFg7" wrapMode="none"></text>
36
- <text when="modeHas8" out="modeL8" out:fg="modeFg8" wrapMode="none"></text>
28
+ <!-- Clicking the mode word flips explorer ⇄ commander. -->
29
+ <box flexDirection="row" overflow="hidden" on:click="!view:toggleMode">
30
+ <text when="modeHas0" out="modeL0" out:fg="modeFg0" wrapMode="none"></text>
31
+ <text when="modeHas1" out="modeL1" out:fg="modeFg1" wrapMode="none"></text>
32
+ <text when="modeHas2" out="modeL2" out:fg="modeFg2" wrapMode="none"></text>
33
+ <text when="modeHas3" out="modeL3" out:fg="modeFg3" wrapMode="none"></text>
34
+ <text when="modeHas4" out="modeL4" out:fg="modeFg4" wrapMode="none"></text>
35
+ <text when="modeHas5" out="modeL5" out:fg="modeFg5" wrapMode="none"></text>
36
+ <text when="modeHas6" out="modeL6" out:fg="modeFg6" wrapMode="none"></text>
37
+ <text when="modeHas7" out="modeL7" out:fg="modeFg7" wrapMode="none"></text>
38
+ <text when="modeHas8" out="modeL8" out:fg="modeFg8" wrapMode="none"></text>
39
+ </box>
37
40
  <text out="topBarStatus" out:fg="topBarStatusFg" wrapMode="none"></text>
38
41
  </box>
39
42
  </box>
@@ -64,7 +67,6 @@
64
67
  in:active="parentActive" in:paneId="parentId" in:loading="parentLoading"
65
68
  in:viewport="listViewport"></filelist>
66
69
  <entrydetail in:entry="parentEntry" in:marked="parentMarked" height="2"></entrydetail>
67
- <summary in:entries="parentEntries" in:disk="parentDisk" height="1"></summary>
68
70
  </box>
69
71
  <box when="showLeftPane" flexDirection="column" flexBasis="0" out:flexGrow="leftGrow" border borderStyle="single"
70
72
  out:border="leftBorder" out:borderColor="leftBorderFg"
@@ -80,7 +82,6 @@
80
82
  in:active="leftActive" in:paneId="leftId" in:loading="leftLoading"
81
83
  in:viewport="listViewport"></filelist>
82
84
  <entrydetail in:entry="leftEntry" in:marked="leftMarked" height="2"></entrydetail>
83
- <summary in:entries="leftEntries" in:disk="leftDisk" height="1"></summary>
84
85
  </box>
85
86
  <box when="showRightPane" flexDirection="column" flexBasis="0" out:flexGrow="rightGrow" border borderStyle="single"
86
87
  out:border="rightBorder" out:borderColor="rightBorderFg"
@@ -96,10 +97,9 @@
96
97
  in:active="rightActive" in:paneId="rightId" in:loading="rightLoading"
97
98
  in:viewport="listViewport"></filelist>
98
99
  <entrydetail in:entry="rightEntry" in:marked="rightMarked" height="2"></entrydetail>
99
- <summary in:entries="rightEntries" in:disk="rightDisk" height="1"></summary>
100
100
  </box>
101
101
  <filepreview when="showInspectorInline" in:preview="preview" in:show="showInspectorInline"
102
- in:entry="appEntry" in:paneId="previewId"
102
+ in:entry="appEntry" in:paneId="previewId" in:scroll="previewScroll"
103
103
  in:dirSegments="previewSegments" in:dirEntries="previewEntries"
104
104
  in:dirCursor="previewCursor" in:dirMarked="previewMarked"
105
105
  in:dirActive="previewActive" in:dirLoading="previewLoading"
@@ -108,22 +108,24 @@
108
108
  <!-- Maximized inspector (`⇧V`): the preview takes the whole pane
109
109
  row, the identity and command bars staying in place. -->
110
110
  <filepreview when="showPreviewFull" in:preview="preview" in:show="showPreviewFull"
111
- in:entry="appEntry" in:paneId="previewId"
111
+ in:entry="appEntry" in:paneId="previewId" in:scroll="previewScroll"
112
112
  in:dirSegments="previewSegments" in:dirEntries="previewEntries"
113
113
  in:dirCursor="previewCursor" in:dirMarked="previewMarked"
114
114
  in:dirActive="previewActive" in:dirLoading="previewLoading"
115
115
  flexGrow="1"></filepreview>
116
116
  <commandbar in:items="commandBarItems" in:notice="notice" in:leftError="leftError"
117
- in:rightError="rightError" in:active="active"
117
+ in:rightError="rightError" in:active="active" in:uiSize="uiSize"
118
118
  in:leftMarked="leftMarked" in:rightMarked="rightMarked" out:height="barHeight"></commandbar>
119
119
  </box>
120
120
  </box>
121
121
 
122
- <!-- Fullscreen inspector for narrow viewports. -->
122
+ <!-- Fullscreen inspector overlay: `v` below the inline breakpoint, or
123
+ Enter on a file the host cannot edit (any width). -->
123
124
  <box when="showViewer" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="10"
124
125
  alignItems="center" justifyContent="center">
125
126
  <box width="80%" height="90%">
126
127
  <filepreview in:preview="preview" in:show="showViewer" in:entry="appEntry" in:paneId="previewId"
128
+ in:scroll="previewScroll"
127
129
  in:dirSegments="previewSegments" in:dirEntries="previewEntries"
128
130
  in:dirCursor="previewCursor" in:dirMarked="previewMarked"
129
131
  in:dirActive="previewActive" in:dirLoading="previewLoading"></filepreview>
@@ -133,15 +135,18 @@
133
135
  <commandpanel when="panelOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="22"
134
136
  in:open="panelOpen" in:entry="panelEntry" in:items="panelItems"
135
137
  in:opCursor="panelOpIndex" in:rows="panelRows" in:argCursor="panelRowIndex"
136
- in:labelWidth="panelLabelWidth"
137
- in:focus="panelFocus" in:command="panelCommand" in:runnable="panelRunnable"
138
- in:picking="panelPicking" in:pickerQuery="pickerQuery" in:pickerPath="pickerPath"
138
+ in:permBit="panelPermBit" in:focus="panelFocus"
139
+ in:preview="panelPreview"
140
+ in:picking="pickerOpen"
141
+ in:pickerPathText="pickerPathText" in:pickerFocus="pickerFocus" in:pickerRows="pickerRows"
139
142
  in:query="panelQuery" in:value="panelValue"></commandpanel>
140
143
  <bookmarklist when="bookmarkOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="25"
141
- in:open="bookmarkOpen" in:mode="bookmarkMode" in:query="bookmarkQuery"
142
- in:target="bookmarkTarget" in:items="bookmarkItems" in:selected="bookmarkIndex"></bookmarklist>
144
+ in:open="bookmarkOpen" in:query="bookmarkQuery"
145
+ in:items="bookmarkItems" in:selected="bookmarkIndex"></bookmarklist>
146
+ <overlaylist when="overlayOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="27"
147
+ in:open="overlay" in:mode="overlay" in:items="overlayItems" in:cursor="overlayIndex"></overlaylist>
143
148
  <confirm when="confirm" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="30"
144
149
  in:confirm="confirm"></confirm>
145
150
  <help when="helpOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="40"
146
- in:open="helpOpen"></help>
151
+ in:open="helpOpen" in:size="uiSize"></help>
147
152
  </box>
@@ -1,13 +1,12 @@
1
- <div class="bookmark-list" when="open" role="dialog" aria-modal="true" aria-label="Bookmarks">
2
- <div class="bookmark-dialog">
1
+ <div class="bookmark-list" when="open" role="dialog" aria-modal="true" aria-label="Bookmarks" on:click="!dialog:bookmark-cancel">
2
+ <div class="bookmark-dialog" on:click="dialogStop">
3
3
  <h2 out="title"></h2>
4
4
  <header>
5
5
  <span class="bookmark-sign" out="sign"></span>
6
- <input class="bookmark-query" type="text" inout="query" aria-label="Bookmark name or query" on:enter="confirm">
6
+ <input class="bookmark-query" type="text" inout="query" aria-label="Filter bookmarks" on:enter="confirm">
7
7
  </header>
8
- <p class="bookmark-target" when="inputVisible" out="target"></p>
9
8
  <p class="bookmark-empty" when="emptyList" out="empty"></p>
10
- <ul class="bookmark-rows" when="listVisible" out="rows"></ul>
9
+ <ul class="bookmark-rows" out="rows"></ul>
11
10
  <footer out="footer"></footer>
12
11
  </div>
13
12
  </div>
@@ -4,61 +4,35 @@
4
4
  // Created: 2026-10-09
5
5
 
6
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
7
+ // BookmarkList: the single bookmarks panel opened with `b`. The input filters
8
+ // the stored bookmarks (matched by name or path); the first row offers to add
9
+ // the current path when it is not bookmarked yet. The row logic lives in
10
10
  // `BookmarkRow.js`; this parent maps its `items` to rows.
11
11
  //
12
12
  // Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
13
- // (footer/path), `textHeadingFg` (title), `rows(data)` returning the keyed rows
14
- // and `scrollTop(self, data)` (TUI viewport tracking).
13
+ // (footer), `textHeadingFg` (title), `rows(data)` returning the keyed rows and
14
+ // `scrollTop(self, data)` (TUI viewport tracking).
15
15
 
16
16
  import { unwrap } from "select/state/cells"
17
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
18
  // Function: createBehaviors
41
19
  // Builds the BookmarkList behavior map for the given renderer environment.
42
20
  function createBehaviors(env) {
43
21
  const behaviors = {
44
22
  bg: () => env.overlayBg,
45
23
  borderFg: () => env.borderPrimaryActiveFg,
46
- confirm: (self, data) => self.pub("bookmark:confirm"),
47
- footer: (_self, data) => bookmarkFooter(data.mode),
24
+ confirm: (self) => self.pub("bookmark:confirm"),
25
+ footer: () => "↑↓ move ⌫/Del remove ↵ open Esc close",
48
26
  footerFg: () => env.textSecondaryFg,
49
- // The input is focused/typing in name mode; the list navigates otherwise.
27
+ // The filter input owns typing; the list navigates with the arrows.
50
28
  focused: (_self, data) => Boolean(unwrap(data.open)),
51
- inputVisible: (_self, data) => unwrap(data.mode) === "name",
52
- listVisible: (_self, data) => unwrap(data.mode) === "list",
53
29
  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),
30
+ sign: () => "›",
31
+ title: () => "bookmarks",
58
32
  titleColor: () => env.textHeadingFg,
59
- emptyList: (_self, data) =>
60
- unwrap(data.mode) === "list" && (unwrap(data.items)?.length ?? 0) === 0,
61
- empty: () => " no bookmarks",
33
+ emptyList: (_self, data) => (unwrap(data.items)?.length ?? 0) === 0,
34
+ empty: (_self, data) =>
35
+ `${unwrap(data.query) ?? ""}`.trim() ? " no matching bookmarks" : " no bookmarks",
62
36
  emptyFg: () => env.textSecondaryFg,
63
37
  }
64
38
  if (env?.rows) {
@@ -70,4 +44,4 @@ function createBehaviors(env) {
70
44
  return behaviors
71
45
  }
72
46
 
73
- export { bookmarkTitle, createBehaviors }
47
+ export { createBehaviors }
@@ -6,9 +6,8 @@
6
6
  <text out="sign" out:fg="titleColor"></text>
7
7
  <input ref="query" inout="query" flexGrow="1" height="1" out:focused="focused" on:enter="confirm"></input>
8
8
  </box>
9
- <text when="inputVisible" out="target" out:fg="pathFg" paddingLeft="1" wrapMode="none" truncate="true"></text>
10
9
  <text when="emptyList" out="empty" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
11
- <scrollbox when="listVisible" ref="list" flexGrow="1">
10
+ <scrollbox ref="list" flexGrow="1">
12
11
  <box out="rows" flexDirection="column"></box>
13
12
  </scrollbox>
14
13
  <box flexDirection="row" height="1" paddingLeft="1">
@@ -1,5 +1,6 @@
1
- <li class="bookmark-row" out:class="rowClass" on:click="item.name!bookmark:select">
1
+ <li class="bookmark-row" out:class="rowClass" on:click="item.key!bookmark:select">
2
2
  <span class="bookmark-marker" out="rowMarker"></span>
3
+ <svg class="bookmark-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="rowIconHref"></use></svg>
3
4
  <span class="bookmark-name" out="rowName"></span>
4
5
  <span class="bookmark-path" out="rowPath"></span>
5
6
  </li>
@@ -4,17 +4,18 @@
4
4
  // Created: 2026-10-09
5
5
 
6
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.
7
+ // One row of the bookmarks panel. Reads `{ item: { name, path, add? }, index,
8
+ // selected }`, the same bag shape in both engines. The synthetic add row reads
9
+ // as an action; stored rows read as `name/` for directories. Selection is
10
+ // snapshotted by the row builder into a plain `selectedRow` boolean and read
11
+ // through the shared `isSelected` predicate, with the `selected` cell as
12
+ // fallback for hand-built bags.
12
13
  //
13
14
  // Environment: `accentFg`, `textFg`, `textSecondaryFg`, `selectionBg` (TUI row
14
15
  // background), `selectedClass` (web), `icons` (show Nerd Font icons).
15
16
 
16
17
  import { isSelected } from "./rows.js"
17
- import { iconFor } from "./icons.js"
18
+ import { iconFor, lucideIconFor } from "./icons.js"
18
19
 
19
20
  // Function: createBehaviors
20
21
  // Builds the BookmarkRow behavior map for the given renderer environment.
@@ -25,13 +26,17 @@ function createBehaviors(env) {
25
26
  rowFg: (_self, data) => (isSelected(data) ? env.accentFg : env.textFg),
26
27
  // The icon is its own slot, dimmer than the name; it collapses when
27
28
  // the environment keeps rows icon-free.
28
- hasIcon: () => Boolean(env.icons),
29
+ hasIcon: () => Boolean(env.icons || env.lucideIcons),
29
30
  rowIcon: (_self, data) => (env.icons ? iconFor(data.item) : ""),
31
+ rowIconHref: (_self, data) => (env.lucideIcons ? `#lucide-${lucideIconFor(data.item)}` : ""),
30
32
  rowIconFg: () => env.textSecondaryFg,
31
33
  rowMarker: (_self, data) => (isSelected(data) ? ">" : " "),
32
- // A directory bookmark reads as `name/`, mirroring the listing.
34
+ // The add row names its action; a directory bookmark reads as
35
+ // `name/`, mirroring the listing.
33
36
  rowName: (_self, data) =>
34
- `${data.item?.name ?? ""}${data.item?.isDir === true ? "/" : ""}`,
37
+ data.item?.add
38
+ ? data.item.name ?? ""
39
+ : `${data.item?.name ?? ""}${data.item?.isDir === true ? "/" : ""}`,
35
40
  rowPath: (_self, data) => `${data.item?.path ?? ""}`,
36
41
  rowPathFg: () => env.textSecondaryFg,
37
42
  }
@@ -1,4 +1,4 @@
1
- <box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="item.name!bookmark:select">
1
+ <box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="item.key!bookmark:select">
2
2
  <text out="rowMarker" width="2" out:fg="rowFg"></text>
3
3
  <text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
4
4
  <box minWidth="12" flexShrink="0">
@@ -1,17 +1,28 @@
1
- <li class="panel-row" out:class="rowClass" out:data-risk="row.risk" out:data-type="row.type" out:data-selected="rowSelected" on:click="row.key!panel:select">
1
+ <li class="panel-row" out:class="rowClass" out:data-risk="row.risk" out:data-group="row.group" out:data-type="row.type" out:data-selected="rowSelected" on:click="row.key!panel:select">
2
2
  <span class="panel-list-indent" when="isList" out="listSpacer"></span>
3
3
  <span class="panel-list-box" when="isList">
4
- <span class="panel-marker" out="marker"></span>
5
- <span class="panel-name" out="name"></span>
6
- <span class="panel-summary" out="summary"></span>
4
+ <span class="panel-marker" when="isItem" out="marker"></span>
5
+ <span class="panel-name" when="isItem" out="name"></span>
6
+ <span class="panel-summary" when="isItem" out="summary"></span>
7
7
  </span>
8
- <span class="panel-marker" when="!isList" out="marker"></span>
9
- <span class="panel-name" when="!isList" out="name"></span>
10
- <span class="panel-field" when="isArg">
8
+ <span class="panel-name panel-group-name" when="isGroup" out="name"></span>
9
+ <span class="panel-marker" when="showPlain" out="marker"></span>
10
+ <span class="panel-name" when="showPlain" out="name"></span>
11
+ <span class="panel-marker" when="isArg" out="marker"></span>
12
+ <span class="panel-name panel-arg-name" when="isArg" out="name"></span>
13
+ <span class="panel-perm" when="isPerm">
14
+ <span class="panel-perm-class" out="permClass"></span>
15
+ <button class="panel-perm-bit" type="button" tabindex="-1" out:class="permRClass" out:aria-pressed="permROn" on:click="row.keyR!panel:perm." out="permR"></button>
16
+ <button class="panel-perm-bit" type="button" tabindex="-1" out:class="permWClass" out:aria-pressed="permWOn" on:click="row.keyW!panel:perm." out="permW"></button>
17
+ <button class="panel-perm-bit" type="button" tabindex="-1" out:class="permXClass" out:aria-pressed="permXOn" on:click="row.keyX!panel:perm." out="permX"></button>
18
+ </span>
19
+ <button class="panel-run-button" when="isRun" type="button" tabindex="-1" out:aria-label="runAriaLabel" out="runLabel"></button>
20
+ <span class="panel-field" when="isArg" out:data-editable="editable">
11
21
  <span class="panel-field-bracket" out="fieldOpen"></span>
12
22
  <input class="panel-value" when="editing" type="text" inout="value" aria-label="Argument value" />
13
23
  <span class="panel-field-value" when="!editing" out="summary"></span>
14
24
  <span class="panel-field-bracket" out="fieldClose"></span>
15
25
  </span>
16
26
  <span class="panel-summary" when="isPlain" out="summary"></span>
27
+ <svg class="panel-op-chevron ui-icon" when="isOp" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use href="#lucide-chevron-right"></use></svg>
17
28
  </li>
@@ -5,53 +5,66 @@
5
5
 
6
6
  // Module: tcmd/components/CommandArgRow
7
7
  // CommandArgRow: one row shared by the command panel's two panes. The left pane
8
- // passes `{ type: "op" }` rows (name = entry name, summary = signature or key
9
- // shortcut, `risk` tints destructive/caution operations, `source` marks the
10
- // namespace) and `{ type: "group" }` header rows ("System" / "Commands"). The
11
- // right pane passes `{ type: "arg" | "item" | "frame" | "entry" }` rows: an
12
- // argument header shows an aligned label and its value — a bracketed field with
13
- // a distinct background for a textual argument, a summary otherwise; an item
14
- // shows a `[x]`/`[ ]` (or radio) marker with its basename and parent; and a
15
- // `frame` row draws the top/bottom of the bordered list a selection's items sit
16
- // in. Reads the row bag `{ item, index, row, cursor, focus, value, labelWidth }`
8
+ // passes `{ type: "op" }` name-only rows (`risk` tints caution/destructive
9
+ // categories) and `{ type: "group" }` headers (Command / File / Create / Edit / Delete). The
10
+ // right pane passes `{ type: "arg" | "item" | "perm" | "frame" | "entry" |
11
+ // "parent" | "run" }` rows: an argument header stacks its label over its value
12
+ // — a bracketed field with a distinct background for textual arguments, a
13
+ // summary otherwise; an item shows a `[x]`/`[ ]` (or radio) marker with its
14
+ // basename and parent; `perm` is a `u/g/o` permission class with three r/w/x
15
+ // cells; a `frame` draws a selection list's border; and `parent` is the
16
+ // picker's `..` navigation row. Reads the row bag
17
+ // `{ item, index, row, cursor, focus, permBit, value }`
17
18
  // built by `rows.js`.
18
19
  //
19
20
  // Environment: `accentFg` (focused row), `textFg`, `textSecondaryFg` (summary),
20
- // `textHeadingFg` (a group header), `accentSecondaryFg` (a selected item),
21
- // `errorFg` (destructive op), `warningFg` (caution op), `selectionBg` (row
22
- // background), `fieldBg`/`fieldFg` (the value field), `borderSecondaryFg` (the
23
- // list border), `focusedClass` (web), `icons` (namespace marker).
21
+ // `textHeadingFg` (general headings), `accentSecondaryFg` (a selected item),
22
+ // `errorFg` (destructive op/error), `warningFg` (caution op), `selectionBg`
23
+ // (row background), `fieldBg`/`fieldFg` (editable values), `readonlyFg`
24
+ // (read-only values), `borderSecondaryFg` (the list border), `focusedClass`
25
+ // (web), `icons` (namespace marker).
24
26
 
25
27
  import { unwrap } from "select/state/cells"
28
+ import { PERMISSION_BITS } from "../core/permissions.js"
26
29
 
27
30
  // Function: riskFg
28
- // The colour a row's risk maps to: red for destructive operations, amber for
29
- // caution (a move), null when the row is not risky.
31
+ // The colour a row's category maps to: red for Delete, amber for Edit, null
32
+ // when the row is neither.
30
33
  function riskFg(row, env) {
31
- if (row?.type === "op" && row.risk === "destructive") return env.errorFg
32
- if (row?.type === "op" && row.risk === "caution") return env.warningFg
34
+ if ((row?.type === "op" || row?.type === "run") && row.risk === "destructive") return env.errorFg
35
+ if ((row?.type === "op" || row?.type === "run") && row.risk === "caution") return env.warningFg
33
36
  return null
34
37
  }
35
38
 
39
+ // Function: groupFg
40
+ // Color the purpose-group headings: Edit is amber and Delete is red.
41
+ function groupFg(group, env) {
42
+ if (group === "Edit") return env.warningFg
43
+ if (group === "Delete") return env.errorFg
44
+ return env.textFg
45
+ }
46
+
36
47
  // Function: rowName
37
- // The row's left cell: a group header's label, an operation's name, an
38
- // argument's label, an item's `[x] basename`, or a picker entry (directories
39
- // carry a trailing slash).
48
+ // The row's label: a group heading, operation name, argument label, `[Run]`
49
+ // action, `[x] basename`, or picker entry (directories carry a trailing slash).
40
50
  function rowName(row) {
41
51
  if (!row) return ""
42
52
  if (row.type === "group") return row.name ?? ""
53
+ if (row.type === "run") return row.error ? "[Retry]" : row.armed ? "[Confirm]" : "[Run]"
43
54
  if (row.type === "item") return `${row.marker ?? "[ ]"} ${row.name ?? ""}`
55
+ if (row.type === "perm") return row.label ?? ""
56
+ if (row.type === "parent") return ".."
44
57
  if (row.type === "entry") return `${row.name ?? ""}${row.isDir ? "/" : ""}`
45
58
  return row.name ?? ""
46
59
  }
47
60
 
48
61
  // Function: rowSummary
49
- // The row's right cell: an entry's argument list or key shortcut, an argument's
50
- // binding summary (or its value), an item's parent directory, or a picker
51
- // entry's kind.
62
+ // The row's right cell: an argument's binding summary (or its value), an
63
+ // item's parent directory, or a picker entry's kind.
52
64
  function rowSummary(row) {
53
65
  if (!row) return ""
54
66
  if (row.type === "item") return row.parent ?? ""
67
+ if (row.type === "parent") return "parent directory"
55
68
  if (row.type === "entry") {
56
69
  if (row.isSymlink && row.target) return `${row.target}`
57
70
  return row.isDir ? "dir" : ""
@@ -81,6 +94,11 @@ function createBehaviors(env) {
81
94
  const focused = (data) => isFocused(data.row, data.index, unwrap(data.cursor), unwrap(data.focus))
82
95
  const type = (data) => data.row?.type
83
96
  const isList = (data) => type(data) === "item" || type(data) === "frame"
97
+ // A permission row has three bit cells; the bit cursor marks the one Space
98
+ // toggles, and a click toggles the cell it lands on.
99
+ const bits = (data) => data.row?.bits ?? []
100
+ const bitAt = (data, index) => bits(data)[index]
101
+ const bitFocused = (data, index) => focused(data) && (unwrap(data.permBit) ?? 0) === index
84
102
  // The web classes that draw a selection's list box: the item rows carry the
85
103
  // sides, the frame rows the top/bottom.
86
104
  const listClass = (data) => {
@@ -91,23 +109,22 @@ function createBehaviors(env) {
91
109
  // A textual argument header edits in place while it owns the cursor: its
92
110
  // summary yields to a value input bound to the panel's edit cell.
93
111
  const editing = (_self, data) => focused(data) && type(data) === "arg" && data.row?.textual === true
94
- return {
112
+ const behaviors = {
95
113
  editing,
96
114
  // --- shared row content ---
97
115
  marker: (_self, data) => (focused(data) ? ">" : " "),
98
116
  markerFg: () => env.accentFg,
99
- // An argument label is padded to the column width so values line up
100
- // across arguments (both renderers honor the spaces).
101
- name: (_self, data) => {
102
- const text = rowName(data.row)
103
- return type(data) === "arg" ? text.padEnd(data.labelWidth ?? 8) : text
104
- },
117
+ // An argument's label sits on its own line, above the value.
118
+ name: (_self, data) => rowName(data.row),
119
+ // A permission row: its class label and three bit cells.
120
+ permClass: (_self, data) => data.row?.label ?? data.row?.classId ?? "",
121
+ isPerm: (_self, data) => type(data) === "perm",
105
122
  rowBg: (_self, data) => (focused(data) ? env.selectionBg : undefined),
106
123
  rowClass: (_self, data) =>
107
124
  [focused(data) ? env.focusedClass : null, listClass(data)].filter(Boolean).join(" ") || null,
108
125
  rowSelected: (_self, data) => `${data.row?.selected === true}`,
109
126
  rowFg: (_self, data) => {
110
- if (type(data) === "group") return env.textHeadingFg
127
+ if (type(data) === "group") return groupFg(data.row?.group ?? data.row?.name, env)
111
128
  const risk = riskFg(data.row, env)
112
129
  if (risk) return risk
113
130
  if (focused(data)) return env.accentFg
@@ -124,27 +141,66 @@ function createBehaviors(env) {
124
141
  // --- layout branches ---
125
142
  isArg: (_self, data) => type(data) === "arg",
126
143
  isItem: (_self, data) => type(data) === "item",
144
+ isOp: (_self, data) => type(data) === "op",
145
+ isGroup: (_self, data) => type(data) === "group",
146
+ isEntry: (_self, data) => type(data) === "entry" || type(data) === "parent",
147
+ isRun: (_self, data) => type(data) === "run",
148
+ hasRunError: (_self, data) => type(data) === "run" && Boolean(data.row?.error),
127
149
  isFrame: (_self, data) => type(data) === "frame",
128
150
  isList: (_self, data) => isList(data),
129
- isPlain: (_self, data) => !isList(data) && type(data) !== "arg",
130
- labelWidth: (_self, data) => data.labelWidth ?? 8,
131
- // The value field: a distinct background and a `[ ]` frame.
132
- fieldBg: () => env.fieldBg,
133
- fieldFg: () => env.fieldFg ?? env.textFg,
134
- fieldOpen: () => "[",
135
- fieldClose: () => "]",
136
- // The list indents under the value column: the marker (2) plus the gap
137
- // (1) plus the label column plus the gap (1). The spacer is NBSP so the
138
- // renderers do not collapse the leading run.
139
- rowIndent: (_self, data) => (isList(data) ? 4 + (data.labelWidth ?? 8) : 0),
140
- listIndent: (_self, data) => 4 + (data.labelWidth ?? 8),
141
- listSpacer: (_self, data) => "\u00a0".repeat(4 + (data.labelWidth ?? 8)),
151
+ showPlain: (_self, data) => type(data) === "op" || type(data) === "entry" || type(data) === "parent",
152
+ isPlain: (_self, data) => type(data) === "entry" || type(data) === "parent",
153
+ // An argument stacks its label over its value; every other row is one
154
+ // line tall.
155
+ rowHeight: (_self, data) => (type(data) === "arg" ? 2 : 1),
156
+ runMarginTop: (_self, data) => (type(data) === "run" ? 1 : 0),
157
+ runLabel: (_self, data) => (data?.row?.error ? "[Retry]" : data?.row?.armed ? "[Confirm]" : "[Run]"),
158
+ runAriaLabel: (_self, data) =>
159
+ data?.row?.error ? "Retry command" : data?.row?.armed ? "Confirm command" : "Run command",
160
+ runError: (_self, data) => data.row?.error ?? "",
161
+ runErrorFg: () => env.errorFg,
162
+ runFg: (_self, data) => riskFg(data.row, env) ?? env.textFg,
163
+ groupAttributes: () => env.groupAttributes ?? 0,
164
+ editable: (_self, data) => `${data.row?.editable === true}`,
165
+ // Editable values have a filled bracketed field; read-only summaries
166
+ // stay plain and muted.
167
+ fieldBg: (_self, data) => (data.row?.textual ? env.fieldBg : undefined),
168
+ fieldFg: (_self, data) =>
169
+ data.row?.textual ? (env.fieldFg ?? env.textFg) : (env.readonlyFg ?? env.textSecondaryFg),
170
+ textual: (_self, data) => data.row?.textual === true,
171
+ fieldOpen: (_self, data) => (data.row?.textual ? "[" : ""),
172
+ fieldClose: (_self, data) => (data.row?.textual ? "]" : ""),
173
+ // Selection lists share the argument pane's one-cell inset; the spacer
174
+ // is NBSP so the renderers do not collapse the leading run.
175
+ rowIndent: (_self, data) => (isList(data) ? 1 : 0),
176
+ listIndent: (_self, data) => 1,
177
+ listSpacer: (_self, data) => "\u00a0",
142
178
  listBorder: (_self, data) => {
143
179
  if (type(data) !== "frame") return ["left", "right"]
144
180
  return data.row?.variant === "top" ? ["top", "left", "right"] : ["bottom", "left", "right"]
145
181
  },
146
182
  borderFg: () => env.borderSecondaryFg,
147
183
  }
184
+ // r/w/x cells of a permission row, in the shared permission order so the
185
+ // templates can bind each cell without looping.
186
+ for (let i = 0; i < PERMISSION_BITS.length; i++) {
187
+ const bit = PERMISSION_BITS[i]
188
+ const name = bit.id.toUpperCase()
189
+ behaviors[`perm${name}`] = (_self, data) => (bitAt(data, i)?.selected ? bit.id : "-")
190
+ behaviors[`perm${name}On`] = (_self, data) => `${bitAt(data, i)?.selected === true}`
191
+ behaviors[`perm${name}Fg`] = (_self, data) =>
192
+ bitAt(data, i)?.selected ? env.accentSecondaryFg : env.textSecondaryFg
193
+ behaviors[`perm${name}Bg`] = (_self, data) =>
194
+ bitFocused(data, i) ? env.accentSecondaryBg : undefined
195
+ behaviors[`perm${name}Class`] = (_self, data) =>
196
+ [
197
+ bitAt(data, i)?.selected ? "is-on" : "is-off",
198
+ bitFocused(data, i) ? "is-bit-focused" : null,
199
+ ]
200
+ .filter(Boolean)
201
+ .join(" ")
202
+ }
203
+ return behaviors
148
204
  }
149
205
 
150
206
  export { createBehaviors, isFocused, riskFg, rowName, rowSummary, wantsFocus }