@ssebastien/tcmd 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +169 -58
  2. package/desktop/dist/index.html +13 -0
  3. package/desktop/dist/main.js +20298 -0
  4. package/desktop/dist/style.css +1515 -0
  5. package/desktop/frontend/index.html +13 -0
  6. package/package.json +17 -5
  7. package/public/fonts/OFL.txt +93 -0
  8. package/public/licenses/LUCIDE.txt +43 -0
  9. package/public/main.js +5066 -1888
  10. package/public/style.css +660 -46
  11. package/scripts/build-desktop.js +31 -0
  12. package/src/js/tcmd/components/App.html +56 -6
  13. package/src/js/tcmd/components/App.js +233 -58
  14. package/src/js/tcmd/components/App.xml +27 -22
  15. package/src/js/tcmd/components/BookmarkList.html +4 -5
  16. package/src/js/tcmd/components/BookmarkList.js +14 -40
  17. package/src/js/tcmd/components/BookmarkList.xml +1 -2
  18. package/src/js/tcmd/components/BookmarkRow.html +2 -1
  19. package/src/js/tcmd/components/BookmarkRow.js +14 -9
  20. package/src/js/tcmd/components/BookmarkRow.xml +1 -1
  21. package/src/js/tcmd/components/CommandArgRow.html +18 -7
  22. package/src/js/tcmd/components/CommandArgRow.js +100 -44
  23. package/src/js/tcmd/components/CommandArgRow.xml +39 -15
  24. package/src/js/tcmd/components/CommandBar.html +1 -1
  25. package/src/js/tcmd/components/CommandBar.js +31 -25
  26. package/src/js/tcmd/components/CommandBar.xml +2 -2
  27. package/src/js/tcmd/components/CommandItem.js +24 -25
  28. package/src/js/tcmd/components/CommandItem.xml +2 -2
  29. package/src/js/tcmd/components/CommandPanel.html +13 -13
  30. package/src/js/tcmd/components/CommandPanel.js +57 -33
  31. package/src/js/tcmd/components/CommandPanel.xml +30 -20
  32. package/src/js/tcmd/components/Confirm.html +2 -2
  33. package/src/js/tcmd/components/EntryDetail.js +21 -7
  34. package/src/js/tcmd/components/FilePicker.html +10 -0
  35. package/src/js/tcmd/components/FilePicker.js +43 -0
  36. package/src/js/tcmd/components/FilePicker.xml +13 -0
  37. package/src/js/tcmd/components/FilePickerRow.html +6 -0
  38. package/src/js/tcmd/components/FilePickerRow.js +27 -0
  39. package/src/js/tcmd/components/FilePickerRow.xml +10 -0
  40. package/src/js/tcmd/components/FilePreview.html +1 -2
  41. package/src/js/tcmd/components/FilePreview.js +40 -4
  42. package/src/js/tcmd/components/FilePreview.xml +17 -10
  43. package/src/js/tcmd/components/FileRow.html +1 -0
  44. package/src/js/tcmd/components/FileRow.js +60 -9
  45. package/src/js/tcmd/components/Frame.js +57 -31
  46. package/src/js/tcmd/components/Help.html +12 -4
  47. package/src/js/tcmd/components/Help.js +198 -47
  48. package/src/js/tcmd/components/Help.xml +13 -4
  49. package/src/js/tcmd/components/HelpRow.html +8 -0
  50. package/src/js/tcmd/components/HelpRow.xml +7 -0
  51. package/src/js/tcmd/components/OverlayList.html +8 -0
  52. package/src/js/tcmd/components/OverlayList.js +69 -0
  53. package/src/js/tcmd/components/OverlayList.xml +13 -0
  54. package/src/js/tcmd/components/OverlayRow.html +5 -0
  55. package/src/js/tcmd/components/OverlayRow.js +47 -0
  56. package/src/js/tcmd/components/OverlayRow.xml +9 -0
  57. package/src/js/tcmd/components/format.js +4 -23
  58. package/src/js/tcmd/components/icons.js +137 -0
  59. package/src/js/tcmd/components/index.js +8 -3
  60. package/src/js/tcmd/components/rows.js +149 -25
  61. package/src/js/tcmd/core/base64.js +64 -0
  62. package/src/js/tcmd/core/bookmarks.js +45 -8
  63. package/src/js/tcmd/core/commands.js +612 -136
  64. package/src/js/tcmd/core/concurrency.js +59 -0
  65. package/src/js/tcmd/core/decompress.js +117 -0
  66. package/src/js/tcmd/core/directory.js +81 -5
  67. package/src/js/tcmd/core/format.js +36 -0
  68. package/src/js/tcmd/core/fs.js +410 -53
  69. package/src/js/tcmd/core/journal.js +93 -0
  70. package/src/js/tcmd/core/keymap.js +39 -3
  71. package/src/js/tcmd/core/operations.js +735 -63
  72. package/src/js/tcmd/core/os.js +29 -3
  73. package/src/js/tcmd/core/pane.js +4 -2
  74. package/src/js/tcmd/core/panel.js +179 -93
  75. package/src/js/tcmd/core/path.js +30 -3
  76. package/src/js/tcmd/core/permissions.js +107 -0
  77. package/src/js/tcmd/core/picker.js +156 -0
  78. package/src/js/tcmd/core/preview.js +102 -0
  79. package/src/js/tcmd/core/session.js +286 -116
  80. package/src/js/tcmd/core/watch.js +153 -0
  81. package/src/js/tcmd/desktop/backend/api.js +165 -0
  82. package/src/js/tcmd/desktop/backend/fs.js +626 -0
  83. package/src/js/tcmd/desktop/backend/main.ts +9 -0
  84. package/src/js/tcmd/desktop/bookmarks.js +23 -0
  85. package/src/js/tcmd/desktop/fs.js +91 -0
  86. package/src/js/tcmd/desktop/main.js +194 -0
  87. package/src/js/tcmd/desktop/os.js +33 -0
  88. package/src/js/tcmd/tui/app.js +111 -74
  89. package/src/js/tcmd/tui/catalogue.js +232 -23
  90. package/src/js/tcmd/tui/index.js +18 -4
  91. package/src/js/tcmd/tui/opentui.js +33 -0
  92. package/src/js/tcmd/tui/syntax.js +91 -0
  93. package/src/js/tcmd/tui/theme.js +27 -2
  94. package/src/js/tcmd/vendor/bzip2.js +276 -0
  95. package/src/js/tcmd/web/app.js +114 -56
  96. package/src/js/tcmd/web/catalogue.js +41 -4
  97. package/src/js/tcmd/web/effects.js +94 -14
  98. package/src/js/tcmd/web/header.html +1 -1
  99. package/src/js/tcmd/web/http.js +13 -0
  100. package/src/js/tcmd/web/index.js +2 -2
  101. package/src/js/tcmd/web/opfs.js +297 -30
  102. package/src/js/tcmd/web/seed.js +1 -1
  103. package/src/js/tcmd/web/server.js +118 -17
  104. package/src/js/tcmd/web/theme.js +3 -0
  105. package/tinyjs.json +16 -0
