harness improvements

This commit is contained in:
2026-07-30 21:30:37 -04:00
parent d8f11040bc
commit 89c5ca2f7e

View File

@@ -36,6 +36,8 @@
* --size WxH terminal size (default 100x30)
* --audio enable real audio backend for the new action only
* --no-settle skip the extra render-settle loops
* --styles print the distinct-styles sample block (off by default)
* --verbose restore the original multi-line pretty output
*/
import { testRender } from "@opentui/solid";
@@ -191,6 +193,8 @@ function parseFlags(rest: string[]): {
if (a.startsWith("--")) {
if (a === "--audio") flags.audio = true;
else if (a === "--no-settle") flags["no-settle"] = true;
else if (a === "--styles") flags.styles = true;
else if (a === "--verbose") flags.verbose = true;
else if (a === "--size") {
flags.size = rest[++i];
const m = /(\d+)x(\d+)/.exec(String(flags.size));
@@ -471,9 +475,11 @@ async function main() {
const nav = navRef;
if (nav) {
state.nav = {
activeTab: nav.activeTab?.(),
activeDepth: nav.activeDepth?.(),
inputFocused: nav.inputFocused?.(),
tab: nav.activeTab?.(),
pane: nav.activePane?.(),
mode: nav.mode?.(),
input: nav.inputFocused?.(),
sel: nav.selectedIds?.()?.length ?? 0,
ready: nav.ready,
};
} else {
@@ -487,11 +493,11 @@ async function main() {
state.audio = {
backend: audioControls.backendName ? audioControls.backendName() : null,
playing: audioControls.isPlaying ? audioControls.isPlaying() : null,
position: audioControls.position ? audioControls.position() : null,
duration: audioControls.duration ? audioControls.duration() : null,
volume: audioControls.volume ? audioControls.volume() : null,
error: audioControls.error ? audioControls.error() : null,
currentEpisodeTitle: audioControls.currentEpisode
pos: audioControls.position ? audioControls.position() : null,
dur: audioControls.duration ? audioControls.duration() : null,
vol: audioControls.volume ? audioControls.volume() : null,
err: audioControls.error ? audioControls.error() : null,
ep: audioControls.currentEpisode
? audioControls.currentEpisode()?.title
: null,
};
@@ -505,53 +511,93 @@ async function main() {
const feeds = fs_.feeds ? fs_.feeds() : [];
state.feed = {
count: feeds?.length ?? 0,
selectedFeedId: fs_.selectedFeedId ? fs_.selectedFeedId() : null,
isLoadingFeeds: fs_.isLoadingFeeds ? fs_.isLoadingFeeds() : null,
sel: fs_.selectedFeedId ? fs_.selectedFeedId() : null,
loading: fs_.isLoadingFeeds ? fs_.isLoadingFeeds() : null,
titles: (feeds ?? []).slice(0, 8).map((f: any) => f?.podcast?.title),
};
} catch (e) {
state.feed = "ERR: " + String(e);
}
try {
writeFileSync(STATE_JSON, JSON.stringify(state, null, 2));
writeFileSync(STATE_JSON, JSON.stringify(state));
} catch {}
// ── Output ──────────────────────────────────────────────────────────────
// ── Output ──────────────────────────────────────────────────────────────
// Compact by default: trimmed frame, one-line state per section, no styles
// block, no boilerplate footer. Use --styles / --verbose to opt back in.
const verbose = !!flags.verbose;
const scope = cmd === "state" ? String(positional[0] || "all") : "all";
console.log(
`\n=== FRAME ${spans.cols}x${spans.rows} (cursor ${spans.cursor[0]},${spans.cursor[1]}) | actions=${actions.length} | ${cmd} ===`,
);
console.log(plainFrame);
if (scope === "all") {
// A line is "visually empty" if it's either fully blank OR contains only
// box-drawing chars + whitespace (i.e. empty-pane interior padding like
// "│ │"). Runs of these collapse to a single `…N` marker so an empty
// 24-row pane costs 1 line, not 18.
const BOX_CHARS = "│┌┐└─┤├┬┴┼┐┘┌└┤├┬┴┼┌┐└┘─│┤├┬┴┼";
const isVisuallyEmpty = (l: string): boolean =>
l === "" || [...l].every((ch) => ch === " " || BOX_CHARS.includes(ch));
const frameTrimmed = (() => {
const lines = plainFrame
.replace(/\n+$/, "")
.split("\n")
.map((l) => l.replace(/\s+$/, ""));
while (lines.length && isVisuallyEmpty(lines[lines.length - 1]))
lines.pop();
const out: string[] = [];
let blank = 0;
const flushBlanks = () => {
if (blank >= 3) out.push(`${blank} empty`);
else for (let i = 0; i < blank; i++) out.push("");
blank = 0;
};
for (const l of lines) {
if (isVisuallyEmpty(l)) {
blank++;
} else {
flushBlanks();
out.push(l);
}
}
flushBlanks();
return out.join("\n");
})();
console.log(
`FRAME ${spans.cols}x${spans.rows} cur=${spans.cursor[0]},${spans.cursor[1]} acts=${actions.length} ${cmd}`,
);
console.log(frameTrimmed);
// ── distinct styles: opt-in only (--styles OR --verbose) ──
if (scope === "all" && (flags.styles || verbose)) {
const styles = distinctStyles(spans);
if (styles.length) {
console.log("--- distinct styles (sample) ---");
console.log("-- styles (top 20) --");
for (const s of styles) console.log(` ${s.tag} ×${s.n}${s.sample}`);
}
}
// State subset.
// ── state: one compact line per requested section ──
const want = (k: string) => scope === "all" || scope === k;
if (want("nav"))
console.log("--- nav ---\n" + JSON.stringify(state.nav, null, 2));
if (want("audio"))
console.log("--- audio ---\n" + JSON.stringify(state.audio, null, 2));
if (want("feed"))
console.log("--- feed ---\n" + JSON.stringify(state.feed, null, 2));
if (want("app"))
console.log("--- app ---\n(src/stores/app.ts not dumped in v1)");
const compact = (obj: unknown): string =>
verbose ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
if (want("nav")) console.log("nav " + compact(state.nav));
if (want("audio")) console.log("audio " + compact(state.audio));
if (want("feed")) console.log("feed " + compact(state.feed));
if (want("app")) console.log("app (not dumped in v1)");
// ── issues: terse ──
if (issues.length) {
console.log("--- issues (" + issues.length + ") ---");
for (const i of issues.slice(0, 40)) console.log(" ! " + i);
console.log(`issues:${issues.length}`);
for (const i of issues.slice(0, 20)) console.log(" ! " + i);
} else {
console.log("--- issues: none ---");
console.log("issues:none");
}
// Footer is identical every run — only print on init or --verbose.
if (cmd === "init" || verbose) {
console.log(
`\n(spans ${FRAME_JSON} | frame ${FRAME_TXT} | state ${STATE_JSON})`,
`(spans ${FRAME_JSON} | frame ${FRAME_TXT} | state ${STATE_JSON})`,
);
}
// Tear down child processes (audio backend) before exit to avoid orphans.
try {