@ssebastien/tcmd 0.1.2 → 0.1.4

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 (79) hide show
  1. package/README.md +121 -38
  2. package/bin/tcmd +11 -3
  3. package/bin/tcmd.js +15 -0
  4. package/package.json +4 -3
  5. package/public/index.html +14 -0
  6. package/public/main.js +17501 -0
  7. package/public/style.css +901 -0
  8. package/scripts/serve-web.js +61 -0
  9. package/src/js/tcmd/components/App.html +28 -12
  10. package/src/js/tcmd/components/App.js +135 -85
  11. package/src/js/tcmd/components/App.xml +64 -15
  12. package/src/js/tcmd/components/BookmarkList.html +3 -2
  13. package/src/js/tcmd/components/BookmarkList.js +1 -1
  14. package/src/js/tcmd/components/Breadcrumbs.js +1 -1
  15. package/src/js/tcmd/components/CommandArgRow.html +17 -0
  16. package/src/js/tcmd/components/CommandArgRow.js +86 -26
  17. package/src/js/tcmd/components/CommandArgRow.xml +31 -4
  18. package/src/js/tcmd/components/CommandBar.html +1 -1
  19. package/src/js/tcmd/components/CommandBar.js +3 -3
  20. package/src/js/tcmd/components/CommandItem.html +1 -1
  21. package/src/js/tcmd/components/CommandItem.js +1 -0
  22. package/src/js/tcmd/components/CommandPanel.html +29 -0
  23. package/src/js/tcmd/components/CommandPanel.js +54 -52
  24. package/src/js/tcmd/components/CommandPanel.xml +38 -24
  25. package/src/js/tcmd/components/Confirm.html +1 -1
  26. package/src/js/tcmd/components/EntryDetail.html +7 -7
  27. package/src/js/tcmd/components/EntryDetail.js +1 -1
  28. package/src/js/tcmd/components/FileList.html +5 -1
  29. package/src/js/tcmd/components/FileList.js +2 -1
  30. package/src/js/tcmd/components/FilePreview.html +14 -3
  31. package/src/js/tcmd/components/FilePreview.js +73 -10
  32. package/src/js/tcmd/components/FilePreview.xml +19 -6
  33. package/src/js/tcmd/components/FileRow.html +1 -1
  34. package/src/js/tcmd/components/FileRow.js +10 -3
  35. package/src/js/tcmd/components/FilterBar.html +1 -1
  36. package/src/js/tcmd/components/FilterBar.js +14 -4
  37. package/src/js/tcmd/components/FilterBar.xml +1 -1
  38. package/src/js/tcmd/{tui/frame.js → components/Frame.js} +120 -91
  39. package/src/js/tcmd/components/Help.html +7 -0
  40. package/src/js/tcmd/components/Help.js +109 -0
  41. package/src/js/tcmd/components/Summary.html +1 -1
  42. package/src/js/tcmd/components/Summary.js +1 -6
  43. package/src/js/tcmd/components/icons.js +39 -5
  44. package/src/js/tcmd/components/index.js +3 -4
  45. package/src/js/tcmd/components/rows.js +37 -33
  46. package/src/js/tcmd/core/commands.js +422 -180
  47. package/src/js/tcmd/core/directory.js +145 -95
  48. package/src/js/tcmd/core/fs.js +29 -2
  49. package/src/js/tcmd/core/history.js +58 -0
  50. package/src/js/tcmd/core/index.js +3 -9
  51. package/src/js/tcmd/core/keymap.js +3 -3
  52. package/src/js/tcmd/core/operations.js +108 -15
  53. package/src/js/tcmd/core/pane.js +31 -8
  54. package/src/js/tcmd/core/panel.js +172 -82
  55. package/src/js/tcmd/core/script.js +7 -95
  56. package/src/js/tcmd/core/selection.js +9 -1
  57. package/src/js/tcmd/core/session.js +280 -152
  58. package/src/js/tcmd/index.js +0 -1
  59. package/src/js/tcmd/tui/app.js +26 -5
  60. package/src/js/tcmd/tui/catalogue.js +6 -19
  61. package/src/js/tcmd/tui/index.js +1 -16
  62. package/src/js/tcmd/tui/theme.js +5 -0
  63. package/src/js/tcmd/web/app.js +158 -32
  64. package/src/js/tcmd/web/bookmarks.js +36 -0
  65. package/src/js/tcmd/web/catalogue.js +45 -39
  66. package/src/js/tcmd/web/effects.js +143 -0
  67. package/src/js/tcmd/web/header.html +1 -1
  68. package/src/js/tcmd/web/header.js +15 -11
  69. package/src/js/tcmd/web/main.js +6 -66
  70. package/src/js/tcmd/web/seed.js +3 -3
  71. package/src/js/tcmd/web/server.js +6 -4
  72. package/src/js/tcmd/web/theme.js +1 -0
  73. package/src/js/tcmd/components/CommandPalette.html +0 -7
  74. package/src/js/tcmd/components/CommandPalette.js +0 -58
  75. package/src/js/tcmd/components/CommandPalette.xml +0 -16
  76. package/src/js/tcmd/components/PaletteRow.html +0 -3
  77. package/src/js/tcmd/components/PaletteRow.js +0 -43
  78. package/src/js/tcmd/components/PaletteRow.xml +0 -4
  79. package/src/js/tcmd/tui/help.js +0 -32
