atris 3.74.8 → 3.74.9
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/commands/gm.js +41 -14
- package/commands/member.js +41 -7
- package/lib/gm-bet.js +62 -27
- package/lib/gm-phone-page.js +64 -21
- package/lib/text-table.js +138 -12
- package/lib/zen-apply.js +1 -1
- package/package.json +1 -1
- package/scripts/det/ytnotes +9 -3
package/commands/gm.js
CHANGED
|
@@ -797,19 +797,20 @@ function tail(text, used, width) {
|
|
|
797
797
|
return clip(text, Math.max(10, width - used));
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
-
// Last guard on width: a long line breaks after a
|
|
801
|
-
// line; anything still too long ends in "...".
|
|
800
|
+
// Last guard on width, in terminal columns: a long line breaks after a
|
|
801
|
+
// comma onto an indented line; anything still too long ends in "...".
|
|
802
802
|
function fitLines(lines, width) {
|
|
803
|
+
const { displayWidth, sliceColumns } = require('../lib/text-table');
|
|
803
804
|
const out = [];
|
|
804
805
|
for (const line of lines) {
|
|
805
806
|
let rest = line;
|
|
806
|
-
while (rest
|
|
807
|
-
const cut = rest.lastIndexOf(', ', width - 1);
|
|
807
|
+
while (displayWidth(rest) > width) {
|
|
808
|
+
const cut = rest.lastIndexOf(', ', sliceColumns(rest, width).length - 1);
|
|
808
809
|
if (cut <= 0) break;
|
|
809
810
|
out.push(rest.slice(0, cut + 1));
|
|
810
811
|
rest = ` ${rest.slice(cut + 2)}`;
|
|
811
812
|
}
|
|
812
|
-
out.push(rest
|
|
813
|
+
out.push(displayWidth(rest) > width ? `${sliceColumns(rest, width - 3)}...` : rest);
|
|
813
814
|
}
|
|
814
815
|
return out;
|
|
815
816
|
}
|
|
@@ -830,13 +831,30 @@ function boardWorkspaceRoot(args = []) {
|
|
|
830
831
|
return taskDb.workspaceRoot(path.resolve(workspaceArg));
|
|
831
832
|
}
|
|
832
833
|
|
|
834
|
+
// The team page's cells (role, ovr, engine) for each roster member, read
|
|
835
|
+
// only when the board draws its box in a terminal; null otherwise.
|
|
836
|
+
function boardCells(board, workspaceRoot, ratings) {
|
|
837
|
+
if (!process.stdout.isTTY || !workspaceRoot) return null;
|
|
838
|
+
return require('../lib/gm-bet').teamCells(workspaceRoot, board.roster.map(r => r.name), { ratings });
|
|
839
|
+
}
|
|
840
|
+
|
|
833
841
|
function loadBoard(workspaceRoot, options = {}) {
|
|
842
|
+
return loadSeasonBoard(workspaceRoot, options).board;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// The board and every member's rating (gm-season memberRatings, the same
|
|
846
|
+
// numbers the orb's team page reads) from one read of the season.
|
|
847
|
+
function loadSeasonBoard(workspaceRoot, options = {}) {
|
|
834
848
|
const season = require('../lib/gm-season');
|
|
835
|
-
|
|
849
|
+
const played = season.loadSeason(workspaceRoot, { lastSeen: options.lastSeen });
|
|
850
|
+
return { board: season.seasonBoard(played, options), ratings: season.memberRatings(played.outcomes || []) };
|
|
836
851
|
}
|
|
837
852
|
|
|
838
853
|
// One screen, about 40 rows at most. Pure: board in, lines out.
|
|
839
|
-
|
|
854
|
+
// In a terminal, given the team page's cells, the roster is the orb's team
|
|
855
|
+
// table as a divided box (member, role, ovr, engine, status); piped, or when
|
|
856
|
+
// the box cannot fit the width, it keeps the plain columns.
|
|
857
|
+
function renderBoardLines(board, { interactive = false, width = termWidth(), bordered = Boolean(process.stdout.isTTY), cells = null } = {}) {
|
|
840
858
|
const { clockLabel } = require('../lib/gm-season');
|
|
841
859
|
const lines = [];
|
|
842
860
|
const nameW = nameWidth([
|
|
@@ -856,7 +874,12 @@ function renderBoardLines(board, { interactive = false, width = termWidth() } =
|
|
|
856
874
|
lines.push('');
|
|
857
875
|
|
|
858
876
|
lines.push('roster, last 30 days');
|
|
859
|
-
|
|
877
|
+
const boxed = bordered && cells && board.roster.length
|
|
878
|
+
? require('../lib/gm-bet').teamBox(board.roster.map(r => ({ member: r.name, status: r.status })), cells, { width: width - 2 })
|
|
879
|
+
: null;
|
|
880
|
+
if (boxed) {
|
|
881
|
+
lines.push(...boxed.split('\n').map(line => ` ${line}`));
|
|
882
|
+
} else if (board.roster.length) {
|
|
860
883
|
lines.push(` ${pad('name', nameW)}${pad('ovr', 8)}${pad('form', 7)}${pad('record', 18)}status`);
|
|
861
884
|
for (const row of board.roster) {
|
|
862
885
|
const record = `landed ${row.landed} of ${row.jobs}`;
|
|
@@ -913,9 +936,10 @@ function renderBoardLines(board, { interactive = false, width = termWidth() } =
|
|
|
913
936
|
}
|
|
914
937
|
lines.push('');
|
|
915
938
|
|
|
916
|
-
lines.push(
|
|
939
|
+
lines.push(`key: ovr 60 to 99 = share of finished jobs landed, ${boxed ? 'new' : 'rookie'} under 3 finished,`);
|
|
917
940
|
lines.push(' stricter than the website rating on purpose (drops and abandons count)');
|
|
918
|
-
lines.push('
|
|
941
|
+
if (boxed) lines.push(' ·· = nothing to show yet: no finished job, no role, or no pinned model');
|
|
942
|
+
else lines.push(' form: last 5, newest right: k landed, d dropped, x sent back, a abandoned');
|
|
919
943
|
lines.push(' hours: claim to result. on a job: moved in the last 6h. review: pending');
|
|
920
944
|
if (interactive) for (const line of PLAY_KEYS) lines.push(line);
|
|
921
945
|
else lines.push(' run atris gm in a terminal to play');
|
|
@@ -990,9 +1014,10 @@ function printLeague() {
|
|
|
990
1014
|
}
|
|
991
1015
|
|
|
992
1016
|
function printBoard(args = []) {
|
|
993
|
-
const
|
|
1017
|
+
const root = boardWorkspaceRoot(args);
|
|
1018
|
+
const { board, ratings } = loadSeasonBoard(root);
|
|
994
1019
|
if (board.error) return failRead(board.error);
|
|
995
|
-
console.log(renderBoardLines(board).join('\n'));
|
|
1020
|
+
console.log(renderBoardLines(board, { cells: boardCells(board, root, ratings) }).join('\n'));
|
|
996
1021
|
return undefined;
|
|
997
1022
|
}
|
|
998
1023
|
|
|
@@ -1107,11 +1132,11 @@ async function gmPlay(args = []) {
|
|
|
1107
1132
|
leagueRowsSeen = [...board.rows, ...(board.hidden_clubs || [])];
|
|
1108
1133
|
write(renderLeagueLines(board, { interactive: true }).join('\n'));
|
|
1109
1134
|
} else {
|
|
1110
|
-
const board =
|
|
1135
|
+
const { board, ratings } = loadSeasonBoard(workspaceRoot, { seen, lastSeen });
|
|
1111
1136
|
lastBoard = board;
|
|
1112
1137
|
seen = new Set(board.last_plays.map(play => play.id));
|
|
1113
1138
|
stuck = board.stuck || [];
|
|
1114
|
-
write(renderBoardLines(board, { interactive: true }).join('\n'));
|
|
1139
|
+
write(renderBoardLines(board, { interactive: true, cells: boardCells(board, workspaceRoot, ratings) }).join('\n'));
|
|
1115
1140
|
season.writeLastSeen(workspaceRoot, board.now);
|
|
1116
1141
|
}
|
|
1117
1142
|
} catch (err) {
|
|
@@ -1986,9 +2011,11 @@ async function gmCommand(...args) {
|
|
|
1986
2011
|
}
|
|
1987
2012
|
|
|
1988
2013
|
module.exports = {
|
|
2014
|
+
boardCells,
|
|
1989
2015
|
displayCommand,
|
|
1990
2016
|
ensureStarterMission,
|
|
1991
2017
|
gmCommand,
|
|
2018
|
+
loadSeasonBoard,
|
|
1992
2019
|
pickClub,
|
|
1993
2020
|
playAction,
|
|
1994
2021
|
renderBoardLines,
|
package/commands/member.js
CHANGED
|
@@ -11664,6 +11664,44 @@ async function memberStandupWalk(room, args = []) {
|
|
|
11664
11664
|
return payload;
|
|
11665
11665
|
}
|
|
11666
11666
|
|
|
11667
|
+
// The team page's cells (role, ovr, engine) per standup member, read only
|
|
11668
|
+
// when the standup can draw its table (a terminal); --json never carries
|
|
11669
|
+
// them. ovr comes from gm-season memberRatings, as on the orb's team page.
|
|
11670
|
+
function standupTableData(standup, cwd = process.cwd()) {
|
|
11671
|
+
if (!process.stdout.isTTY) return { bordered: false };
|
|
11672
|
+
const season = require('../lib/gm-season');
|
|
11673
|
+
const root = require('../lib/task-db').workspaceRoot(path.resolve(cwd));
|
|
11674
|
+
let ratings = new Map();
|
|
11675
|
+
try {
|
|
11676
|
+
const played = season.loadSeason(root, { lastSeen: null });
|
|
11677
|
+
if (played && !played.error) ratings = season.memberRatings(played.outcomes || []);
|
|
11678
|
+
} catch { ratings = new Map(); }
|
|
11679
|
+
const cells = require('../lib/gm-bet').teamCells(root, standup.map((s) => s.member), { ratings });
|
|
11680
|
+
return { bordered: true, cells };
|
|
11681
|
+
}
|
|
11682
|
+
|
|
11683
|
+
// The standup: in a terminal the orb's team table as a divided box
|
|
11684
|
+
// (member, role, ovr, engine, status) with each member's found and push
|
|
11685
|
+
// lines under it; piped, or when the box cannot fit, the boxes it always had.
|
|
11686
|
+
function memberStandupLines(standup, { bordered = false, width = process.stdout.columns, cells = null } = {}) {
|
|
11687
|
+
if (bordered && cells && standup.length) {
|
|
11688
|
+
const table = require('../lib/gm-bet').teamBox(standup.map((s) => ({ member: s.member, status: s.status })), cells, { width: Number(width) - 2 });
|
|
11689
|
+
if (table) {
|
|
11690
|
+
return [
|
|
11691
|
+
...table.split('\n').map((line) => ` ${line}`),
|
|
11692
|
+
...standup.flatMap((s) => [` ${s.member}`, ` found: ${s.latest_update}`, ` push: ${s.next_push}`]),
|
|
11693
|
+
];
|
|
11694
|
+
}
|
|
11695
|
+
}
|
|
11696
|
+
return standup.flatMap((s, idx) => [
|
|
11697
|
+
`+-----------------------------------------------------------------------+`,
|
|
11698
|
+
`| ${idx + 1}. ${s.member.toUpperCase()} (${s.role}) · ${s.status} · engine:${s.engine}`,
|
|
11699
|
+
`| Found: ${s.latest_update}`,
|
|
11700
|
+
`| Push: ${s.next_push}`,
|
|
11701
|
+
`+-----------------------------------------------------------------------+`,
|
|
11702
|
+
]);
|
|
11703
|
+
}
|
|
11704
|
+
|
|
11667
11705
|
async function memberRoom(...args) {
|
|
11668
11706
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
11669
11707
|
console.log('Usage: atris member [day|standup|room] [--walk] [--limit N] [--decisions "y|idea|n"] [--json]');
|
|
@@ -11680,13 +11718,7 @@ async function memberRoom(...args) {
|
|
|
11680
11718
|
? `Your team runs on ${teamEngines.length > 1 ? `${teamEngines.slice(0, -1).join(', ')} and ${teamEngines[teamEngines.length - 1]}` : teamEngines[0]}.`
|
|
11681
11719
|
: 'Your team picks an engine for each job.';
|
|
11682
11720
|
|
|
11683
|
-
const standupRows = room.standup
|
|
11684
|
-
`+-----------------------------------------------------------------------+`,
|
|
11685
|
-
`| ${idx + 1}. ${s.member.toUpperCase()} (${s.role}) · ${s.status} · engine:${s.engine}`,
|
|
11686
|
-
`| Found: ${s.latest_update}`,
|
|
11687
|
-
`| Push: ${s.next_push}`,
|
|
11688
|
-
`+-----------------------------------------------------------------------+`,
|
|
11689
|
-
]);
|
|
11721
|
+
const standupRows = memberStandupLines(room.standup, standupTableData(room.standup));
|
|
11690
11722
|
|
|
11691
11723
|
const handoffRows = room.handoffs.length
|
|
11692
11724
|
? room.handoffs.map((h) => ` - ${h.from} -> ${h.to}: ${h.summary} (run: ${h.next_command})`)
|
|
@@ -12019,6 +12051,8 @@ async function memberCommand(subcommand, ...args) {
|
|
|
12019
12051
|
|
|
12020
12052
|
module.exports = {
|
|
12021
12053
|
logDateForScan,
|
|
12054
|
+
memberStandupLines,
|
|
12055
|
+
standupTableData,
|
|
12022
12056
|
memberRunDefaultWallSeconds,
|
|
12023
12057
|
memberRunWantsReferee,
|
|
12024
12058
|
proposalPromptForGoal,
|
package/lib/gm-bet.js
CHANGED
|
@@ -849,6 +849,65 @@ function statusSentence(row) {
|
|
|
849
849
|
return `${row.status}: ${on}, ${row.time}`;
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
+
// The ovr cell from one member's entry in gm-season memberRatings: the
|
|
853
|
+
// rating, "new" with 1 or 2 finished, two dots with none finished.
|
|
854
|
+
function ovrCell(rated) {
|
|
855
|
+
if (!rated || !rated.finished) return NO_RATING;
|
|
856
|
+
return rated.ovr == null ? 'new' : String(rated.ovr);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// The engine pick the runner makes for a member, with the roster read once.
|
|
860
|
+
function teamEngineResolver(workspaceRoot) {
|
|
861
|
+
const reg = require('./engine-registry');
|
|
862
|
+
const { resolveEngineForMember } = require('./member-engine');
|
|
863
|
+
let rosterState = null;
|
|
864
|
+
try { rosterState = reg.readRosterState(workspaceRoot); } catch { rosterState = null; }
|
|
865
|
+
return name => resolveEngineForMember(name, workspaceRoot, { quiet: true, ...(rosterState ? { rosterState } : {}) });
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// role, ovr, engine, and parked for each name: the team page's cells, and
|
|
869
|
+
// the same cells the board and the standup draw in a terminal, so every
|
|
870
|
+
// team view says the same thing. ratings is gm-season memberRatings over one
|
|
871
|
+
// loaded season; resolve gives a member's engine pick. A name with no card
|
|
872
|
+
// and no roster line is not on the team, so no engine shows for it.
|
|
873
|
+
function teamCells(workspaceRoot, names = [], { ratings = new Map(), resolve = null } = {}) {
|
|
874
|
+
const { memberTypeOf } = require('./member-type');
|
|
875
|
+
const pick = resolve || teamEngineResolver(workspaceRoot);
|
|
876
|
+
const out = new Map();
|
|
877
|
+
for (const name of names) {
|
|
878
|
+
const key = String(name || '');
|
|
879
|
+
if (!key || /[\\/]/.test(key) || key === '.' || key === '..') {
|
|
880
|
+
out.set(key, { role: NO_RATING, ovr: ovrCell(ratings.get(key)), engine: NO_RATING, parked: false });
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const card = memberCardFacts(workspaceRoot, key);
|
|
884
|
+
const kind = memberTypeOf({ type: card.type });
|
|
885
|
+
let engine = kind.type === 'human' ? 'person' : NO_RATING;
|
|
886
|
+
if (kind.runnable) {
|
|
887
|
+
let picked = null;
|
|
888
|
+
try { picked = pick(key); } catch { picked = null; }
|
|
889
|
+
engine = picked && picked.found === false ? NO_RATING : engineWords(picked);
|
|
890
|
+
}
|
|
891
|
+
out.set(key, { role: memberRoleWords(key, card), ovr: ovrCell(ratings.get(key)), engine, parked: card.parked });
|
|
892
|
+
}
|
|
893
|
+
return out;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// The team table as a divided box for a terminal, in the team page's
|
|
897
|
+
// columns: one line per { member, status } with that member's cells from
|
|
898
|
+
// teamCells. A long role or status wraps to fit the width (the exact model
|
|
899
|
+
// never splits); null when even that does not fit.
|
|
900
|
+
function teamBox(rows, cells, { width } = {}) {
|
|
901
|
+
const { renderBorderedTable } = require('./text-table');
|
|
902
|
+
const wraps = new Set(['role', 'status']);
|
|
903
|
+
const columns = require('./gm-card').TEAM_COLUMNS.map(header => ({ header, wrap: wraps.has(header) }));
|
|
904
|
+
const blank = { role: NO_RATING, ovr: NO_RATING, engine: NO_RATING };
|
|
905
|
+
return renderBorderedTable(columns, (rows || []).map((row) => {
|
|
906
|
+
const c = (cells && cells.get(row.member)) || blank;
|
|
907
|
+
return [row.member, c.role, c.ovr, c.engine, row.status];
|
|
908
|
+
}), { width });
|
|
909
|
+
}
|
|
910
|
+
|
|
852
911
|
// A verify command that always passes proves nothing. Binary checks only:
|
|
853
912
|
// every piece of it is one of these, or it swallows failure with || true.
|
|
854
913
|
const TRIVIAL_VERIFY = [
|
|
@@ -4252,33 +4311,7 @@ function createBetDesk(deps) {
|
|
|
4252
4311
|
const loaded = (deps.loadSeason || (root => season.loadSeason(root, { now: now(), lastSeen: null })))(deps.workspaceRoot);
|
|
4253
4312
|
if (loaded && !loaded.error) ratings = season.memberRatings(loaded.outcomes || []);
|
|
4254
4313
|
} catch { ratings = new Map(); }
|
|
4255
|
-
|
|
4256
|
-
if (!resolve) {
|
|
4257
|
-
const reg = require('./engine-registry');
|
|
4258
|
-
const { resolveEngineForMember } = require('./member-engine');
|
|
4259
|
-
let rosterState = null;
|
|
4260
|
-
try { rosterState = reg.readRosterState(deps.workspaceRoot); } catch { rosterState = null; }
|
|
4261
|
-
resolve = name => resolveEngineForMember(name, deps.workspaceRoot, { quiet: true, ...(rosterState ? { rosterState } : {}) });
|
|
4262
|
-
}
|
|
4263
|
-
const facts = new Map();
|
|
4264
|
-
const { memberTypeOf } = require('./member-type');
|
|
4265
|
-
for (const m of team || []) {
|
|
4266
|
-
const card = memberCardFacts(deps.workspaceRoot, m.name);
|
|
4267
|
-
const kind = memberTypeOf({ type: card.type });
|
|
4268
|
-
let engine = kind.type === 'human' ? 'person' : NO_RATING;
|
|
4269
|
-
if (kind.runnable) {
|
|
4270
|
-
let picked = null;
|
|
4271
|
-
try { picked = resolve(m.name); } catch { picked = null; }
|
|
4272
|
-
engine = engineWords(picked);
|
|
4273
|
-
}
|
|
4274
|
-
const rated = ratings.get(m.name);
|
|
4275
|
-
facts.set(m.name, {
|
|
4276
|
-
role: memberRoleWords(m.name, card),
|
|
4277
|
-
ovr: !rated || !rated.finished ? NO_RATING : rated.ovr == null ? 'new' : String(rated.ovr),
|
|
4278
|
-
engine,
|
|
4279
|
-
parked: card.parked,
|
|
4280
|
-
});
|
|
4281
|
-
}
|
|
4314
|
+
const facts = teamCells(deps.workspaceRoot, (team || []).map(m => m.name), { ratings, resolve: deps.resolveEngine || null });
|
|
4282
4315
|
rosterCache = { at: now(), facts };
|
|
4283
4316
|
return facts;
|
|
4284
4317
|
}
|
|
@@ -4857,4 +4890,6 @@ module.exports = {
|
|
|
4857
4890
|
writerCommand,
|
|
4858
4891
|
WRITER_ANGLES,
|
|
4859
4892
|
validateBet,
|
|
4893
|
+
teamBox,
|
|
4894
|
+
teamCells,
|
|
4860
4895
|
};
|
package/lib/gm-phone-page.js
CHANGED
|
@@ -357,8 +357,10 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
357
357
|
// when a spiral starts, each dot keeps the speed it had and lets it fade
|
|
358
358
|
// at CARRY a frame, so the dots turn inward without a stop
|
|
359
359
|
var CARRY = 0.82;
|
|
360
|
-
// what the dots did on the last frame, kept on the canvas so the motion can be
|
|
361
|
-
|
|
360
|
+
// what the dots did on the last frame, kept on the canvas so the motion can be
|
|
361
|
+
// measured, and ms: how much of the dots' own clock that frame was, which is
|
|
362
|
+
// at most 50ms however long the frame took
|
|
363
|
+
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0 };
|
|
362
364
|
canvas.__orb = stats;
|
|
363
365
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
364
366
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
@@ -523,7 +525,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
523
525
|
ctx.arc(d.sx, d.sy, d.r * (front ? 0.6 + l * 0.7 : 0.6) * shrink, 0, Math.PI * 2);
|
|
524
526
|
ctx.fill();
|
|
525
527
|
}
|
|
526
|
-
stats.frame += 1; stats.mode = mode; stats.step = most; stats.mean = sum / Math.min(64, dots.length || 1);
|
|
528
|
+
stats.frame += 1; stats.mode = mode; stats.step = most; stats.mean = sum / Math.min(64, dots.length || 1); stats.ms = dt * 1000;
|
|
527
529
|
if (mode === 'pull') {
|
|
528
530
|
// where the dots land: one bright point that swells until the lava is born
|
|
529
531
|
var arrived = Math.min(1, Math.max(0, (age - 0.3) / 0.7));
|
|
@@ -556,7 +558,9 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
556
558
|
function start() { if (!raf) { lastT = 0; raf = requestAnimationFrame(frame); } }
|
|
557
559
|
function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; }
|
|
558
560
|
|
|
559
|
-
|
|
561
|
+
// at: the frame time this place is for, when it is a step of a motion, kept on
|
|
562
|
+
// the lava so the motion can be measured on the clock it runs on
|
|
563
|
+
function place(s, x, y, at) { lava.style.transform = 'translate(' + (x - base / 2) + 'px, ' + (y - base / 2) + 'px) scale(' + s + ')'; lava.__at = at; }
|
|
560
564
|
function restPoint() {
|
|
561
565
|
var top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-top')) || 0;
|
|
562
566
|
return { x: document.documentElement.clientWidth / 2, y: window.scrollY + Math.max(16, top) + 8 + REST / 2 };
|
|
@@ -574,6 +578,9 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
574
578
|
var contained = pace < 1;
|
|
575
579
|
var growMs = quick ? ACT_GROW_MS : contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
576
580
|
var settleMs = quick ? ACT_SETTLE_MS : contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
|
|
581
|
+
// after an act the card comes up at the bloom's peak; any other card once the lava has settled
|
|
582
|
+
planned('bloom', growMs);
|
|
583
|
+
if (!quick) planned('settle', settleMs);
|
|
577
584
|
// the point the dots drew, where the sphere is on screen (smaller and higher on the calm line)
|
|
578
585
|
var vx = rect.left + rect.width / 2, vy = rect.top + rect.height / 2;
|
|
579
586
|
var bloom = quick ? ACT_BLOOM : BLOOM;
|
|
@@ -598,7 +605,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
598
605
|
var t0 = performance.now();
|
|
599
606
|
var step = function (now) {
|
|
600
607
|
var t = Math.min(1, (now - t0) / growMs);
|
|
601
|
-
place(sMin + (sMax - sMin) * ease(t), origin.x, origin.y);
|
|
608
|
+
place(sMin + (sMax - sMin) * ease(t), origin.x, origin.y, now);
|
|
602
609
|
lava.style.opacity = String(smooth(0.02, 0.22, t));
|
|
603
610
|
core.style.opacity = String(1 - smooth(0.05, 0.4, t));
|
|
604
611
|
dim.style.opacity = contained ? '0' : String(smooth(0.88, 1, t));
|
|
@@ -612,7 +619,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
612
619
|
var s0 = performance.now();
|
|
613
620
|
var settle = function (n2) {
|
|
614
621
|
var u = ease(Math.min(1, (n2 - s0) / settleMs));
|
|
615
|
-
place(sMax + (rest - sMax) * u, origin.x + (p.x - origin.x) * u, origin.y + (p.y - origin.y) * u);
|
|
622
|
+
place(sMax + (rest - sMax) * u, origin.x + (p.x - origin.x) * u, origin.y + (p.y - origin.y) * u, n2);
|
|
616
623
|
dim.style.opacity = contained ? '0' : String(1 - smooth(0.35, 0.85, u));
|
|
617
624
|
if (u < 1) { requestAnimationFrame(settle); return; }
|
|
618
625
|
settled();
|
|
@@ -636,7 +643,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
636
643
|
var u = Math.min(1, (now - t0) / total);
|
|
637
644
|
var s = u < k ? sMin + (sMax - sMin) * (0.5 - 0.5 * Math.cos(Math.PI * u / k)) : sMax + (rest - sMax) * ease((u - k) / (1 - k));
|
|
638
645
|
var m = ease(u);
|
|
639
|
-
place(s, origin.x + (p.x - origin.x) * m, origin.y + (p.y - origin.y) * m);
|
|
646
|
+
place(s, origin.x + (p.x - origin.x) * m, origin.y + (p.y - origin.y) * m, now);
|
|
640
647
|
lava.style.opacity = String(smooth(0, 0.1, u));
|
|
641
648
|
core.style.opacity = String(1 - smooth(0.04, 0.5, u));
|
|
642
649
|
dim.style.opacity = '0';
|
|
@@ -700,6 +707,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
700
707
|
mode = fast ? 'swarm' : 'idle';
|
|
701
708
|
bridgeAt = reduced ? 0 : performance.now();
|
|
702
709
|
start();
|
|
710
|
+
if (!reduced) planned('beat', beatMs);
|
|
703
711
|
setTimeout(cb, reduced ? 0 : beatMs);
|
|
704
712
|
};
|
|
705
713
|
if (reduced || lava.style.visibility !== 'visible') { reform(); return; }
|
|
@@ -709,12 +717,13 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
709
717
|
lava.style.position = 'fixed';
|
|
710
718
|
place(from.s, from.x, from.y);
|
|
711
719
|
var sEnd = 30 / base;
|
|
720
|
+
planned('glide', glideMs);
|
|
712
721
|
var t0 = performance.now();
|
|
713
722
|
var step = function (now) {
|
|
714
723
|
var k = Math.min(1, (now - t0) / glideMs);
|
|
715
724
|
var t = fast ? 0.5 - 0.5 * Math.cos(Math.PI * k) : ease(k);
|
|
716
725
|
if (now - t0 >= leaveMs) takeOff();
|
|
717
|
-
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
726
|
+
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t, now);
|
|
718
727
|
core.style.opacity = String(smooth(0.3, 0.9, k));
|
|
719
728
|
lava.style.opacity = String(1 - smooth(0.7, 1, k));
|
|
720
729
|
if (k < 1) { requestAnimationFrame(step); return; }
|
|
@@ -767,6 +776,8 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
767
776
|
pace = act ? ACT_PACE : fast ? FAST : 1;
|
|
768
777
|
// after an act the dots spiral in from where the re-formed sphere is
|
|
769
778
|
if (mode !== 'pull') startPull(fast || Boolean(act));
|
|
779
|
+
// the lava is born at 1.05 on the spiral's own clock, which runs at pace
|
|
780
|
+
planned('spiral', Math.max(0, 1.05 - age) * pace * 1000);
|
|
770
781
|
timed = true;
|
|
771
782
|
start();
|
|
772
783
|
},
|
|
@@ -817,7 +828,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
817
828
|
// card, and nothing from the card before survives a draw. Every tap posts
|
|
818
829
|
// the card's id and the option's id, or the words typed in its box, to
|
|
819
830
|
// /card; the Mac refuses anything else, and its answer is the next card.
|
|
820
|
-
var bet = { shown: null, watching: null, tapAt: 0, noting: false, clock: null };
|
|
831
|
+
var bet = { shown: null, watching: null, tapAt: 0, plan: [], waited: 0, noting: false, clock: null };
|
|
821
832
|
// while an act or a turn is under way the card takes no taps; the page
|
|
822
833
|
// marks it on the body too, so anyone reading the page can tell
|
|
823
834
|
var busyNow = false;
|
|
@@ -991,9 +1002,9 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
991
1002
|
var input = spot.querySelector('input');
|
|
992
1003
|
if (input) { input.readOnly = true; input.blur(); }
|
|
993
1004
|
bet.busy = true;
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
return
|
|
1005
|
+
var at = tapped();
|
|
1006
|
+
planned('hold', HOLD_MS);
|
|
1007
|
+
return at;
|
|
997
1008
|
}
|
|
998
1009
|
|
|
999
1010
|
function afterHold(heldAt) {
|
|
@@ -1088,12 +1099,16 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1088
1099
|
function flow(answer, from, typed, option) {
|
|
1089
1100
|
var landed = null;
|
|
1090
1101
|
var sphere = false;
|
|
1091
|
-
var lineAt = 0;
|
|
1102
|
+
var lineAt = 0, waitFrom = 0;
|
|
1092
1103
|
var land = function () {
|
|
1104
|
+
// the sphere is ready before the answer is in: it waits from now
|
|
1105
|
+
if (sphere && !landed) waitFrom = performance.now();
|
|
1093
1106
|
if (!landed || !sphere) return;
|
|
1107
|
+
if (waitFrom) bet.waited += performance.now() - waitFrom;
|
|
1094
1108
|
var r = landed;
|
|
1095
1109
|
landed = null;
|
|
1096
1110
|
var wait = r.ack && lineAt ? Math.max(0, AFTER_MS - (performance.now() - lineAt)) : 0;
|
|
1111
|
+
if (wait) planned('line', wait);
|
|
1097
1112
|
setTimeout(function () {
|
|
1098
1113
|
// nothing more to show: the sphere stays, with its line, and its dots settle into the idle drift
|
|
1099
1114
|
if (r.ok && (r.home || !r.card)) { bet.busy = false; bet.shown = null; orb.calm(); line.set(r.home ? 'not now. tap when you want another' : 'tap to think', false); return; }
|
|
@@ -1133,6 +1148,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1133
1148
|
// reduced motion: no travel; the card and the small orb fade, then the sphere is there
|
|
1134
1149
|
if (reduced) {
|
|
1135
1150
|
document.getElementById('lava').style.opacity = '0';
|
|
1151
|
+
planned('leave', 200);
|
|
1136
1152
|
setTimeout(function () { swap(); stageFade(); orb.melt(then, to, function () {}, fast); }, 200);
|
|
1137
1153
|
return;
|
|
1138
1154
|
}
|
|
@@ -1185,12 +1201,35 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1185
1201
|
}
|
|
1186
1202
|
|
|
1187
1203
|
// ---- showing a card ---------------------------------------------------------------------
|
|
1188
|
-
//
|
|
1189
|
-
// the speed can be measured
|
|
1204
|
+
// The time from the tap to the card fully on screen, kept on the page so
|
|
1205
|
+
// the speed can be measured. data-reveal-ms is the clock. data-reveal-plan
|
|
1206
|
+
// is each step of the motion in order with the length it is set to take,
|
|
1207
|
+
// and data-reveal-waited-ms is how long the sphere waited on the Mac's
|
|
1208
|
+
// answer, so late frames on a busy machine are told apart from the motion.
|
|
1209
|
+
var REVEAL = ['data-reveal-ms', 'data-reveal-plan', 'data-reveal-waited-ms'];
|
|
1210
|
+
function tapped() {
|
|
1211
|
+
bet.tapAt = performance.now();
|
|
1212
|
+
bet.plan = [];
|
|
1213
|
+
bet.waited = 0;
|
|
1214
|
+
REVEAL.forEach(function (a) { document.body.removeAttribute(a); });
|
|
1215
|
+
return bet.tapAt;
|
|
1216
|
+
}
|
|
1217
|
+
function planned(name, ms) { if (bet.tapAt) bet.plan.push(name + ' ' + Math.round(ms)); }
|
|
1190
1218
|
function measured() {
|
|
1191
|
-
if (bet.tapAt)
|
|
1219
|
+
if (bet.tapAt) {
|
|
1220
|
+
document.body.setAttribute('data-reveal-plan', bet.plan.join(', '));
|
|
1221
|
+
document.body.setAttribute('data-reveal-waited-ms', String(Math.round(bet.waited)));
|
|
1222
|
+
document.body.setAttribute('data-reveal-ms', String(Math.round(performance.now() - bet.tapAt)));
|
|
1223
|
+
}
|
|
1192
1224
|
bet.tapAt = 0;
|
|
1193
1225
|
}
|
|
1226
|
+
// how long a card's own fade up is set to take, read from its style
|
|
1227
|
+
function fadeMs(n) {
|
|
1228
|
+
var s = getComputedStyle(n);
|
|
1229
|
+
var i = Math.max(0, s.transitionProperty.split(', ').indexOf('opacity'));
|
|
1230
|
+
var at = function (list) { var v = list.split(', '); return (parseFloat(v[i % v.length]) || 0) * 1000; };
|
|
1231
|
+
return at(s.transitionDelay) + at(s.transitionDuration);
|
|
1232
|
+
}
|
|
1194
1233
|
|
|
1195
1234
|
// fast: the quick fade from the big orb; act: the short one after an act
|
|
1196
1235
|
function showCard(card, animate, fast, act) {
|
|
@@ -1213,7 +1252,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1213
1252
|
var done = function (e) { if (e.propertyName === 'opacity') { b.removeEventListener('transitionend', done); measured(); } };
|
|
1214
1253
|
b.addEventListener('transitionend', done);
|
|
1215
1254
|
void b.offsetWidth;
|
|
1216
|
-
requestAnimationFrame(function () { b.classList.add('up'); });
|
|
1255
|
+
requestAnimationFrame(function () { b.classList.add('up'); planned('fade', fadeMs(b)); });
|
|
1217
1256
|
}
|
|
1218
1257
|
|
|
1219
1258
|
// ---- the calm line: the dots keep moving while the next idea is written -----------------
|
|
@@ -1487,12 +1526,14 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1487
1526
|
b.classList.remove('turn-in');
|
|
1488
1527
|
b.style.height = to + 'px';
|
|
1489
1528
|
ghost.classList.add('gone');
|
|
1529
|
+
var turnMs = 80 + TURN_IN_MS + 20;
|
|
1530
|
+
planned('turn', turnMs);
|
|
1490
1531
|
setTimeout(function () {
|
|
1491
1532
|
ghost.remove();
|
|
1492
1533
|
b.classList.remove('turning');
|
|
1493
1534
|
b.style.height = '';
|
|
1494
1535
|
after();
|
|
1495
|
-
},
|
|
1536
|
+
}, turnMs);
|
|
1496
1537
|
}
|
|
1497
1538
|
|
|
1498
1539
|
// While the calm line shows, look every WAIT_LOOK_MS: its live words are
|
|
@@ -1536,8 +1577,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1536
1577
|
if (waiting()) { goHome(); return; }
|
|
1537
1578
|
buzz();
|
|
1538
1579
|
bet.busy = true;
|
|
1539
|
-
|
|
1540
|
-
document.body.removeAttribute('data-reveal-ms');
|
|
1580
|
+
tapped();
|
|
1541
1581
|
var failed = function () {
|
|
1542
1582
|
bet.busy = false;
|
|
1543
1583
|
orb.relax();
|
|
@@ -1548,6 +1588,8 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1548
1588
|
line.set('', false);
|
|
1549
1589
|
// nothing ready yet: the calm line, with the sphere where it is and its dots moving
|
|
1550
1590
|
if (isWait(r.card)) { enterWait(r.card); return; }
|
|
1591
|
+
// the big orb moves once the answer is in
|
|
1592
|
+
bet.waited = performance.now() - bet.tapAt;
|
|
1551
1593
|
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(r.card, true, true); } }, true);
|
|
1552
1594
|
}, failed);
|
|
1553
1595
|
}
|
|
@@ -1598,11 +1640,12 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1598
1640
|
if (bet.busy) return;
|
|
1599
1641
|
buzz();
|
|
1600
1642
|
bet.busy = true;
|
|
1601
|
-
|
|
1643
|
+
tapped();
|
|
1602
1644
|
var answer = api('/act', { open: which, card_id: bet.shown ? bet.shown.id : null }).catch(offline);
|
|
1603
1645
|
if (!document.body.classList.contains('has-card')) {
|
|
1604
1646
|
answer.then(function (r) {
|
|
1605
1647
|
if (!r.ok || !r.card) { bet.busy = false; line.set((r && r.message) || 'that did not go through. tap to try again', true); return; }
|
|
1648
|
+
bet.waited = performance.now() - bet.tapAt;
|
|
1606
1649
|
spiralTo(r.card, null);
|
|
1607
1650
|
});
|
|
1608
1651
|
return;
|
package/lib/text-table.js
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
// than the terminal, columns marked `clip` or `wrap` shrink (rightmost
|
|
7
7
|
// first): a clip column ends in "…", a wrap column carries on underneath.
|
|
8
8
|
// Every other column keeps its full width.
|
|
9
|
+
//
|
|
10
|
+
// renderBorderedTable draws the same rows as a divided box, the look of the
|
|
11
|
+
// orb's team table. A box wider than the terminal narrows its `wrap`
|
|
12
|
+
// columns, the widest first, and their words carry on underneath; when it
|
|
13
|
+
// still does not fit it returns null and the caller prints its plain view.
|
|
14
|
+
//
|
|
15
|
+
// Every width here is terminal columns, not string length (displayWidth):
|
|
16
|
+
// a wide or fullwidth character takes 2, a joiner, variation selector, or
|
|
17
|
+
// combining mark takes 0, and a joined emoji is one 2-column cluster. Cuts
|
|
18
|
+
// and wraps fall between clusters, never inside one.
|
|
9
19
|
|
|
10
20
|
const GAP = ' ';
|
|
11
21
|
const ELLIPSIS = '…';
|
|
@@ -16,25 +26,110 @@ function terminalWidth(width) {
|
|
|
16
26
|
return Number.isFinite(value) && value >= 20 ? value : FALLBACK_WIDTH;
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
// East Asian wide and fullwidth code points, as [first, last] ranges.
|
|
30
|
+
const WIDE = [
|
|
31
|
+
[0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], [0x2e80, 0x303e], [0x3041, 0x33ff],
|
|
32
|
+
[0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xa000, 0xa4cf], [0xa960, 0xa97f], [0xac00, 0xd7a3],
|
|
33
|
+
[0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe6f], [0xff00, 0xff60], [0xffe0, 0xffe6],
|
|
34
|
+
[0x16fe0, 0x16fe4], [0x17000, 0x18cff], [0x1aff0, 0x1b2ff], [0x1f200, 0x1f2ff],
|
|
35
|
+
[0x20000, 0x2fffd], [0x30000, 0x3fffd],
|
|
36
|
+
];
|
|
37
|
+
// takes no column: combining marks, joiners and other format characters,
|
|
38
|
+
// variation selectors, and the Hangul vowel and final jamo that join a syllable
|
|
39
|
+
const ZERO = /^[\p{Mn}\p{Me}\p{Cf}\u1160-\u11ff\ud7b0-\ud7ff]$/u;
|
|
40
|
+
// drawn as a 2-column emoji: emoji presentation, or an emoji asked for one
|
|
41
|
+
// with the U+FE0F selector or a keycap
|
|
42
|
+
const EMOJI = /\p{Emoji_Presentation}|\p{Emoji}\ufe0f|\u20e3/u;
|
|
43
|
+
const SEGMENTER = typeof Intl === 'object' && typeof Intl.Segmenter === 'function'
|
|
44
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
function isWide(code) {
|
|
48
|
+
for (const [first, last] of WIDE) {
|
|
49
|
+
if (code < first) return false;
|
|
50
|
+
if (code <= last) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The text as grapheme clusters: what a reader sees as one character.
|
|
56
|
+
// Without Intl.Segmenter a zero-width code point joins the cluster before it.
|
|
57
|
+
function graphemes(text) {
|
|
58
|
+
const value = String(text);
|
|
59
|
+
if (SEGMENTER) return Array.from(SEGMENTER.segment(value), (part) => part.segment);
|
|
60
|
+
const out = [];
|
|
61
|
+
let joined = false;
|
|
62
|
+
for (const ch of value) {
|
|
63
|
+
if (out.length && (joined || ZERO.test(ch))) out[out.length - 1] += ch;
|
|
64
|
+
else out.push(ch);
|
|
65
|
+
joined = ch === '\u200d';
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Terminal columns for one cluster: 0, 1, or 2.
|
|
71
|
+
function clusterWidth(cluster) {
|
|
72
|
+
const code = cluster.codePointAt(0);
|
|
73
|
+
if (cluster.length === 1 && code >= 0x20 && code < 0x7f) return 1;
|
|
74
|
+
if (code < 0x20 || (code >= 0x7f && code < 0xa0)) return 0;
|
|
75
|
+
if (EMOJI.test(cluster)) return 2;
|
|
76
|
+
for (const ch of cluster) {
|
|
77
|
+
if (!ZERO.test(ch)) return isWide(ch.codePointAt(0)) ? 2 : 1;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Terminal columns the text takes.
|
|
83
|
+
function displayWidth(text) {
|
|
84
|
+
const value = String(text);
|
|
85
|
+
// printable ascii is one column a character
|
|
86
|
+
if (/^[\x20-\x7e]*$/.test(value)) return value.length;
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (const cluster of graphemes(value)) total += clusterWidth(cluster);
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The longest start of the text that fits in that many columns, whole
|
|
93
|
+
// clusters only.
|
|
94
|
+
function sliceColumns(text, columns) {
|
|
95
|
+
let out = '';
|
|
96
|
+
let used = 0;
|
|
97
|
+
for (const cluster of graphemes(text)) {
|
|
98
|
+
const w = clusterWidth(cluster);
|
|
99
|
+
if (used + w > columns) break;
|
|
100
|
+
out += cluster;
|
|
101
|
+
used += w;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pads with spaces to that many columns.
|
|
107
|
+
function padColumns(text, columns) {
|
|
108
|
+
const value = String(text);
|
|
109
|
+
return value + ' '.repeat(Math.max(0, columns - displayWidth(value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
19
112
|
function clipText(text, width) {
|
|
20
113
|
const value = String(text);
|
|
21
|
-
if (value
|
|
114
|
+
if (displayWidth(value) <= width) return value;
|
|
22
115
|
if (width <= 1) return ELLIPSIS.slice(0, Math.max(0, width));
|
|
23
|
-
return `${value
|
|
116
|
+
return `${sliceColumns(value, width - 1).trimEnd()}${ELLIPSIS}`;
|
|
24
117
|
}
|
|
25
118
|
|
|
26
|
-
// Word-wrap one line to a width; a word
|
|
119
|
+
// Word-wrap one line to a width in columns; a word wider than the width is
|
|
120
|
+
// split between clusters (a lone cluster wider than the width stands alone).
|
|
27
121
|
function wrapText(text, width) {
|
|
28
122
|
const out = [];
|
|
29
123
|
let line = '';
|
|
30
124
|
for (let word of String(text).split(' ')) {
|
|
31
|
-
while (word
|
|
125
|
+
while (displayWidth(word) > width) {
|
|
32
126
|
if (line) { out.push(line); line = ''; }
|
|
33
|
-
|
|
34
|
-
|
|
127
|
+
const head = sliceColumns(word, width) || graphemes(word)[0];
|
|
128
|
+
out.push(head);
|
|
129
|
+
word = word.slice(head.length);
|
|
35
130
|
}
|
|
36
131
|
const next = line ? `${line} ${word}` : word;
|
|
37
|
-
if (next
|
|
132
|
+
if (displayWidth(next) > width && line) { out.push(line); line = word; } else line = next;
|
|
38
133
|
}
|
|
39
134
|
out.push(line);
|
|
40
135
|
return out;
|
|
@@ -54,19 +149,19 @@ function renderTable(columns, rows, { width } = {}) {
|
|
|
54
149
|
const max = terminalWidth(width);
|
|
55
150
|
const cells = rows.map((row) => columns.map((_, index) => cellLines(row[index])));
|
|
56
151
|
const widths = columns.map((column, index) => Math.max(
|
|
57
|
-
|
|
58
|
-
...cells.map((row) => Math.max(...row[index].map(
|
|
152
|
+
displayWidth(column.header),
|
|
153
|
+
...cells.map((row) => Math.max(...row[index].map(displayWidth))),
|
|
59
154
|
));
|
|
60
155
|
let over = widths.reduce((sum, value) => sum + value, 0) + GAP.length * (columns.length - 1) - max;
|
|
61
156
|
for (let index = columns.length - 1; index >= 0 && over > 0; index -= 1) {
|
|
62
157
|
if (!columns[index].clip && !columns[index].wrap) continue;
|
|
63
|
-
const floor = Math.max(
|
|
158
|
+
const floor = Math.max(displayWidth(columns[index].header), columns[index].min || 8);
|
|
64
159
|
const cut = Math.min(over, Math.max(0, widths[index] - floor));
|
|
65
160
|
widths[index] -= cut;
|
|
66
161
|
over -= cut;
|
|
67
162
|
}
|
|
68
163
|
const line = (parts) => parts
|
|
69
|
-
.map((text, index) => clipText(text, widths[index])
|
|
164
|
+
.map((text, index) => padColumns(clipText(text, widths[index]), index === parts.length - 1 ? 0 : widths[index]))
|
|
70
165
|
.join(GAP)
|
|
71
166
|
.trimEnd();
|
|
72
167
|
const out = [line(columns.map((column) => column.header))];
|
|
@@ -78,4 +173,35 @@ function renderTable(columns, rows, { width } = {}) {
|
|
|
78
173
|
return out.join('\n');
|
|
79
174
|
}
|
|
80
175
|
|
|
81
|
-
|
|
176
|
+
// columns: [{ header, wrap?, min? }]; rows: arrays of cells. A divided box,
|
|
177
|
+
// or null when it cannot fit the terminal.
|
|
178
|
+
function renderBorderedTable(columns, rows, { width } = {}) {
|
|
179
|
+
const cells = rows.map((row) => columns.map((_, index) => cellLines(row[index])));
|
|
180
|
+
const widths = columns.map((column, index) => Math.max(
|
|
181
|
+
displayWidth(column.header),
|
|
182
|
+
...cells.map((row) => Math.max(...row[index].map(displayWidth))),
|
|
183
|
+
));
|
|
184
|
+
const floor = (index) => Math.max(displayWidth(columns[index].header), columns[index].min || 8);
|
|
185
|
+
let over = widths.reduce((sum, value) => sum + value + 3, 1) - terminalWidth(width);
|
|
186
|
+
while (over > 0) {
|
|
187
|
+
let widest = -1;
|
|
188
|
+
columns.forEach((column, index) => {
|
|
189
|
+
if (column.wrap && widths[index] > floor(index) && (widest < 0 || widths[index] > widths[widest])) widest = index;
|
|
190
|
+
});
|
|
191
|
+
if (widest < 0) return null;
|
|
192
|
+
widths[widest] -= 1;
|
|
193
|
+
over -= 1;
|
|
194
|
+
}
|
|
195
|
+
const rule = (left, mid, right) => `${left}${widths.map((w) => '─'.repeat(w + 2)).join(mid)}${right}`;
|
|
196
|
+
const line = (parts) => `│ ${parts.map((text, index) => padColumns(text, widths[index])).join(' │ ')} │`;
|
|
197
|
+
const out = [rule('┌', '┬', '┐'), line(columns.map((column) => column.header)), rule('├', '┼', '┤')];
|
|
198
|
+
for (const raw of cells) {
|
|
199
|
+
const row = raw.map((lines, index) => (columns[index].wrap ? lines.flatMap((text) => wrapText(text, widths[index])) : lines));
|
|
200
|
+
const height = Math.max(...row.map((lines) => lines.length));
|
|
201
|
+
for (let at = 0; at < height; at += 1) out.push(line(row.map((lines) => lines[at] || '')));
|
|
202
|
+
}
|
|
203
|
+
out.push(rule('└', '┴', '┘'));
|
|
204
|
+
return out.join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { FALLBACK_WIDTH, clipText, displayWidth, renderBorderedTable, renderTable, sliceColumns, terminalWidth, wrapText };
|
package/lib/zen-apply.js
CHANGED
|
@@ -547,7 +547,7 @@ function applyZen({ cwd = process.cwd(), fetch = true, freshMinutes, abandonedDa
|
|
|
547
547
|
if (copies.busy.length) left.push(...sideCopyLines(copies.busy, staleDays, nextSaveIn(copies.busy, staleDays, now.getTime())));
|
|
548
548
|
if (copies.scheduled) left.push(`${plural(copies.scheduled, 'side copy is', 'side copies are')} used by a scheduled job, so ${copies.scheduled === 1 ? 'it stays' : 'they stay'}`);
|
|
549
549
|
if (copies.locked) left.push(`${plural(copies.locked, 'side copy is', 'side copies are')} locked by ${copies.locked === 1 ? 'its owner, so it stays' : 'their owners, so they stay'}`);
|
|
550
|
-
if (copies.inUse) left.push(`${plural(copies.inUse, 'side copy was', 'side copies were')} used in ${window}`);
|
|
550
|
+
if (copies.inUse) left.push(`${plural(copies.inUse, 'side copy was', 'side copies were')} used in ${window}, so ${copies.inUse === 1 ? 'it stays' : 'they stay'}`);
|
|
551
551
|
|
|
552
552
|
const stray = saveStrayEdits(root, teamState, tag, cutoff);
|
|
553
553
|
if (stray.saved) {
|
package/package.json
CHANGED
package/scripts/det/ytnotes
CHANGED
|
@@ -428,10 +428,14 @@ run_haiku() {
|
|
|
428
428
|
# The Antigravity editor ships its own `agy` (a VS Code-style launcher script).
|
|
429
429
|
# It has no -p mode: it hands -p and --model to Electron and prints nothing, so
|
|
430
430
|
# it is not a writer. Only the Antigravity CLI is.
|
|
431
|
+
# Only grep's answer counts, never head's exit. In `head | grep -q` under
|
|
432
|
+
# pipefail, grep quits on the match and a head that touches the pipe once
|
|
433
|
+
# more (uutils head on Ubuntu 26.04 does, to find the end of the file) dies
|
|
434
|
+
# of SIGPIPE, so a busy machine read the launcher as the CLI.
|
|
431
435
|
agy_is_cli() {
|
|
432
436
|
local bin
|
|
433
437
|
bin=$(command -v agy 2>/dev/null) || return 1
|
|
434
|
-
! head -c 4096 "$bin" 2>/dev/null
|
|
438
|
+
! grep -q 'VSCODE_IPC_HOOK_CLI' < <(head -c 4096 "$bin" 2>/dev/null)
|
|
435
439
|
}
|
|
436
440
|
|
|
437
441
|
# An agy that is not signed in opens a Google login and waits up to 60s, which
|
|
@@ -441,7 +445,7 @@ agy_is_cli() {
|
|
|
441
445
|
agy_signed_in() {
|
|
442
446
|
local out
|
|
443
447
|
out=$(run_with_timeout 10 agy models </dev/null 2>&1) || true
|
|
444
|
-
!
|
|
448
|
+
! grep -qi 'sign in' <<< "$out"
|
|
445
449
|
}
|
|
446
450
|
|
|
447
451
|
run_gemini() {
|
|
@@ -524,7 +528,9 @@ run_auto() {
|
|
|
524
528
|
echo "$tried did not answer; writing the notes with $label instead." >&2
|
|
525
529
|
fi
|
|
526
530
|
rm -f "$WORK/yt_${ID}.kill"
|
|
527
|
-
|
|
531
|
+
# A here-string, not printf | grep -q: under pipefail printf can die of
|
|
532
|
+
# SIGPIPE after grep's match and turn a real answer into "did not answer".
|
|
533
|
+
if out=$(run_writer "$cmd" "$model" "$effort" 2>>"$WORK/yt_${ID}.engine.err") && grep -q '[[:alnum:]]' <<< "$out"; then
|
|
528
534
|
printf '%s\n' "$out"
|
|
529
535
|
echo "notes by $label" >&2
|
|
530
536
|
return 0
|