@askalf/dario 3.38.6 → 4.0.1

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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * TUI rendering primitives — pure ANSI escape sequence helpers.
3
+ *
4
+ * Every function in this module is a pure string-returning helper. No
5
+ * side effects, no console writes. The App's render() loop accumulates
6
+ * these strings into a single buffer, then flushes once to stdout. This
7
+ * keeps render testable (assert string equality against fixtures) and
8
+ * keeps flicker minimal (single write call per frame).
9
+ *
10
+ * Color set: a deliberate 16-color subset of the ANSI palette plus a
11
+ * handful of 256-color brand accents. Reasoning: 16-color is universal,
12
+ * the brand greens (#00ff88-ish) are used sparingly for the dario
13
+ * accent and degrade gracefully on terminals that can't render them.
14
+ *
15
+ * What this module deliberately does NOT do:
16
+ * - Track cursor position (callers pass row/col explicitly)
17
+ * - Diff frames (full-screen redraw per frame is fast enough at
18
+ * ~3000 cells; complexity not worth it)
19
+ * - Handle terminal capabilities probing (use ANSI + assume modern)
20
+ */
21
+ /**
22
+ * Move the cursor to (row, col). 1-indexed, matching the ANSI spec —
23
+ * row=1 is the top line, col=1 is the leftmost column.
24
+ */
25
+ export declare function moveTo(row: number, col: number): string;
26
+ /** Hide the blinking cursor (TUI doesn't need it). */
27
+ export declare const hideCursor = "\u001B[?25l";
28
+ /** Restore the cursor. ALWAYS run on exit to leave the terminal sane. */
29
+ export declare const showCursor = "\u001B[?25h";
30
+ /** Enter the alternate screen buffer — TUI lives here so quit restores prior shell content. */
31
+ export declare const enterAltScreen = "\u001B[?1049h";
32
+ /** Leave the alternate screen buffer. Pair with enterAltScreen on exit. */
33
+ export declare const leaveAltScreen = "\u001B[?1049l";
34
+ /** Clear the entire screen and move cursor to home. */
35
+ export declare const clearScreen = "\u001B[2J\u001B[H";
36
+ /** Clear from cursor to end of line. */
37
+ export declare const clearLineRight = "\u001B[K";
38
+ /** Reset all SGR (color + style) attributes. */
39
+ export declare const reset = "\u001B[0m";
40
+ /**
41
+ * Foreground color names mapped to ANSI codes. `default` resets to
42
+ * the terminal's default foreground.
43
+ */
44
+ export declare const FG: {
45
+ readonly default: 39;
46
+ readonly black: 30;
47
+ readonly red: 31;
48
+ readonly green: 32;
49
+ readonly yellow: 33;
50
+ readonly blue: 34;
51
+ readonly magenta: 35;
52
+ readonly cyan: 36;
53
+ readonly white: 37;
54
+ readonly brightBlack: 90;
55
+ readonly brightRed: 91;
56
+ readonly brightGreen: 92;
57
+ readonly brightYellow: 93;
58
+ readonly brightBlue: 94;
59
+ readonly brightMagenta: 95;
60
+ readonly brightCyan: 96;
61
+ readonly brightWhite: 97;
62
+ };
63
+ /** Background color names mapped to ANSI codes. */
64
+ export declare const BG: {
65
+ readonly default: 49;
66
+ readonly black: 40;
67
+ readonly red: 41;
68
+ readonly green: 42;
69
+ readonly yellow: 43;
70
+ readonly blue: 44;
71
+ readonly magenta: 45;
72
+ readonly cyan: 46;
73
+ readonly white: 47;
74
+ readonly brightBlack: 100;
75
+ readonly brightRed: 101;
76
+ readonly brightGreen: 102;
77
+ readonly brightYellow: 103;
78
+ readonly brightBlue: 104;
79
+ readonly brightMagenta: 105;
80
+ readonly brightCyan: 106;
81
+ readonly brightWhite: 107;
82
+ };
83
+ export type FgColor = keyof typeof FG;
84
+ export type BgColor = keyof typeof BG;
85
+ /**
86
+ * Wrap text in foreground-color SGR codes. Resets to default fg at
87
+ * the end so subsequent uncolored text isn't accidentally colored.
88
+ *
89
+ * Example: `fg('green', 'OK')` → `\x1b[32mOK\x1b[39m`
90
+ */
91
+ export declare function fg(color: FgColor, text: string): string;
92
+ export declare function bg(color: BgColor, text: string): string;
93
+ export declare function bold(text: string): string;
94
+ export declare function dim(text: string): string;
95
+ export declare function inverse(text: string): string;
96
+ export declare function underline(text: string): string;
97
+ /**
98
+ * Brand accent — the askalf green (#00ff88-ish) via the 256-color
99
+ * palette index 48. Falls back gracefully on terminals that don't
100
+ * render 256-color (they show as bright green).
101
+ */
102
+ export declare function brand(text: string): string;
103
+ /**
104
+ * Visible-width of a string. ANSI escape sequences and zero-width
105
+ * sequences contribute 0. Tabs count as 1 (terminals vary; better
106
+ * to under- than over-estimate). Multi-byte characters (CJK, emoji)
107
+ * are NOT special-cased in v4.0 — the TUI's content is ASCII-dominant
108
+ * (model names, numbers, labels). A future revision can add a
109
+ * `string-width`-style lookup if non-ASCII becomes common.
110
+ */
111
+ export declare function visibleWidth(s: string): number;
112
+ /**
113
+ * Truncate `text` to at most `maxWidth` visible chars, appending `…`
114
+ * if anything was clipped. ANSI sequences within the truncated portion
115
+ * are preserved verbatim; truncation only counts visible characters.
116
+ *
117
+ * Example: truncate('hello world', 8) → 'hello w…'
118
+ */
119
+ export declare function truncate(text: string, maxWidth: number, ellipsis?: string): string;
120
+ /** Pad `text` (right-aligned by default 'left' fills right side) to `width`. */
121
+ export declare function pad(text: string, width: number, align?: 'left' | 'right' | 'center'): string;
122
+ /**
123
+ * Render a horizontal progress bar.
124
+ *
125
+ * value: 0..1 (clamped)
126
+ * width: total cell count of the bar
127
+ *
128
+ * Uses the full-block ▓ / shade ░ characters; passes through as plain
129
+ * ASCII on terminals that don't render the box-drawing set (they look
130
+ * like `?` but the layout still works).
131
+ */
132
+ export declare function progressBar(value: number, width: number, opts?: {
133
+ filled?: string;
134
+ empty?: string;
135
+ }): string;
136
+ /**
137
+ * Box-drawing characters for borders. Set picked to render well on
138
+ * most terminals (Unicode box-drawing). Override via `customChars` if
139
+ * a terminal renders these badly — but the default targets the
140
+ * 99%-of-users case.
141
+ */
142
+ export declare const BOX: {
143
+ readonly topLeft: "┌";
144
+ readonly topRight: "┐";
145
+ readonly bottomLeft: "└";
146
+ readonly bottomRight: "┘";
147
+ readonly horizontal: "─";
148
+ readonly vertical: "│";
149
+ readonly cross: "┼";
150
+ readonly tLeft: "├";
151
+ readonly tRight: "┤";
152
+ readonly tTop: "┬";
153
+ readonly tBottom: "┴";
154
+ };
155
+ /**
156
+ * Render a box at (row, col) with the given width and height. Returns
157
+ * the full ANSI string (positioned writes + box characters). The
158
+ * interior is NOT cleared — callers paint inside the box separately.
159
+ */
160
+ export declare function drawBox(row: number, col: number, width: number, height: number): string;
161
+ /**
162
+ * Frame builder. The App's render() collects strings into one of these
163
+ * then calls `flush(stdout)` for a single write. Single-write rendering
164
+ * eliminates flicker on most terminals.
165
+ */
166
+ export declare class Frame {
167
+ private chunks;
168
+ /** Append a string. No positioning — caller is responsible. */
169
+ write(s: string): void;
170
+ /** Append a positioned write at (row, col). */
171
+ writeAt(row: number, col: number, s: string): void;
172
+ /** Return the accumulated frame as a single string. */
173
+ toString(): string;
174
+ /** Number of accumulated chunks (debugging aid). */
175
+ get length(): number;
176
+ /** Drop everything. Reuses the array allocation. */
177
+ clear(): void;
178
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * TUI rendering primitives — pure ANSI escape sequence helpers.
3
+ *
4
+ * Every function in this module is a pure string-returning helper. No
5
+ * side effects, no console writes. The App's render() loop accumulates
6
+ * these strings into a single buffer, then flushes once to stdout. This
7
+ * keeps render testable (assert string equality against fixtures) and
8
+ * keeps flicker minimal (single write call per frame).
9
+ *
10
+ * Color set: a deliberate 16-color subset of the ANSI palette plus a
11
+ * handful of 256-color brand accents. Reasoning: 16-color is universal,
12
+ * the brand greens (#00ff88-ish) are used sparingly for the dario
13
+ * accent and degrade gracefully on terminals that can't render them.
14
+ *
15
+ * What this module deliberately does NOT do:
16
+ * - Track cursor position (callers pass row/col explicitly)
17
+ * - Diff frames (full-screen redraw per frame is fast enough at
18
+ * ~3000 cells; complexity not worth it)
19
+ * - Handle terminal capabilities probing (use ANSI + assume modern)
20
+ */
21
+ // ── Escape sequences ────────────────────────────────────────────────
22
+ /** Control Sequence Introducer. */
23
+ const ESC = '\x1b[';
24
+ /**
25
+ * Move the cursor to (row, col). 1-indexed, matching the ANSI spec —
26
+ * row=1 is the top line, col=1 is the leftmost column.
27
+ */
28
+ export function moveTo(row, col) {
29
+ return `${ESC}${row};${col}H`;
30
+ }
31
+ /** Hide the blinking cursor (TUI doesn't need it). */
32
+ export const hideCursor = `${ESC}?25l`;
33
+ /** Restore the cursor. ALWAYS run on exit to leave the terminal sane. */
34
+ export const showCursor = `${ESC}?25h`;
35
+ /** Enter the alternate screen buffer — TUI lives here so quit restores prior shell content. */
36
+ export const enterAltScreen = `${ESC}?1049h`;
37
+ /** Leave the alternate screen buffer. Pair with enterAltScreen on exit. */
38
+ export const leaveAltScreen = `${ESC}?1049l`;
39
+ /** Clear the entire screen and move cursor to home. */
40
+ export const clearScreen = `${ESC}2J${ESC}H`;
41
+ /** Clear from cursor to end of line. */
42
+ export const clearLineRight = `${ESC}K`;
43
+ /** Reset all SGR (color + style) attributes. */
44
+ export const reset = `${ESC}0m`;
45
+ // ── Colors (16-color ANSI + brand accent) ──────────────────────────
46
+ /**
47
+ * Foreground color names mapped to ANSI codes. `default` resets to
48
+ * the terminal's default foreground.
49
+ */
50
+ export const FG = {
51
+ default: 39,
52
+ black: 30, red: 31, green: 32, yellow: 33,
53
+ blue: 34, magenta: 35, cyan: 36, white: 37,
54
+ brightBlack: 90, brightRed: 91, brightGreen: 92, brightYellow: 93,
55
+ brightBlue: 94, brightMagenta: 95, brightCyan: 96, brightWhite: 97,
56
+ };
57
+ /** Background color names mapped to ANSI codes. */
58
+ export const BG = {
59
+ default: 49,
60
+ black: 40, red: 41, green: 42, yellow: 43,
61
+ blue: 44, magenta: 45, cyan: 46, white: 47,
62
+ brightBlack: 100, brightRed: 101, brightGreen: 102, brightYellow: 103,
63
+ brightBlue: 104, brightMagenta: 105, brightCyan: 106, brightWhite: 107,
64
+ };
65
+ /**
66
+ * Wrap text in foreground-color SGR codes. Resets to default fg at
67
+ * the end so subsequent uncolored text isn't accidentally colored.
68
+ *
69
+ * Example: `fg('green', 'OK')` → `\x1b[32mOK\x1b[39m`
70
+ */
71
+ export function fg(color, text) {
72
+ return `${ESC}${FG[color]}m${text}${ESC}${FG.default}m`;
73
+ }
74
+ export function bg(color, text) {
75
+ return `${ESC}${BG[color]}m${text}${ESC}${BG.default}m`;
76
+ }
77
+ export function bold(text) {
78
+ return `${ESC}1m${text}${ESC}22m`;
79
+ }
80
+ export function dim(text) {
81
+ return `${ESC}2m${text}${ESC}22m`;
82
+ }
83
+ export function inverse(text) {
84
+ return `${ESC}7m${text}${ESC}27m`;
85
+ }
86
+ export function underline(text) {
87
+ return `${ESC}4m${text}${ESC}24m`;
88
+ }
89
+ /**
90
+ * Brand accent — the askalf green (#00ff88-ish) via the 256-color
91
+ * palette index 48. Falls back gracefully on terminals that don't
92
+ * render 256-color (they show as bright green).
93
+ */
94
+ export function brand(text) {
95
+ return `${ESC}38;5;48m${text}${ESC}${FG.default}m`;
96
+ }
97
+ // ── String helpers ─────────────────────────────────────────────────
98
+ /**
99
+ * Visible-width of a string. ANSI escape sequences and zero-width
100
+ * sequences contribute 0. Tabs count as 1 (terminals vary; better
101
+ * to under- than over-estimate). Multi-byte characters (CJK, emoji)
102
+ * are NOT special-cased in v4.0 — the TUI's content is ASCII-dominant
103
+ * (model names, numbers, labels). A future revision can add a
104
+ * `string-width`-style lookup if non-ASCII becomes common.
105
+ */
106
+ export function visibleWidth(s) {
107
+ // Strip ANSI escape sequences (CSI + everything up to terminating byte).
108
+ const stripped = s.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, '');
109
+ return stripped.length;
110
+ }
111
+ /**
112
+ * Truncate `text` to at most `maxWidth` visible chars, appending `…`
113
+ * if anything was clipped. ANSI sequences within the truncated portion
114
+ * are preserved verbatim; truncation only counts visible characters.
115
+ *
116
+ * Example: truncate('hello world', 8) → 'hello w…'
117
+ */
118
+ export function truncate(text, maxWidth, ellipsis = '…') {
119
+ if (maxWidth <= 0)
120
+ return '';
121
+ if (visibleWidth(text) <= maxWidth)
122
+ return text;
123
+ const ellipsisWidth = visibleWidth(ellipsis);
124
+ if (maxWidth <= ellipsisWidth)
125
+ return ellipsis.slice(0, maxWidth);
126
+ // Walk the string, counting visible chars, stop when we'd exceed
127
+ // maxWidth - ellipsisWidth so we have room to append.
128
+ const target = maxWidth - ellipsisWidth;
129
+ let out = '';
130
+ let visible = 0;
131
+ let i = 0;
132
+ while (i < text.length && visible < target) {
133
+ if (text[i] === '\x1b' && text[i + 1] === '[') {
134
+ // Copy the full escape sequence
135
+ const m = text.slice(i).match(/^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/);
136
+ if (m) {
137
+ out += m[0];
138
+ i += m[0].length;
139
+ continue;
140
+ }
141
+ }
142
+ out += text[i];
143
+ visible++;
144
+ i++;
145
+ }
146
+ return out + ellipsis;
147
+ }
148
+ /** Pad `text` (right-aligned by default 'left' fills right side) to `width`. */
149
+ export function pad(text, width, align = 'left') {
150
+ const w = visibleWidth(text);
151
+ if (w >= width)
152
+ return truncate(text, width);
153
+ const gap = width - w;
154
+ if (align === 'right')
155
+ return ' '.repeat(gap) + text;
156
+ if (align === 'center') {
157
+ const left = Math.floor(gap / 2);
158
+ const right = gap - left;
159
+ return ' '.repeat(left) + text + ' '.repeat(right);
160
+ }
161
+ return text + ' '.repeat(gap);
162
+ }
163
+ /**
164
+ * Render a horizontal progress bar.
165
+ *
166
+ * value: 0..1 (clamped)
167
+ * width: total cell count of the bar
168
+ *
169
+ * Uses the full-block ▓ / shade ░ characters; passes through as plain
170
+ * ASCII on terminals that don't render the box-drawing set (they look
171
+ * like `?` but the layout still works).
172
+ */
173
+ export function progressBar(value, width, opts = {}) {
174
+ const filled = opts.filled ?? '█';
175
+ const empty = opts.empty ?? '░';
176
+ const clamped = Math.max(0, Math.min(1, value));
177
+ const cells = Math.round(clamped * width);
178
+ return filled.repeat(cells) + empty.repeat(width - cells);
179
+ }
180
+ /**
181
+ * Box-drawing characters for borders. Set picked to render well on
182
+ * most terminals (Unicode box-drawing). Override via `customChars` if
183
+ * a terminal renders these badly — but the default targets the
184
+ * 99%-of-users case.
185
+ */
186
+ export const BOX = {
187
+ topLeft: '┌', topRight: '┐',
188
+ bottomLeft: '└', bottomRight: '┘',
189
+ horizontal: '─', vertical: '│',
190
+ cross: '┼', tLeft: '├', tRight: '┤',
191
+ tTop: '┬', tBottom: '┴',
192
+ };
193
+ /**
194
+ * Render a box at (row, col) with the given width and height. Returns
195
+ * the full ANSI string (positioned writes + box characters). The
196
+ * interior is NOT cleared — callers paint inside the box separately.
197
+ */
198
+ export function drawBox(row, col, width, height) {
199
+ if (width < 2 || height < 2)
200
+ return '';
201
+ const out = [];
202
+ // Top border
203
+ out.push(moveTo(row, col));
204
+ out.push(BOX.topLeft + BOX.horizontal.repeat(width - 2) + BOX.topRight);
205
+ // Sides
206
+ for (let r = 1; r < height - 1; r++) {
207
+ out.push(moveTo(row + r, col));
208
+ out.push(BOX.vertical);
209
+ out.push(moveTo(row + r, col + width - 1));
210
+ out.push(BOX.vertical);
211
+ }
212
+ // Bottom border
213
+ out.push(moveTo(row + height - 1, col));
214
+ out.push(BOX.bottomLeft + BOX.horizontal.repeat(width - 2) + BOX.bottomRight);
215
+ return out.join('');
216
+ }
217
+ // ── Frame buffer ───────────────────────────────────────────────────
218
+ /**
219
+ * Frame builder. The App's render() collects strings into one of these
220
+ * then calls `flush(stdout)` for a single write. Single-write rendering
221
+ * eliminates flicker on most terminals.
222
+ */
223
+ export class Frame {
224
+ chunks = [];
225
+ /** Append a string. No positioning — caller is responsible. */
226
+ write(s) {
227
+ this.chunks.push(s);
228
+ }
229
+ /** Append a positioned write at (row, col). */
230
+ writeAt(row, col, s) {
231
+ this.chunks.push(moveTo(row, col));
232
+ this.chunks.push(s);
233
+ }
234
+ /** Return the accumulated frame as a single string. */
235
+ toString() {
236
+ return this.chunks.join('');
237
+ }
238
+ /** Number of accumulated chunks (debugging aid). */
239
+ get length() {
240
+ return this.chunks.length;
241
+ }
242
+ /** Drop everything. Reuses the array allocation. */
243
+ clear() {
244
+ this.chunks.length = 0;
245
+ }
246
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Common interface every tab implements.
3
+ *
4
+ * A tab is a self-contained state machine with:
5
+ * - its own slice of state (typed locally; opaque to the parent)
6
+ * - a render function: state + viewport → string
7
+ * - a key handler: state + key → next state or undefined (no change)
8
+ * - optional lifecycle hooks for mount / unmount / periodic tick
9
+ *
10
+ * The TuiApp composes a fixed set of tabs; it doesn't dynamically
11
+ * register them at runtime. That keeps the type system happy (each
12
+ * tab's state is statically known to the parent) while still letting
13
+ * each tab evolve independently.
14
+ *
15
+ * Tabs that need to fetch data from the proxy receive a
16
+ * `TabContext` with the ProxyClient. Tabs that don't (e.g. Status,
17
+ * which reads local state only) can ignore it.
18
+ */
19
+ import type { Key } from './input.js';
20
+ import type { ProxyClient } from './proxy-client.js';
21
+ export interface TabContext<S = unknown> {
22
+ /** The proxy HTTP client. Tabs use it to fetch /analytics, subscribe to /analytics/stream, etc. */
23
+ client: ProxyClient;
24
+ /**
25
+ * Update the active tab's state slice. Tabs use this from async
26
+ * callbacks (HTTP responses, SSE messages, timers) — the synchronous
27
+ * render/onKey path returns the next state directly. The parent
28
+ * applies the updater to whatever tab is currently active.
29
+ *
30
+ * Loosely-typed across tab implementations; each tab passes its
31
+ * own S as a generic, the parent's dispatcher takes care of the
32
+ * type-erasure.
33
+ */
34
+ setState: (updater: Partial<S> | ((prev: S) => S)) => void;
35
+ /**
36
+ * Register a cleanup function that runs on tab unmount (user switches
37
+ * away) or App shutdown. Tabs use this to close SSE subscriptions,
38
+ * clear intervals, drop event listeners — anything stateful set up
39
+ * in onMount that won't garbage-collect on its own.
40
+ */
41
+ registerCleanup: (fn: () => void) => void;
42
+ }
43
+ /**
44
+ * A renderable tab. S is the tab's local state shape. The parent
45
+ * TuiApp holds a record of all tab states and dispatches based on
46
+ * the active tab id.
47
+ */
48
+ export interface Tab<S> {
49
+ /** Short id, must be unique. Used as the state-record key. */
50
+ id: string;
51
+ /** Human-readable label rendered in the tab strip. */
52
+ label: string;
53
+ /** Single-letter hotkey to jump to this tab. */
54
+ hotkey?: string;
55
+ /** Build the initial state. Called once on TuiApp construction. */
56
+ initialState(): S;
57
+ /**
58
+ * Render the tab's body region into a single string. Caller passes
59
+ * the dimensions of the body region (NOT the full screen — header,
60
+ * footer, and tab strip are drawn by the parent).
61
+ */
62
+ render(state: S, dim: {
63
+ cols: number;
64
+ rows: number;
65
+ }): string;
66
+ /**
67
+ * Handle a keypress. Return the next state (or undefined for no
68
+ * change). Global keys (Tab, q, Ctrl+C, hotkeys) are intercepted
69
+ * by the parent before reaching this; the tab only sees keys the
70
+ * parent didn't claim.
71
+ */
72
+ onKey?(state: S, key: Key): S | undefined;
73
+ /**
74
+ * Run once when this tab becomes active. May fire async work that
75
+ * calls `ctx.setState(...)` to update the slice once the data lands.
76
+ * The synchronous return value (if any) is applied before render.
77
+ */
78
+ onMount?(state: S, ctx: TabContext<S>): S | undefined | Promise<S | undefined>;
79
+ /** Run once when this tab loses focus (user switched tabs). */
80
+ onUnmount?(state: S): void;
81
+ /**
82
+ * Called by the parent's heartbeat (every 100ms by default). Tabs
83
+ * that need periodic refresh (Analytics polling, Hits stale-check)
84
+ * consult their state and decide whether to act. Don't do
85
+ * expensive work synchronously here; kick off async + call
86
+ * ctx.setState when it lands.
87
+ */
88
+ onTick?(state: S, ctx: TabContext<S>): void;
89
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Common interface every tab implements.
3
+ *
4
+ * A tab is a self-contained state machine with:
5
+ * - its own slice of state (typed locally; opaque to the parent)
6
+ * - a render function: state + viewport → string
7
+ * - a key handler: state + key → next state or undefined (no change)
8
+ * - optional lifecycle hooks for mount / unmount / periodic tick
9
+ *
10
+ * The TuiApp composes a fixed set of tabs; it doesn't dynamically
11
+ * register them at runtime. That keeps the type system happy (each
12
+ * tab's state is statically known to the parent) while still letting
13
+ * each tab evolve independently.
14
+ *
15
+ * Tabs that need to fetch data from the proxy receive a
16
+ * `TabContext` with the ProxyClient. Tabs that don't (e.g. Status,
17
+ * which reads local state only) can ignore it.
18
+ */
19
+ export {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Accounts tab — list of OAuth subscription accounts in the pool.
3
+ *
4
+ * Read-mostly. Mutations (add/remove) require the CLI — the tab
5
+ * shows the relevant command in the footer.
6
+ *
7
+ * Layout:
8
+ *
9
+ * ┌─ Accounts ──────────────────────────────────────┐
10
+ * │ alias expires util5h util7d │
11
+ * │ ───── ─────── ────── ────── │
12
+ * │ default 7h 41m 12% 4% │
13
+ * │ alt expired 0% 0% │
14
+ * │ … │
15
+ * └─────────────────────────────────────────────────┘
16
+ * To add: `dario accounts add <alias>`
17
+ * To remove: `dario accounts remove <alias>`
18
+ */
19
+ import type { Tab } from '../tab.js';
20
+ export interface AccountsState {
21
+ loading: boolean;
22
+ accounts: Array<{
23
+ alias: string;
24
+ expiresAt: number;
25
+ /** Optional rate-limit fields populated when /accounts endpoint exists. */
26
+ util5h?: number;
27
+ util7d?: number;
28
+ }>;
29
+ error: string | null;
30
+ }
31
+ export declare const AccountsTab: Tab<AccountsState>;
32
+ export declare function refreshAccounts(): Promise<AccountsState>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Accounts tab — list of OAuth subscription accounts in the pool.
3
+ *
4
+ * Read-mostly. Mutations (add/remove) require the CLI — the tab
5
+ * shows the relevant command in the footer.
6
+ *
7
+ * Layout:
8
+ *
9
+ * ┌─ Accounts ──────────────────────────────────────┐
10
+ * │ alias expires util5h util7d │
11
+ * │ ───── ─────── ────── ────── │
12
+ * │ default 7h 41m 12% 4% │
13
+ * │ alt expired 0% 0% │
14
+ * │ … │
15
+ * └─────────────────────────────────────────────────┘
16
+ * To add: `dario accounts add <alias>`
17
+ * To remove: `dario accounts remove <alias>`
18
+ */
19
+ import { fg, dim, brand, pad } from '../render.js';
20
+ import { renderKvRow } from '../layout.js';
21
+ export const AccountsTab = {
22
+ id: 'accounts',
23
+ label: 'Accounts',
24
+ hotkey: 'a',
25
+ initialState() {
26
+ return { loading: true, accounts: [], error: null };
27
+ },
28
+ async onMount(_state, _ctx) {
29
+ return refreshAccounts();
30
+ },
31
+ onKey(state, key) {
32
+ if (key.name === 'printable' && key.ch === 'r' && !key.ctrl) {
33
+ return { ...state, loading: true };
34
+ }
35
+ return undefined;
36
+ },
37
+ render(state, dimv) {
38
+ const lines = [];
39
+ const w = dimv.cols;
40
+ lines.push(' ' + brand('Accounts'));
41
+ if (state.loading && state.accounts.length === 0) {
42
+ lines.push('');
43
+ lines.push(' ' + dim('Loading accounts…'));
44
+ return lines.join('\n');
45
+ }
46
+ if (state.accounts.length === 0) {
47
+ lines.push('');
48
+ lines.push(' ' + dim('No accounts in the pool.'));
49
+ lines.push(' ' + 'Add one: ' + fg('cyan', 'dario accounts add <alias>'));
50
+ return lines.join('\n');
51
+ }
52
+ // Header row
53
+ lines.push(' ' + dim(pad('alias', 20) + pad('expires', 16) + pad('source', 24)));
54
+ lines.push(' ' + dim('─'.repeat(Math.min(w - 4, 60))));
55
+ for (const acc of state.accounts) {
56
+ const aliasCol = pad(acc.alias, 20);
57
+ const expiresCol = pad(formatExpiry(acc.expiresAt), 16);
58
+ const sourceCol = '~/.dario/accounts/' + acc.alias + '.json';
59
+ lines.push(' ' + aliasCol + expiresCol + dim(sourceCol));
60
+ }
61
+ lines.push('');
62
+ lines.push(' ' + dim('Mutations via CLI:'));
63
+ lines.push(' ' + fg('cyan', 'dario accounts add <alias>'));
64
+ lines.push(' ' + fg('cyan', 'dario accounts remove <alias>'));
65
+ // Refresh hint
66
+ lines.push('');
67
+ lines.push(' ' + renderKvRow('', '', w - 2)); // spacer
68
+ lines.push(' ' + dim(`Press ${fg('cyan', 'r')} to refresh.`));
69
+ return lines.join('\n');
70
+ },
71
+ };
72
+ export async function refreshAccounts() {
73
+ try {
74
+ const { listAccountAliases, loadAllAccounts } = await import('../../accounts.js');
75
+ const aliases = await listAccountAliases();
76
+ if (aliases.length === 0) {
77
+ // Single-account / login.json path — show a synthetic "default"
78
+ // entry sourced from ~/.dario/credentials.json or keychain.
79
+ return { loading: false, accounts: [], error: null };
80
+ }
81
+ const all = await loadAllAccounts();
82
+ return {
83
+ loading: false,
84
+ accounts: all.map(a => ({
85
+ alias: a.alias,
86
+ expiresAt: a.expiresAt,
87
+ })),
88
+ error: null,
89
+ };
90
+ }
91
+ catch (e) {
92
+ return { loading: false, accounts: [], error: e.message };
93
+ }
94
+ }
95
+ function formatExpiry(expiresAt) {
96
+ if (expiresAt === 0)
97
+ return dim('—');
98
+ const remainingMs = expiresAt - Date.now();
99
+ if (remainingMs < 0)
100
+ return fg('yellow', 'expired');
101
+ const hours = Math.floor(remainingMs / 3_600_000);
102
+ const minutes = Math.floor((remainingMs % 3_600_000) / 60_000);
103
+ if (hours > 24) {
104
+ const days = Math.floor(hours / 24);
105
+ return fg('green', `${days}d ${hours % 24}h`);
106
+ }
107
+ if (hours > 0)
108
+ return fg('green', `${hours}h ${minutes}m`);
109
+ return fg('green', `${minutes}m`);
110
+ }