tuiboard 0.8.2 → 0.8.5
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/.tuiboard/config.example.yaml +9 -0
- package/CHANGELOG.md +69 -0
- package/README.md +77 -0
- package/bin/tuiboard.ts +62 -5
- package/package.json +3 -1
- package/src/app.tsx +52 -0
- package/src/cli/headless.test.ts +202 -0
- package/src/cli/summary.ts +284 -0
- package/src/cli/task.ts +235 -0
- package/src/config/loader.ts +23 -0
- package/src/input/handleKey.ts +14 -0
- package/src/store/index.test.ts +1 -0
- package/src/ui/Modal.tsx +5 -2
- package/src/ui/splash-boot.ts +21 -0
- package/src/ui/splash.ts +133 -0
- package/src/views/Dashboard.tsx +35 -7
- package/tsconfig.json +27 -0
package/src/ui/splash.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot splash — a raw-ANSI "tuiboard" wordmark printed the instant the process
|
|
3
|
+
* starts, so the ~1s cold-start window (Bun init + module load + store build +
|
|
4
|
+
* first calendar/agents read) isn't a blank terminal.
|
|
5
|
+
*
|
|
6
|
+
* Why raw ANSI and not an OpenTUI component: the slow part is *synchronous* and
|
|
7
|
+
* happens BEFORE OpenTUI mounts (createTuiStore alone is ~600ms), so a reactive
|
|
8
|
+
* component can't paint during it. We print straight to stdout first; when
|
|
9
|
+
* OpenTUI mounts it enters the alternate screen buffer (`?1049h`), which hides
|
|
10
|
+
* this splash and shows the dashboard. No animation — the main thread is busy
|
|
11
|
+
* the whole time — so we lean on a static wordmark with a subtle colour ramp.
|
|
12
|
+
*
|
|
13
|
+
* The wordmark is the FIGlet "Rectangles" font; the colour is the tool's light
|
|
14
|
+
* "today" yellow (#eaf6ad), rendered as a gentle top-to-bottom gradient.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** "tuiboard" in the FIGlet Rectangles font (4 glyph rows, 31 cols). */
|
|
18
|
+
const WORDMARK = [
|
|
19
|
+
" _ _ _ _ ",
|
|
20
|
+
"| |_ _ _|_| |_ ___ ___ ___ _| |",
|
|
21
|
+
"| _| | | | . | . | .'| _| . |",
|
|
22
|
+
"|_| |___|_|___|___|__,|_| |___|",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Top→bottom gradient of light yellows around the #eaf6ad "today" accent. */
|
|
26
|
+
const GRADIENT: Array<[number, number, number]> = [
|
|
27
|
+
[244, 250, 200], // #f4fac8
|
|
28
|
+
[238, 247, 182], // #eef7b6
|
|
29
|
+
[234, 246, 173], // #eaf6ad (the tool's todayPale)
|
|
30
|
+
[224, 239, 154], // #e0ef9a
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const SUBTITLE = "terminal kanban · agenda · agents";
|
|
34
|
+
|
|
35
|
+
const ESC = "\x1b[";
|
|
36
|
+
const RESET = `${ESC}0m`;
|
|
37
|
+
const HIDE_CURSOR = `${ESC}?25l`;
|
|
38
|
+
const SHOW_CURSOR = `${ESC}?25h`;
|
|
39
|
+
const fg = (r: number, g: number, b: number) => `${ESC}38;2;${r};${g};${b}m`;
|
|
40
|
+
const DIM = `${ESC}38;2;110;120;110m`; // muted grey-green for the sub-lines
|
|
41
|
+
|
|
42
|
+
/** Visible width of a string (the wordmark/subtitle are plain ASCII). */
|
|
43
|
+
function center(line: string, cols: number): string {
|
|
44
|
+
const pad = Math.max(0, Math.floor((cols - line.length) / 2));
|
|
45
|
+
return " ".repeat(pad) + line;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the full splash frame for a terminal of `cols`×`rows`. Clears the
|
|
50
|
+
* screen, vertically centres the block, and colours each wordmark row with its
|
|
51
|
+
* gradient shade. Returns the raw string to write.
|
|
52
|
+
*/
|
|
53
|
+
export function splashFrame(cols: number, rows: number, version: string): string {
|
|
54
|
+
const blockHeight = WORDMARK.length + 3; // wordmark + blank + subtitle + version
|
|
55
|
+
const top = Math.max(0, Math.floor((rows - blockHeight) / 2));
|
|
56
|
+
|
|
57
|
+
let out = `${ESC}2J${ESC}H`; // clear + home
|
|
58
|
+
out += "\n".repeat(top);
|
|
59
|
+
|
|
60
|
+
WORDMARK.forEach((line, i) => {
|
|
61
|
+
const [r, g, b] = GRADIENT[Math.min(i, GRADIENT.length - 1)]!;
|
|
62
|
+
out += fg(r, g, b) + center(line, cols) + RESET + "\n";
|
|
63
|
+
});
|
|
64
|
+
out += "\n";
|
|
65
|
+
out += DIM + center(SUBTITLE, cols) + RESET + "\n";
|
|
66
|
+
out += DIM + center(bootingLine(version, 0), cols) + RESET;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Cycling dot suffixes for the booting line — a gentle left-to-right wave.
|
|
71
|
+
* All frames are the same visible width so the centred line never jitters. */
|
|
72
|
+
const BOOT_FRAMES = [" ", "· ", "·· ", "···", " ··", " ·"];
|
|
73
|
+
|
|
74
|
+
/** The booting line text for animation frame `f` (without colour/centering). */
|
|
75
|
+
function bootingLine(version: string, f: number): string {
|
|
76
|
+
return `booting v${version} ${BOOT_FRAMES[f % BOOT_FRAMES.length]}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Animate the booting line in place (the launcher calls this while the child
|
|
81
|
+
* cold-starts). Rewrites just that one line — the cursor is already parked on
|
|
82
|
+
* it after `printSplash`. Returns a `stop` function the caller MUST invoke
|
|
83
|
+
* before the child takes the screen, so we never draw onto the dashboard.
|
|
84
|
+
* No-ops (returns a no-op stop) when output isn't an animatable TTY.
|
|
85
|
+
*/
|
|
86
|
+
export function animateBooting(version: string): () => void {
|
|
87
|
+
if (!process.stdout.isTTY || process.env.TUIBOARD_NO_SPLASH) return () => {};
|
|
88
|
+
const cols = process.stdout.columns ?? 0;
|
|
89
|
+
const rows = process.stdout.rows ?? 0;
|
|
90
|
+
if (cols < 34 || rows < 9) return () => {};
|
|
91
|
+
let f = 1;
|
|
92
|
+
const tick = () => {
|
|
93
|
+
try {
|
|
94
|
+
process.stdout.write(`\r${ESC}2K` + DIM + center(bootingLine(version, f), cols) + RESET);
|
|
95
|
+
f++;
|
|
96
|
+
} catch {
|
|
97
|
+
/* ignore */
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const handle = setInterval(tick, 230);
|
|
101
|
+
return () => clearInterval(handle);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Print the splash to stdout if it makes sense to: an interactive TTY, wide and
|
|
106
|
+
* tall enough not to garble, and not disabled via `TUIBOARD_NO_SPLASH`. Safe to
|
|
107
|
+
* call more than once; safe to call when not a TTY (it just no-ops).
|
|
108
|
+
*/
|
|
109
|
+
export function printSplash(version: string): void {
|
|
110
|
+
try {
|
|
111
|
+
if (!process.stdout.isTTY) return;
|
|
112
|
+
if (process.env.TUIBOARD_NO_SPLASH) return;
|
|
113
|
+
const cols = process.stdout.columns ?? 0;
|
|
114
|
+
const rows = process.stdout.rows ?? 0;
|
|
115
|
+
if (cols < 34 || rows < 9) return; // too small — skip rather than mangle
|
|
116
|
+
// Hide the terminal cursor so its blinking bar doesn't sit next to the
|
|
117
|
+
// booting dots. ALWAYS paired with showCursor() on exit (see splash-boot.ts
|
|
118
|
+
// and the bin launcher) so the shell never ends up cursor-less.
|
|
119
|
+
process.stdout.write(HIDE_CURSOR + splashFrame(cols, rows, version));
|
|
120
|
+
} catch {
|
|
121
|
+
// Cosmetic only — never let the splash break startup.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Restore the terminal cursor that the splash hid. Idempotent; safe to call on
|
|
126
|
+
* every exit path and when no splash was ever shown. */
|
|
127
|
+
export function showCursor(): void {
|
|
128
|
+
try {
|
|
129
|
+
if (process.stdout.isTTY) process.stdout.write(SHOW_CURSOR);
|
|
130
|
+
} catch {
|
|
131
|
+
/* ignore */
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -54,18 +54,46 @@ export function Dashboard(props: { store: TuiStore }) {
|
|
|
54
54
|
* Board + planner share BoardOnly because both live in the top-left
|
|
55
55
|
* zone of the normal layout and BoardOnly already respects ui.zoomed
|
|
56
56
|
* to render only the active panel between them.
|
|
57
|
+
*
|
|
58
|
+
* Modals: the normal layout drops them into the Agenda's slot, which doesn't
|
|
59
|
+
* exist while zoomed — so here the modal floats as a centered absolute overlay
|
|
60
|
+
* on top of the zoomed view. The user stays zoomed; close the modal and the
|
|
61
|
+
* zoomed view is exactly as they left it (no exit-zoom / re-zoom flip).
|
|
57
62
|
*/
|
|
58
63
|
function ZoomedLayout(props: { store: TuiStore }) {
|
|
59
|
-
const
|
|
64
|
+
const ui = () => props.store.state.ui;
|
|
65
|
+
const zone = () => ui().activeZone;
|
|
60
66
|
|
|
61
67
|
return (
|
|
62
|
-
|
|
63
|
-
<Show when={zone() === "
|
|
64
|
-
<
|
|
68
|
+
<>
|
|
69
|
+
<Show when={zone() === "timeline"} fallback={
|
|
70
|
+
<Show when={zone() === "agents"} fallback={<BoardOnly store={props.store} />}>
|
|
71
|
+
<AgentsOnly store={props.store} />
|
|
72
|
+
</Show>
|
|
73
|
+
}>
|
|
74
|
+
<TimelineOnly store={props.store} />
|
|
65
75
|
</Show>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
{/* Modal overlay — only while zoomed AND a modal is open. Absolute + high
|
|
77
|
+
zIndex so it paints over the zoomed view; centered; transparent
|
|
78
|
+
backdrop so the board stays visible behind the (opaque) modal panel. */}
|
|
79
|
+
<Show when={ui().modal}>
|
|
80
|
+
<box
|
|
81
|
+
style={{
|
|
82
|
+
position: "absolute",
|
|
83
|
+
top: 0,
|
|
84
|
+
left: 0,
|
|
85
|
+
right: 0,
|
|
86
|
+
bottom: 0,
|
|
87
|
+
zIndex: 100,
|
|
88
|
+
flexDirection: "column",
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
justifyContent: "center",
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<ModalLayer store={props.store} />
|
|
94
|
+
</box>
|
|
95
|
+
</Show>
|
|
96
|
+
</>
|
|
69
97
|
);
|
|
70
98
|
}
|
|
71
99
|
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext"],
|
|
7
|
+
"types": ["bun-types"],
|
|
8
|
+
|
|
9
|
+
"jsx": "preserve",
|
|
10
|
+
"jsxImportSource": "@opentui/solid",
|
|
11
|
+
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"noImplicitOverride": true,
|
|
15
|
+
"allowImportingTsExtensions": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"resolveJsonModule": true,
|
|
20
|
+
|
|
21
|
+
"baseUrl": ".",
|
|
22
|
+
"paths": {
|
|
23
|
+
"~/*": ["src/*"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"include": ["src/**/*"]
|
|
27
|
+
}
|