@xsft/panels 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/table.js +37 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Run the command in the folder of the product. Each judge starts in that folder,
|
|
|
31
31
|
|
|
32
32
|
On a terminal, `panels run` shows the score table, and each judge runs in its interactive harness in a pseudo-terminal inside the command. Each judge writes `judges/N/answer.json` and stays open. The table writes the report when each judge has an answer or an error, and the command exits from `report.json` when you quit. Without a terminal, the judges run in the background (`claude -p`, `codex exec`).
|
|
33
33
|
|
|
34
|
-
A tab strip on the last
|
|
34
|
+
A tab strip on the last three lines shows a hairline, a tab per view, and a status line with the progress, the elapsed time, and the keys. The tabs are the scores, then each judge with its number, name, and mark (score, ✗ for an error, · while queued, ! while the harness asks whether to trust the folder). The label of a running judge glimmers. The strip is the navigation area. With the focus on the strip, `←` and `→` move the active tab and switch the view, `1` to `9` jump to a started judge, `0` to the scores, `enter` gives the focus to the judge, and `q` quits and asks `y` while a judge still runs. With the focus on a judge, each key goes to the judge, and `ctrl-q` gives the focus back to the strip. The scores take no input, so the strip keeps the focus there. A harness that has not seen the folder asks whether to trust it. The first judge starts alone; press `1`, answer, and the other judges start.
|
|
35
35
|
|
|
36
36
|
## Run folder
|
|
37
37
|
|
package/lib/table.js
CHANGED
|
@@ -31,9 +31,21 @@ const TABLE_MODES = '\x1b[?1049h\x1b[r\x1b[0m\x1b[4l\x1b[?6l\x1b[?7h\x1b[?1l\x1b
|
|
|
31
31
|
* trust is true while the harness of a running judge asks whether to trust the folder.
|
|
32
32
|
* final is null until the report exists, then { status, path }. confirm is true while q waits for y.
|
|
33
33
|
*/
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
const
|
|
34
|
+
// The glimmer on the text of a running judge: grey text, and a bright highlight three characters wide that
|
|
35
|
+
// sweeps from left to right one character per frame and wraps.
|
|
36
|
+
const GLIMMER = { base: '\x1b[38;5;244m', mid: '\x1b[38;5;250m', high: '\x1b[38;5;255m' };
|
|
37
|
+
const GLIMMER_ORANGE = { base: ORANGE, mid: '\x1b[38;2;255;170;140m', high: '\x1b[38;5;255m' };
|
|
38
|
+
|
|
39
|
+
/** text with the glimmer at frame. colours is { base, mid, high }; prefix (such as bold) goes before each piece. */
|
|
40
|
+
export function glimmer(text, frame, colours = GLIMMER, prefix = '') {
|
|
41
|
+
const centre = frame % text.length;
|
|
42
|
+
return [...text].map((c, i) => {
|
|
43
|
+
const d = Math.min(Math.abs(i - centre), text.length - Math.abs(i - centre));
|
|
44
|
+
return `${prefix}${d === 0 ? colours.high : d === 1 ? colours.mid : colours.base}${c}`;
|
|
45
|
+
}).join('') + RESET;
|
|
46
|
+
}
|
|
47
|
+
// The tab strip takes the last three lines of the terminal: a hairline, the tabs, then the status and the keys.
|
|
48
|
+
const STRIP_ROWS = 3;
|
|
37
49
|
|
|
38
50
|
export function render({ title, rows, final, confirm, width, frame = 0 }) {
|
|
39
51
|
const rule = `${GREY}${'─'.repeat(width)}${RESET}`;
|
|
@@ -42,10 +54,10 @@ export function render({ title, rows, final, confirm, width, frame = 0 }) {
|
|
|
42
54
|
for (const r of rows) {
|
|
43
55
|
const head = `${String(r.rank).padStart(3)} ${r.name.padEnd(pad)}`;
|
|
44
56
|
if (r.state === 'running' && r.trust) lines.push(`${ORANGE}${head}press ${r.rank} and answer the trust prompt${RESET}`);
|
|
45
|
-
else if (r.state === 'running') lines.push(`${
|
|
57
|
+
else if (r.state === 'running') lines.push(`${String(r.rank).padStart(3)} ${glimmer(r.name, frame)}`);
|
|
46
58
|
else if (r.state === 'queued') lines.push(`${head}${GREY}queued${RESET}`);
|
|
47
59
|
else if (r.state === 'error') lines.push(`${head}error`);
|
|
48
|
-
else lines.push(`${head}${r.result.score}
|
|
60
|
+
else lines.push(`${head}${r.result.score}/4`);
|
|
49
61
|
}
|
|
50
62
|
lines.push(rule);
|
|
51
63
|
if (final) {
|
|
@@ -57,19 +69,19 @@ export function render({ title, rows, final, confirm, width, frame = 0 }) {
|
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/**
|
|
60
|
-
* The
|
|
61
|
-
* the
|
|
62
|
-
*
|
|
63
|
-
* while the harness asks whether to trust the folder. The names step
|
|
64
|
-
* the first word, or no name.
|
|
65
|
-
*
|
|
72
|
+
* The three lines of the tab strip: a grey hairline, the tabs, then a grey status line with the progress and
|
|
73
|
+
* the elapsed time at the left and the keys of the focus at the right. The first tab is the scores; each judge
|
|
74
|
+
* tab reads "N name mark". A running judge has no mark and its label glimmers. The mark is the score (bold at
|
|
75
|
+
* 4), ✗ for an error, · while queued, or ! while the harness asks whether to trust the folder. The names step
|
|
76
|
+
* down to fit the width: the whole name, the first word, or no name. focus is 'strip' (the active tab is bold
|
|
77
|
+
* orange, the arrows move it) or 'judge' (the active tab is bold ink, the keys go to the judge).
|
|
66
78
|
*/
|
|
67
79
|
export function renderStrip({ rows, view, final, frame = 0, width, focus = 'strip', elapsed = 0 }) {
|
|
68
80
|
const mark = (r) => {
|
|
69
|
-
if (r.state === 'running') return r.trust ? '!' :
|
|
81
|
+
if (r.state === 'running') return r.trust ? '!' : '';
|
|
70
82
|
if (r.state === 'queued') return '·';
|
|
71
83
|
if (r.state === 'error') return '✗';
|
|
72
|
-
return
|
|
84
|
+
return `${r.result.score}/4`;
|
|
73
85
|
};
|
|
74
86
|
const finished = rows.filter((r) => r.state === 'done' || r.state === 'error').length;
|
|
75
87
|
const clock = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, '0')}`;
|
|
@@ -79,34 +91,28 @@ export function renderStrip({ rows, view, final, frame = 0, width, focus = 'stri
|
|
|
79
91
|
...rows.map((r) => ({ rank: String(r.rank), name: level === 0 ? r.name : level === 1 ? r.name.split(' ')[0] : '', mark: mark(r) })),
|
|
80
92
|
];
|
|
81
93
|
const plain = (t) => [t.rank, t.name, t.mark].filter(Boolean).join(' ');
|
|
82
|
-
const used = (list
|
|
83
|
-
|
|
84
|
-
const choices = [0, 1, 2].flatMap((level) => [`${finished} of ${rows.length} done · ${clock} · ${keys}`, keys].map((right) => ({ list: tabs(level), right })));
|
|
85
|
-
const { list, right } = choices.find((c) => used(c.list, c.right) <= width) ?? choices.at(-1);
|
|
94
|
+
const used = (list) => 1 + list.reduce((n, t) => n + plain(t).length, 0) + 2 * (list.length - 1);
|
|
95
|
+
const list = [0, 1, 2].map(tabs).find((l) => used(l) <= width) ?? tabs(2);
|
|
86
96
|
let line = ' ';
|
|
87
|
-
let col = 1;
|
|
88
|
-
let open = [1, 1];
|
|
89
97
|
list.forEach((t, i) => {
|
|
90
|
-
if (i)
|
|
98
|
+
if (i) line += ' ';
|
|
91
99
|
const active = i === view;
|
|
92
100
|
const base = active ? (focus === 'strip' ? `${ORANGE}${BOLD}` : BOLD) : GREY;
|
|
93
101
|
const text = plain(t);
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
const row = rows[i - 1];
|
|
103
|
+
if (row?.state === 'running' && !row.trust) {
|
|
104
|
+
line += glimmer(text, frame, active && focus === 'strip' ? GLIMMER_ORANGE : GLIMMER, active ? BOLD : '');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
96
107
|
const pieces = [];
|
|
97
108
|
if (t.rank) pieces.push(`${base}${t.rank}${RESET}`);
|
|
98
109
|
if (t.name) pieces.push(`${base}${t.name}${RESET}`);
|
|
99
|
-
if (t.mark) {
|
|
100
|
-
const row = rows[i - 1];
|
|
101
|
-
const style = row.state === 'running' && !row.trust ? ORANGE : row.state === 'done' && row.result.score === 4 ? `${base}${BOLD}` : base;
|
|
102
|
-
pieces.push(`${style}${t.mark}${RESET}`);
|
|
103
|
-
}
|
|
110
|
+
if (t.mark) pieces.push(`${row.state === 'done' && row.result.score === 4 ? `${base}${BOLD}` : base}${t.mark}${RESET}`);
|
|
104
111
|
line += pieces.join(' ');
|
|
105
112
|
});
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return [`${GREY}${rule}${RESET}`, line];
|
|
113
|
+
const progress = `${finished} of ${rows.length} done · ${clock}`;
|
|
114
|
+
const status = ` ${progress}${' '.repeat(Math.max(2, width - progress.length - keys.length - 2))}${keys}`;
|
|
115
|
+
return [`${GREY}${'─'.repeat(width)}${RESET}`, line, `${GREY}${status}${RESET}`];
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|