@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
@@ -18,9 +18,15 @@ import appXml from "../components/App.xml" with { type: "text" }
18
18
  import { elideCrumbs } from "../components/format.js"
19
19
  import { appBehaviors, appEvents, appInit } from "../components/index.js"
20
20
  import { registerComponents } from "./catalogue.js"
21
- import { createFrameBehaviors, INSPECTOR_INLINE_WIDTH, isNarrow } from "./frame.js"
21
+ import { createFrameBehaviors, currentColumnId, INSPECTOR_INLINE_WIDTH, isExplorer, isNarrow } from "../components/Frame.js"
22
22
  import { tuiTheme } from "./theme.js"
23
23
 
24
+ // Constant: CURRENT_GROW
25
+ // How much wider the explorer's middle (current) column is than its
26
+ // neighbours — enough to read as the working pane, not enough to starve the
27
+ // parent and preview columns.
28
+ const CURRENT_GROW = 1.35
29
+
24
30
  // Function: createAppComponent
25
31
  // Builds the root component bound to a `session`.
26
32
  //
@@ -57,6 +63,9 @@ function createAppComponent(session, options = {}) {
57
63
  const rightCrumbs = derived([session.panes.right.segments, crumbWidth], ([segments, width]) =>
58
64
  elideCrumbs(segments, width),
59
65
  )
66
+ const parentCrumbs = derived([session.panes.parent.segments, crumbWidth], ([segments, width]) =>
67
+ elideCrumbs(segments, width),
68
+ )
60
69
 
61
70
  const quit = options.quit ?? ((self) => {
62
71
  // Destroying the renderer from within a keypress deadlocks the native
@@ -67,7 +76,7 @@ function createAppComponent(session, options = {}) {
67
76
  }, 100)
68
77
  })
69
78
 
70
- const baseKeys = appBehaviors({ panel: true, quit }).keys
79
+ const baseKeys = appBehaviors({ quit }).keys
71
80
  const frame = createFrameBehaviors({ theme: tuiTheme })
72
81
 
73
82
  return ui
@@ -78,6 +87,7 @@ function createAppComponent(session, options = {}) {
78
87
  helpOpen: help,
79
88
  viewerOpen: viewer,
80
89
  leftSegments: leftCrumbs,
90
+ parentSegments: parentCrumbs,
81
91
  rightSegments: rightCrumbs,
82
92
  }))