@@ -0,0 +1,17 @@
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">
2
+ <span class="panel-list-indent" when="isList" out="listSpacer"></span>
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>
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">
11
+ <span class="panel-field-bracket" out="fieldOpen"></span>
12
+ <input class="panel-value" when="editing" type="text" inout="value" aria-label="Argument value" />
13
+ <span class="panel-field-value" when="!editing" out="summary"></span>
14
+ <span class="panel-field-bracket" out="fieldClose"></span>
15
+ </span>
16
+ <span class="panel-summary" when="isPlain" out="summary"></span>
17
+ </li>
@@ -4,19 +4,23 @@
4
4
  // Created: 2026-10-09
5
5
 
6
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`.
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 }`
17
+ // built by `rows.js`.
15
18
  //
16
19
  // 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
+ // `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).
20
24
 
21
25
  import { unwrap } from "select/state/cells"
22
26
 
@@ -30,35 +34,44 @@ function riskFg(row, env) {
30
34
  }
31
35
 
32
36
  // Function: rowName
33
- // The row's left cell: the operation title, an argument's name, or an item's
34
- // `[x] basename`.
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).
35
40
  function rowName(row) {
36
41
  if (!row) return ""
42
+ if (row.type === "group") return row.name ?? ""
37
43
  if (row.type === "item") return `${row.marker ?? "[ ]"} ${row.name ?? ""}`
44
+ if (row.type === "entry") return `${row.name ?? ""}${row.isDir ? "/" : ""}`
38
45
  return row.name ?? ""
39
46
  }
40
47
 
41
48
  // Function: rowSummary
42
- // The row's right cell: the operation signature, an argument's binding
43
- // summary, or an item's parent directory.
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.
44
52
  function rowSummary(row) {
45
53
  if (!row) return ""
46
54
  if (row.type === "item") return row.parent ?? ""
47
- if (row.type === "op") return row.summary ?? ""
55
+ if (row.type === "entry") {
56
+ if (row.isSymlink && row.target) return `${row.target}`
57
+ return row.isDir ? "dir" : ""
58
+ }
48
59
  return row.summary ?? ""
49
60
  }
50
61
 
51
62
  // Function: wantsFocus
52
- // The focus token a row belongs to: the operation column ("ops") or the
53
- // argument column ("args").
63
+ // The focus token a row belongs to: the command list ("ops"), the argument
64
+ // pane ("args"), or none (a group header or a list frame never takes the
65
+ // cursor).
54
66
  function wantsFocus(row) {
67
+ if (row?.type === "group" || row?.type === "frame") return "none"
55
68
  return row?.type === "op" ? "ops" : "args"
56
69
  }
57
70
 
58
71
  // Function: isFocused
59
- // True when this row owns the cursor in the focused column.
72
+ // True when this row owns the cursor in the focused region.
60
73
  function isFocused(row, index, cursor, focus) {
61
- if (!row || !focus) return false
74
+ if (!row || !focus || row.type === "group" || row.type === "frame") return false
62
75
  return focus === wantsFocus(row) && index === cursor
63
76
  }
64
77
 
