@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
@@ -1,4 +1,4 @@
1
- <box flexDirection="row" height="1" out:backgroundColor="rowBg" on:click="row.key!panel:select">
1
+ <box flexDirection="row" out:height="rowHeight" out:marginTop="runMarginTop" out:backgroundColor="rowBg" on:click="row.key!panel:select">
2
2
  <!-- A selection's item list: indented under the value column and framed
3
3
  by the top/bottom border rows. -->
4
4
  <text when="isList" out="listSpacer" wrapMode="none"></text>
@@ -8,23 +8,47 @@
8
8
  <text when="isItem" out="listName" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
9
9
  <text when="isItem" out="listSummary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
10
10
  </box>
11
- <!-- An argument header: an aligned label and a bracketed value field. -->
12
- <box when="isArg" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
13
- <text out="marker" width="2" out:fg="markerFg"></text>
14
- <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
15
- <box when="editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
16
- <text out="fieldOpen" out:fg="fieldFg"></text>
17
- <input inout="value" flexGrow="1" minWidth="6" height="1" out:focused="editing"></input>
18
- <text out="fieldClose" out:fg="fieldFg"></text>
11
+ <!-- An argument header: its label on one line, the value field under it.
12
+ Both start one cell in from the pane border. -->
13
+ <box when="isArg" flexDirection="column" flexGrow="1" flexShrink="1" minWidth="0" paddingLeft="1">
14
+ <box flexDirection="row" height="1" columnGap="1" minWidth="0">
15
+ <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
19
16
  </box>
20
- <box when="!editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
21
- <text out="fieldOpen" out:fg="fieldFg"></text>
22
- <text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
23
- <text out="fieldClose" out:fg="fieldFg"></text>
17
+ <box flexDirection="row" height="1" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
18
+ <box when="editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
19
+ <text when="textual" out="fieldOpen" out:fg="fieldFg"></text>
20
+ <input inout="value" flexGrow="1" minWidth="6" height="1" out:focused="editing"></input>
21
+ <text when="textual" out="fieldClose" out:fg="fieldFg"></text>
22
+ </box>
23
+ <box when="!editing" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" out:backgroundColor="fieldBg" overflow="hidden">
24
+ <text when="textual" out="fieldOpen" out:fg="fieldFg"></text>
25
+ <text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
26
+ <text when="textual" out="fieldClose" out:fg="fieldFg"></text>
27
+ </box>
24
28
  </box>
25
29
  </box>
26
- <!-- Operation, group and picker-entry rows: marker + name + summary. -->
27
- <box when="isPlain" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
30
+ <!-- A permission class: its label and three clickable r/w/x cells. The bit
31
+ cursor marks the cell Space toggles. -->
32
+ <box when="isPerm" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" height="1" columnGap="1" paddingLeft="1">
33
+ <text out="permClass" width="7" out:fg="rowFg" wrapMode="none"></text>
34
+ <box height="1" on:click="row.keyR!panel:perm." out:backgroundColor="permRBg"><text out="permR" width="1" out:fg="permRFg"></text></box>
35
+ <box height="1" on:click="row.keyW!panel:perm." out:backgroundColor="permWBg"><text out="permW" width="1" out:fg="permWFg"></text></box>
36
+ <box height="1" on:click="row.keyX!panel:perm." out:backgroundColor="permXBg"><text out="permX" width="1" out:fg="permXFg"></text></box>
37
+ </box>
38
+ <box when="isRun" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1" justifyContent="flex-start">
39
+ <text out="runLabel" out:fg="runFg" wrapMode="none"></text>
40
+ </box>
41
+ <!-- Bold color-group title; operations below it are indented. -->
42
+ <box when="isGroup" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" paddingLeft="1">
43
+ <text out="name" out:fg="rowFg" out:attributes="groupAttributes" wrapMode="none"></text>
44
+ </box>
45
+ <!-- Name-only operation row, indented beneath its group title. -->
46
+ <box when="isOp" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1" paddingLeft="2">
47
+ <text out="marker" width="2" out:fg="markerFg"></text>
48
+ <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
49
+ </box>
50
+ <!-- Picker entries retain their cursor marker and trailing kind summary. -->
51
+ <box when="isEntry" flexDirection="row" flexGrow="1" flexShrink="1" minWidth="0" columnGap="1">
28
52
  <text out="marker" width="2" out:fg="markerFg"></text>
