SmaugC137 commited on
Commit
d6ee634
·
1 Parent(s): 031f45c

web: editable path bar with Tab completion in the file browser

Browse files
Files changed (3) hide show
  1. web/index.html +2 -1
  2. web/src/app.js +37 -8
  3. web/style.css +1 -1
web/index.html CHANGED
@@ -168,7 +168,8 @@
168
  <button id="fs-close" class="icon-btn">✕</button></div>
169
  <div class="fs-body">
170
  <div class="fs-browser">
171
- <div id="fs-path" class="fs-path"></div>
 
172
  <div id="fs-list" class="fs-list"></div>
173
  <div id="fs-error" class="fs-error"></div>
174
  </div>
 
168
  <button id="fs-close" class="icon-btn">✕</button></div>
169
  <div class="fs-body">
170
  <div class="fs-browser">
171
+ <input id="fs-input" class="fs-input" type="text" spellcheck="false"
172
+ placeholder="/path/to/dir (Enter: go/open · Tab: complete)" />
173
  <div id="fs-list" class="fs-list"></div>
174
  <div id="fs-error" class="fs-error"></div>
175
  </div>
web/src/app.js CHANGED
@@ -242,6 +242,14 @@ function wireControls() {
242
  $("open-file").onclick = openFsBrowser;
243
  $("fs-close").onclick = () => { $("fs-modal").hidden = true; };
244
  $("fs-modal").addEventListener("click", (e) => { if (e.target.id === "fs-modal") $("fs-modal").hidden = true; });
 
 
 
 
 
 
 
 
245
 
246
  $("frame").oninput = (e) => { $("frame-label").textContent = `frame ${+e.target.value + 1} / ${session.n_frames}`; };
247
  $("frame").onchange = (e) => run(api.cmd("/api/frame", { index: +e.target.value }));
@@ -334,17 +342,38 @@ function renderOpenViews() {
334
  box.appendChild(row);
335
  }
336
  }
 
 
 
 
 
 
 
 
337
  async function fsNavigate(path) {
338
  try {
339
  const d = await api.fsList(path);
340
- fsPath = d.path;
341
- $("fs-path").textContent = d.path;
342
- const open = manager.openFilePaths(); // files already shown in views
343
- const list = $("fs-list");
344
- list.replaceChildren();
345
- if (d.parent) list.appendChild(fsEntry({ name: "..", path: d.parent, is_dir: true, openable: true }, open));
346
- for (const e of d.entries) list.appendChild(fsEntry(e, open));
347
- } catch (e) { $("fs-error").textContent = "⚠ " + e.message; }
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  }
349
  function fsEntry(e, open) {
350
  const loaded = !e.is_dir && open.has(e.path);
 
242
  $("open-file").onclick = openFsBrowser;
243
  $("fs-close").onclick = () => { $("fs-modal").hidden = true; };
244
  $("fs-modal").addEventListener("click", (e) => { if (e.target.id === "fs-modal") $("fs-modal").hidden = true; });
245
+ $("fs-input").onkeydown = async (e) => {
246
+ if (e.key === "Tab") { e.preventDefault(); await fsComplete(); }
247
+ else if (e.key === "Enter") {
248
+ e.preventDefault();
249
+ const v = $("fs-input").value.trim();
250
+ if (v) try { await fsNavigate(v); } catch { openFile(v); } // dir → list, else open the file
251
+ }
252
+ };
253
 
254
  $("frame").oninput = (e) => { $("frame-label").textContent = `frame ${+e.target.value + 1} / ${session.n_frames}`; };
255
  $("frame").onchange = (e) => run(api.cmd("/api/frame", { index: +e.target.value }));
 
342
  box.appendChild(row);
343
  }
344
  }
345
+ function renderListing(d) {
346
+ fsPath = d.path;
347
+ const open = manager.openFilePaths(); // files already shown in views
348
+ const list = $("fs-list");
349
+ list.replaceChildren();
350
+ if (d.parent) list.appendChild(fsEntry({ name: "..", path: d.parent, is_dir: true, openable: true }, open));
351
+ for (const e of d.entries) list.appendChild(fsEntry(e, open));
352
+ }
353
  async function fsNavigate(path) {
354
  try {
355
  const d = await api.fsList(path);
356
+ renderListing(d);
357
+ $("fs-input").value = d.path.replace(/\/?$/, "/"); // show current dir, ready to type a child
358
+ $("fs-error").textContent = "";
359
+ } catch (e) { $("fs-error").textContent = "⚠ " + e.message; throw e; }
360
+ }
361
+ // Tab-complete the typed path against the directory it points into.
362
+ async function fsComplete() {
363
+ const v = $("fs-input").value;
364
+ const slash = v.lastIndexOf("/");
365
+ const dir = slash <= 0 ? "/" : v.slice(0, slash);
366
+ const partial = v.slice(slash + 1);
367
+ let d;
368
+ try { d = await api.fsList(dir); } catch { return; }
369
+ renderListing(d);
370
+ const matches = d.entries.filter((e) => e.name.startsWith(partial));
371
+ if (!matches.length) return;
372
+ let lcp = matches[0].name; // longest common prefix
373
+ for (const m of matches) while (!m.name.startsWith(lcp)) lcp = lcp.slice(0, -1);
374
+ let done = (dir === "/" ? "" : dir) + "/" + lcp;
375
+ if (matches.length === 1 && matches[0].is_dir) done += "/";
376
+ $("fs-input").value = done;
377
  }
378
  function fsEntry(e, open) {
379
  const loaded = !e.is_dir && open.has(e.path);
web/style.css CHANGED
@@ -244,7 +244,7 @@ input[type="checkbox"] { cursor: pointer; }
244
  .fs-open-entry { display: flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 11px; }
245
  .fs-open-tag { flex: none; color: var(--accent); font-weight: 700; font-size: 9.5px; }
246
  .fs-open-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
247
- .fs-path { font-family: var(--mono); font-size: 11px; color: var(--muted); margin: 4px 0 8px; word-break: break-all; }
248
  .fs-list { max-height: 52vh; overflow-y: auto; border: 1px solid var(--line-strong); }
249
  .fs-entry {
250
  padding: 6px 10px; cursor: pointer; font-family: var(--mono); font-size: 12px;
 
244
  .fs-open-entry { display: flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 11px; }
245
  .fs-open-tag { flex: none; color: var(--accent); font-weight: 700; font-size: 9.5px; }
246
  .fs-open-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
247
+ .fs-input { width: 100%; font-family: var(--mono); font-size: 11px; margin-bottom: 8px; }
248
  .fs-list { max-height: 52vh; overflow-y: auto; border: 1px solid var(--line-strong); }
249
  .fs-entry {
250
  padding: 6px 10px; cursor: pointer; font-family: var(--mono); font-size: 12px;