helmd: inbäddad webbklient (vanilla SPA) + Docker-image för Pi5 (LAN-only)
- web/: token-login, sessionslista, ANSI-skärmvy (SSE+poll), promptfält, frågekort, tangentrad, delningsflik (bild/HTML-sandbox/markdown), inställningar - go:embed via webfs.go — servern förblir en enda binär, strikt CSP - browser-verifierad mot riktig agy (fråga -> kort -> svar -> kört) - Dockerfile (alpine + tmux/bash) + helmd-image.yaml -> localhost:5000/helmd - integrationstest utökat till 29 gröna Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
78
helmd/web/ansi.js
Normal file
78
helmd/web/ansi.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// Minimal ANSI(SGR) -> HTML. Stödjer 16/256-färg, truecolor, bold/dim/
|
||||
// invers/understruket. Allt annat (cursorstyrning m.m.) strippas —
|
||||
// helmd skickar capture-pane-text, så bara färgkoder förekommer.
|
||||
|
||||
const BASE = [
|
||||
"#1c1f26", "#e5605c", "#3fca7c", "#e0a63f",
|
||||
"#4f9cf9", "#b877d9", "#3fc4c4", "#cfd6e4",
|
||||
"#5b6472", "#ff7b76", "#5fe09a", "#ffc65c",
|
||||
"#7bc4ff", "#d49df0", "#6ee0e0", "#ffffff",
|
||||
];
|
||||
|
||||
function color256(n) {
|
||||
n = Number(n);
|
||||
if (n < 16) return BASE[n];
|
||||
if (n < 232) {
|
||||
const c = n - 16, v = [0, 95, 135, 175, 215, 255];
|
||||
return `rgb(${v[(c / 36) | 0]},${v[((c / 6) | 0) % 6]},${v[c % 6]})`;
|
||||
}
|
||||
const g = 8 + (n - 232) * 10;
|
||||
return `rgb(${g},${g},${g})`;
|
||||
}
|
||||
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
export function ansiToHtml(text) {
|
||||
let out = "", open = false;
|
||||
const st = { fg: null, bg: null, bold: false, dim: false, ul: false, inv: false };
|
||||
|
||||
const flushSpan = () => { if (open) { out += "</span>"; open = false; } };
|
||||
const openSpan = () => {
|
||||
const css = [];
|
||||
let fg = st.fg, bg = st.bg;
|
||||
if (st.inv) [fg, bg] = [bg || "#cfd6e4", fg || "#05070b"];
|
||||
if (fg) css.push(`color:${fg}`);
|
||||
if (bg) css.push(`background:${bg}`);
|
||||
if (st.bold) css.push("font-weight:700");
|
||||
if (st.dim) css.push("opacity:.6");
|
||||
if (st.ul) css.push("text-decoration:underline");
|
||||
if (css.length) { out += `<span style="${css.join(";")}">`; open = true; }
|
||||
};
|
||||
|
||||
// dela på escape-sekvenser; behåll SGR, släng övriga
|
||||
const parts = text.split(/(\x1b\[[0-9;:]*[A-Za-z])/);
|
||||
for (const p of parts) {
|
||||
if (!p) continue;
|
||||
if (p.startsWith("\x1b[")) {
|
||||
if (!p.endsWith("m")) continue; // icke-SGR: ignorera
|
||||
const codes = p.slice(2, -1).split(/[;:]/).map((x) => (x === "" ? 0 : Number(x)));
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const c = codes[i];
|
||||
if (c === 0) { st.fg = st.bg = null; st.bold = st.dim = st.ul = st.inv = false; }
|
||||
else if (c === 1) st.bold = true;
|
||||
else if (c === 2) st.dim = true;
|
||||
else if (c === 4) st.ul = true;
|
||||
else if (c === 7) st.inv = true;
|
||||
else if (c === 22) { st.bold = st.dim = false; }
|
||||
else if (c === 24) st.ul = false;
|
||||
else if (c === 27) st.inv = false;
|
||||
else if (c >= 30 && c <= 37) st.fg = BASE[c - 30];
|
||||
else if (c === 38 && codes[i + 1] === 5) { st.fg = color256(codes[i + 2]); i += 2; }
|
||||
else if (c === 38 && codes[i + 1] === 2) { st.fg = `rgb(${codes[i + 2]},${codes[i + 3]},${codes[i + 4]})`; i += 4; }
|
||||
else if (c === 39) st.fg = null;
|
||||
else if (c >= 40 && c <= 47) st.bg = BASE[c - 40];
|
||||
else if (c === 48 && codes[i + 1] === 5) { st.bg = color256(codes[i + 2]); i += 2; }
|
||||
else if (c === 48 && codes[i + 1] === 2) { st.bg = `rgb(${codes[i + 2]},${codes[i + 3]},${codes[i + 4]})`; i += 4; }
|
||||
else if (c === 49) st.bg = null;
|
||||
else if (c >= 90 && c <= 97) st.fg = BASE[c - 90 + 8];
|
||||
else if (c >= 100 && c <= 107) st.bg = BASE[c - 100 + 8];
|
||||
}
|
||||
flushSpan();
|
||||
openSpan();
|
||||
} else {
|
||||
out += esc(p.replace(/\x1b./g, ""));
|
||||
}
|
||||
}
|
||||
flushSpan();
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user