╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#b5b304a47251337402c2a615e991e34386d7a63a",│ │ 7 - .hash = "xit-0.0.0-Ub3nDDKUTwGdFIicDICV0hIreAd_AGopMZc8YG2y_znN", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#8b6cd585cc14c9c5ecbddd20d89c27d79850eee8",│ │ 7 + .hash = "xit-0.0.0-Ub3nDK-dTwFAPg8KGsPIJd89khqpgzm8fBCMOZZiji_4", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │ 8 │ │ 9 const MIN_COLS = 30; │ │10 │ │11 -// measured once after the font is loaded. recomputed lazily if it ever │ │12 -// reads back as zero (e.g. measured before the font was actually ready). │ │11 +// measured once after the font is loaded. recomputed lazily if either reads│ │12 +// back as zero (e.g. measured before the font was actually ready). │ │13 let cellWidth = null; │ │14 +let cellHeight = null; │ │15 │ │15 -function measureCellWidth() { │ │16 +function measureCell() { │ │17 const probe = document.createElement("span"); │ │18 probe.style.position = "absolute"; │ │19 probe.style.visibility = "hidden"; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │20 probe.style.whiteSpace = "pre"; │ │21 probe.textContent = "X".repeat(100); │ │22 grid.appendChild(probe); │ │22 - const w = probe.getBoundingClientRect().width / 100;│ │23 + const rect = probe.getBoundingClientRect(); │ │24 probe.remove(); │ │24 - return w; │ │25 + cellWidth = rect.width / 100; │ │26 + cellHeight = rect.height; │ │27 } │ │28 │ │29 function maxCols() { │ ╰────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │28 - if (!cellWidth) cellWidth = measureCellWidth(); │ │30 + if (!cellWidth) measureCell(); │ │31 return Math.max(MIN_COLS, Math.floor(document.body.clientWidth / cellWidth)); │ │32 +} │ │33 + │ │34 +// minimum row count for the wasm build — the TUI fills at least the │ │35 +// viewport height, longer content extends past it and the browser scrolls. │ │36 +function minRows() { │ │37 + if (!cellHeight) measureCell(); │ │38 + return Math.max(1, Math.floor(document.documentElement.clientHeight / cellHeight));│ │39 } │ │40 │ │41 function readWasmString(ptr, len) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 } │ │72 } │ │73 grid.innerHTML = html; │ │65 - if (savedFocusId !== null) { │ │66 - const restored = grid.querySelector(`[data-focus-id="${savedFocusId}"]`); │ │67 - if (restored && typeof restored.focus === "function") { │ │68 - restored.focus(); │ │69 - if (savedStart !== null && restored.setSelectionRange) { │ │70 - try { restored.setSelectionRange(savedStart, savedEnd); } catch (_) {} │ │71 - } │ │74 + // prefer whatever wasm marked as focused — that way arrow-key │ │75 + // navigation in the TUI also moves DOM focus to the matching │ │76 + // <input>. fall back to the previously focused element so │ │77 + // typing doesn't lose its caret position on re-renders that │ │78 + // don't change focus. │ │79 + const focused = grid.querySelector('[data-focused="true"]'); │ │80 + const target = focused || (savedFocusId !== null │ │81 + ? grid.querySelector(`[data-focus-id="${savedFocusId}"]`) │ │82 + : null); │ │83 + if (target && typeof target.focus === "function") { │ │84 + target.focus(); │ │85 + // only restore selection when we land on the same element │ │86 + // we captured from; otherwise the offsets are meaningless. │ │87 + if (savedStart !== null && target.dataset && target.dataset.focusId === savedFocusId && target.setSelectionRange) {│ │88 + try { target.setSelectionRange(savedStart, savedEnd); } catch (_) {} │ │89 } │ │90 } │ │91 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │ 97 const ptr = wasmInstance.exports._alloc(bytes.length); │ │ 98 new Uint8Array(wasmInstance.exports.memory.buffer, ptr, bytes.length).set(bytes);│ │ 99 wasmInstance.exports._setTextInputValue(focusId, ptr, bytes.length); │ │ 83 - wasmInstance.exports._tick(maxCols()); │ │100 + wasmInstance.exports._tick(minRows(), maxCols()); │ │101 } │ │102 │ │103 // POST the current input values to the server form route. on response (any │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │128 const jsonBytes = Uint8Array.from(atob(pageJsonBase64), (c) => c.charCodeAt(0)); │ │129 const ptr = wasmInstance.exports._alloc(jsonBytes.length); │ │130 new Uint8Array(wasmInstance.exports.memory.buffer, ptr, jsonBytes.length).set(jsonBytes);│ │114 - wasmInstance.exports._start(ptr, jsonBytes.length, maxCols()); │ │131 + wasmInstance.exports._start(ptr, jsonBytes.length, minRows(), maxCols()); │ │132 │ │133 document.addEventListener("keydown", (event) => { │ │134 // when an overlay input owns focus, the browser handles typing & │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │157 event.preventDefault(); │ │158 } │ │159 wasmInstance.exports._onKeyDown(event.keyCode); │ │143 - wasmInstance.exports._tick(maxCols()); │ │160 + wasmInstance.exports._tick(minRows(), maxCols()); │ │161 }); │ │162 │ │163 // event delegation: each render rebuilds the input elements (innerHTML│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │173 const t = event.target; │ │174 if (!t || !t.dataset || !t.dataset.focusId) return; │ │175 wasmInstance.exports._onMouseClick(Number(t.dataset.focusId));│ │159 - wasmInstance.exports._tick(maxCols()); │ │176 + wasmInstance.exports._tick(minRows(), maxCols()); │ │177 }); │ │178 │ │179 grid.addEventListener("click", (event) => { │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │190 // (e.g. a button click runs its submit handler). widgets that don't│ │191 // care about Enter just ignore it. │ │192 wasmInstance.exports._onKeyDown(13); │ │176 - wasmInstance.exports._tick(maxCols()); │ │193 + wasmInstance.exports._tick(minRows(), maxCols()); │ │194 }); │ │195 │ │196 let resizeTimer = null; │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