@@ -66,24 +79,71 @@ function isFocused(row, index, cursor, focus) {
66
79
  // Builds the CommandArgRow behavior map for the given renderer environment.
67
80
  function createBehaviors(env) {
68
81
  const focused = (data) => isFocused(data.row, data.index, unwrap(data.cursor), unwrap(data.focus))
82
+ const type = (data) => data.row?.type
83
+ const isList = (data) => type(data) === "item" || type(data) === "frame"
84
+ // The web classes that draw a selection's list box: the item rows carry the
85
+ // sides, the frame rows the top/bottom.
86
+ const listClass = (data) => {
87
+ if (!isList(data)) return null
88
+ if (type(data) === "frame") return data.row?.variant === "top" ? "is-list-top" : "is-list-bottom"
89
+ return "is-list-item"
90
+ }
91
+ // A textual argument header edits in place while it owns the cursor: its
92
+ // summary yields to a value input bound to the panel's edit cell.
93
+ const editing = (_self, data) => focused(data) && type(data) === "arg" && data.row?.textual === true
69
94
  return {
95
+ editing,
96
+ // --- shared row content ---
70
97
  marker: (_self, data) => (focused(data) ? ">" : " "),
71
98
  markerFg: () => env.accentFg,
72
- name: (_self, data) => rowName(data.row),
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
+ },
73
105
  rowBg: (_self, data) => (focused(data) ? env.selectionBg : undefined),
74
- rowClass: (_self, data) => (focused(data) ? env.focusedClass : null),
106
+ rowClass: (_self, data) =>
107
+ [focused(data) ? env.focusedClass : null, listClass(data)].filter(Boolean).join(" ") || null,
108
+ rowSelected: (_self, data) => `${data.row?.selected === true}`,
75
109
  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.
110
+ if (type(data) === "group") return env.textHeadingFg
79
111
  const risk = riskFg(data.row, env)
80
112
  if (risk) return risk
81
113
  if (focused(data)) return env.accentFg
82
- if (data.row?.type === "item" && data.row?.selected) return env.accentSecondaryFg
114
+ if (type(data) === "item" && data.row?.selected) return env.accentSecondaryFg
83
115
  return env.textFg
84
116
  },
85
117
  summary: (_self, data) => rowSummary(data.row),
86
118
  summaryFg: () => env.textSecondaryFg,
119
+ // The list branch repeats the marker/name/summary nodes, so it carries
120
+ // its own out-names (duplicate `out` names in one template collide).
121
+ listMarker: (_self, data) => (focused(data) ? ">" : " "),
122
+ listName: (_self, data) => rowName(data.row, env.icons),
123
+ listSummary: (_self, data) => rowSummary(data.row),
124
+ // --- layout branches ---
125
+ isArg: (_self, data) => type(data) === "arg",
126
+ isItem: (_self, data) => type(data) === "item",
127
+ isFrame: (_self, data) => type(data) === "frame",
128
+ 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)),
142
+ listBorder: (_self, data) => {
143
+ if (type(data) !== "frame") return ["left", "right"]
144
+ return data.row?.variant === "top" ? ["top", "left", "right"] : ["bottom", "left", "right"]
145
+ },
146
+ borderFg: () => env.borderSecondaryFg,
87
147
  }
88
148
  }
89
149
 
@@ -1,5 +1,32 @@
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>
1
+ <box flexDirection="row" height="1" out:backgroundColor="rowBg" on:click="row.key!panel:select">
2
+ <!-- A selection's item list: indented under the value column and framed
3
+ by the top/bottom border rows. -->
4
+ <text when="isList" out="listSpacer" wrapMode="none"></text>
5
+ <box when="isList" flexGrow="1" flexShrink="1" minWidth="0" flexDirection="row" columnGap="1"
6
+ out:border="listBorder" out:borderColor="borderFg">
7
+ <text when="isItem" out="listMarker" width="2" out:fg="markerFg"></text>
8
+ <text when="isItem" out="listName" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
9
+ <text when="isItem" out="listSummary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
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>
19
+ </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>
24
+ </box>
25
+ </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">
28
+ <text out="marker" width="2" out:fg="markerFg"></text>
29
+ <text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
30
+ <text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
31
+ </box>
5
32
  </box>
@@ -1,4 +1,4 @@
1
1
  <footer class="command-bar">
2
2
  <nav when="showKeys" out:class="listClass" out="rows"></nav>
3
- <span when="showStatus" out="status" out:class="statusClass"></span>
3
+ <span when="showStatus" out="status" out:class="statusClass" out:data-kind="notice.kind" role="status"></span>
4
4
  </footer>
