fix(mobile-web): collapse carriage-return redraws in the console terminal

readline repaints its prompt in place with a lone \r (…└─# \r└─# cmd); the
browser turned that \r into a newline, stacking the pre-redraw prompt as a
phantom extra line (the mobile-only "extra --#"). Add a collapseCR() mini-vt
pass that resolves \r as a column-0 overwrite (ANSI escapes treated as
zero-width so colours survive; honours \x1b[K erase-to-EOL) before ansiToHtml,
so the real prompt stays and only the duplicate is dropped. TUI was already
correct via its real vt emulator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-07-16 11:07:17 -07:00
parent 012b7c15e5
commit 8ba04316c1
+35 -1
View File
@@ -193,6 +193,40 @@ function ansiToHtml(raw) {
return out;
}
/* ── carriage-return overwrite (mini vt) ──
A real terminal treats a lone \\r as "cursor to column 0" so readline's
in-place prompt redraw (…└─# \\r└─# cmd) overwrites the old line. The DOM
instead turns \\r into \\n, stacking the pre-redraw prompt as a phantom
extra line. Resolve overwrites per \\n-delimited line, treating ANSI escapes
as zero-width (attached to the next column) so colours survive; honour
erase-to-EOL (\\x1b[K) so a shrinking redraw leaves no tail chars. */
function collapseCR(raw) {
const escRe = /^(?:\\x1b\\[[0-9;?]*[ -\\/]*[@-~]|\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)|\\x1b[@-Z\\\\-_])/;
const elRe = /^\\x1b\\[[02]?K$/;
const lines = [];
let cells = [], col = 0, pending = "";
function flush() {
let s = "";
for (let k = 0; k < cells.length; k++) s += cells[k] ? (cells[k].pre + cells[k].ch) : "";
lines.push(s + pending); pending = ""; cells = []; col = 0;
}
for (let i = 0; i < raw.length; ) {
const m = escRe.exec(raw.slice(i));
if (m) {
const seq = m[0];
if (elRe.test(seq)) cells.length = col; // erase to end of line
else pending += seq; // zero-width; ride the next char
i += seq.length; continue;
}
const ch = raw[i++];
if (ch === "\\n") flush();
else if (ch === "\\r") col = 0; // overwrite from column 0
else { cells[col] = {pre: pending, ch: ch}; pending = ""; col++; }
}
flush();
return lines.join("\\n");
}
/* ── terminal poll ── */
async function pollScreen() {
for (;;) {
@@ -204,7 +238,7 @@ async function pollScreen() {
termSeq = d.seq;
const t = $("term");
const bottom = t.scrollHeight - t.scrollTop - t.clientHeight < 40;
if (d.text && d.text.length) t.innerHTML = ansiToHtml(d.text);
if (d.text && d.text.length) t.innerHTML = ansiToHtml(collapseCR(d.text));
if (bottom) t.scrollTop = t.scrollHeight;
}
}