83
93
  .does({
@@ -87,7 +97,6 @@ function createAppComponent(session, options = {}) {
87
97
  const name = key?.name
88
98
  const isQuestion = name === "?" || (name === "/" && key?.shift)
89
99
  const busy =
90
- unwrap(data.paletteOpen) ||
91
100
  unwrap(data.confirm) ||
92
101
  unwrap(data.filtering) ||
93
102
  unwrap(data.bookmarkOpen) ||
@@ -113,8 +122,8 @@ function createAppComponent(session, options = {}) {
113
122
  return
114
123
  }
115
124
  // Below the inline breakpoint `v` toggles the viewer overlay
116
- // instead of the side panel.
117
- if (!busy && name === "v" && isNarrow(data, self)) {
125
+ // instead of the side panel (`⇧V` still maximizes).
126
+ if (!busy && name === "v" && !key.shift && isNarrow(data, self)) {
118
127
  viewer.set(!viewer.value)
119
128
  return
120
129
  }
@@ -125,11 +134,23 @@ function createAppComponent(session, options = {}) {
125
134
  unwrap(data.active) === "left" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
126
135
  rightBorderFg: (_self, data) =>
127
136
  unwrap(data.active) === "right" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
137
+ parentBorderFg: (_self, data) =>
138
+ unwrap(data.active) === "parent" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
128
139
  // The header rule carries the same focus tint as the pane border.
129
140
  leftHeadRuleFg: (_self, data) =>
130
141
  unwrap(data.active) === "left" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
131
142
  rightHeadRuleFg: (_self, data) =>
132
143
  unwrap(data.active) === "right" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
144
+ parentHeadRuleFg: (_self, data) =>
145
+ unwrap(data.active) === "parent" ? tuiTheme.borderSecondaryActiveFg : tuiTheme.borderSecondaryFg,
146
+ // In explorer the current column takes the extra width; in
147
+ // commander both panes share the row evenly. The columns carry
148
+ // `flexBasis="0"` so the whole row is distributed by these
149
+ // growers instead of by the content each column would claim.
150
+ leftGrow: (_self, data) =>
151
+ isExplorer(data) && currentColumnId(data) === "left" ? CURRENT_GROW : 1,
152
+ rightGrow: (_self, data) =>
153
+ isExplorer(data) && currentColumnId(data) === "right" ? CURRENT_GROW : 1,
133
154
  })
134
155
  .sub(appEvents({ quit }))
135
156
  .using(appXml)
@@ -13,7 +13,9 @@
13
13
  // `rows` environment. Components sharing a presentation bind the same source
14
14
  // (`Breadcrumbs` uses `RowList.xml`).
15
15
  //
16
- // TUI-only components (`Help`) live in `tui/` and are registered here only.
16
+ // The workspace chrome (`Frame.js`) and the key reference (`Help.js`) are
17
+ // shared logic bound here to their `.xml` presentations; the browser view
18
+ // binds the same modules to `.html`.
17
19
 
18
20
  import { extend, ui } from "select.tui"
19
21
  import { unwrap } from "select/state/cells"
@@ -23,7 +25,6 @@ import commandBarXml from "../components/CommandBar.xml" with { type: "text" }
23
25
  import commandArgRowXml from "../components/CommandArgRow.xml" with { type: "text" }
24
26
  import commandItemXml from "../components/CommandItem.xml" with { type: "text" }
25
27
  import commandPanelXml from "../components/CommandPanel.xml" with { type: "text" }
26
- import commandPaletteXml from "../components/CommandPalette.xml" with { type: "text" }
27
28
  import confirmXml from "../components/Confirm.xml" with { type: "text" }
28
29
  import crumbXml from "../components/Crumb.xml" with { type: "text" }
29
30
  import entryDetailXml from "../components/EntryDetail.xml" with { type: "text" }
@@ -32,7 +33,6 @@ import filePreviewXml from "../components/FilePreview.xml" with { type: "text" }
32
33
  import fileRowXml from "../components/FileRow.xml" with { type: "text" }
33
34
  import filterBarXml from "../components/FilterBar.xml" with { type: "text" }
34
35
  import helpXml from "../components/Help.xml" with { type: "text" }
35
- import paletteRowXml from "../components/PaletteRow.xml" with { type: "text" }
36
36
  import rowListXml from "../components/RowList.xml" with { type: "text" }
37
37
  import summaryXml from "../components/Summary.xml" with { type: "text" }
38
38
 
@@ -48,7 +48,6 @@ import {
48
48
  commandItemRows,
49
49
  commandBarMarksItem,
50
50
  commandPanelBehaviors,
51
- commandPaletteBehaviors,
52
51
  confirmBehaviors,
53
52
  crumbBehaviors,
54
53
  crumbRows,
@@ -60,14 +59,12 @@ import {
60
59
  fileWindow,
61
60
  filterBarBehaviors,
62
61
  iconsEnabled,
63
- paletteRowBehaviors,
64
- paletteRows,
65
62
  panelArgRows,
66
63
  panelOpRows,
67
64
  summaryBehaviors,
68
65
  } from "../components/index.js"
69
- import { createBehaviors as helpBehaviors } from "./help.js"
70
- import { INSPECTOR_INLINE_WIDTH } from "./frame.js"
66
+ import { createBehaviors as helpBehaviors } from "../components/Help.js"
67
+ import { INSPECTOR_INLINE_WIDTH } from "../components/Frame.js"
71
68
  import { tuiTheme } from "./theme.js"
72
69
 
73
70
  let registered = false
@@ -90,6 +87,7 @@ function tuiEnv() {
90
87
  dirFg: tuiTheme.dirFg,
91
88
  errorBg: tuiTheme.errorBg,
92
89
  errorFg: tuiTheme.errorFg,
90
+ fieldBg: tuiTheme.fieldBg,
93
91
  fileFg: tuiTheme.fileFg,
94
92
  // Nerd Font icons in rows, on unless `TCMD_ICONS` opts out.
95
93
  icons: iconsEnabled(process.env),
@@ -177,9 +175,6 @@ function fileListScrollTop(self, data) {
177
175
  return keepVisible(self.ref.list, local, len)
178
176
  }
179
177
  return keepVisible(self.ref.list, unwrap(data.cursor) ?? 0, unwrap(data.entries)?.length ?? 0)
180
- }// Keep the palette selection inside its viewport.
181
- function paletteScrollTop(self, data) {
182
- return keepVisible(self.ref.list, unwrap(data.selected) ?? 0, unwrap(data.items)?.length ?? 0)
183
178
  }
184
179
 
185
180
  // Keep the bookmark picker selection inside its viewport.
@@ -214,7 +209,6 @@ function createComponents() {
214
209
  const crumb = bind(ui, "Crumb", crumbXml, crumbBehaviors(env))
215
210
  const fileRow = bind(ui, "FileRow", fileRowXml, fileRowBehaviors(env))
216
211
  const filterBar = bind(ui, "FilterBar", filterBarXml, filterBarBehaviors(env))
217
- const paletteRow = bind(ui, "PaletteRow", paletteRowXml, paletteRowBehaviors(env))
218
212
  const bookmarkRow = bind(ui, "BookmarkRow", bookmarkRowXml, bookmarkRowBehaviors(env))
219
213
  return {
220
214
  BookmarkRow: bookmarkRow,
@@ -222,7 +216,6 @@ function createComponents() {
222
216
  CommandItem: commandItem,
223
217
  Crumb: crumb,
224
218
  FileRow: fileRow,
225
- PaletteRow: paletteRow,
226
219
  Breadcrumbs: bind(ui, "Breadcrumbs", rowListXml, breadcrumbsBehaviors({
227
220
  ...env,
228
221
  rows: crumbRows(crumb),
@@ -242,11 +235,6 @@ function createComponents() {
242
235
  argRows: panelArgRows(commandArgRow),
243
236
  argScrollTop: panelArgScrollTop,
244
237
  })),
245
- CommandPalette: bind(ui, "CommandPalette", commandPaletteXml, commandPaletteBehaviors({
246
- ...env,
247
- rows: paletteRows(paletteRow),
248
- scrollTop: paletteScrollTop,
249
- })),
250
238
  Confirm: bind(ui, "Confirm", confirmXml, confirmBehaviors(env)),
251
239
  EntryDetail: bind(ui, "EntryDetail", entryDetailXml, entryDetailBehaviors({
252
240
  ...env,
@@ -283,7 +271,6 @@ function registerComponents() {
283
271
  breadcrumbs: components.Breadcrumbs,
284
272
  commandbar: components.CommandBar,
285
273
  commandpanel: components.CommandPanel,
286
- commandpalette: components.CommandPalette,
287
274
  confirm: components.Confirm,
288
275
  entrydetail: components.EntryDetail,
289
276
  filelist: components.FileList,
@@ -15,24 +15,10 @@ import nodePath from "node:path"
15
15
  import { mkdir, readFile, writeFile } from "node:fs/promises"
16
16
  import { cell } from "select/state/cells"
17
17
  import { render } from "select.tui"
18
- import { commandRow, getPaletteCommands } from "../core/commands.js"
19
18
  import { createBookmarks, createFileBookmarkStore } from "../core/bookmarks.js"
20
19
  import { createNodeFs, createNodeOs, createPane, createSession, load } from "../core/index.js"
21
20
  import { createAppComponent } from "./app.js"
22
21
 
23
- // The TUI palette lists meaningful actions; raw cursor movement is already on
24
- // the arrow keys and is noise in the list. Hiding it here (rather than in core)
25
- // keeps the web palette and the shared registry unchanged.
26
- const PALETTE_NOISE = /^nav\./
27
-
28
- // Function: tuiPaletteCommands
29
- // The command rows the TUI palette offers.
30
- function tuiPaletteCommands() {
31
- return getPaletteCommands()
32
- .filter((command) => !PALETTE_NOISE.test(command.id))
33
- .map(commandRow)
34
- }
35
-
36
22
  // Function: readSize
37
23
  // Normalizes a renderer resize payload. OpenTUI's live renderer emits
38
24
  // `(width, height)` as two arguments; the test renderer emits a bare width;
@@ -78,7 +64,6 @@ async function main(paths = []) {
78
64
  }),
79
65
  })
80
66
  const session = createSession({ fs, os, left, right, host: nodeOs.hostname(), bookmarks })
81
- session.paletteCommands.set(tuiPaletteCommands())
82
67
  await load(session)
83
68
  const size = cell({ width: 0, height: 0 })
84
69
  const help = cell(false)
@@ -99,6 +84,6 @@ async function main(paths = []) {
99
84
  return session
100
85
  }
101
86
 
102
- export { main, readSize, tuiPaletteCommands }
87
+ export { main, readSize }
103
88
 
104
89
  // EOF
@@ -43,6 +43,8 @@ const DARK = {
43
43
  cursorBg: "#274a9e",
44
44
  cursorInactiveBg: "#1b2a51",
45
45
  selectionBg: "#2b3f78",
46
+ // The value fields of the command panel's argument rows.
47
+ fieldBg: "#1b2a51",
46
48
  // Text
47
49
  textFg: "#e6ecff",
48
50
  textSecondaryFg: "#8fa3d9",
@@ -88,6 +90,8 @@ const LIGHT = {
88
90
  cursorBg: "#2f56b8",
89
91
  cursorInactiveBg: "#c6d1ee",
90
92
  selectionBg: "#c9d5f4",
93
+ // The value fields of the command panel's argument rows.
94
+ fieldBg: "#dbe4fb",
91
95
  // Text
92
96
  textFg: "#101a33",
93
97
  textSecondaryFg: "#495785",
@@ -131,6 +135,7 @@ const MONO = {
131
135
  cursorBg: undefined,
132
136
  cursorInactiveBg: undefined,
133
137
  selectionBg: undefined,
138
+ fieldBg: undefined,
134
139
  textFg: undefined,
135
140
  textSecondaryFg: undefined,
136
141
  textHeadingFg: undefined,
@@ -9,10 +9,13 @@
9
9
  // `tcmd/tui/app.js` for the DOM backend.
10
10
 
11
11
  import { ui } from "select/ui"
12
- import { unwrap } from "select/state/cells"
12
+ import { cell, derived, unwrap } from "select/state/cells"
13
13
  import appHtml from "../components/App.html" with { type: "text" }
14
14
  import { appBehaviors, appEvents, appInit } from "../components/index.js"
15
15
  import { registerWebComponents } from "./catalogue.js"
16
+ import { elideCrumbs } from "../components/format.js"
17
+ import { createFrameBehaviors, INSPECTOR_INLINE_WIDTH, isNarrow } from "../components/Frame.js"
18
+ import { connectEffects } from "./effects.js"
16
19
  import { webTheme } from "./theme.js"
17
20
 
18
21
  // Map DOM key names to the core keymap chord names.
@@ -45,9 +48,18 @@ function normalizeKey(event) {
45
48
  // Child data keeps the session cells (as `in:` slots do in the TUI), so the
46
49
  // child instances subscribe and update granularly; behaviors read cells with
47
50
  // `unwrap` at the point of use.
51
+
52
+ // The classes one pane column carries: focus (`is-active`) and, in explorer
53
+ // mode, the extra width of the middle (`is-major`) column.
54
+ function paneClass(data, paneId) {
55
+ const active = unwrap(data.active) === paneId
56
+ const major = unwrap(data.viewMode) === "explorer" && `${unwrap(data.currentId)}` === paneId
57
+ return [active && webTheme.activeClass, major && "is-major"].filter(Boolean).join(" ") || null
58
+ }
59
+
48
60
  function webViewBehaviors() {
49
61
  return {
50
- headerView: (_self, data) => ({ host: data.appHost, path: data.appPath }),
62
+ headerView: (_self, data) => ({ host: data.appHost, path: data.appPath, mode: data.viewMode }),
51
63
  bookmarkView: (_self, data) => ({
52
64
  items: data.bookmarkItems,
53
65
  mode: data.bookmarkMode,
@@ -61,92 +73,206 @@ function webViewBehaviors() {
61
73
  leftError: data.leftError,
62
74
  notice: data.notice,
63
75
  rightError: data.rightError,
76
+ active: data.active,
77
+ leftMarked: data.leftMarked,
78
+ rightMarked: data.rightMarked,
64
79
  }),
65
80
  confirmView: (_self, data) => ({ confirm: data.confirm }),
66
81
  leftCrumbsView: (_self, data) => ({
67
82
  segments: data.leftSegments,
68
83
  paneId: data.leftId,
69
84
  }),
70
- leftDetailView: (_self, data) => ({ entry: data.leftEntry, marked: data.leftMarked }),
85
+ leftDetailView: (_self, data) => ({ entry: data.leftEntry, marked: data.leftMarked, width: data.paneWidth }),
71
86
  leftFilterView: (_self, data) => ({
72
87
  query: data.leftFilter,
73
88
  paneId: data.leftId,
74
89
  filtering: data.filtering,
75
90
  entries: data.leftEntries,
91
+ setFilter: data.leftSetFilter,
76
92
  }),
77
- leftLabelView: (_self, data) => ({ paneId: data.leftId, active: data.active }),
78
93
  leftListView: (_self, data) => ({
79
94
  entries: data.leftEntries,
80
95
  cursor: data.leftCursor,
81
96
  marked: data.leftMarked,
82
97
  paneId: data.leftId,
98
+ active: data.leftActive,
99
+ loading: data.leftLoading,
83
100
  }),
84
- leftPaneClass: (_self, data) => (unwrap(data.active) === "left" ? webTheme.activeClass : null),
101
+ leftPaneClass: (_self, data) => paneClass(data, "left"),
85
102
  leftSummaryView: (_self, data) => ({
86
103
  entries: data.leftEntries,
87
104
  disk: data.leftDisk,
105
+ width: data.paneWidth,
88
106
  }),
89
- paletteView: (_self, data) => ({
90
- // Cells pass through (as `in:` slots do in the TUI); `query` is written
91
- // back by the input's `inout` two-way binding.
92
- items: data.paletteItems,
93
- mode: data.paletteMode,
94
- open: data.paletteOpen,
95
- query: data.paletteQuery,
96
- selected: data.paletteIndex,
107
+ // The explorer's first column: the parent of the current directory,
108
+ // a full pane of its own (header, filter, listing, detail, summary).
109
+ parentCrumbsView: (_self, data) => ({
110
+ segments: data.parentSegments,
111
+ paneId: data.parentId,
112
+ }),
113
+ parentDetailView: (_self, data) => ({
114
+ entry: data.parentEntry,
115
+ marked: data.parentMarked,
116
+ width: data.paneWidth,
117
+ }),
118
+ parentFilterView: (_self, data) => ({
119
+ query: data.parentFilter,
120
+ paneId: data.parentId,
121
+ filtering: data.filtering,
122
+ entries: data.parentEntries,
123
+ setFilter: data.parentSetFilter,
97
124
  }),
125
+ parentListView: (_self, data) => ({
126
+ entries: data.parentEntries,
127
+ cursor: data.parentCursor,
128
+ marked: data.parentMarked,
129
+ paneId: data.parentId,
130
+ active: data.parentActive,
131
+ loading: data.parentLoading,
132
+ }),
133
+ parentPaneClass: (_self, data) => paneClass(data, "parent"),
134
+ parentSummaryView: (_self, data) => ({
135
+ entries: data.parentEntries,
136
+ disk: data.parentDisk,
137
+ width: data.paneWidth,
138
+ }),
139
+ panelView: (_self, data) => ({
140
+ // Cells pass through (as `in:` slots do in the TUI); `query`/`value`
141
+ // are written back by the inputs' `inout` two-way bindings.
142
+ open: data.panelOpen,
143
+ entry: data.panelEntry,
144
+ items: data.panelItems,
145
+ opCursor: data.panelOpIndex,
146
+ rows: data.panelRows,
147
+ argCursor: data.panelRowIndex,
148
+ labelWidth: data.panelLabelWidth,
149
+ focus: data.panelFocus,
150
+ command: data.panelCommand,
151
+ runnable: data.panelRunnable,
152
+ picking: data.panelPicking,
153
+ pickerQuery: data.pickerQuery,
154
+ pickerPath: data.pickerPath,
155
+ query: data.panelQuery,
156
+ value: data.panelValue,
157
+ }),
158
+ helpView: (_self, data) => ({ open: data.helpOpen }),
98
159
  previewView: (_self, data) => ({
99
160
  preview: data.preview,
100
- show: data.showPreview,
161
+ show: true,
162
+ entry: data.appEntry,
163
+ paneId: data.previewId,
164
+ // The directory column: the inspector pane's own listing, so a
165
+ // previewed directory reads and behaves like a pane.
166
+ dirSegments: data.previewSegments,
167
+ dirEntries: data.previewEntries,
168
+ dirCursor: data.previewCursor,
169
+ dirMarked: data.previewMarked,
170
+ dirActive: data.previewActive,
171
+ dirLoading: data.previewLoading,
101
172
  }),
102
173
  rightCrumbsView: (_self, data) => ({
103
174
  segments: data.rightSegments,
104
175
  paneId: data.rightId,
105
176
  }),
106
- rightDetailView: (_self, data) => ({ entry: data.rightEntry, marked: data.rightMarked }),
177
+ rightDetailView: (_self, data) => ({ entry: data.rightEntry, marked: data.rightMarked, width: data.paneWidth }),
107
178
  rightFilterView: (_self, data) => ({
108
179
  query: data.rightFilter,
109
180
  paneId: data.rightId,
110
181
  filtering: data.filtering,
111
182
  entries: data.rightEntries,
183
+ setFilter: data.rightSetFilter,
112
184
  }),
113
- rightLabelView: (_self, data) => ({ paneId: data.rightId, active: data.active }),
114
185
  rightListView: (_self, data) => ({
115
186
  entries: data.rightEntries,
116
187
  cursor: data.rightCursor,
117
188
  marked: data.rightMarked,
118
189
  paneId: data.rightId,
190
+ active: data.rightActive,
191
+ loading: data.rightLoading,
119
192
  }),
120
- rightPaneClass: (_self, data) => (unwrap(data.active) === "right" ? webTheme.activeClass : null),
193
+ rightPaneClass: (_self, data) => paneClass(data, "right"),
121
194
  rightSummaryView: (_self, data) => ({
122
195
  entries: data.rightEntries,
123
196
  disk: data.rightDisk,
197
+ width: data.paneWidth,
124
198
  }),
125
199
  }
126
200
  }
127
201
 
128
202
  // Function: createWebApp
129
203
  // Builds the root App component for `session` on the web presentation.
130
- function createWebApp(session) {
204
+ function createWebApp(session, options = {}) {
131
205
  registerWebComponents()
132
- // The command panel is a TUI-only view; disable it so no web dispatch path
133
- // (key, palette, command bar) opens an invisible modal, and drop its
134
- // palette entries (`:` and `m` both fall back to the script palette).
135
- session.panelEnabled?.set(false)
136
- const palette = session.paletteCommands?.value
137
- if (palette) {
138
- session.paletteCommands.set(palette.filter((row) => row.id !== "panel.toggle" && row.id !== "panel.open"))
139
- }
206
+ const size = options.size ?? cell({ width: 0, height: 0 })
207
+ const help = cell(false)
208
+ const viewer = cell(false)
209
+ const paneWidth = derived([size, session.showPreview], ([value, show]) => {
210
+ if (!value.width) return 0
211
+ const columns = show && value.width >= INSPECTOR_INLINE_WIDTH ? 3 : 2
212
+ return Math.max(1, Math.floor(value.width / columns) - 4)
213
+ })
214
+ const crumbs = (pane) => derived([pane.segments, paneWidth], ([segments, width]) => elideCrumbs(segments, width))
215
+ const quit = (self) => self.unmount?.()
216
+ // DOM inputs have no native `enter` event; the root routes Enter for them.
217
+ const baseKeys = appBehaviors({ normalizeKey, quit }).keys
218
+ const events = appEvents({ quit })
140
219
  return ui(appHtml)
141
- .init(appInit(session))
220
+ .init(() => ({
221
+ ...appInit(session)(),
222
+ uiSize: size,
223
+ helpOpen: help,
224
+ viewerOpen: viewer,
225
+ paneWidth,
226
+ leftSegments: crumbs(session.panes.left),
227
+ parentSegments: crumbs(session.panes.parent),
228
+ rightSegments: crumbs(session.panes.right),
229
+ }))
142
230
  .does({
143
- ...appBehaviors({
144
- normalizeKey,
145
- quit: (self) => self.unmount?.(),
146
- }),
231
+ ...createFrameBehaviors({ theme: webTheme }),
147
232
  ...webViewBehaviors(),
233
+ minSizeTitle: () => "Window too small",
234
+ "effects!": (self) => {
235
+ if (!self._effects) self.effect(() => {
236
+ self._effects = connectEffects(session, self.ref.root, { size, help, viewer })
237
+ return () => self._effects.dispose()
238
+ })
239
+ self._effects.schedule()
240
+ },
241
+ keys: (self, data, event) => {
242
+ if (event.isComposing) return
243
+ const key = normalizeKey(event)
244
+ const busy = unwrap(data.confirm) || unwrap(data.bookmarkOpen) || unwrap(data.panelOpen) || unwrap(data.filtering)
245
+ if (help.value) {
246
+ if (["escape", "q", "?"].includes(key.name)) help.set(false)
247
+ return
248
+ }
249
+ if (!busy && key.name === "?") {
250
+ help.set(true)
251
+ return
252
+ }
253
+ // The viewer only exists while narrow: if the viewport widened
254
+ // underneath it, drop the stale state and keep handling the key.
255
+ if (viewer.value && !isNarrow(data, self)) viewer.set(false)
256
+ if (viewer.value) {
257
+ if (key.name === "escape" || key.name === "v") viewer.set(false)
258
+ else if (key.shift && key.name === "o") session.dispatch("file.open")
259
+ else if (key.shift && key.name === "e") session.dispatch("file.edit")
260
+ return
261
+ }
262
+ if (!busy && key.name === "v" && !key.shift && isNarrow(data, self)) {
263
+ viewer.set(!viewer.value)
264
+ return
265
+ }
266
+ return baseKeys(self, data, event)
267
+ },
268
+ })
269
+ .sub({
270
+ ...events,
271
+ "commandbar:run": (self, data, event) => {
272
+ if (event.data === "view.togglePreview" && isNarrow(data, self)) viewer.set(!viewer.value)
273
+ else events["commandbar:run"](self, data, event)
274
+ },
148
275
  })
149
- .sub(appEvents({ quit: (self) => self.unmount?.() }))
150
276
  }
151
277
 
152
278
  export { createWebApp, normalizeKey }
@@ -0,0 +1,36 @@
1
+ // Project: TinyCommand
2
+ // Author: Sebastien Pierre
3
+ // License: BSD
4
+ // Created: 2026-10-13
5
+
6
+ // Module: tcmd/web/bookmarks
7
+ // The `core/bookmarks` store contract backed by localStorage
8
+ // (`createWebBookmarkStore`, the web twin of the TUI's
9
+ // `createFileBookmarkStore`). Backend/root namespaces keep browser-private and
10
+ // host-filesystem paths apart. Persistence is best-effort, just like the TUI
11
+ // store (private browsing may deny storage).
12
+
13
+ // Function: createWebBookmarkStore
14
+ function createWebBookmarkStore({ mode, root, storage } = {}) {
15
+ const key = JSON.stringify(["tcmd.bookmarks", mode, root])
16
+ return {
17
+ async load() {
18
+ try {
19
+ return JSON.parse((storage ?? globalThis.localStorage).getItem(key) ?? "{}")
20
+ } catch {
21
+ return {}
22
+ }
23
+ },
24
+ async save(map) {
25
+ try {
26
+ (storage ?? globalThis.localStorage).setItem(key, JSON.stringify(map))
27
+ } catch {
28
+ // Keep in-memory bookmarks usable when persistent storage is denied.
29
+ }
30
+ },
31
+ }
32
+ }
33
+
34
+ export { createWebBookmarkStore }
35
+
36
+ // EOF