saterminal 0.2.0 → 0.3.0

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.
@@ -1,8 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { hasHtmlTable, htmlToText, parseHtmlSegments, wrapSegments } from "../text.ts";
2
+ import { hasHtmlTable, parseHtmlSegments, wrapSegments } from "../text.ts";
3
3
  import type { PracticeQuestion, QuestionDetail } from "../types.ts";
4
- import { lineAt, printWrappedAt, renderStyledLine, term } from "./terminal.ts";
5
- import type { DisplayRow, PaneLayout } from "./types.ts";
4
+ import type { DisplayRow } from "./types.ts";
6
5
 
7
6
  export function answerKeys(question?: PracticeQuestion): string[] {
8
7
  return Object.keys(question?.detail.answerOptions ?? {}).sort();
@@ -17,10 +16,6 @@ export function questionNeedsExternalDisplay(detail: QuestionDetail): boolean {
17
16
  );
18
17
  }
19
18
 
20
- export function questionPageSize(): number {
21
- return Math.max(1, term.height - 6);
22
- }
23
-
24
19
  export function questionRows(detail: QuestionDetail, width: number): DisplayRow[] {
25
20
  const rows: DisplayRow[] = [];
26
21
  const stimulus = wrapSegments(parseHtmlSegments(detail.stimulus), width).map((segments) => ({ segments }));
@@ -34,59 +29,6 @@ export function questionRows(detail: QuestionDetail, width: number): DisplayRow[
34
29
  return rows;
35
30
  }
36
31
 
37
- export function renderQuestionPane(detail: QuestionDetail, x: number, width: number, scroll: number): void {
38
- const rows = questionRows(detail, width);
39
- const maxRows = questionPageSize();
40
- const maxScroll = Math.max(0, rows.length - maxRows);
41
- const start = Math.min(scroll, maxScroll);
42
-
43
- for (const [offset, row] of rows.slice(start, start + maxRows).entries()) {
44
- const y = 4 + offset;
45
- renderStyledLine(x, y, width, row.segments, row.bold);
46
- }
47
-
48
- if (start > 0) {
49
- lineAt(x + width - 4, 4, 4, "^");
50
- }
51
- if (start < maxScroll) {
52
- lineAt(x + width - 4, term.height - 3, 4, "v");
53
- }
54
- }
55
-
56
- export function renderUnsupportedQuestion(question: PracticeQuestion, panes: PaneLayout): void {
57
- const { meta, detail } = question;
58
- const appUrl = practiceQuestionUrl(question);
59
- const apiUrl = practiceQuestionApiUrl(question);
60
- let y = 4;
61
-
62
- lineAt(panes.leftX, y++, panes.leftWidth, "This question contains a table.");
63
- y++;
64
- y = printWrappedAt(
65
- "The terminal renderer cannot display this table accurately enough to answer the question here.",
66
- panes.leftX,
67
- y,
68
- panes.leftWidth,
69
- term.height - 3,
70
- );
71
- y++;
72
- y = printWrappedAt("Open it in PracticeSAT, search this question ID, then skip it in this app.", panes.leftX, y, panes.leftWidth, term.height - 3);
73
- y++;
74
- y = printWrappedAt(`Question ID: ${meta.questionId}`, panes.leftX, y, panes.leftWidth, term.height - 3);
75
- y++;
76
- y = printWrappedAt(appUrl, panes.leftX, y, panes.leftWidth, term.height - 3);
77
- y++;
78
- y = printWrappedAt(`API fallback: ${apiUrl}`, panes.leftX, y, panes.leftWidth, term.height - 3);
79
-
80
- let rightY = 4;
81
- lineAt(panes.rightX, rightY++, panes.rightWidth, `${meta.primary_class_cd} | ${meta.skill_cd}`);
82
- lineAt(panes.rightX, rightY++, panes.rightWidth, `difficulty ${meta.difficulty} | ${meta.questionId}`);
83
- rightY++;
84
- lineAt(panes.rightX, rightY++, panes.rightWidth, "o open externally");
85
- lineAt(panes.rightX, rightY++, panes.rightWidth, "n/x/enter skip");
86
- rightY++;
87
- printWrappedAt(htmlToText(detail.stem), panes.rightX, rightY, panes.rightWidth, term.height - 3, true);
88
- }
89
-
90
32
  export function practiceQuestionUrl(question: PracticeQuestion): string {
91
33
  return `https://practicesat.vercel.app/question?questionId=${encodeURIComponent(question.meta.questionId)}`;
92
34
  }
package/src/tui/render.ts CHANGED
@@ -2,6 +2,8 @@ import { buildSummaryRows, displayStateDir, stateDir } from "../state.ts";
2
2
  import { focusSummary } from "../focus.ts";
3
3
  import { htmlToText, wrapText } from "../text.ts";
4
4
  import type { TextSegment } from "../text.ts";
5
+ import { progressBarText } from "../progress.ts";
6
+ import { Frame, FrameRenderer, TerminalFrameOutput, truncate, type TextAttr } from "./frame.ts";
5
7
  import { focusGrid, normalizeFocusGridPosition, type FocusGridColumn, type FocusGridRow } from "./focus-grid.ts";
6
8
  import { historyRows } from "./history.ts";
7
9
  import {
@@ -11,64 +13,59 @@ import {
11
13
  questionRows,
12
14
  } from "./question.ts";
13
15
  import { elapsedQuestionSeconds, formatElapsed, timerStatus } from "./timer.ts";
14
- import { terminalSize, tk, type TkDocument } from "./kit.ts";
16
+ import { term, terminalSize } from "./kit.ts";
15
17
  import { PANE_BODY_Y, PANE_HEADER_Y, paneLayout, paneViewportHeight } from "./layout.ts";
16
18
  import { detailPaneRows, practiceAnswerPaneRows, reviewPaneRows, type PaneTextRow } from "./pane-content.ts";
17
19
  import type { AppState, PaneLayout } from "./types.ts";
18
20
  import type { Attempt, Outcome, QuestionMeta, SummaryRow } from "../types.ts";
19
21
  import { clampScroll, maxScroll, type PaneViewport } from "./viewport.ts";
20
22
 
21
- type TextAttr = Record<string, unknown>;
22
-
23
- export function createDocument(): TkDocument {
24
- return new tk.Document({
25
- eventSource: tk.terminal,
26
- outputDst: tk.terminal,
27
- backgroundAttr: {},
28
- });
23
+ export function createFrameRenderer(): FrameRenderer {
24
+ return new FrameRenderer(new TerminalFrameOutput(term));
29
25
  }
30
26
 
31
- export function render(doc: TkDocument, state: AppState): void {
32
- doc.clear();
27
+ export function render(renderer: FrameRenderer, state: AppState): void {
28
+ const { width, height } = terminalSize();
29
+ const frame = new Frame(width, height);
33
30
 
34
31
  if (state.view === "practice" && state.timerPaused) {
35
- renderPaused(doc);
36
- footer(doc, state);
37
- doc.draw();
32
+ renderPaused(frame);
33
+ footer(frame, state);
34
+ renderer.draw(frame);
38
35
  return;
39
36
  }
40
37
 
41
- header(doc, state);
38
+ header(frame, state);
42
39
 
43
40
  if (state.view === "loading") {
44
- renderLoading(doc);
41
+ renderLoading(frame);
45
42
  } else if (state.view === "setup") {
46
- renderSetup(doc);
43
+ renderSetup(frame);
47
44
  } else if (state.view === "focus") {
48
- renderFocus(doc, state);
45
+ renderFocus(frame, state);
49
46
  } else if (state.view === "practice") {
50
- renderPractice(doc, state);
47
+ renderPractice(frame, state);
51
48
  } else if (state.view === "review") {
52
- renderReview(doc, state);
49
+ renderReview(frame, state);
53
50
  } else if (state.view === "history") {
54
- renderHistory(doc, state);
51
+ renderHistory(frame, state);
55
52
  } else if (state.view === "summary") {
56
- renderSummary(doc, state);
53
+ renderSummary(frame, state);
57
54
  } else if (state.view === "detail") {
58
- renderDetail(doc, state);
55
+ renderDetail(frame, state);
59
56
  } else if (state.view === "error") {
60
- renderError(doc, state);
57
+ renderError(frame, state);
61
58
  }
62
59
 
63
- footer(doc, state);
64
- doc.draw();
60
+ footer(frame, state);
61
+ renderer.draw(frame);
65
62
  }
66
63
 
67
- function renderLoading(doc: TkDocument): void {
64
+ function renderLoading(doc: Frame): void {
68
65
  text(doc, 0, 3, "Loading...");
69
66
  }
70
67
 
71
- function renderSetup(doc: TkDocument): void {
68
+ function renderSetup(doc: Frame): void {
72
69
  const { width } = terminalSize();
73
70
  const location = displayStateDir(stateDir);
74
71
 
@@ -78,22 +75,23 @@ function renderSetup(doc: TkDocument): void {
78
75
  text(doc, 0, 9, location, { color: "cyan" }, width);
79
76
  }
80
77
 
81
- function renderPaused(doc: TkDocument): void {
78
+ function renderPaused(doc: Frame): void {
82
79
  const { width, height } = terminalSize();
83
80
  const label = "PAUSED";
84
81
  text(doc, Math.max(0, Math.floor((width - label.length) / 2)), Math.max(0, Math.floor(height / 2)), label, { bold: true });
85
82
  }
86
83
 
87
- function header(doc: TkDocument, state: AppState): void {
84
+ function header(doc: Frame, state: AppState): void {
88
85
  const { width } = terminalSize();
89
86
  const answered = state.attempts.size;
87
+ const mode = state.reviewMode ? "review queue" : "reading/writing";
90
88
  const timer = state.timerHidden ? "" : ` time ${formatElapsed(elapsedQuestionSeconds(state))}${timerStatus(state)}`;
91
89
  text(doc, 0, 0, "sat", { bold: true });
92
- text(doc, 7, 0, `reading/writing answered ${answered}${timer}`, { color: "gray" }, width - 7);
90
+ text(doc, 7, 0, `${mode} answered ${answered}${timer}`, { color: "gray" }, width - 7);
93
91
  text(doc, 0, 1, "-".repeat(width), { color: "gray" }, width);
94
92
  }
95
93
 
96
- function footer(doc: TkDocument, state: AppState): void {
94
+ function footer(doc: Frame, state: AppState): void {
97
95
  const { width, height } = terminalSize();
98
96
  const controls = state.view === "setup"
99
97
  ? "y allow | n decline | q quit"
@@ -115,7 +113,7 @@ function footer(doc: TkDocument, state: AppState): void {
115
113
  text(doc, 0, Math.max(0, height - 1), controls, { color: "gray" }, width);
116
114
  }
117
115
 
118
- function renderFocus(doc: TkDocument, state: AppState): void {
116
+ function renderFocus(doc: Frame, state: AppState): void {
119
117
  const { width } = terminalSize();
120
118
  const columns = focusGrid(state.focus);
121
119
  const position = normalizeFocusGridPosition(columns, { column: state.focusColumn, row: state.focusRow });
@@ -131,7 +129,7 @@ function renderFocus(doc: TkDocument, state: AppState): void {
131
129
  renderDomainColumns(doc, columns.slice(1), position.column - 1, position.row);
132
130
  }
133
131
 
134
- function renderDifficultyColumn(doc: TkDocument, column: FocusGridColumn, selectedRow: number): void {
132
+ function renderDifficultyColumn(doc: Frame, column: FocusGridColumn, selectedRow: number): void {
135
133
  text(doc, 0, 6, column.title, { color: "cyan", bold: true });
136
134
 
137
135
  for (const [index, row] of column.rows.entries()) {
@@ -140,7 +138,7 @@ function renderDifficultyColumn(doc: TkDocument, column: FocusGridColumn, select
140
138
  }
141
139
  }
142
140
 
143
- function renderDomainColumns(doc: TkDocument, columns: FocusGridColumn[], selectedColumn: number, selectedRow: number): void {
141
+ function renderDomainColumns(doc: Frame, columns: FocusGridColumn[], selectedColumn: number, selectedRow: number): void {
144
142
  const { width } = terminalSize();
145
143
  const startY = 11;
146
144
  const gap = 4;
@@ -170,7 +168,7 @@ function focusOptionText(row: FocusGridRow, focused: boolean): string {
170
168
  }
171
169
 
172
170
  function writeFocusOption(
173
- doc: TkDocument,
171
+ doc: Frame,
174
172
  x: number,
175
173
  y: number,
176
174
  value: string,
@@ -182,7 +180,7 @@ function writeFocusOption(
182
180
  text(doc, x, y, value, attr, width);
183
181
  }
184
182
 
185
- function renderPractice(doc: TkDocument, state: AppState): void {
183
+ function renderPractice(doc: Frame, state: AppState): void {
186
184
  if (!state.question) {
187
185
  return;
188
186
  }
@@ -205,7 +203,7 @@ function renderPractice(doc: TkDocument, state: AppState): void {
205
203
  renderPaneRows(doc, answerContent, panes.rightX, panes.rightWidth, answerViewport, state.activePane === "answers");
206
204
  }
207
205
 
208
- function renderReview(doc: TkDocument, state: AppState): void {
206
+ function renderReview(doc: Frame, state: AppState): void {
209
207
  if (!state.question) {
210
208
  return;
211
209
  }
@@ -227,11 +225,14 @@ function renderReview(doc: TkDocument, state: AppState): void {
227
225
  renderPaneRows(doc, reviewContent, panes.rightX, panes.rightWidth, answerViewport, state.activePane === "answers");
228
226
  }
229
227
 
230
- function renderHistory(doc: TkDocument, state: AppState): void {
228
+ function renderHistory(doc: Frame, state: AppState): void {
231
229
  const attempts = historyRows(state);
232
230
  const { width, height } = terminalSize();
233
231
  let y = 3;
234
232
  text(doc, 0, y++, "answered questions", { color: "cyan", bold: true });
233
+ if (state.notice) {
234
+ text(doc, 0, y++, state.notice, { color: "yellow" }, width);
235
+ }
235
236
  y++;
236
237
 
237
238
  if (attempts.length === 0) {
@@ -239,7 +240,7 @@ function renderHistory(doc: TkDocument, state: AppState): void {
239
240
  return;
240
241
  }
241
242
 
242
- text(doc, 0, y++, " question status updated", { color: "gray" }, width);
243
+ text(doc, 0, y++, " question status skill updated", { color: "gray" }, width);
243
244
 
244
245
  const visibleRows = Math.max(1, height - 8);
245
246
  const start = Math.max(0, Math.min(state.historyIndex - visibleRows + 1, attempts.length - visibleRows));
@@ -249,7 +250,7 @@ function renderHistory(doc: TkDocument, state: AppState): void {
249
250
  }
250
251
  }
251
252
 
252
- function renderSummary(doc: TkDocument, state: AppState): void {
253
+ function renderSummary(doc: Frame, state: AppState): void {
253
254
  const { width } = terminalSize();
254
255
  let y = 3;
255
256
  text(doc, 0, y++, "stats summary", { color: "cyan", bold: true });
@@ -260,7 +261,7 @@ function renderSummary(doc: TkDocument, state: AppState): void {
260
261
  }
261
262
  }
262
263
 
263
- function renderDetail(doc: TkDocument, state: AppState): void {
264
+ function renderDetail(doc: Frame, state: AppState): void {
264
265
  const panes = paneLayout();
265
266
  if (!state.detailQuestion) {
266
267
  text(doc, 0, 3, "Could not fetch details for this question.");
@@ -286,12 +287,12 @@ function renderDetail(doc: TkDocument, state: AppState): void {
286
287
  renderPaneRows(doc, detailContent, panes.rightX, panes.rightWidth, answerViewport, state.activePane === "answers");
287
288
  }
288
289
 
289
- function renderError(doc: TkDocument, state: AppState): void {
290
+ function renderError(doc: Frame, state: AppState): void {
290
291
  text(doc, 0, 3, "Something went wrong.", { bold: true, color: "red" });
291
292
  printWrappedAt(doc, state.error ?? "Unknown error.", 0, 5, terminalSize().width - 1, terminalSize().height - 4);
292
293
  }
293
294
 
294
- function renderUnsupportedQuestion(doc: TkDocument, state: AppState, panes: PaneLayout): void {
295
+ function renderUnsupportedQuestion(doc: Frame, state: AppState, panes: PaneLayout): void {
295
296
  if (!state.question) {
296
297
  return;
297
298
  }
@@ -351,7 +352,7 @@ function renderUnsupportedQuestion(doc: TkDocument, state: AppState, panes: Pane
351
352
  }
352
353
 
353
354
  function renderPaneHeaders(
354
- doc: TkDocument,
355
+ doc: Frame,
355
356
  panes: PaneLayout,
356
357
  state: AppState,
357
358
  questionViewport: PaneViewport,
@@ -362,7 +363,7 @@ function renderPaneHeaders(
362
363
  renderPaneHeader(doc, panes.rightX, panes.rightWidth, rightLabel, state.activePane === "answers", answerViewport);
363
364
  }
364
365
 
365
- function renderPaneHeader(doc: TkDocument, x: number, width: number, label: string, active: boolean, viewport: PaneViewport): void {
366
+ function renderPaneHeader(doc: Frame, x: number, width: number, label: string, active: boolean, viewport: PaneViewport): void {
366
367
  const attr = active ? { color: "cyan", underline: true } : { color: "gray" };
367
368
  text(doc, x, PANE_HEADER_Y, label, attr, width);
368
369
 
@@ -376,7 +377,7 @@ function renderPaneHeader(doc: TkDocument, x: number, width: number, label: stri
376
377
  }
377
378
 
378
379
  function renderQuestionRows(
379
- doc: TkDocument,
380
+ doc: Frame,
380
381
  rows: ReturnType<typeof questionRows>,
381
382
  x: number,
382
383
  width: number,
@@ -390,7 +391,7 @@ function renderQuestionRows(
390
391
  renderScrollGutter(doc, x, width, viewport, active);
391
392
  }
392
393
 
393
- function renderPaneRows(doc: TkDocument, rows: PaneTextRow[], x: number, width: number, viewport: PaneViewport, active: boolean): void {
394
+ function renderPaneRows(doc: Frame, rows: PaneTextRow[], x: number, width: number, viewport: PaneViewport, active: boolean): void {
394
395
  for (const [offset, row] of rows.slice(viewport.scroll, viewport.scroll + viewport.height).entries()) {
395
396
  text(doc, x, PANE_BODY_Y + offset, row.text, paneRowAttr(row), width);
396
397
  }
@@ -398,7 +399,7 @@ function renderPaneRows(doc: TkDocument, rows: PaneTextRow[], x: number, width:
398
399
  renderScrollGutter(doc, x, width, viewport, active);
399
400
  }
400
401
 
401
- function renderScrollGutter(doc: TkDocument, x: number, width: number, viewport: PaneViewport, active: boolean): void {
402
+ function renderScrollGutter(doc: Frame, x: number, width: number, viewport: PaneViewport, active: boolean): void {
402
403
  const scrollX = Math.min(terminalSize().width - 1, x + width);
403
404
  const attr = active ? { color: "cyan" } : { color: "gray" };
404
405
  if (viewport.scroll > 0) {
@@ -436,7 +437,7 @@ function paneRowAttr(row: PaneTextRow): TextAttr {
436
437
  return attr;
437
438
  }
438
439
 
439
- function renderStyledLine(doc: TkDocument, x: number, y: number, width: number, segments: TextSegment[], forceBold = false): void {
440
+ function renderStyledLine(doc: Frame, x: number, y: number, width: number, segments: TextSegment[], forceBold = false): void {
440
441
  let col = 0;
441
442
  for (const segment of segments) {
442
443
  if (col >= width) {
@@ -458,7 +459,7 @@ function renderStyledLine(doc: TkDocument, x: number, y: number, width: number,
458
459
  }
459
460
 
460
461
  function printWrappedAt(
461
- doc: TkDocument,
462
+ doc: Frame,
462
463
  value: string | undefined,
463
464
  x: number,
464
465
  y: number,
@@ -477,15 +478,16 @@ function printWrappedAt(
477
478
  return y;
478
479
  }
479
480
 
480
- function renderHistoryRow(doc: TkDocument, attempt: Attempt, selected: boolean, y: number, width: number): void {
481
+ function renderHistoryRow(doc: Frame, attempt: Attempt, selected: boolean, y: number, width: number): void {
481
482
  const strong = selected ? { bold: true } : {};
482
483
  text(doc, 0, y, selected ? ">" : " ", selected ? { color: "yellow", bold: true } : { color: "gray" }, 1);
483
484
  text(doc, 2, y, attempt.question_id, { color: selected ? "yellow" : "cyan", ...strong }, 10);
484
485
  text(doc, 13, y, attempt.outcome, { ...outcomeAttr(attempt.outcome), ...strong }, 9);
485
- text(doc, 24, y, attempt.updated_at, { color: selected ? "yellow" : "gray", ...strong }, width - 24);
486
+ text(doc, 24, y, attempt.skill ?? "-", { color: selected ? "yellow" : "green", ...strong }, 5);
487
+ text(doc, 31, y, shortTimestamp(attempt.updated_at), { color: selected ? "yellow" : "gray", ...strong }, width - 31);
486
488
  }
487
489
 
488
- function renderSummaryRow(doc: TkDocument, row: SummaryRow, y: number, width: number): void {
490
+ function renderSummaryRow(doc: Frame, row: SummaryRow, y: number, width: number): void {
489
491
  const value = summaryValue(row);
490
492
  const attr = summaryAttr(row);
491
493
 
@@ -499,7 +501,7 @@ function renderSummaryRow(doc: TkDocument, row: SummaryRow, y: number, width: nu
499
501
  }
500
502
 
501
503
  function renderQuestionMetadata(
502
- doc: TkDocument,
504
+ doc: Frame,
503
505
  meta: QuestionMeta,
504
506
  attempt: Attempt | undefined,
505
507
  x: number,
@@ -525,6 +527,19 @@ function formatSkill(meta: QuestionMeta): string {
525
527
  return meta.skill_desc ? `${meta.skill_cd} ${meta.skill_desc}` : meta.skill_cd;
526
528
  }
527
529
 
530
+ function shortTimestamp(value: string): string {
531
+ const date = new Date(value);
532
+ if (Number.isNaN(date.getTime())) {
533
+ return value;
534
+ }
535
+
536
+ const month = String(date.getMonth() + 1).padStart(2, "0");
537
+ const day = String(date.getDate()).padStart(2, "0");
538
+ const hour = String(date.getHours()).padStart(2, "0");
539
+ const minute = String(date.getMinutes()).padStart(2, "0");
540
+ return `${month}/${day} ${hour}:${minute}`;
541
+ }
542
+
528
543
  function outcomeAttr(outcome: Outcome): TextAttr {
529
544
  if (outcome === "correct") {
530
545
  return { color: "green" };
@@ -584,9 +599,7 @@ function summaryAttr(row: SummaryRow): TextAttr {
584
599
 
585
600
  function accuracyBar(value: string, width: number): string {
586
601
  const barWidth = Math.max(8, width - 2);
587
- const ratio = readRatio(value);
588
- const filled = Math.round(ratio * barWidth);
589
- return `[${"#".repeat(filled)}${"-".repeat(barWidth - filled)}]`;
602
+ return `[${progressBarText(readRatio(value), barWidth)}]`;
590
603
  }
591
604
 
592
605
  function readRatio(value: string): number {
@@ -597,34 +610,12 @@ function readRatio(value: string): number {
597
610
  return Math.min(1, Math.max(0, ratio));
598
611
  }
599
612
 
600
- function text(doc: TkDocument, x: number, y: number, value: string, attr: TextAttr = {}, width = terminalSize().width - x): void {
601
- if (y < 0 || y >= terminalSize().height || width <= 0) {
613
+ function text(doc: Frame, x: number, y: number, value: string, attr: TextAttr = {}, width = doc.width - x): void {
614
+ if (y < 0 || y >= doc.height || width <= 0) {
602
615
  return;
603
616
  }
604
617
 
605
- const content = truncate(value, width);
606
- if (!content) {
607
- return;
608
- }
609
-
610
- new tk.Text({
611
- parent: doc,
612
- x,
613
- y,
614
- content,
615
- attr,
616
- noDraw: true,
617
- });
618
- }
619
-
620
- function truncate(value: string, width: number): string {
621
- if (value.length <= width) {
622
- return value;
623
- }
624
- if (width <= 1) {
625
- return value.slice(0, width);
626
- }
627
- return `${value.slice(0, width - 1)}…`;
618
+ doc.writeText(x, y, value, attr, width);
628
619
  }
629
620
 
630
621
  function practiceControls(state: AppState): string {
package/src/tui/types.ts CHANGED
@@ -8,6 +8,8 @@ export type AppState = {
8
8
  attempts: Map<string, Attempt>;
9
9
  skippedIds: Set<string>;
10
10
  nextQuestion?: Promise<PracticeQuestion | undefined>;
11
+ reviewMode?: boolean;
12
+ reviewQuestionIds?: string[];
11
13
  focus: Focus;
12
14
  focusIndex: number;
13
15
  focusColumn: number;
@@ -26,6 +28,7 @@ export type AppState = {
26
28
  lastCorrect?: boolean;
27
29
  detailQuestion?: PracticeQuestion;
28
30
  historyIndex: number;
31
+ notice?: string;
29
32
  error?: string;
30
33
  };
31
34
 
@@ -19,6 +19,9 @@ declare module "terminal-kit" {
19
19
  underline: Terminal;
20
20
  inverse: Terminal;
21
21
  defaultColor: Terminal;
22
+ italic: Terminal;
23
+ styleReset(): Terminal;
24
+ eraseLineAfter(): Terminal;
22
25
  fullscreen(enabled: boolean): void;
23
26
  hideCursor(enabled?: boolean): void;
24
27
  grabInput(enabled?: boolean): void;
package/src/types.ts CHANGED
@@ -5,6 +5,23 @@ export type Attempt = {
5
5
  outcome: Outcome;
6
6
  updated_at: string;
7
7
  elapsed_seconds: number;
8
+ difficulty?: string;
9
+ domain?: string;
10
+ domain_desc?: string;
11
+ skill?: string;
12
+ skill_desc?: string;
13
+ };
14
+
15
+ export type AttemptEvent = {
16
+ question_id: string;
17
+ correct: boolean;
18
+ answered_at: string;
19
+ elapsed_seconds: number;
20
+ difficulty: string;
21
+ domain: string;
22
+ domain_desc?: string;
23
+ skill: string;
24
+ skill_desc?: string;
8
25
  };
9
26
 
10
27
  export type SummaryRow = {