@@ -0,0 +1,276 @@
1
+ // Project: TinyCommand
2
+ // Vendored dependency
3
+ //
4
+ // Module: tcmd/vendor/bzip2
5
+ // bzip2.js — a small bzip2 decompression implementation.
6
+ //
7
+ // Source: https://github.com/antimatter15/bzip2.js (bzip2.js)
8
+ // Author: antimatter15
9
+ // License: MIT
10
+ //
11
+ // Vendored verbatim (only this header and the trailing export were added) so
12
+ // TinyCommand can preview `.bz2` files without adding a runtime dependency.
13
+ // `array` wraps the bytes in a bit reader; `header` returns the block size and
14
+ // `decompress` decodes one block, returning `-1` on the stream terminator.
15
+ // Decoded output is a binary string (one char code per byte), so callers must
16
+ // decode it as UTF-8 themselves.
17
+ //
18
+ // MIT License
19
+ //
20
+ // Copyright (c) 2011 by antimatter15 (antimatter15@gmail.com).
21
+ //
22
+ // Permission is hereby granted, free of charge, to any person obtaining a
23
+ // copy of this software and associated documentation files (the "Software"),
24
+ // to deal in the Software without restriction, including without limitation
25
+ // the rights to use, copy, modify, merge, publish, distribute, sublicense,
26
+ // and/or sell copies of the Software, and to permit persons to whom the
27
+ // Software is furnished to do so, subject to the following conditions:
28
+ //
29
+ // The above copyright notice and this permission notice shall be included in
30
+ // all copies or substantial portions of the Software.
31
+ //
32
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
37
+ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
38
+ // IN THE SOFTWARE.
39
+
40
+ var bzip2 = {};
41
+
42
+ bzip2.array = function(bytes){
43
+ var bit = 0, byte = 0;
44
+ var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF ];
45
+ return function(n){
46
+ var result = 0;
47
+ while(n > 0){
48
+ var left = 8 - bit;
49
+ if(n >= left){
50
+ result <<= left;
51
+ result |= (BITMASK[left] & bytes[byte++]);
52
+ bit = 0;
53
+ n -= left;
54
+ }else{
55
+ result <<= n;
56
+ result |= ((bytes[byte] & (BITMASK[n] << (8 - n - bit))) >> (8 - n - bit));
57
+ bit += n;
58
+ n = 0;
59
+ }
60
+ }
61
+ return result
62
+ }
63
+ }
64
+
65
+ bzip2.simple = function(bits){
66
+ var size = bzip2.header(bits);
67
+ var all = '', chunk = '';
68
+ do{
69
+ all += chunk;
70
+ chunk = bzip2.decompress(bits, size);
71
+ }while(chunk != -1);
72
+ return all;
73
+ }
74
+
75
+ bzip2.header = function(bits){
76
+ if(bits(8*3) != 4348520) throw "No magic number found";
77
+ var i = bits(8) - 48;
78
+ if(i < 1 || i > 9) throw "Not a BZIP archive";
79
+ return i;
80
+ };
81
+
82
+
83
+ //takes a function for reading the block data (starting with 0x314159265359)
84
+ //a block size (0-9) (optional, defaults to 9)
85
+ //a length at which to stop decompressing and return the output
86
+ bzip2.decompress = function(bits, size, len){
87
+ var MAX_HUFCODE_BITS = 20;
88
+ var MAX_SYMBOLS = 258;
89
+ var SYMBOL_RUNA = 0;
90
+ var SYMBOL_RUNB = 1;
91
+ var GROUP_SIZE = 50;
92
+
93
+ var bufsize = 100000 * size;
94
+ for(var h = '', i = 0; i < 6; i++) h += bits(8).toString(16);
95
+ if(h == "177245385090") return -1; //last block
96
+ if(h != "314159265359") throw "eek not valid bzip data";
97
+ bits(32); //ignore CRC codes
98
+ if(bits(1)) throw "unsupported obsolete version";
99
+ var origPtr = bits(24);
100
+ if(origPtr > bufsize) throw "Initial position larger than buffer size";
101
+ var t = bits(16);
102
+ var symToByte = new Uint8Array(256),
103
+ symTotal = 0;
104
+ for (i = 0; i < 16; i++) {
105
+ if(t & (1 << (15 - i))) {
106
+ var k = bits(16);
107
+ for(j = 0; j < 16; j++){
108
+ if(k & (1 << (15 - j))){
109
+ symToByte[symTotal++] = (16 * i) + j;
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ var groupCount = bits(3);
116
+ if(groupCount < 2 || groupCount > 6) throw "another error";
117
+ var nSelectors = bits(15);
118
+ if(nSelectors == 0) throw "meh";
119
+ var mtfSymbol = []; //TODO: possibly replace JS array with typed arrays
120
+ for(var i = 0; i < groupCount; i++) mtfSymbol[i] = i;
121
+ var selectors = new Uint8Array(32768);
122
+
123
+ for(var i = 0; i < nSelectors; i++){
124
+ for(var j = 0; bits(1); j++) if(j >= groupCount) throw "whoops another error";
125
+ var uc = mtfSymbol[j];
126
+ mtfSymbol.splice(j, 1); //this is a probably inefficient MTF transform
127
+ mtfSymbol.splice(0, 0, uc);
128
+ selectors[i] = uc;
129
+ }
130
+
131
+ var symCount = symTotal + 2;
132
+ var groups = [];
133
+ for(var j = 0; j < groupCount; j++){
134
+ var length = new Uint8Array(MAX_SYMBOLS),
135
+ temp = new Uint8Array(MAX_HUFCODE_BITS+1);
136
+ t = bits(5); //lengths
137
+ for(var i = 0; i < symCount; i++){
138
+ while(true){
139
+ if (t < 1 || t > MAX_HUFCODE_BITS) throw "I gave up a while ago on writing error messages";
140
+ if(!bits(1)) break;
141
+ if(!bits(1)) t++;
142
+ else t--;
143
+ }
144
+ length[i] = t;
145
+ }
146
+ var minLen, maxLen;
147
+ minLen = maxLen = length[0];
148
+ for(var i = 1; i < symCount; i++){
149
+ if(length[i] > maxLen) maxLen = length[i];
150
+ else if(length[i] < minLen) minLen = length[i];
151
+ }
152
+ var hufGroup;
153
+ hufGroup = groups[j] = {};
154
+ hufGroup.permute = new Uint32Array(MAX_SYMBOLS);
155
+ hufGroup.limit = new Uint32Array(MAX_HUFCODE_BITS + 1);
156
+ hufGroup.base = new Uint32Array(MAX_HUFCODE_BITS + 1);
157
+ hufGroup.minLen = minLen;
158
+ hufGroup.maxLen = maxLen;
159
+ var base = hufGroup.base.subarray(1);
160
+ var limit = hufGroup.limit.subarray(1);
161
+ var pp = 0;
162
+ for(var i = minLen; i <= maxLen; i++)
163
+ for(var t = 0; t < symCount; t++)
164
+ if(length[t] == i) hufGroup.permute[pp++] = t;
165
+ for(i = minLen; i <= maxLen; i++) temp[i] = limit[i] = 0;
166
+ for(i = 0; i < symCount; i++) temp[length[i]]++;
167
+ pp = t = 0;
168
+ for(i = minLen; i < maxLen; i++) {
169
+ pp += temp[i];
170
+ limit[i] = pp - 1;
171
+ pp <<= 1;
172
+ base[i+1] = pp - (t += temp[i]);
173
+ }
174
+ limit[maxLen]=pp+temp[maxLen]-1;
175
+ base[minLen]=0;
176
+ }
177
+ var byteCount = new Uint32Array(256);
178
+ for(var i = 0; i < 256; i++) mtfSymbol[i] = i;
179
+ var runPos, count, symCount, selector;
180
+ runPos = count = symCount = selector = 0;
181
+ var buf = new Uint32Array(bufsize);
182
+ while(true){
183
+ if(!(symCount--)){
184
+ symCount = GROUP_SIZE - 1;
185
+ if(selector >= nSelectors) throw "meow i'm a kitty, that's an error";
186
+ hufGroup = groups[selectors[selector++]];
187
+ base = hufGroup.base.subarray(1);
188
+ limit = hufGroup.limit.subarray(1);
189
+ }
190
+ i = hufGroup.minLen;
191
+ j = bits(i);
192
+ while(true){
193
+ if(i > hufGroup.maxLen) throw "rawr i'm a dinosaur";
194
+ if(j <= limit[i]) break;
195
+ i++;
196
+ j = (j << 1) | bits(1);
197
+ }
198
+ j -= base[i];
199
+ if(j < 0 || j >= MAX_SYMBOLS) throw "moo i'm a cow";
200
+ var nextSym = hufGroup.permute[j];
201
+ if (nextSym == SYMBOL_RUNA || nextSym == SYMBOL_RUNB) {
202
+ if(!runPos){
203
+ runPos = 1;
204
+ t = 0;
205
+ }
206
+ if(nextSym == SYMBOL_RUNA) t += runPos;
207
+ else t += 2 * runPos;
208
+ runPos <<= 1;
209
+ continue;
210
+ }
211
+ if(runPos){
212
+ runPos = 0;
213
+ if(count + t >= bufsize) throw "Boom.";
214
+ uc = symToByte[mtfSymbol[0]];
215
+ byteCount[uc] += t;
216
+ while(t--) buf[count++] = uc;
217
+ }
218
+ if(nextSym > symTotal) break;
219
+ if(count >= bufsize) throw "I can't think of anything. Error";
220
+ i = nextSym -1;
221
+ uc = mtfSymbol[i];
222
+ mtfSymbol.splice(i, 1);
223
+ mtfSymbol.splice(0, 0, uc);
224
+ uc = symToByte[uc];
225
+ byteCount[uc]++;
226
+ buf[count++] = uc;
227
+ }
228
+ if(origPtr < 0 || origPtr >= count) throw "I'm a monkey and I'm throwing something at someone, namely you";
229
+ var j = 0;
230
+ for(var i = 0; i < 256; i++){
231
+ k = j + byteCount[i];
232
+ byteCount[i] = j;
233
+ j = k;
234
+ }
235
+ for(var i = 0; i < count; i++){
236
+ uc = buf[i] & 0xff;
237
+ buf[byteCount[uc]] |= (i << 8);
238
+ byteCount[uc]++;
239
+ }
240
+ var pos = 0, current = 0, run = 0;
241
+ if(count) {
242
+ pos = buf[origPtr];
243
+ current = (pos & 0xff);
244
+ pos >>= 8;
245
+ run = -1;
246
+ }
247
+ count = count;
248
+ var output = '';
249
+ var copies, previous, outbyte;
250
+ if(!len) len = Infinity;
251
+ while(count){
252
+ count--;
253
+ previous = current;
254
+ pos = buf[pos];
255
+ current = pos & 0xff;
256
+ pos >>= 8;
257
+ if(run++ == 3){
258
+ copies = current;
259
+ outbyte = previous;
260
+ current = -1;
261
+ }else{
262
+ copies = 1;
263
+ outbyte = current;
264
+ }
265
+ while(copies--){
266
+ output += (String.fromCharCode(outbyte));
267
+ if(!--len) return output;
268
+ }
269
+ if(current != previous) run = 0;
270
+ }
271
+ return output;
272
+ }
273
+
274
+ export { bzip2 }
275
+
276
+ // EOF
@@ -14,8 +14,8 @@ 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
16
  import { elideCrumbs } from "../components/format.js"
17
- import { createFrameBehaviors, INSPECTOR_INLINE_WIDTH, isNarrow } from "../components/Frame.js"
18
- import { connectEffects } from "./effects.js"
17
+ import { createFrameBehaviors, INSPECTOR_INLINE_WIDTH, paneColumnWidth } from "../components/Frame.js"
18
+ import { connectEffects, isToolbarTarget } from "./effects.js"
19
19
  import { webTheme } from "./theme.js"
20
20
 
21
21
  // Map DOM key names to the core keymap chord names.
@@ -59,23 +59,31 @@ function paneClass(data, paneId) {
59
59
 
60
60
  function webViewBehaviors() {
61
61
  return {
62
+ // The view mode as an app-level class (`is-explorer` / `is-commander`),
63
+ // so the CSS can tint the active pane per mode (blue/gold).
64
+ appModeClass: (_self, data) => [
65
+ `is-${unwrap(data.viewMode) === "explorer" ? "explorer" : "commander"}`,
66
+ unwrap(data.desktopMode) ? "is-desktop" : null,
67
+ ].filter(Boolean).join(" "),
62
68
  headerView: (_self, data) => ({ host: data.appHost, path: data.appPath, mode: data.viewMode }),
69
+ desktopViewLabel: (_self, data) => unwrap(data.viewMode) === "explorer" ? "Explorer" : "Commander",
70
+ desktopPreviewLabel: (_self, data) => unwrap(data.showPreview) ? "Hide preview" : "Show preview",
63
71
  bookmarkView: (_self, data) => ({
64
72
  items: data.bookmarkItems,
65
- mode: data.bookmarkMode,
66
73
  open: data.bookmarkOpen,
67
74
  query: data.bookmarkQuery,
68
75
  selected: data.bookmarkIndex,
69
- target: data.bookmarkTarget,
70
76
  }),
71
77
  commandBarView: (_self, data) => ({
72
78
  items: data.commandBarItems,
79
+ desktopMode: data.desktopMode,
73
80
  leftError: data.leftError,
74
81
  notice: data.notice,
75
82
  rightError: data.rightError,
76
83
  active: data.active,
77
84
  leftMarked: data.leftMarked,
78
85
  rightMarked: data.rightMarked,
86
+ uiSize: data.uiSize,
79
87
  }),
80
88
  confirmView: (_self, data) => ({ confirm: data.confirm }),
81
89
  leftCrumbsView: (_self, data) => ({
@@ -99,13 +107,8 @@ function webViewBehaviors() {
99
107
  loading: data.leftLoading,
100
108
  }),
101
109
  leftPaneClass: (_self, data) => paneClass(data, "left"),
102
- leftSummaryView: (_self, data) => ({
103
- entries: data.leftEntries,
104
- disk: data.leftDisk,
105
- width: data.paneWidth,
106
- }),
107
110
  // The explorer's first column: the parent of the current directory,
108
- // a full pane of its own (header, filter, listing, detail, summary).
111
+ // a full pane of its own (header, filter, listing, detail).
109
112
  parentCrumbsView: (_self, data) => ({
110
113
  segments: data.parentSegments,
111
114
  paneId: data.parentId,
@@ -131,11 +134,6 @@ function webViewBehaviors() {
131
134
  loading: data.parentLoading,
132
135
  }),
133
136
  parentPaneClass: (_self, data) => paneClass(data, "parent"),
134
- parentSummaryView: (_self, data) => ({
135
- entries: data.parentEntries,
136
- disk: data.parentDisk,
137
- width: data.paneWidth,
138
- }),
139
137
  panelView: (_self, data) => ({
140
138
  // Cells pass through (as `in:` slots do in the TUI); `query`/`value`
141
139
  // are written back by the inputs' `inout` two-way bindings.
@@ -145,17 +143,22 @@ function webViewBehaviors() {
145
143
  opCursor: data.panelOpIndex,
146
144
  rows: data.panelRows,
147
145
  argCursor: data.panelRowIndex,
148
- labelWidth: data.panelLabelWidth,
149
146
  focus: data.panelFocus,
150
- command: data.panelCommand,
151
- runnable: data.panelRunnable,
152
- picking: data.panelPicking,
153
- pickerQuery: data.pickerQuery,
154
- pickerPath: data.pickerPath,
147
+ preview: data.panelPreview,
148
+ picking: data.pickerOpen,
149
+ pickerPathText: data.pickerPathText,
150
+ pickerFocus: data.pickerFocus,
151
+ pickerRows: data.pickerRows,
155
152
  query: data.panelQuery,
156
153
  value: data.panelValue,
157
154
  }),
158
155
  helpView: (_self, data) => ({ open: data.helpOpen }),
156
+ overlayView: (_self, data) => ({
157
+ open: data.overlay,
158
+ mode: data.overlay,
159
+ items: data.overlayItems,
160
+ cursor: data.overlayIndex,
161
+ }),
159
162
  previewView: (_self, data) => ({
160
163
  preview: data.preview,
161
164
  show: true,
@@ -191,11 +194,6 @@ function webViewBehaviors() {
191
194
  loading: data.rightLoading,
192
195
  }),
193
196
  rightPaneClass: (_self, data) => paneClass(data, "right"),
194
- rightSummaryView: (_self, data) => ({
195
- entries: data.rightEntries,
196
- disk: data.rightDisk,
197
- width: data.paneWidth,
198
- }),
199
197
  }
200
198
  }
201
199
 
@@ -205,28 +203,46 @@ function createWebApp(session, options = {}) {
205
203
  registerWebComponents()
206
204
  const size = options.size ?? cell({ width: 0, height: 0 })
207
205
  const help = cell(false)
208
- const viewer = cell(false)
206
+ // The fullscreen inspector overlay lives in the session (core), so a
207
+ // command — Enter on a file that cannot be edited — can open it; the shared
208
+ // controller routes its keys (`components/App#createBehaviors`).
209
+ const viewer = session.viewerOpen
209
210
  const paneWidth = derived([size, session.showPreview], ([value, show]) => {
210
211
  if (!value.width) return 0
211
212
  const columns = show && value.width >= INSPECTOR_INLINE_WIDTH ? 3 : 2
212
213
  return Math.max(1, Math.floor(value.width / columns) - 4)
213
214
  })
214
- const crumbs = (pane) => derived([pane.segments, paneWidth], ([segments, width]) => elideCrumbs(segments, width))
215
- const quit = (self) => self.unmount?.()
215
+ // Breadcrumb budgets follow each pane's OWN share of the row (explorer
216
+ // grows the current column, so its mirrors are narrower than an even
217
+ // split), or the crumbs overflow the narrower columns.
218
+ const crumbWidth = (paneId) =>
219
+ derived([size, session.showPreview, session.viewMode, session.currentId], ([value, showPreview, viewMode, currentId]) => {
220
+ const columns = paneColumnWidth(value?.width, paneId, { viewMode, currentId, showPreview })
221
+ return columns > 0 ? Math.max(1, columns - 4) : 0
222
+ })
223
+ const crumbs = (paneId, pane) =>
224
+ derived([pane.segments, crumbWidth(paneId)], ([segments, width]) => elideCrumbs(segments, width))
225
+ const quit = options.onQuit ?? ((self) => self.unmount?.())
216
226
  // DOM inputs have no native `enter` event; the root routes Enter for them.
217
227
  const baseKeys = appBehaviors({ normalizeKey, quit }).keys
218
- const events = appEvents({ quit })
228
+ const events = appEvents()
219
229
  return ui(appHtml)
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
- }))
230
+ .init((self) => {
231
+ // The panel's Quit command reaches the host through this hook.
232
+ session.quit = () => quit(self)
233
+ return {
234
+ ...appInit(session)(),
235
+ desktopMode: Boolean(options.desktopMode),
236
+ uiSize: size,
237
+ helpOpen: help,
238
+ viewerOpen: viewer,
239
+ paneWidth,
240
+ leftSegments: crumbs("left", session.panes.left),
241
+ parentSegments: crumbs("parent", session.panes.parent),
242
+ rightSegments: crumbs("right", session.panes.right),
243
+ previewSegments: crumbs("preview", session.panes.preview),
244
+ }
245
+ })
230
246
  .does({
231
247
  ...createFrameBehaviors({ theme: webTheme }),
232
248
  ...webViewBehaviors(),
@@ -240,8 +256,23 @@ function createWebApp(session, options = {}) {
240
256
  },
241
257
  keys: (self, data, event) => {
242
258
  if (event.isComposing) return
259
+ const busy = unwrap(data.confirm) || unwrap(data.bookmarkOpen) || unwrap(data.panelOpen) || unwrap(data.filtering) || unwrap(data.overlay)
260
+ if (event.key === "F6" && unwrap(data.desktopMode) && !busy && !help.value && !viewer.value) {
261
+ event.preventDefault()
262
+ const target = isToolbarTarget(event.target) ? self.ref.root : self.ref.root.querySelector(".desktop-toolbar button")
263
+ target?.focus({ preventScroll: true })
264
+ return
265
+ }
266
+ // Native controls own their activation and traversal keys, not the
267
+ // pane underneath them. Escape hands focus back to the workspace.
268
+ if (isToolbarTarget(event.target)) {
269
+ if (event.key === "Escape") {
270
+ event.preventDefault()
271
+ self.ref.root.focus({ preventScroll: true })
272
+ }
273
+ return
274
+ }
243
275
  const key = normalizeKey(event)
244
- const busy = unwrap(data.confirm) || unwrap(data.bookmarkOpen) || unwrap(data.panelOpen) || unwrap(data.filtering)
245
276
  if (help.value) {
246
277
  if (["escape", "q", "?"].includes(key.name)) help.set(false)
247
278
  return
@@ -250,27 +281,54 @@ function createWebApp(session, options = {}) {
250
281
  help.set(true)
251
282
  return
252
283
  }
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
- }
284
+ // The viewer overlay and the narrow-`v` shortcut are shared
285
+ // modal routing, handled by `components/App#createBehaviors`.
266
286
  return baseKeys(self, data, event)
267
287
  },
288
+ dialogStop: (_self, _data, event) => event.stopPropagation(),
268
289
  })
269
290
  .sub({
270
291
  ...events,
292
+ "dialog:panel-cancel": (_self, data) => {
293
+ if (!unwrap(data.desktopMode)) return
294
+ data.dispatch(unwrap(data.pickerOpen) ? "picker.cancel" : "panel.cancel")
295
+ },
296
+ "dialog:bookmark-cancel": (_self, data) => {
297
+ if (unwrap(data.desktopMode)) data.dispatch("bookmark.cancel")
298
+ },
299
+ "dialog:overlay-cancel": (_self, data) => {
300
+ if (unwrap(data.desktopMode)) data.dispatch("overlay.cancel")
301
+ },
302
+ "dialog:confirm-cancel": (_self, data) => {
303
+ if (unwrap(data.desktopMode)) data.dispatch("confirm.no")
304
+ },
305
+ "dialog:help-cancel": (_self, data) => {
306
+ if (unwrap(data.desktopMode)) help.set(false)
307
+ },
308
+ "dialog:viewer-cancel": (_self, data) => {
309
+ if (unwrap(data.desktopMode)) data.dispatch("view.toggleViewer")
310
+ },
311
+ "toolbar:open-folder": async () => {
312
+ try {
313
+ await options.onOpenFolder?.()
314
+ } catch (error) {
315
+ await options.onError?.(error)
316
+ }
317
+ },
318
+ "toolbar:back": (_self, data) => data.dispatch("pane.back"),
319
+ "toolbar:forward": (_self, data) => data.dispatch("pane.forward"),
320
+ "toolbar:up": (_self, data) => data.dispatch("pane.up"),
321
+ "toolbar:refresh": (_self, data) => data.dispatch("pane.reload"),
322
+ "toolbar:toggle-mode": (_self, data) => data.dispatch("view.toggleMode"),
323
+ "toolbar:toggle-preview": (_self, data) => data.dispatch("view.togglePreview"),
324
+ "toolbar:commands": (_self, data) => data.dispatch("panel.open"),
325
+ "toolbar:help": () => help.set(true),
271
326
  "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)
327
+ if (event.data === "app.help") {
328
+ help.set(!help.value)
329
+ return
330
+ }
331
+ events["commandbar:run"](self, data, event)
274
332
  },
275
333
  })
276
334
  }
@@ -24,11 +24,16 @@ import confirmHtml from "../components/Confirm.html" with { type: "text" }
24
24
  import crumbHtml from "../components/Crumb.html" with { type: "text" }
25
25
  import entryDetailHtml from "../components/EntryDetail.html" with { type: "text" }
26
26
  import fileListHtml from "../components/FileList.html" with { type: "text" }
27
+ import filePickerHtml from "../components/FilePicker.html" with { type: "text" }
28
+ import filePickerRowHtml from "../components/FilePickerRow.html" with { type: "text" }
27
29
  import filePreviewHtml from "../components/FilePreview.html" with { type: "text" }
28
30
  import fileRowHtml from "../components/FileRow.html" with { type: "text" }
29
31
  import filterBarHtml from "../components/FilterBar.html" with { type: "text" }
30
32
  import helpHtml from "../components/Help.html" with { type: "text" }
33
+ import helpRowHtml from "../components/HelpRow.html" with { type: "text" }
31
34
  import headerHtml from "./header.html" with { type: "text" }
35
+ import overlayListHtml from "../components/OverlayList.html" with { type: "text" }
36
+ import overlayRowHtml from "../components/OverlayRow.html" with { type: "text" }
32
37
  import rowListHtml from "../components/RowList.html" with { type: "text" }
33
38
  import summaryHtml from "../components/Summary.html" with { type: "text" }
34
39
 
@@ -39,6 +44,7 @@ import {
39
44
  bookmarkRows,
40
45
  breadcrumbsBehaviors,
41
46
  commandBarBehaviors,
47
+ commandBarFit,
42
48
  commandBarMarksItem,
43
49
  commandArgRowBehaviors,
44
50
  commandItemBehaviors,
@@ -49,13 +55,22 @@ import {
49
55
  crumbRows,
50
56
  entryDetailBehaviors,
51
57
  fileListBehaviors,
58
+ filePickerBehaviors,
59
+ filePickerRowBehaviors,
60
+ filePickerRows,
52
61
  filePreviewBehaviors,
53
62
  fileRowBehaviors,
54
63
  fileRows,
55
64
  filterBarBehaviors,
56
65
  helpBehaviors,
66
+ helpRowBehaviors,
67
+ helpRows,
68
+ overlayListBehaviors,
69
+ overlayRowBehaviors,
70
+ overlayRows,
57
71
  panelArgRows,
58
72
  panelOpRows,
73
+ panelRunRows,
59
74
  summaryBehaviors,
60
75
  } from "../components/index.js"
61
76
  import { createBehaviors as headerBehaviors } from "./header.js"
@@ -79,16 +94,19 @@ function elide(text, width) {
79
94
  // Binds every shared component to its `.html` presentation.
80
95
  function createComponents() {
81
96
  const commandItem = bind(ui, "CommandItem", commandItemHtml, commandItemBehaviors(webTheme))
82
- // Browsers lack the Nerd Font, so panel entries carry the text tag instead
83
- // of the glyph.
97
+ // Keep command namespace markers textual; the bundled Nerd face is reserved
98
+ // for filesystem names and paths.
84
99
  const commandArgRow = bind(ui, "CommandArgRow", commandArgRowHtml, commandArgRowBehaviors({
85
100
  ...webTheme,
86
101
  icons: false,
87
102
  }))
88
103
  const crumb = bind(ui, "Crumb", crumbHtml, crumbBehaviors(webTheme))
89
104
  const fileRow = bind(ui, "FileRow", fileRowHtml, fileRowBehaviors(webTheme))
105
+ const filePickerRow = bind(ui, "FilePickerRow", filePickerRowHtml, filePickerRowBehaviors(webTheme))
90
106
  const filterBar = bind(ui, "FilterBar", filterBarHtml, filterBarBehaviors(webTheme))
91
107
  const bookmarkRow = bind(ui, "BookmarkRow", bookmarkRowHtml, bookmarkRowBehaviors(webTheme))
108
+ const overlayRow = bind(ui, "OverlayRow", overlayRowHtml, overlayRowBehaviors(webTheme))
109
+ const helpRow = bind(ui, "HelpRow", helpRowHtml, helpRowBehaviors({ theme: webTheme }))
92
110
  return {
93
111
  BookmarkRow: bookmarkRow,
94
112
  CommandItem: commandItem,
@@ -108,15 +126,24 @@ function createComponents() {
108
126
  ...webTheme,
109
127
  listClass: "command-bar-keys",
110
128
  rows: (self, data) => {
129
+ const viewport = unwrap(data.uiSize)
130
+ const viewportWidth = Math.floor(viewport?.width ?? 0)
131
+ // The measured box is exact but still shows the previous width on
132
+ // the frame a resize arrives; prefer the injected viewport then.
133
+ const width = viewportWidth > 0 ? viewportWidth - 1 : columnWidth(self.ref.box)
111
134
  const marks = commandBarMarksItem(data)
112
135
  const items = unwrap(data.items) ?? []
113
- return commandItemRows(commandItem)(self, { ...data, items: marks ? [...items, marks] : items })
136
+ return commandItemRows(commandItem)(self, {
137
+ ...data,
138
+ items: commandBarFit(marks ? [...items, marks] : items, width),
139
+ })
114
140
  },
115
141
  })),
116
142
  CommandPanel: bind(ui, "CommandPanel", commandPanelHtml, commandPanelBehaviors({
117
143
  ...webTheme,
118
144
  opRows: panelOpRows(commandArgRow),
119
145
  argRows: panelArgRows(commandArgRow),
146
+ runRows: panelRunRows(commandArgRow),
120
147
  })),
121
148
  Confirm: bind(ui, "Confirm", confirmHtml, confirmBehaviors(webTheme)),
122
149
  EntryDetail: bind(ui, "EntryDetail", entryDetailHtml, entryDetailBehaviors({ ...webTheme, width: paneWidth, elide })),
@@ -124,6 +151,10 @@ function createComponents() {
124
151
  ...webTheme,
125
152
  rows: fileRows(fileRow),
126
153
  })),
154
+ FilePicker: bind(ui, "FilePicker", filePickerHtml, filePickerBehaviors({
155
+ ...webTheme,
156
+ rows: filePickerRows(filePickerRow),
157
+ })),
127
158
  FilePreview: bind(ui, "FilePreview", filePreviewHtml, filePreviewBehaviors({
128
159
  ...webTheme,
129
160
  // Open/edit/run are TUI-only: the browser says so instead of
@@ -131,8 +162,12 @@ function createComponents() {
131
162
  actions: () => " Open / edit are unavailable in the browser",
132
163
  })),
133
164
  FilterBar: filterBar,
134
- Help: bind(ui, "Help", helpHtml, helpBehaviors({ theme: webTheme })),
165
+ Help: bind(ui, "Help", helpHtml, helpBehaviors({ theme: webTheme, rows: helpRows(helpRow) })),
135
166
  Header: bind(ui, "Header", headerHtml, headerBehaviors(webTheme)),
167
+ OverlayList: bind(ui, "OverlayList", overlayListHtml, overlayListBehaviors({
168
+ ...webTheme,
169
+ rows: overlayRows(overlayRow),
170
+ })),
136
171
  Summary: bind(ui, "Summary", summaryHtml, summaryBehaviors({ ...webTheme, width: paneWidth, elide })),
137
172
  }
138
173
  }
@@ -152,10 +187,12 @@ function registerWebComponents() {
152
187
  ui.format("Confirm", components.Confirm)
153
188
  ui.format("EntryDetail", components.EntryDetail)
154
189
  ui.format("FileList", components.FileList)
190
+ ui.format("FilePicker", components.FilePicker)
155
191
  ui.format("FilePreview", components.FilePreview)
156
192
  ui.format("FilterBar", components.FilterBar)
157
193
  ui.format("Header", components.Header)
158
194
  ui.format("Help", components.Help)
195
+ ui.format("OverlayList", components.OverlayList)
159
196
  ui.format("Summary", components.Summary)
160
197
  return components
161
198
  }