@ssebastien/tcmd 0.1.3 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +256 -62
  2. package/bin/tcmd +10 -2
  3. package/bin/tcmd.js +13 -2
  4. package/desktop/dist/index.html +13 -0
  5. package/desktop/dist/main.js +20298 -0
  6. package/desktop/dist/style.css +1515 -0
  7. package/desktop/frontend/index.html +13 -0
  8. package/package.json +19 -6
  9. package/public/fonts/OFL.txt +93 -0
  10. package/public/index.html +14 -0
  11. package/public/licenses/LUCIDE.txt +43 -0
  12. package/public/main.js +20679 -0
  13. package/public/style.css +1515 -0
  14. package/scripts/build-desktop.js +31 -0
  15. package/scripts/serve-web.js +61 -0
  16. package/src/js/tcmd/components/App.html +80 -14
  17. package/src/js/tcmd/components/App.js +339 -114
  18. package/src/js/tcmd/components/App.xml +76 -22
  19. package/src/js/tcmd/components/BookmarkList.html +5 -5
  20. package/src/js/tcmd/components/BookmarkList.js +15 -41
  21. package/src/js/tcmd/components/BookmarkList.xml +1 -2
  22. package/src/js/tcmd/components/BookmarkRow.html +2 -1
  23. package/src/js/tcmd/components/BookmarkRow.js +14 -9
  24. package/src/js/tcmd/components/BookmarkRow.xml +1 -1
  25. package/src/js/tcmd/components/Breadcrumbs.js +1 -1
  26. package/src/js/tcmd/components/CommandArgRow.html +28 -0
  27. package/src/js/tcmd/components/CommandArgRow.js +146 -30
  28. package/src/js/tcmd/components/CommandArgRow.xml +55 -4
  29. package/src/js/tcmd/components/CommandBar.html +2 -2
  30. package/src/js/tcmd/components/CommandBar.js +33 -27
  31. package/src/js/tcmd/components/CommandBar.xml +2 -2
  32. package/src/js/tcmd/components/CommandItem.html +1 -1
  33. package/src/js/tcmd/components/CommandItem.js +25 -25
  34. package/src/js/tcmd/components/CommandItem.xml +2 -2
  35. package/src/js/tcmd/components/CommandPanel.html +29 -0
  36. package/src/js/tcmd/components/CommandPanel.js +78 -52
  37. package/src/js/tcmd/components/CommandPanel.xml +49 -25
  38. package/src/js/tcmd/components/Confirm.html +2 -2
  39. package/src/js/tcmd/components/EntryDetail.html +7 -7
  40. package/src/js/tcmd/components/EntryDetail.js +22 -8
  41. package/src/js/tcmd/components/FileList.html +5 -1
  42. package/src/js/tcmd/components/FileList.js +2 -1
  43. package/src/js/tcmd/components/FilePicker.html +10 -0
  44. package/src/js/tcmd/components/FilePicker.js +43 -0
  45. package/src/js/tcmd/components/FilePicker.xml +13 -0
  46. package/src/js/tcmd/components/FilePickerRow.html +6 -0
  47. package/src/js/tcmd/components/FilePickerRow.js +27 -0
  48. package/src/js/tcmd/components/FilePickerRow.xml +10 -0
  49. package/src/js/tcmd/components/FilePreview.html +14 -4
  50. package/src/js/tcmd/components/FilePreview.js +111 -12
  51. package/src/js/tcmd/components/FilePreview.xml +28 -8
  52. package/src/js/tcmd/components/FileRow.html +2 -1
  53. package/src/js/tcmd/components/FileRow.js +70 -12
  54. package/src/js/tcmd/components/FilterBar.html +1 -1
  55. package/src/js/tcmd/components/FilterBar.js +14 -4
  56. package/src/js/tcmd/components/FilterBar.xml +1 -1
  57. package/src/js/tcmd/components/Frame.js +324 -0
  58. package/src/js/tcmd/components/Help.html +15 -0
  59. package/src/js/tcmd/components/Help.js +260 -0
  60. package/src/js/tcmd/components/Help.xml +13 -4
  61. package/src/js/tcmd/components/HelpRow.html +8 -0
  62. package/src/js/tcmd/components/HelpRow.xml +7 -0
  63. package/src/js/tcmd/components/OverlayList.html +8 -0
  64. package/src/js/tcmd/components/OverlayList.js +69 -0
  65. package/src/js/tcmd/components/{CommandPalette.xml → OverlayList.xml} +2 -5
  66. package/src/js/tcmd/components/OverlayRow.html +5 -0
  67. package/src/js/tcmd/components/OverlayRow.js +47 -0
  68. package/src/js/tcmd/components/OverlayRow.xml +9 -0
  69. package/src/js/tcmd/components/Summary.html +1 -1
  70. package/src/js/tcmd/components/Summary.js +1 -6
  71. package/src/js/tcmd/components/format.js +4 -23
  72. package/src/js/tcmd/components/icons.js +176 -5
  73. package/src/js/tcmd/components/index.js +9 -5
  74. package/src/js/tcmd/components/rows.js +176 -48
  75. package/src/js/tcmd/core/base64.js +64 -0
  76. package/src/js/tcmd/core/bookmarks.js +45 -8
  77. package/src/js/tcmd/core/commands.js +978 -260
  78. package/src/js/tcmd/core/concurrency.js +59 -0
  79. package/src/js/tcmd/core/decompress.js +117 -0
  80. package/src/js/tcmd/core/directory.js +219 -93
  81. package/src/js/tcmd/core/format.js +36 -0
  82. package/src/js/tcmd/core/fs.js +438 -54
  83. package/src/js/tcmd/core/history.js +58 -0
  84. package/src/js/tcmd/core/index.js +3 -9
  85. package/src/js/tcmd/core/journal.js +93 -0
  86. package/src/js/tcmd/core/keymap.js +42 -6
  87. package/src/js/tcmd/core/operations.js +813 -48
  88. package/src/js/tcmd/core/os.js +29 -3
  89. package/src/js/tcmd/core/pane.js +33 -8
  90. package/src/js/tcmd/core/panel.js +280 -104
  91. package/src/js/tcmd/core/path.js +30 -3
  92. package/src/js/tcmd/core/permissions.js +107 -0
  93. package/src/js/tcmd/core/picker.js +156 -0
  94. package/src/js/tcmd/core/preview.js +102 -0
  95. package/src/js/tcmd/core/script.js +7 -95
  96. package/src/js/tcmd/core/selection.js +9 -1
  97. package/src/js/tcmd/core/session.js +498 -200
  98. package/src/js/tcmd/core/watch.js +153 -0
  99. package/src/js/tcmd/desktop/backend/api.js +165 -0
  100. package/src/js/tcmd/desktop/backend/fs.js +626 -0
  101. package/src/js/tcmd/desktop/backend/main.ts +9 -0
  102. package/src/js/tcmd/desktop/bookmarks.js +23 -0
  103. package/src/js/tcmd/desktop/fs.js +91 -0
  104. package/src/js/tcmd/desktop/main.js +194 -0
  105. package/src/js/tcmd/desktop/os.js +33 -0
  106. package/src/js/tcmd/index.js +0 -1
  107. package/src/js/tcmd/tui/app.js +118 -60
  108. package/src/js/tcmd/tui/catalogue.js +238 -42
  109. package/src/js/tcmd/tui/index.js +18 -19
  110. package/src/js/tcmd/tui/opentui.js +33 -0
  111. package/src/js/tcmd/tui/syntax.js +91 -0
  112. package/src/js/tcmd/tui/theme.js +32 -2
  113. package/src/js/tcmd/vendor/bzip2.js +276 -0
  114. package/src/js/tcmd/web/app.js +225 -41
  115. package/src/js/tcmd/web/bookmarks.js +36 -0
  116. package/src/js/tcmd/web/catalogue.js +81 -38
  117. package/src/js/tcmd/web/effects.js +223 -0
  118. package/src/js/tcmd/web/header.html +1 -1
  119. package/src/js/tcmd/web/header.js +15 -11
  120. package/src/js/tcmd/web/http.js +13 -0
  121. package/src/js/tcmd/web/index.js +2 -2
  122. package/src/js/tcmd/web/main.js +6 -66
  123. package/src/js/tcmd/web/opfs.js +297 -30
  124. package/src/js/tcmd/web/seed.js +4 -4
  125. package/src/js/tcmd/web/server.js +124 -21
  126. package/src/js/tcmd/web/theme.js +4 -0
  127. package/tinyjs.json +16 -0
  128. package/src/js/tcmd/components/CommandPalette.html +0 -7
  129. package/src/js/tcmd/components/CommandPalette.js +0 -58
  130. package/src/js/tcmd/components/PaletteRow.html +0 -3
  131. package/src/js/tcmd/components/PaletteRow.js +0 -43
  132. package/src/js/tcmd/components/PaletteRow.xml +0 -4
  133. package/src/js/tcmd/tui/frame.js +0 -269
  134. package/src/js/tcmd/tui/help.js +0 -32
