web: editable path bar with Tab completion in the file browser
Browse files- web/index.html +2 -1
- web/src/app.js +37 -8
- 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 |
-
<
|
|
|
|
| 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 |
-
|
| 341 |
-
$("fs-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 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;
|