@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.
- package/README.md +136 -25
- package/dist/analytics.d.ts +32 -3
- package/dist/analytics.js +48 -3
- package/dist/cc-template-data.json +36 -5
- package/dist/cli.js +111 -25
- package/dist/config-file.d.ts +157 -0
- package/dist/config-file.js +326 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.js +93 -23
- package/dist/tui/app.d.ts +96 -0
- package/dist/tui/app.js +178 -0
- package/dist/tui/input.d.ts +57 -0
- package/dist/tui/input.js +206 -0
- package/dist/tui/layout.d.ts +66 -0
- package/dist/tui/layout.js +152 -0
- package/dist/tui/proxy-client.d.ts +60 -0
- package/dist/tui/proxy-client.js +166 -0
- package/dist/tui/render.d.ts +178 -0
- package/dist/tui/render.js +246 -0
- package/dist/tui/tab.d.ts +89 -0
- package/dist/tui/tab.js +19 -0
- package/dist/tui/tabs/accounts.d.ts +32 -0
- package/dist/tui/tabs/accounts.js +110 -0
- package/dist/tui/tabs/analytics.d.ts +53 -0
- package/dist/tui/tabs/analytics.js +161 -0
- package/dist/tui/tabs/backends.d.ts +19 -0
- package/dist/tui/tabs/backends.js +77 -0
- package/dist/tui/tabs/config.d.ts +35 -0
- package/dist/tui/tabs/config.js +267 -0
- package/dist/tui/tabs/hits.d.ts +34 -0
- package/dist/tui/tabs/hits.js +223 -0
- package/dist/tui/tabs/status.d.ts +45 -0
- package/dist/tui/tabs/status.js +132 -0
- package/dist/tui/tui-app.d.ts +41 -0
- package/dist/tui/tui-app.js +217 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/tui/tab.js
ADDED
|
@@ -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
|
+
}
|