@@ -4,95 +4,121 @@
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 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.
16
13
  //
17
14
  // 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).
15
+ // (footer), `textHeadingFg` (title/sign/command name), `opRows(self, data)` /
16
+ // `argRows(self, data)` returning keyed row lists, and
17
+ // the TUI viewport hooks `opScrollTop` / `argScrollTop`.
22
18
 
23
19
  import { unwrap } from "select/state/cells"
24
20
  import { PANEL_FOCUS } from "../core/panel.js"
25
21
 
26
22
  // Function: panelTitle
27
- // " Command · copy " while an operation is selected (spaces so the title sits
23
+ // " Command · copy " while an entry is selected (spaces so the title sits
28
24
  // clear of the border corners).
29
- function panelTitle(op) {
30
- const name = unwrap(op)?.name
25
+ function panelTitle(entry) {
26
+ const name = unwrap(entry)?.name
31
27
  return name ? ` Command · ${name} ` : " Command "
32
28
  }
33
29
 
34
30
  // Function: panelFooter
35
- const PANEL_FOOTER = "type to filter ⇥ args Spc toggle ↵ run Esc close"
31
+ const PANEL_FOOTER = "type to filter ⇥ args ⌃O browse Spc toggle ↵ run"
32
+
33
+ // Function: pickerFooter
34
+ const PICKER_FOOTER = "type path ↑↓ rows → open ↵ go ⌃↵ here ⇥ focus Esc back"
36
35
 
37
36
  // 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).
37
+ // The filter prompt (`:`) before the command filter input.
40
38
  function panelSign() {
41
- return "› "
39
+ return ":"
42
40
  }
43
41
 
44
42
  // Function: createBehaviors
45
43
  // Builds the CommandPanel behavior map for the given renderer environment.
46
44
  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)
