tuiboard 0.8.2 → 0.8.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to **tuiboard** are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.3] - 2026-06-04
9
+
10
+ ### Added
11
+ - **Boot splash.** Launching tuiboard now paints a `tuiboard` wordmark (FIGlet
12
+ "Rectangles", in the tool's light-yellow accent) the instant the process
13
+ starts, so the ~1s cold start (runtime + store build + first calendar/agents
14
+ read) isn't a blank terminal. The launcher animates the booting dots while the
15
+ dashboard process loads in parallel, then hands the screen over cleanly — no
16
+ startup time added. Set `TUIBOARD_NO_SPLASH=1` to disable; it also no-ops when
17
+ output isn't a TTY or the terminal is tiny.
18
+
8
19
  ## [0.8.2] - 2026-06-04
9
20
 
10
21
  ### Changed
@@ -163,6 +174,7 @@ First public release on npm. This entry captures the full feature set at launch.
163
174
 
164
175
  Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
165
176
 
177
+ [0.8.3]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.3
166
178
  [0.8.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.2
167
179
  [0.8.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.1
168
180
  [0.8.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.0
package/README.md CHANGED
@@ -437,6 +437,9 @@ session (until the next terminal resize).
437
437
 
438
438
  See [CHANGELOG.md](CHANGELOG.md) for the full release history.
439
439
 
440
+ - **v0.8** — write to Google Calendar from the Agenda: create, edit, and delete
441
+ events (opt-in), set their date and time in the modal, plus all-day events in
442
+ the top strip, consistent `t`/`m` date shortcuts, and a boot splash.
440
443
  - **v0.7** — configurable zones: turn the planner, agenda, or agents view off
441
444
  (or start it collapsed) via the `zones:` config, so tuiboard can be a pure
442
445
  kanban, kanban + calendar, or any mix.
package/bin/tuiboard.ts CHANGED
@@ -11,10 +11,15 @@
11
11
  * any CLI args, and inherit stdio so the TUI keeps the real terminal.
12
12
  */
13
13
 
14
- import { spawnSync } from "node:child_process";
14
+ import { spawn } from "node:child_process";
15
+ import { existsSync, rmSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
15
17
  import { dirname, join } from "node:path";
16
18
  import { fileURLToPath } from "node:url";
17
19
 
20
+ import pkg from "../package.json";
21
+ import { animateBooting, printSplash, showCursor } from "../src/ui/splash.ts";
22
+
18
23
  const here = dirname(fileURLToPath(import.meta.url));
19
24
  const appPath = join(here, "..", "src", "app.tsx");
20
25
 
@@ -26,10 +31,54 @@ if (process.argv[2] === "calendar-setup") {
26
31
  }
27
32
  const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
28
33
 
29
- const result = spawnSync(
34
+ // Paint the splash from the (already-running) launcher and animate its booting
35
+ // dots while the child cold-starts. Using `spawn` (not `spawnSync`) keeps this
36
+ // process's event loop free to run the animation. We MUST stop animating the
37
+ // instant before the child enters OpenTUI's alternate screen, or our writes
38
+ // would land on the dashboard — so the child drops a "ready" flag file just
39
+ // before render() and we poll for it.
40
+ printSplash(pkg.version);
41
+ const stopAnim = animateBooting(pkg.version);
42
+ // The splash hides the cursor; make sure it comes back when the launcher exits
43
+ // (after the child has torn down), so the shell is never left cursor-less.
44
+ process.on("exit", showCursor);
45
+ const readyFlag = join(tmpdir(), `tuiboard-ready-${process.pid}`);
46
+ try { rmSync(readyFlag, { force: true }); } catch { /* ignore */ }
47
+
48
+ let poll: ReturnType<typeof setInterval> | undefined;
49
+ let safety: ReturnType<typeof setTimeout> | undefined;
50
+ let stopped = false;
51
+ const stopSplash = () => {
52
+ if (stopped) return;
53
+ stopped = true;
54
+ stopAnim();
55
+ if (poll) clearInterval(poll);
56
+ if (safety) clearTimeout(safety);
57
+ try { rmSync(readyFlag, { force: true }); } catch { /* ignore */ }
58
+ };
59
+ poll = setInterval(() => { if (existsSync(readyFlag)) stopSplash(); }, 30);
60
+ safety = setTimeout(stopSplash, 4000); // fallback if the child never signals
61
+
62
+ // Ctrl-C reaches the child directly (same process group); it cleans up and
63
+ // exits, then we mirror its code below. Ignore the signal here so the launcher
64
+ // doesn't die first and orphan the child mid-teardown.
65
+ process.on("SIGINT", () => {});
66
+ process.on("SIGTERM", () => {});
67
+
68
+ const child = spawn(
30
69
  process.execPath, // the bun binary running this script
31
70
  ["--preload", preload, appPath, ...process.argv.slice(2)],
32
- { stdio: "inherit" },
71
+ {
72
+ stdio: "inherit",
73
+ env: { ...process.env, TUIBOARD_SPLASH_DONE: "1", TUIBOARD_READY_FLAG: readyFlag },
74
+ },
33
75
  );
34
-
35
- process.exit(result.status ?? 1);
76
+ child.on("exit", (code, signal) => {
77
+ stopSplash();
78
+ process.exit(code ?? (signal ? 1 : 0));
79
+ });
80
+ child.on("error", (err) => {
81
+ stopSplash();
82
+ console.error(String(err));
83
+ process.exit(1);
84
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuiboard",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live Claude Code agent view. Use only the panels you want.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/app.tsx CHANGED
@@ -12,6 +12,10 @@
12
12
  * root layout component changes.
13
13
  */
14
14
 
15
+ // FIRST import on purpose: paints the boot splash before the heavy imports
16
+ // (OpenTUI) and the ~600ms store build below run. See ui/splash-boot.ts.
17
+ import "~/ui/splash-boot";
18
+
15
19
  import { createMemo } from "solid-js";
16
20
  import { render, useKeyboard } from "@opentui/solid";
17
21
 
@@ -150,4 +154,18 @@ function App() {
150
154
  );
151
155
  }
152
156
 
157
+ // Signal the launcher (if we were spawned by the `tuiboard` bin) that we're
158
+ // about to take the screen, so it stops animating the splash a beat before
159
+ // OpenTUI enters the alternate buffer — otherwise its writes would land on the
160
+ // dashboard. The short delay gives the launcher's poll a cycle to notice.
161
+ if (process.env.TUIBOARD_READY_FLAG) {
162
+ try {
163
+ const { writeFileSync } = await import("node:fs");
164
+ writeFileSync(process.env.TUIBOARD_READY_FLAG, "1");
165
+ await new Promise((r) => setTimeout(r, 70));
166
+ } catch {
167
+ // Cosmetic only — never block startup on the splash handshake.
168
+ }
169
+ }
170
+
153
171
  await render(() => <App />, { useMouse: true });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Side-effect module: paints the boot splash the moment it's imported.
3
+ *
4
+ * app.tsx imports this FIRST so the splash prints before `@opentui/solid`
5
+ * (and the ~600ms store build) load — ES modules run imported modules in
6
+ * source order, so a first-position side-effect import is the only way to
7
+ * paint before the heavy imports execute.
8
+ *
9
+ * When launched via the `tuiboard` bin, the launcher already printed the splash
10
+ * (and sets TUIBOARD_SPLASH_DONE), so this no-ops to avoid a double paint.
11
+ */
12
+
13
+ import pkg from "../../package.json";
14
+ import { printSplash, showCursor } from "./splash";
15
+
16
+ if (!process.env.TUIBOARD_SPLASH_DONE) printSplash(pkg.version);
17
+
18
+ // The splash hides the cursor; guarantee it's restored on every exit path of
19
+ // this process, so quitting never leaves the shell without a cursor. (OpenTUI
20
+ // also restores on clean exit; this is the belt-and-suspenders backstop.)
21
+ process.on("exit", showCursor);
@@ -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
+ }