29
53
  <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
30
54
  <text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
@@ -1,4 +1,4 @@
1
- <footer class="command-bar">
1
+ <footer class="command-bar" ref="box" when="showBar">
2
2
  <nav when="showKeys" out:class="listClass" out="rows"></nav>
3
3
  <span when="showStatus" out="status" out:class="statusClass" out:data-kind="notice.kind" role="status"></span>
4
4
  </footer>
@@ -5,16 +5,17 @@
5
5
 
6
6
  // Module: tcmd/components/CommandBar
7
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 hints of the
8
+ // current context (the command registry's `BAR_COMMANDS`, or the hints of the
9
9
  // overlay that owns the keyboard); 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.
10
+ // its place. Entries are bracket shorthands (see `CommandItem.js`) that stay
11
+ // on one line: entries that do not fit are dropped and a trailing ellipsis
12
+ // takes their place, while the active pane's marked count joins the list as a
13
+ // dimmed, non-clickable entry. The row logic lives in `CommandItem.js`; this
14
+ // parent maps its `items` to rows.
14
15
  //
15
16
  // The web presentation swaps the key list for a single status line
16
17
  // (`showKeys`/`showStatus`/`status`/`statusClass`); the TUI presentation
17
- // keeps the wrapped key list and swaps it for the status when there is one.
18
+ // keeps the single-line key list and swaps it for the status when there is one.
18
19
  //
19
20
  // Environment: `rows(data)` returning the keyed row list, `listClass` (web
20
21
  // root class), `errorClass` (web), and the TUI colors plus the
@@ -23,6 +24,10 @@
23
24
  import { unwrap } from "select/state/cells"
24
25
  import { commandWidth } from "./CommandItem.js"
25
26
 
27
+ // Constant: ELLIPSIS
28
+ // The trailing marker shown when the bar cannot fit every entry.
29
+ const ELLIPSIS = { id: null, title: "…", dim: true }
30
+
26
31
  // Function: commandBarStatus
27
32
  // The one-line status the bar shows when something needs attention: the
28
33
  // session notice first, then a pane error. Empty when everything is fine.
@@ -52,25 +57,25 @@ function commandBarMarksItem(data) {
52
57
  return marks ? { id: null, title: marks, dim: true } : null
53
58
  }
54
59
 
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
60
+ // Function: commandBarFit
61
+ // Truncates entries to `width` columns, appending a dim ellipsis when an entry
62
+ // had to be dropped. `width <= 0` (not measured yet) keeps every entry and
63
+ // lets the renderer clip. The bar is always one line: entries never wrap.
64
+ function commandBarFit(items, width) {
65
+ const list = items ?? []
66
+ if (!width || width <= 0) return list
67
+ let used = 0
68
+ for (let i = 0; i < list.length; i++) {
69
+ const next = used + commandWidth(list[i])
70
+ if (next > width) {
71
+ const kept = list.slice(0, i)
72
+ // The ellipsis needs its own column plus the separating space.
73
+ while (kept.length && used + 2 > width) used -= commandWidth(kept.pop())
74
+ return [...kept, ELLIPSIS]
69
75
  }
70
- cols += w
71
- index++
76
+ used = next
72
77
  }
73
- return rows
78
+ return list
74
79
  }
75
80
 
76
81
  // Function: isInfo