45
+ const panelOpen = (_self, data) => Boolean(unwrap(data.open))
51
46
  const behaviors = {
52
47
  bg: () => env.overlayBg,
53
48
  borderFg: () => env.borderPrimaryActiveFg,
54
- // Enter in a text field runs the operation (the App key handler routes
55
- // Enter for the non-input cases).
49
+ // The right pane's header: the selected command's name. The close
50
+ // shorthand lives in the dialog's top border.
51
+ commandName: (_self, data) => unwrap(data.entry)?.name ?? "",
52
+ commandNameFg: () => env.textHeadingFg,
53
+ closeHint: () => "[Close Esc]",
54
+ closeHintFg: () => env.textSecondaryFg,
56
55
  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}: ` : ""
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.
74
+ picking: (_self, data) => Boolean(unwrap(data.picking)),
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
64
90
  },
65
- footer: () => PANEL_FOOTER,
66
91
  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.
92
+ // Dividers: a vertical rule between the panes and a horizontal rule under
93
+ // the filter, in the secondary border colour.
70
94
  ruleFg: () => env.borderSecondaryFg,
71
95
  sideRule: () => ["left"],
72
96
  headRule: () => ["top"],
73
97
  labelFg: () => env.textSecondaryFg,
74
98
  focusArgs: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.ARGS,
75
- focusOps: (_self, data) => unwrap(data.focus) !== PANEL_FOCUS.ARGS,
99
+ focusOps: (_self, data) => unwrap(data.focus) === PANEL_FOCUS.OPS,
76
100
  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
- 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,
101
+ // The filter input owns the keyboard while the panel is open, the
102
+ // command list is focused and the picker is not browsing.
103
+ queryFocused: (self, data) =>
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%",
87
111
  sign: () => panelSign(),
88
112
  signFg: () => env.textHeadingFg,
89
- title: (_self, data) => panelTitle(data.operation),
113
+ title: (_self, data) => panelTitle(data.entry),
90
114
  titleColor: () => env.textHeadingFg,
91
115
  }
92
- if (env?.opRows) behaviors.opRows = (_self, data) => env.opRows(self, data)
93
- if (env?.argRows) behaviors.argRows = (_self, data) => env.argRows(self, data)
116
+ if (env?.opRows) behaviors.opRows = (self, data) => env.opRows(self, data)
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)
94
120
  if (env?.argScrollTop) behaviors.argScrollTop = (self, data) => env.argScrollTop(self, data)
95
121
  return behaviors
96
122
  }
97
123
 
98
- export { PANEL_FOOTER, createBehaviors, panelSign, panelTitle }
124
+ export { PANEL_FOOTER, PICKER_FOOTER, createBehaviors, panelSign, panelTitle }
@@ -1,35 +1,59 @@
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
- <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 ref="ops" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="opScrollTop">
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, arguments and trailing Run action. -->
20
+ <box flexDirection="column" flexGrow="3" flexBasis="0" overflow="hidden">
21
+ <!-- The command name heads the pane. -->
22
+ <box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
23
+ <text out="commandName" out:fg="commandNameFg" flexGrow="1" flexShrink="1" minWidth="0" wrapMode="none" truncate="true"></text>
24
+ </box>
25
+ <box height="1" out:border="headRule" out:borderColor="ruleFg"></box>
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>
42
+ </box>
43
+ </scrollbox>
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>
47
+ </box>
30
48
  </box>
31
49
  <box flexDirection="row" height="1" paddingLeft="1">
32
50
  <text out="footer" out:fg="footerFg"></text>
33
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>
34
58
  </box>
35
59
  </box>
@@ -1,3 +1,3 @@
1
- <div class="confirm" when="visible">
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>
@@ -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>
@@ -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
@@ -135,7 +148,7 @@ function createBehaviors(env = {}) {
135
148
  const entry = unwrap(data.entry)
136
149
  const marked = unwrap(data.marked)
137
150
  const markedCount = marked?.size ?? 0
138
- const width = env.width ? env.width(self) : 0
151
+ const width = env.width ? env.width(self, data) : 0
139
152
  const kept = fitBlocks(detailBlocks(entry, markedCount), width, measure, env.elide)
140
153
  const keys = new Set(kept.map((block) => block.key))
141
154
  const texts = {}
@@ -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
  }
@@ -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)
@@ -0,0 +1,10 @@
1
+ <div class="panel-picker-pane" when="open" aria-label="File picker">
2
+ <div class="panel-picker">
3
+ <label class="panel-picker-line">
4
+ <svg class="panel-picker-icon ui-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use href="#lucide-folder-open"></use></svg>
5
+ <input class="panel-picker-path" type="text" inout="path" aria-label="Path" spellcheck="false" autocomplete="off" />
6
+ </label>
7
+ </div>
8
+ <ul class="panel-args panel-picker-rows" aria-label="Picker entries" out="rows"></ul>
9
+ <p class="panel-args-empty" when="empty" out="emptyText"></p>
10
+ </div>
@@ -0,0 +1,43 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-20
5
+
6
+ // Module: tcmd/components/FilePicker
7
+ // The command panel's file picker: an editable path line over a file-panel
8
+ // listing (`FilePickerRow`), with the `..` parent row kept at the top. Typing
9
+ // in the path narrows the listing (or enters a directory) through the session's
10
+ // path resolution; the row logic lives in `FilePickerRow.js`.
11
+ //
12
+ // Environment: `rows(self, data)` returning keyed `FilePickerRow`s and
13
+ // `scrollTop(self, data)` (TUI viewport tracking).
14
+
15
+ import { unwrap } from "select/state/cells"
16
+
17
+ // Function: createBehaviors
18
+ // Builds the FilePicker behavior map for the given renderer environment.
19
+ function createBehaviors(env) {
20
+ const open = (_self, data) => Boolean(unwrap(data.open))
21
+ const behaviors = {
22
+ open,
23
+ // The editable path owns the keyboard by default; Down/Tab hands it to
24
+ // the entry list.
25
+ pathFocused: (_self, data) => open(_self, data) && (unwrap(data.focus) ?? "path") === "path",
26
+ pathSign: () => "› ",
27
+ pathSignFg: () => env.accentFg,
28
+ // The parent row is a navigation row, not a match: an empty listing is
29
+ // an empty result even while `..` stays visible.
30
+ empty: (_self, data) =>
31
+ open(_self, data) && !(unwrap(data.rows) ?? []).some((row) => row.type === "entry"),
32
+ emptyText: () => " no matches",
33
+ emptyFg: () => env.textSecondaryFg,
34
+ rule: () => ["bottom"],
35
+ ruleFg: () => env.borderSecondaryFg,
36
+ borderFg: () => env.borderSecondaryFg,
37
+ }
38
+ if (env?.rows) behaviors.rows = (self, data) => env.rows(self, data)
39
+ if (env?.scrollTop) behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
40
+ return behaviors
41
+ }
42
+
43
+ export { createBehaviors }
@@ -0,0 +1,13 @@
1
+ <box when="open" flexDirection="column" flexGrow="1" flexShrink="1" minHeight="0" paddingLeft="1" paddingRight="1" paddingBottom="1">
2
+ <box flexDirection="column" flexGrow="1" flexShrink="1" minHeight="0" border borderStyle="single" out:borderColor="borderFg">
3
+ <box flexDirection="row" height="1" paddingLeft="1" paddingRight="1" overflow="hidden">
4
+ <text out="pathSign" out:fg="pathSignFg" wrapMode="none"></text>
5
+ <input ref="path" inout="path" flexGrow="1" minWidth="0" height="1" out:focused="pathFocused"></input>
6
+ </box>
7
+ <box height="1" out:border="rule" out:borderColor="ruleFg"></box>
8
+ <scrollbox ref="list" flexGrow="1" flexShrink="1" minHeight="0" out:scrollTop="scrollTop">
9
+ <box out="rows" flexDirection="column"></box>
10
+ </scrollbox>
11
+ <text when="empty" out="emptyText" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
12
+ </box>
13
+ </box>
@@ -0,0 +1,6 @@
1
+ <li class="file-row picker-row" out:class="rowClass" out:data-marked="rowMarked" out:data-active="rowActive" out:data-kind="rowKind" out:data-type="rowType" out:data-key="rowHref" on:click="key!panel:select">
2
+ <span class="marker" out="rowMarker"></span>
3
+ <svg class="file-row-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="rowIconHref"></use></svg>
4
+ <span class="name" out:class="rowNameClass" out="rowName"></span>
5
+ <span class="size" out="rowSize"></span>
6
+ </li>
@@ -0,0 +1,27 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-20
5
+
6
+ // Module: tcmd/components/FilePickerRow
7
+ // One entry in the file picker. The row presentation deliberately mirrors the
8
+ // file panes (`FileRow`): marker, kind icon, name and right-aligned size. It
9
+ // only overrides the `..` parent row (no trailing slash) and its click event,
10
+ // which the command panel routes back through `panel:select`.
11
+
12
+ import { createBehaviors as fileRowBehaviors, rowName as fileRowName } from "./FileRow.js"
13
+
14
+ // Function: createBehaviors
15
+ // Builds the FilePickerRow behavior map from the shared FileRow behaviors.
16
+ function createBehaviors(env) {
17
+ const behaviors = fileRowBehaviors(env)
18
+ return {
19
+ ...behaviors,
20
+ rowName: (_self, data) => (data.item?.isParent ? ".." : fileRowName(data.item)),
21
+ rowSize: (_self, data) => (data.item?.isParent ? "" : behaviors.rowSize?.(_self, data) ?? ""),
22
+ rowType: (_self, data) => (data.item?.isParent ? "parent" : data.item?.type ?? "entry"),
23
+ rowHref: (_self, data) => data.key ?? data.item?.path ?? "",
24
+ }
25
+ }
26
+
27
+ export { createBehaviors }
@@ -0,0 +1,10 @@
1
+ <box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="key!panel:select">
2
+ <text out="rowMarker" width="2" out:fg="rowMarkerFg"></text>
3
+ <box flexGrow="1" minWidth="0" overflow="hidden" flexDirection="row" columnGap="1">
4
+ <text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
5
+ <text out="rowName" out:fg="rowNameFg" wrapMode="none" truncate="true"></text>
6
+ </box>
7
+ <box width="9" justifyContent="flex-end">
8
+ <text out="rowSize" out:fg="rowSizeFg" wrapMode="none"></text>
9
+ </box>
10
+ </box>
@@ -1,5 +1,15 @@
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>
4
- <footer class="preview-actions" out="actions"></footer>
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>
5
15
  </aside>