@@ -5,8 +5,8 @@
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 palette
9
- // hints while the palette is open); a pane error or a session notice takes
8
+ // current context (the command registry's `bar` commands, or the hints of the
9
+ // overlay that owns the keyboard); a pane error or a session notice takes
10
10
  // its place. Entries are bracket shorthands (see `CommandItem.js`) that wrap
11
11
  // onto as many rows as the width demands; the active pane's marked count
12
12
  // joins the list as a dimmed, non-clickable entry. The row logic lives in
@@ -116,7 +116,7 @@ function createBehaviors(env) {
116
116
  },
117
117
  }
118
118
  if (env?.rows) {
119
- behaviors.rows = (_self, data) => env.rows(self, data)
119
+ behaviors.rows = (self, data) => env.rows(self, data)
120
120
  }
121
121
  return behaviors
122
122
  }
@@ -1,3 +1,3 @@
1
- <button class="command-key" type="button" on:click="item.id!commandbar:run">
1
+ <button class="command-key" type="button" out:disabled="itemHint" on:click="item.id!commandbar:run">
2
2
  <span out="itemLabel"></span>
3
3
  </button>
@@ -61,6 +61,7 @@ function createBehaviors(env) {
61
61
  return {
62
62
  // Web: the whole shorthand as one label.
63
63
  itemLabel: (_self, data) => commandLabel(data.item),
64
+ itemHint: (_self, data) => !data.item?.id,
64
65
  // TUI: the dim pipe divider, then the bracket + title head, the
65
66
  // accent key glyph, the closer.
66
67
  itemSep: (_self, data) => (data.index > 0 ? COMMAND_SEP : ""),
@@ -0,0 +1,29 @@
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>
4
+ <div class="panel-columns">
5
+ <div class="panel-command-column">
6
+ <label class="panel-query-line">
7
+ <span out="sign"></span>
8
+ <input class="panel-query" type="text" inout="query" aria-label="Filter commands" />
9
+ </label>
10
+ <ul class="panel-ops" aria-label="Commands" out="opRows"></ul>
11
+ </div>
12
+ <div class="panel-args-column">
13
+ <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>
20
+ </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
+ </div>
26
+ </div>
27
+ <footer out="footer"></footer>
28
+ </div>
29
+ </section>
@@ -4,95 +4,97 @@
4
4
  // Created: 2026-10-09
5
5
 
6
6
  // Module: tcmd/components/CommandPanel
7
- // CommandPanel: the Commando-style overlay (`m` / `:`). A filter input at the
8
- // top narrows the left operation list as you type (the matching command takes
9
- // the cursor); a right column shows the selected operation's arguments: one
10
- // header row per argument (its name and binding summary) and, for a selection,
11
- // one row per path with a `[x]`/`[ ]` toggle. A vertical rule divides the two
12
- // columns, a horizontal one separates them from the "runs" line below, which
13
- // shows the assembled command the draft maps to. The footer advertises the
14
- // panel keys. The row logic lives in `CommandArgRow.js`; this parent maps both
15
- // the operation list and the flattened argument rows.
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.
16
14
  //
17
15
  // Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
18
- // (footer), `textHeadingFg` (title/sign), `accentFg` (preview),
19
- // `errorFg` (unrunnable preview), `opRows(self, data)` / `argRows(self, data)`
20
- // returning the keyed row lists, and `argScrollTop(self, data)` (TUI viewport
21
- // tracking).
16
+ // (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).
22
19
 
23
20
  import { unwrap } from "select/state/cells"
24
21
  import { PANEL_FOCUS } from "../core/panel.js"
25
22
 
26
23
  // Function: panelTitle
27
- // " Command · copy " while an operation is selected (spaces so the title sits
24
+ // " Command · copy " while an entry is selected (spaces so the title sits
28
25
  // clear of the border corners).
29
- function panelTitle(op) {
30
- const name = unwrap(op)?.name
26
+ function panelTitle(entry) {
27
+ const name = unwrap(entry)?.name
31
28
  return name ? ` Command · ${name} ` : " Command "
32
29
  }
33
30
 
34
31
  // Function: panelFooter
35
- const PANEL_FOOTER = "type to filter ⇥ args Spc toggle ↵ run Esc close"
32
+ const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run Esc close"
33
+
34
+ // Function: pickerFooter
35
+ const PICKER_FOOTER = "type to filter ↑↓ row → open ↵ choose ⌃↵ this dir ⌫ up Esc back"
36
36
 
37
37
  // Function: panelSign
38
- // The prompt before the command filter input (a trailing space so the typed
39
- // text doesn't crowd the arrow; overridable per renderer).
38
+ // The filter prompt (`:`) before the command filter input.
40
39
  function panelSign() {
41
- return "› "
40
+ return ":"
42
41
  }
43
42
 
44
43
  // Function: createBehaviors
45
44
  // Builds the CommandPanel behavior map for the given renderer environment.
46
45
  function createBehaviors(env) {
47
- // The assembled command line's colour, shared by the line and its "runs"
48
- // label: the preview reads as an error until every required argument is
49
- // filled.
50
- const previewColor = (data) => (unwrap(data.runnable) === false ? env.errorFg ?? env.accentFg : env.accentFg)
46
+ const panelOpen = (_self, data) => Boolean(unwrap(data.open))
51
47
  const behaviors = {
52
48
  bg: () => env.overlayBg,
53
49
  borderFg: () => env.borderPrimaryActiveFg,
54
- // Enter in a text field runs the operation (the App key handler routes
55
- // Enter for the non-input cases).
50
+ // The right pane's header: the selected command's name.
51
+ commandName: (_self, data) => unwrap(data.entry)?.name ?? "",
52
+ commandNameFg: () => env.textHeadingFg,
56
53
  confirm: (self) => self.pub("panel:confirm"),
57
- // The argument value field appears only while a textual argument owns
58
- // the cursor; otherwise the command filter input keeps focus.
59
- fieldVisible: (_self, data) => Boolean(unwrap(data.textual)) && unwrap(data.focus) === PANEL_FOCUS.ARGS,
60
- fieldFocused: (_self, data) => Boolean(unwrap(data.textual)) && unwrap(data.focus) === PANEL_FOCUS.ARGS,
61
- fieldPrompt: (_self, data) => {
62
- const row = unwrap(data.argRow)
63
- return row?.type === "arg" ? `${row.name}: ` : ""
64
- },
65
- footer: () => PANEL_FOOTER,
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).
61
+ 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),
66
69
  footerFg: () => env.textSecondaryFg,
67
- // Dividers: a vertical rule between the operation list and the argument
68
- // sidebar, a horizontal one above the "runs" preview — both in the
69
- // secondary border colour, like the pane header rules.
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.
70
73
  ruleFg: () => env.borderSecondaryFg,
71
74
  sideRule: () => ["left"],
72
75
  headRule: () => ["top"],
73
76
  labelFg: () => env.textSecondaryFg,
74
77
  focusArgs: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.ARGS,
75
- focusOps: (_self, data) => unwrap(data.focus) !== PANEL_FOCUS.ARGS,
78
+ focusOps: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.OPS,
76
79
  open: (_self, data) => Boolean(unwrap(data.open)),
77
- // The assembled command line the draft maps to, and its colour (see
78
- // `previewColor`); the "runs" label wears the same colour so the two
79
- // read as one block.
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.
80
82
  preview: (_self, data) => unwrap(data.command) ?? "",
81
- previewFg: (_self, data) => previewColor(data),
82
- previewLabel: () => "runs",
83
- previewLabelFg: (_self, data) => previewColor(data),
84
- // The filter input owns the keyboard while the panel is open and the
85
- // command column is focused; typing narrows the operation list.
86
- queryFocused: (_self, data) => Boolean(unwrap(data.open)) && unwrap(data.focus) !== PANEL_FOCUS.ARGS,
83
+ runnableState: (_self, data) => `${unwrap(data.runnable) !== false}`,
84
+ previewFg: () => env.textSecondaryFg,
85
+ // The filter input owns the keyboard while the panel is open, the
86
+ // command list is focused and the picker is not browsing.
87
+ queryFocused: (self, data) =>
88
+ panelOpen(self, data) && !unwrap(data.picking) && unwrap(data.focus) !== PANEL_FOCUS.ARGS,
87
89
  sign: () => panelSign(),
88
90
  signFg: () => env.textHeadingFg,
89
- title: (_self, data) => panelTitle(data.operation),
91
+ title: (_self, data) => panelTitle(data.entry),
90
92
  titleColor: () => env.textHeadingFg,
91
93
  }
92
- if (env?.opRows) behaviors.opRows = (_self, data) => env.opRows(self, data)
93
- if (env?.argRows) behaviors.argRows = (_self, data) => env.argRows(self, data)
94
+ if (env?.opRows) behaviors.opRows = (self, data) => env.opRows(self, data)
95
+ if (env?.argRows) behaviors.argRows = (self, data) => env.argRows(self, data)
94
96
  if (env?.argScrollTop) behaviors.argScrollTop = (self, data) => env.argScrollTop(self, data)
95
97
  return behaviors
96
98
  }
97
99
 
98
- export { PANEL_FOOTER, createBehaviors, panelSign, panelTitle }
100
+ export { PANEL_FOOTER, PICKER_FOOTER, createBehaviors, panelSign, panelTitle }
@@ -2,31 +2,45 @@
2
2
  <box width="80%" height="72%" border borderStyle="rounded"
3
3
  out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
4
4
  out:backgroundColor="bg" flexDirection="column">
5
- <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
6
- <text out="sign" out:fg="signFg" wrapMode="none"></text>
7
- <input ref="query" inout="query" flexGrow="1" height="1" out:focused="queryFocused" on:enter="confirm"></input>
8
- </box>
9
- <!-- Horizontal rule under the filter input. -->
10
- <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
11
- <box flexDirection="row" flexGrow="1" overflow="hidden" columnGap="1">
12
- <scrollbox flexGrow="2">
13
- <box out="opRows" flexDirection="column"></box>
14
- </scrollbox>
15
- <!-- Vertical rule between the operation list and the argument sidebar. -->
5
+ <box flexDirection="row" flexGrow="1" overflow="hidden">
6
+ <!-- Left pane: the `:` filter over the grouped command list. -->
7
+ <box flexDirection="column" flexGrow="2" flexBasis="0" overflow="hidden">
8
+ <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
9
+ <text out="sign" out:fg="signFg" wrapMode="none"></text>
10
+ <input ref="query" inout="query" flexGrow="1" height="1" out:focused="queryFocused"></input>
11
+ </box>
12
+ <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
13
+ <scrollbox flexGrow="1">
14
+ <box out="opRows" flexDirection="column"></box>
15
+ </scrollbox>
16
+ </box>
17
+ <!-- Vertical rule between the panes. -->
16
18
  <box width="1" out:border="sideRule" out:borderColor="ruleFg"></box>
17
- <scrollbox ref="args" flexGrow="3" out:scrollTop="argScrollTop">
18
- <box out="argRows" flexDirection="column"></box>
19
- </scrollbox>
20
- </box>
21
- <!-- Horizontal rule above the assembled command preview. -->
22
- <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
23
- <box flexDirection="column" paddingLeft="1" paddingRight="1" flexShrink="0" overflow="hidden">
24
- <text out="previewLabel" out:fg="previewLabelFg" wrapMode="none"></text>
25
- <text out="preview" out:fg="previewFg" wrapMode="word"></text>
26
- </box>
27
- <box when="fieldVisible" flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
28
- <text out="fieldPrompt" out:fg="labelFg" wrapMode="none"></text>
29
- <input inout="value" flexGrow="1" height="1" out:focused="fieldFocused" on:enter="confirm"></input>
19
+ <!-- Right pane: the command name, its arguments, the resolved command. -->
20
+ <box flexDirection="column" flexGrow="3" flexBasis="0" overflow="hidden">
21
+ <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
22
+ <text out="commandName" out:fg="commandNameFg" wrapMode="none"></text>
23
+ </box>
24
+ <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>
37
+ </box>
38
+ </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>
43
+ </box>
30
44
  </box>
31
45
  <box flexDirection="row" height="1" paddingLeft="1">
32
46
  <text out="footer" out:fg="footerFg"></text>
@@ -1,3 +1,3 @@
1
- <div class="confirm" when="visible">
1
+ <div class="confirm" when="visible" role="dialog" aria-modal="true" aria-label="Confirm operation" tabindex="-1">
2
2
  <p out="text"></p>
3
3
  </div>
@@ -1,15 +1,15 @@
1
- <div class="entry-detail">
1
+ <div class="entry-detail" ref="box">
2
2
  <div class="entry-detail-line">
3
- <span class="entry-detail-indent" out="detailIndent"></span>
4
- <span class="entry-detail-marked" out="detailMarked"></span>
3
+ <span class="entry-detail-indent" when="detailHasIndent" out="detailIndent"></span>
4
+ <span class="entry-detail-marked" when="detailHasMarked" out="detailMarked"></span>
5
5
  <span class="entry-detail-sep" when="detailSepTime" out="detailSep"></span>
6
- <span class="entry-detail-time" out="detailTime"></span>
6
+ <span class="entry-detail-time" when="detailHasTime" out="detailTime"></span>
7
7
  <span class="entry-detail-sep" when="detailSepType" out="detailSep"></span>
8
- <span class="entry-detail-type" out:class="detailTypeClass" out="detailType"></span>
8
+ <span class="entry-detail-type" when="detailHasType" out:class="detailTypeClass" out="detailType"></span>
9
9
  <span class="entry-detail-sep" when="detailSepSize" out="detailSep"></span>
10
- <span class="entry-detail-size" out="detailSize"></span>
10
+ <span class="entry-detail-size" when="detailHasSize" out="detailSize"></span>
11
11
  <span class="entry-detail-spacer" when="detailHasMode"></span>
12
12
  <span class="entry-detail-sep" when="detailSepMode" out="detailSep"></span>
13
- <span class="entry-detail-mode" out="detailMode"></span>
13
+ <span class="entry-detail-mode" when="detailHasMode" out="detailMode"></span>
14
14
  </div>
15
15
  </div>
@@ -135,7 +135,7 @@ function createBehaviors(env = {}) {
135
135
  const entry = unwrap(data.entry)
136
136
  const marked = unwrap(data.marked)
137
137
  const markedCount = marked?.size ?? 0
138
- const width = env.width ? env.width(self) : 0
138
+ const width = env.width ? env.width(self, data) : 0
139
139
  const kept = fitBlocks(detailBlocks(entry, markedCount), width, measure, env.elide)
140
140
  const keys = new Set(kept.map((block) => block.key))
141
141
  const texts = {}
@@ -1 +1,5 @@
1
- <ul class="file-list" ref="list" out="rows"></ul>
1
+ <div class="file-list">
2
+ <ul ref="list" out="rows"></ul>
3
+ <p when="showLoading" out="loadingText"></p>
4
+ <p when="showEmpty" out="emptyText"></p>
5
+ </div>
@@ -25,7 +25,8 @@ function createBehaviors(env) {
25
25
  loadingFg: () => env?.textSecondaryFg,
26
26
  showEmpty: (_self, data) =>
27
27
  !unwrap(data.loading) && (unwrap(data.entries)?.length ?? 0) === 0,
28
- showLoading: (_self, data) => Boolean(unwrap(data.loading)),
28
+ showLoading: (_self, data) =>
29
+ Boolean(unwrap(data.loading)) && (unwrap(data.entries)?.length ?? 0) === 0,
29
30
  }
30
31
  if (env?.rows) {
31
32
  behaviors.rows = (self, data) => env.rows(self, data)
@@ -1,5 +1,16 @@
1
- <aside class="preview" when="show" out:title="previewTitle">
2
- <pre when="hasText" out="previewText" class="preview-text"></pre>
3
- <div when="hasCard" out="cardText" class="preview-card"></div>
1
+ <!-- The inspector: a card/text body for a file, and a directory column
2
+ (breadcrumbs + cursorable listing) for a directory, so a previewed
3
+ directory reads as a pane. The body and the column share the frame, and
4
+ the whole column is clickable to take focus. -->
5
+ <aside class="preview" when="show" out:title="headerTitle" on:click="paneId!pane:activate">
6
+ <header when="showBody" out="headerTitle"></header>
7
+ <div class="pane-header" when="isDirPane">
8
+ <div out="dirCrumbsView|Breadcrumbs"></div>
9
+ </div>
10
+ <div class="preview-list" when="isDirPane" data-slot="list" out="dirListView|FileList"></div>
11
+ <pre when="showText" out="previewText" class="preview-text"></pre>
12
+ <pre when="showCard" out="cardText" class="preview-card"></pre>
13
+ <pre when="showEntryCard" out="entryText" class="preview-card"></pre>
14
+ <pre when="showEmpty" out="emptyText" class="preview-card"></pre>
4
15
  <footer class="preview-actions" out="actions"></footer>
5
16
  </aside>