@@ -91,7 +96,8 @@ function statusKind(data) {
91
96
  function createBehaviors(env) {
92
97
  const behaviors = {
93
98
  listClass: () => env?.listClass,
94
- showKeys: (_self, data) => !commandBarStatus(data),
99
+ showBar: (_self, data) => !unwrap(data?.desktopMode) || Boolean(commandBarStatus(data)),
100
+ showKeys: (_self, data) => !unwrap(data?.desktopMode) && !commandBarStatus(data),
95
101
  showStatus: (_self, data) => Boolean(commandBarStatus(data)),
96
102
  status: (_self, data) => commandBarStatus(data),
97
103
  statusBg: (_self, data) => {
@@ -121,6 +127,6 @@ function createBehaviors(env) {
121
127
  return behaviors
122
128
  }
123
129
 
124
- export { commandBarMarks, commandBarMarksItem, commandBarRowCount, commandBarStatus, createBehaviors }
130
+ export { commandBarFit, commandBarMarks, commandBarMarksItem, commandBarStatus, createBehaviors }
125
131
 
126
132
  // EOF
@@ -1,4 +1,4 @@
1
- <box flexDirection="column" overflow="hidden">
2
- <box when="showKeys" out="rows" flexDirection="row" flexWrap="wrap" flexGrow="1" overflow="hidden"></box>
1
+ <box ref="box" flexDirection="column" overflow="hidden">
2
+ <box when="showKeys" out="rows" flexDirection="row" flexWrap="nowrap" flexGrow="1" overflow="hidden"></box>
3
3
  <text when="showStatus" out="status" out:fg="statusFg" out:bg="statusBg" flexShrink="0"></text>
4
4
  </box>
@@ -8,16 +8,16 @@
8
8
  // both engines. `item.id` may be null for pure key hints, in which case a
9
9
  // click dispatches nothing.
10
10
  //
11
- // Each entry renders as a plain bracket shorthand, preceded by a dim `|`
12
- // divider (except the first entry):
11
+ // Each entry renders as a plain bracket shorthand:
13
12
  //
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.
13
+ // [Open ⏎] — the brackets in the dim text colour, the title in the text
14
+ // colour, the key glyph in the accent (primary) or secondary
15
+ // accent, and a trailing space separating it from the next
16
+ // entry. Entries are separated by that space alone — no
17
+ // divider glyph.
17
18
  //
18
19
  // 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).
20
+ // with no brackets. The web renders the same label as a single string.
21
21
  //
22
22
  // Environment: `accentFg` (primary), `accentSecondaryFg` (secondary),
23
23
  // `textFg`, `textSecondaryFg`.
@@ -25,8 +25,12 @@
25
25
  // Function: keyGlyph
26
26
  // The shorthand glyph for a shortcut label: ⏎ for Enter, ⇥ for Tab.
27
27
  function keyGlyph(keys) {
28
+ if (!keys) return keys
28
29
  const GLYPHS = { Enter: "⏎", "↵": "⏎", Tab: "⇥" }
29
- return GLYPHS[keys] ?? keys
30
+ return keys
31
+ .split("/")
32
+ .map((key) => GLYPHS[key] ?? (key.endsWith("Tab") ? `${key.slice(0, -3)}⇥` : key))
33
+ .join("/")
30
34
  }
31
35
 
32
36
  // Function: commandLabel
@@ -36,16 +40,10 @@ function commandLabel(item) {
36
40
  return item.keys ? `[${item.title} ${keyGlyph(item.keys)}] ` : `${item.title} `
37
41
  }
38
42
 
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
43
  // 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)
44
+ // Columns the entry occupies: its label, brackets and trailing space included.
45
+ function commandWidth(item) {
46
+ return commandLabel(item).length
49
47
  }
50
48
 
51
49
  // Function: isPrimary
@@ -57,23 +55,24 @@ function isPrimary(item) {
57
55
  // Function: createBehaviors
58
56
  // Builds the CommandItem behavior map for the given renderer environment.
59
57
  function createBehaviors(env) {
60
- const head = (_self, data) => (data.item?.keys ? `[${data.item.title} ` : `${data.item?.title ?? ""} `)
61
58
  return {
62
59
  // Web: the whole shorthand as one label.
63
60
  itemLabel: (_self, data) => commandLabel(data.item),
64
61
  itemHint: (_self, data) => !data.item?.id,
65
- // TUI: the dim pipe divider, then the bracket + title head, the
66
- // accent key glyph, the closer.
67
- itemSep: (_self, data) => (data.index > 0 ? COMMAND_SEP : ""),
68
- itemSepFg: () => env?.textSecondaryFg,
69
- itemHead: head,
62
+ // TUI: the dim brackets around the bright title, then the accent key
63
+ // glyph and the closer. Keyless entries (the marked count) render as a
64
+ // plain dim word, with no brackets.
65
+ itemOpen: (_self, data) => (data.item?.keys ? "[" : ""),
66
+ itemOpenFg: () => env?.textSecondaryFg,
67
+ itemHead: (_self, data) => `${data.item?.title ?? ""} `,
70
68
  itemFg: (_self, data) => (data.item?.keys ? env?.textFg : env?.textSecondaryFg),
71
69
  keyText: (_self, data) => (data.item?.keys ? keyGlyph(data.item.keys) : ""),
72
70
  keyFg: (_self, data) => (isPrimary(data.item) ? env?.accentFg : env?.accentSecondaryFg),
73
- endText: (_self, data) => (data.item?.keys ? "] " : ""),
71
+ itemClose: (_self, data) => (data.item?.keys ? "] " : ""),
72
+ itemCloseFg: () => env?.textSecondaryFg,
74
73
  }
75
74
  }
76
75
 
77
- export { COMMAND_SEP, commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
76
+ export { commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
78
77
 
79
78
  // EOF
@@ -1,6 +1,6 @@
1
1
  <box flexDirection="row" height="1" flexShrink="0" on:click="item.id!commandbar:run">
2
- <text out="itemSep" out:fg="itemSepFg" wrapMode="none"></text>
2
+ <text out="itemOpen" out:fg="itemOpenFg" wrapMode="none"></text>
3
3
  <text out="itemHead" out:fg="itemFg" wrapMode="none"></text>
4
4
  <text out="keyText" out:fg="keyFg" wrapMode="none"></text>
5
- <text out="endText" out:fg="itemFg" wrapMode="none"></text>
5
+ <text out="itemClose" out:fg="itemCloseFg" wrapMode="none"></text>
6
6
  </box>
@@ -1,6 +1,7 @@
1
- <section class="command-panel" when="open" role="dialog" aria-modal="true" aria-label="Command panel" tabindex="-1">
2
- <div class="command-dialog">
3
- <header out="title"></header>
1
+ <section class="command-panel" when="open" role="dialog" aria-modal="true" aria-label="Command panel" tabindex="-1" on:click="!dialog:panel-cancel">
2
+ <div class="command-dialog" on:click="dialogStop">
3
+ <header class="panel-title" out="title"></header>
4
+ <button class="panel-close" type="button" tabindex="-1" aria-label="Close command panel" on:click="!panel:close." out="closeHint"></button>
4
5
  <div class="panel-columns">
5
6
  <div class="panel-command-column">
6
7
  <label class="panel-query-line">
@@ -11,17 +12,16 @@
11
12
  </div>
12
13
  <div class="panel-args-column">
13
14
  <p class="panel-command-name" out="commandName"></p>
14
- <div class="panel-picker" when="picking">
15
- <p class="panel-picker-path" out="pickerPath"></p>
16
- <label class="panel-picker-line">
17
- <span out="pickerSign"></span>
18
- <input class="panel-picker-query" type="text" inout="pickerQuery" aria-label="Filter entries" />
19
- </label>
15
+ <div when="picking" out="pickerView|FilePicker"></div>
16
+ <div class="panel-args-scroll" when="!picking">
17
+ <ul class="panel-args" aria-label="Arguments" out="argRows"></ul>
18
+ <pre class="panel-preview" when="hasPreview" out="preview"></pre>
19
+ <ul class="panel-run-action" aria-label="Run command" out="runRows"></ul>
20
+ <div class="panel-run-error-block" when="hasRunError">
21
+ <p out="runError"></p>
22
+ </div>
23
+ <p class="panel-confirm-command" when="hasConfirmCommand" out="confirmCommand" out:data-risk="confirmRisk"></p>
20
24
  </div>
21
- <p class="panel-args-empty" when="argsEmpty" out="argsEmptyText"></p>
22
- <ul class="panel-args" aria-label="Arguments" out="argRows"></ul>
23
- <p class="panel-args-empty" when="pickerEmpty" out="pickerEmptyText"></p>
24
- <div class="panel-runs" out:data-runnable="runnableState"><pre out="preview"></pre></div>
25
25
  </div>
26
26
  </div>
27
27
  <footer out="footer"></footer>
@@ -5,17 +5,16 @@
5
5
 
6
6
  // Module: tcmd/components/CommandPanel
7
7
  // CommandPanel: the one entry point (`:`), a two-pane palette. The left pane
8
- // holds a `:` filter above the command list — the CLI operations, flat. The
9
- // right pane lists the selected operation's arguments — one row per argument,
10
- // its label and its value (a bracketed field, or a checkbox/radio list of the
11
- // selected paths) — under the operation's name, with the resolved command at
12
- // the pane's foot. The row logic lives in `CommandArgRow.js`; this parent maps
13
- // the command list and the argument rows.
8
+ // holds a `:` filter above categorized commands. The right pane lists the
9
+ // selected operation's arguments followed by its [Run]/[Retry]
10
+ // row.
11
+ // The row logic lives in `CommandArgRow.js`; this parent maps the operation
12
+ // list, scrolling argument rows, custom preview and pinned action row.
14
13
  //
15
14
  // Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
16
15
  // (footer), `textHeadingFg` (title/sign/command name), `opRows(self, data)` /
17
- // `argRows(self, data)` returning the keyed row lists, and
18
- // `argScrollTop(self, data)` (TUI viewport tracking).
16
+ // `argRows(self, data)` returning keyed row lists, and
17
+ // the TUI viewport hooks `opScrollTop` / `argScrollTop`.
19
18
 
20
19
  import { unwrap } from "select/state/cells"
21
20
  import { PANEL_FOCUS } from "../core/panel.js"
@@ -29,10 +28,10 @@ function panelTitle(entry) {
29
28
  }
30
29
 
31
30
  // Function: panelFooter
32
- const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run Esc close"
31
+ const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run"
33
32
 
34
33
  // Function: pickerFooter
35
- const PICKER_FOOTER = "type to filter ↑↓ row → open ↵ choose ⌃↵ this dir ⌫ up Esc back"
34
+ const PICKER_FOOTER = "type path ↑↓ rows → open ↵ go ⌃↵ here ⇥ focus Esc back"
36
35
 
37
36
  // Function: panelSign
38
37
  // The filter prompt (`:`) before the command filter input.
@@ -47,29 +46,51 @@ function createBehaviors(env) {
47
46
  const behaviors = {
48
47
  bg: () => env.overlayBg,
49
48
  borderFg: () => env.borderPrimaryActiveFg,
50
- // The right pane's header: the selected command's name.
49
+ // The right pane's header: the selected command's name. The close
50
+ // shorthand lives in the dialog's top border.
51
51
  commandName: (_self, data) => unwrap(data.entry)?.name ?? "",
52
52
  commandNameFg: () => env.textHeadingFg,
53
+ closeHint: () => "[Close Esc]",
54
+ closeHintFg: () => env.textSecondaryFg,
53
55
  confirm: (self) => self.pub("panel:confirm"),
54
- // The argument pane's empty state: an operation with no bound rows has
55
- // nothing to fill. While picking the picker's own empty state stands in.
56
- argsEmpty: (_self, data) =>
57
- !unwrap(data.picking) && (unwrap(data.rows)?.length ?? 0) === 0,
58
- argsEmptyText: () => "no arguments",
59
- // The file picker sub-mode: a path line, its own filter input, and the
60
- // entry rows (rendered through the shared arg-row map).
56
+ // The armed action prints the command it will execute below `[Confirm]`,
57
+ // tinted with the action's risk colour.
58
+ confirmCommand: (_self, data) =>
59
+ (unwrap(data?.rows) ?? []).find((row) => row.type === "run")?.command ?? "",
60
+ hasConfirmCommand: (_self, data) =>
61
+ Boolean((unwrap(data?.rows) ?? []).find((row) => row.type === "run")?.command),
62
+ confirmCommandFg: (_self, data) =>
63
+ unwrap(data?.entry)?.risk === "destructive" ? env.errorFg : env.warningFg,
64
+ confirmRisk: (_self, data) => unwrap(data?.entry)?.risk ?? null,
65
+ // A failed run moves its error out of the action row into a framed
66
+ // block below the button.
67
+ hasRunError: (_self, data) =>
68
+ Boolean((unwrap(data?.rows) ?? []).find((row) => row.type === "run" && row.error)?.error),
69
+ runError: (_self, data) =>
70
+ (unwrap(data?.rows) ?? []).find((row) => row.type === "run" && row.error)?.error ?? "",
71
+ runErrorFg: () => env.errorFg,
72
+ // The file picker sub-mode: the nested FilePicker owns its editable path,
73
+ // file-panel rows and scrollbar.
61
74
  picking: (_self, data) => Boolean(unwrap(data.picking)),
62
- pickerPath: (_self, data) => unwrap(data.pickerPath) ?? "",
63
- pickerSign: () => "⌕ ",
64
- pickerFocused: (self, data) => panelOpen(self, data) && Boolean(unwrap(data.picking)),
65
- pickerEmpty: (_self, data) =>
66
- Boolean(unwrap(data.picking)) && (unwrap(data.rows)?.length ?? 0) === 0,
67
- pickerEmptyText: () => " empty",
68
- footer: (_self, data) => (unwrap(data?.picking) ? PICKER_FOOTER : PANEL_FOOTER),
75
+ pickerView: (_self, data) => ({
76
+ open: data.picking,
77
+ path: data.pickerPathText,
78
+ focus: data.pickerFocus,
79
+ rows: data.pickerRows,
80
+ cursor: data.argCursor,
81
+ }),
82
+ footer: (_self, data) => {
83
+ if (unwrap(data?.picking)) return PICKER_FOOTER
84
+ // An armed risky action turns Enter into the confirmation step. The
85
+ // run row carries the flag (the rows input is already reactive).
86
+ const armed = (unwrap(data?.rows) ?? []).some((row) => row.type === "run" && row.armed)
87
+ const base = armed ? PANEL_FOOTER.replace("↵ run", "↵ confirm") : PANEL_FOOTER
88
+ // The permission grid adds a bit cursor to the usual keys.
89
+ return unwrap(data?.entry)?.name === "chmod" ? `${base} ←→ bits` : base
90
+ },
69
91
  footerFg: () => env.textSecondaryFg,
70
- // Dividers: a vertical rule between the panes, horizontal ones under the
71
- // filter and above the resolved command — all in the secondary border
72
- // colour, like the pane header rules.
92
+ // Dividers: a vertical rule between the panes and a horizontal rule under
93
+ // the filter, in the secondary border colour.
73
94
  ruleFg: () => env.borderSecondaryFg,
74
95
  sideRule: () => ["left"],
75
96
  headRule: () => ["top"],
@@ -77,15 +98,16 @@ function createBehaviors(env) {
77
98
  focusArgs: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.ARGS,
78
99
  focusOps: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.OPS,
79
100
  open: (_self, data) => Boolean(unwrap(data.open)),
80
- // The resolved command the draft expands to, dim, at the foot of the
81
- // argument pane; a `«NAME»` marks a required argument still to fill.
82
- preview: (_self, data) => unwrap(data.command) ?? "",
83
- runnableState: (_self, data) => `${unwrap(data.runnable) !== false}`,
84
- previewFg: () => env.textSecondaryFg,
85
101
  // The filter input owns the keyboard while the panel is open, the
86
102
  // command list is focused and the picker is not browsing.
87
103
  queryFocused: (self, data) =>
88
104
  panelOpen(self, data) && !unwrap(data.picking) && unwrap(data.focus) !== PANEL_FOCUS.ARGS,
105
+ // Only operation-specific preflight output is rendered; the assembled
106
+ // shell command remains available from the session but is not echoed.
107
+ hasPreview: (_self, data) => !unwrap(data.picking) && Boolean(unwrap(data.preview)),
108
+ preview: (_self, data) => unwrap(data.preview) ?? "",
109
+ previewFg: () => env.textSecondaryFg,
110
+ panelHeight: (self, data) => env.panelHeight?.(self, data) ?? "72%",
89
111
  sign: () => panelSign(),
90
112
  signFg: () => env.textHeadingFg,
91
113
  title: (_self, data) => panelTitle(data.entry),
@@ -93,6 +115,8 @@ function createBehaviors(env) {
93
115
  }
94
116
  if (env?.opRows) behaviors.opRows = (self, data) => env.opRows(self, data)
95
117
  if (env?.argRows) behaviors.argRows = (self, data) => env.argRows(self, data)
118
+ if (env?.runRows) behaviors.runRows = (self, data) => env.runRows(self, data)
119
+ if (env?.opScrollTop) behaviors.opScrollTop = (self, data) => env.opScrollTop(self, data)
96
120
  if (env?.argScrollTop) behaviors.argScrollTop = (self, data) => env.argScrollTop(self, data)
97
121
  return behaviors
98
122
  }
@@ -1,5 +1,5 @@
1
1
  <box when="open" width="100%" height="100%" alignItems="center" justifyContent="center">
2
- <box width="80%" height="72%" border borderStyle="rounded"
2
+ <box width="80%" out:height="panelHeight" border borderStyle="rounded"
3
3
  out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
4
4
  out:backgroundColor="bg" flexDirection="column">
5
5
  <box flexDirection="row" flexGrow="1" overflow="hidden">
@@ -10,40 +10,50 @@
10
10
  <input ref="query" inout="query" flexGrow="1" height="1" out:focused="queryFocused"></input>
11
11
  </box>
12
12
  <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
13
- <scrollbox flexGrow="1">
13
+ <scrollbox ref="ops" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="opScrollTop">
14
14
  <box out="opRows" flexDirection="column"></box>
15
15
  </scrollbox>
16
16
  </box>
17
17
  <!-- Vertical rule between the panes. -->
18
18
  <box width="1" out:border="sideRule" out:borderColor="ruleFg"></box>
19
- <!-- Right pane: the command name, its arguments, the resolved command. -->
19
+ <!-- Right pane: the command name, arguments and trailing Run action. -->
20
20
  <box flexDirection="column" flexGrow="3" flexBasis="0" overflow="hidden">
21
+ <!-- The command name heads the pane. -->
21
22
  <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
22
- <text out="commandName" out:fg="commandNameFg" wrapMode="none"></text>
23
+ <text out="commandName" out:fg="commandNameFg" flexGrow="1" flexShrink="1" minWidth="0" wrapMode="none" truncate="true"></text>
23
24
  </box>
24
25
  <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
25
- <scrollbox ref="args" flexGrow="1" out:scrollTop="argScrollTop">
26
- <box flexDirection="column">
27
- <!-- File picker sub-mode: the browsed path, its own filter,
28
- then the entry rows (which arrive through `argRows`). -->
29
- <text when="picking" out="pickerPath" out:fg="labelFg" paddingLeft="1" wrapMode="none" truncate="true"></text>
30
- <box when="picking" flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
31
- <text out="pickerSign" out:fg="signFg" wrapMode="none"></text>
32
- <input ref="pickQuery" inout="pickerQuery" flexGrow="1" height="1" out:focused="pickerFocused"></input>
33
- </box>
34
- <text when="argsEmpty" out="argsEmptyText" out:fg="labelFg" paddingLeft="1" wrapMode="none"></text>
35
- <box out="argRows" flexDirection="column"></box>
36
- <text when="pickerEmpty" out="pickerEmptyText" out:fg="labelFg" paddingLeft="1" wrapMode="none"></text>
26
+ <!-- Arguments, preflight preview and the Run action share one
27
+ scroller, so Run lands one line below the last field rather
28
+ than pinned to the bottom of the pane. -->
29
+ <scrollbox when="!picking" ref="args" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="argScrollTop">
30
+ <box out="argRows" flexDirection="column"></box>
31
+ <box ref="previewBox" when="hasPreview" flexDirection="column" flexShrink="0" maxHeight="5" paddingLeft="1" overflow="hidden">
32
+ <text out="preview" out:fg="previewFg" wrapMode="word"></text>
33
+ </box>
34
+ <box out="runRows" flexDirection="column" flexShrink="0" paddingLeft="1" paddingRight="1"></box>
35
+ <box when="hasRunError" border borderStyle="single" out:borderColor="runErrorFg"
36
+ flexDirection="column" flexShrink="0" marginTop="1" marginLeft="1" marginRight="1"
37
+ paddingLeft="1" paddingRight="1">
38
+ <text out="runError" out:fg="runErrorFg" wrapMode="word"></text>
39
+ </box>
40
+ <box when="hasConfirmCommand" paddingLeft="1">
41
+ <text out="confirmCommand" out:fg="confirmCommandFg" wrapMode="word"></text>
37
42
  </box>
38
43
  </scrollbox>
39
- <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
40
- <box flexDirection="column" flexShrink="0" paddingLeft="1" paddingRight="1" overflow="hidden">
41
- <text out="preview" out:fg="previewFg" wrapMode="word"></text>
42
- </box>
44
+ <filepicker when="picking" flexGrow="1" flexShrink="1" minHeight="0"
45
+ in:open="picking" in:path="pickerPathText" in:focus="pickerFocus"
46
+ in:rows="pickerRows" in:cursor="argCursor"></filepicker>
43
47
  </box>
44
48
  </box>
45
49
  <box flexDirection="row" height="1" paddingLeft="1">
46
50
  <text out="footer" out:fg="footerFg"></text>
47
51
  </box>
52
+ <!-- The close action sits in the top border, upper-right, like a close
53
+ button; the background masks the border run underneath. -->
54
+ <box position="absolute" top="-1" right="2" height="1" zIndex="3"
55
+ out:backgroundColor="bg" on:click="!panel:close.">
56
+ <text out="closeHint" out:fg="closeHintFg" wrapMode="none"></text>
57
+ </box>
48
58
  </box>
49
59
  </box>
@@ -1,3 +1,3 @@
1
- <div class="confirm" when="visible" role="dialog" aria-modal="true" aria-label="Confirm operation" tabindex="-1">
2
- <p out="text"></p>
1
+ <div class="confirm" when="visible" role="dialog" aria-modal="true" aria-label="Confirm operation" tabindex="-1" on:click="!dialog:confirm-cancel">
2
+ <p out="text" on:click="dialogStop"></p>
3
3
  </div>
@@ -10,7 +10,8 @@
10
10
  //
11
11
  // - `selected` (the pane's marked count, above one) leads in the mark colour;
12
12
  // - `date` (mtime) and `size` are the numeric fields, drawn bright;
13
- // - `type` (file/dir/link/dev) is coloured like the listing;
13
+ // - `type` (file/dir/link/dev) is coloured like the listing, a plain file
14
+ // taking its executable/read-only colour too;
14
15
  // - `rights` (permission mode) is accented.
15
16
  //
16
17
  // When the pane is too narrow the trailing blocks give way first (`rights`,
@@ -21,6 +22,7 @@
21
22
  // out.
22
23
 
23
24
  import { unwrap } from "select/state/cells"
25
+ import { isExecutable, isReadonly, nameFg } from "./FileRow.js"
24
26
  import { formatMode, formatSize, formatTime } from "./format.js"
25
27
 
26
28
  // Constant: KIND_LABELS
@@ -49,13 +51,24 @@ function entryKind(entry) {
49
51
  }
50
52
 
51
53
  // Function: typeColor
52
- // The listing colour for an entry's kind, mirroring `FileRow.nameFg`.
54
+ // The listing colour for an entry's kind, delegating to `FileRow.nameFg` so
55
+ // the detail's type reads exactly like the row's name — directory, symlink and
56
+ // device kinds, plus the executable/read-only states of a plain file.
53
57
  function typeColor(entry, env) {
58
+ return nameFg(entry, env)
59
+ }
60
+
61
+ // Function: typeClass
62
+ // The web state classes for the detail's type span: the kind's `is-<kind>`
63
+ // class, plus the executable/read-only class when the entry is a plain file
64
+ // (mirroring `FileRow.nameClass`, but on the type-specific `is-<kind>` base).
65
+ function typeClass(entry, env) {
54
66
  const kind = entryKind(entry)
55
- if (kind === "symlink") return env.linkFg
56
- if (kind === "directory") return env.dirFg
57
- if (kind === "device") return env.devFg
58
- return env.fileFg
67
+ const base = `is-${kind}`
68
+ if (kind !== "file") return base
69
+ if (isExecutable(entry)) return `${base} ${env.execClass}`
70
+ if (isReadonly(entry)) return `${base} ${env.readonlyClass}`
71
+ return base
59
72
  }
60
73
 
61
74
  // Function: detailBlocks
@@ -168,7 +181,7 @@ function createBehaviors(env = {}) {
168
181
  detailTime: (self, data) => text(self, data, "time"),
169
182
  detailHasType: (self, data) => has(self, data, "type"),
170
183
  detailType: (self, data) => text(self, data, "type"),
171
- detailTypeClass: (_self, data) => `is-${entryKind(unwrap(data.entry))}`,
184
+ detailTypeClass: (_self, data) => typeClass(unwrap(data.entry), env),
172
185
  detailTypeFg: (_self, data) => typeColor(unwrap(data.entry), env),
173
186
  detailHasSize: (self, data) => has(self, data, "size"),
174
187
  detailSize: (self, data) => text(self, data, "size"),
@@ -190,5 +203,6 @@ export {
190
203
  detailBlocks,
191
204
  detailLine,
192
205
  entryKind,
206
+ typeClass,
193
207
  typeColor,
194
208
  }