omegon 0.7.3 → 0.7.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/extensions/00-splash/index.ts +96 -11
- package/extensions/bootstrap/index.ts +16 -18
- package/package.json +1 -1
|
@@ -43,10 +43,19 @@ export interface SplashItem {
|
|
|
43
43
|
state: "hidden" | "pending" | "active" | "done" | "failed";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface QueuedNotification {
|
|
47
|
+
message: string;
|
|
48
|
+
type: "info" | "warning" | "error";
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
export interface SplashState {
|
|
47
52
|
items: SplashItem[];
|
|
48
53
|
/** Set to true when all session_start hooks have returned */
|
|
49
54
|
loadingComplete: boolean;
|
|
55
|
+
/** True while splash is displayed — notifications should be queued */
|
|
56
|
+
active: boolean;
|
|
57
|
+
/** Queued notifications to flush after splash dismissal */
|
|
58
|
+
notificationQueue: QueuedNotification[];
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
function getSharedState(): SplashState {
|
|
@@ -61,6 +70,8 @@ function getSharedState(): SplashState {
|
|
|
61
70
|
{ label: "tools", state: "pending" },
|
|
62
71
|
],
|
|
63
72
|
loadingComplete: false,
|
|
73
|
+
active: true,
|
|
74
|
+
notificationQueue: [],
|
|
64
75
|
};
|
|
65
76
|
(globalThis as any)[SPLASH_KEY] = state;
|
|
66
77
|
}
|
|
@@ -111,6 +122,7 @@ class SplashHeader implements Component {
|
|
|
111
122
|
private onTransition: (() => void) | null = null;
|
|
112
123
|
private cachedLines: string[] | undefined;
|
|
113
124
|
private cachedWidth: number | undefined;
|
|
125
|
+
private promptBlink = false;
|
|
114
126
|
|
|
115
127
|
private markRows: number;
|
|
116
128
|
private logoWidth: number;
|
|
@@ -128,6 +140,21 @@ class SplashHeader implements Component {
|
|
|
128
140
|
this.timer = setInterval(() => this.tick(), FRAME_INTERVAL_MS);
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
/** Called externally (e.g. on keypress) to dismiss the splash. */
|
|
144
|
+
dismiss(): void {
|
|
145
|
+
if (this.transitioned) return;
|
|
146
|
+
this.transitioned = true;
|
|
147
|
+
this.dispose();
|
|
148
|
+
this.onTransition?.();
|
|
149
|
+
this.onTransition = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** True when the animation is done and loading is complete — ready for dismissal. */
|
|
153
|
+
get readyToDismiss(): boolean {
|
|
154
|
+
const state = getSharedState();
|
|
155
|
+
return this.animDone && this.holdCount >= HOLD_FRAMES && state.loadingComplete;
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
private tick(): void {
|
|
132
159
|
this.frame++;
|
|
133
160
|
this.scanFrame = (this.scanFrame + 1) % SCAN_FRAMES.length;
|
|
@@ -139,13 +166,9 @@ class SplashHeader implements Component {
|
|
|
139
166
|
|
|
140
167
|
if (this.animDone && !this.transitioned) {
|
|
141
168
|
this.holdCount++;
|
|
142
|
-
|
|
143
|
-
if (this.holdCount
|
|
144
|
-
this.
|
|
145
|
-
this.dispose();
|
|
146
|
-
this.onTransition?.();
|
|
147
|
-
this.onTransition = null; // prevent double-fire
|
|
148
|
-
return;
|
|
169
|
+
// Blink the "press any key" prompt every ~500ms (10 frames at 50ms)
|
|
170
|
+
if (this.readyToDismiss && this.holdCount % 10 === 0) {
|
|
171
|
+
this.promptBlink = !this.promptBlink;
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
@@ -178,6 +201,15 @@ class SplashHeader implements Component {
|
|
|
178
201
|
lines.push(""); // spacer
|
|
179
202
|
const checklistLines = this.renderChecklist(width);
|
|
180
203
|
lines.push(...checklistLines);
|
|
204
|
+
|
|
205
|
+
// "Press any key" prompt when ready
|
|
206
|
+
if (this.readyToDismiss) {
|
|
207
|
+
lines.push(""); // spacer
|
|
208
|
+
const prompt = "press any key to continue";
|
|
209
|
+
const promptPad = Math.max(0, Math.floor((width - prompt.length) / 2));
|
|
210
|
+
const promptColor = this.promptBlink ? DIM : PRIMARY;
|
|
211
|
+
lines.push(" ".repeat(promptPad) + `${promptColor}${prompt}${RESET}`);
|
|
212
|
+
}
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
lines.push(""); // bottom spacer
|
|
@@ -460,8 +492,48 @@ export default function splashExtension(pi: ExtensionAPI): void {
|
|
|
460
492
|
const canFitCompact = termWidth >= COMPACT_LINE_WIDTH + 4 && termRows >= COMPACT_LOGO_LINES.length + 6;
|
|
461
493
|
const canFitWordmark = termWidth >= LINE_WIDTH + 4 && termRows >= WORDMARK_LINES.length + 6;
|
|
462
494
|
|
|
495
|
+
// -----------------------------------------------------------------------
|
|
496
|
+
// Notification queueing — intercept ctx.ui.notify while splash is active
|
|
497
|
+
// -----------------------------------------------------------------------
|
|
498
|
+
const state = getSharedState();
|
|
499
|
+
const originalNotify = ctx.ui.notify.bind(ctx.ui);
|
|
500
|
+
ctx.ui.notify = (message: string, type?: "info" | "warning" | "error") => {
|
|
501
|
+
if (state.active) {
|
|
502
|
+
state.notificationQueue.push({ message, type: type ?? "info" });
|
|
503
|
+
} else {
|
|
504
|
+
originalNotify(message, type);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
/** Flush queued notifications and restore original notify. */
|
|
509
|
+
const flushAndRestore = () => {
|
|
510
|
+
state.active = false;
|
|
511
|
+
ctx.ui.notify = originalNotify;
|
|
512
|
+
for (const n of state.notificationQueue) {
|
|
513
|
+
originalNotify(n.message, n.type);
|
|
514
|
+
}
|
|
515
|
+
state.notificationQueue.length = 0;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// -----------------------------------------------------------------------
|
|
519
|
+
// Splash header setup
|
|
520
|
+
// -----------------------------------------------------------------------
|
|
521
|
+
let activeSplash: SplashHeader | null = null;
|
|
522
|
+
|
|
523
|
+
const dismissSplash = () => {
|
|
524
|
+
if (!activeSplash) return;
|
|
525
|
+
activeSplash.dismiss();
|
|
526
|
+
activeSplash = null;
|
|
527
|
+
if (unsubInput) { unsubInput(); unsubInput = null; }
|
|
528
|
+
flushAndRestore();
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
let unsubInput: (() => void) | null = null;
|
|
532
|
+
|
|
463
533
|
if (!canFitCompact && !canFitWordmark) {
|
|
464
|
-
// Too small for any animation — minimal branded header
|
|
534
|
+
// Too small for any animation — minimal branded header, no splash gate
|
|
535
|
+
state.active = false;
|
|
536
|
+
ctx.ui.notify = originalNotify;
|
|
465
537
|
ctx.ui.setHeader(() => new BrandedHeader(version));
|
|
466
538
|
} else {
|
|
467
539
|
let artLines: string[];
|
|
@@ -484,10 +556,21 @@ export default function splashExtension(pi: ExtensionAPI): void {
|
|
|
484
556
|
const splash = new SplashHeader(tui, () => {
|
|
485
557
|
// Transition to minimal branded header
|
|
486
558
|
ctx.ui.setHeader((_, _t) => new BrandedHeader(version));
|
|
559
|
+
flushAndRestore();
|
|
487
560
|
}, artLines, markRows, logoWidth);
|
|
488
561
|
splash.start();
|
|
562
|
+
activeSplash = splash;
|
|
489
563
|
return splash;
|
|
490
564
|
});
|
|
565
|
+
|
|
566
|
+
// Listen for any keypress to dismiss splash once ready
|
|
567
|
+
unsubInput = ctx.ui.onTerminalInput((data) => {
|
|
568
|
+
if (activeSplash?.readyToDismiss && data) {
|
|
569
|
+
dismissSplash();
|
|
570
|
+
return { consume: true };
|
|
571
|
+
}
|
|
572
|
+
return undefined;
|
|
573
|
+
});
|
|
491
574
|
}
|
|
492
575
|
|
|
493
576
|
// Poll shared state to detect when all subsystems have reported.
|
|
@@ -503,17 +586,19 @@ export default function splashExtension(pi: ExtensionAPI): void {
|
|
|
503
586
|
}
|
|
504
587
|
}, FRAME_INTERVAL_MS);
|
|
505
588
|
|
|
506
|
-
// Safety timeout —
|
|
507
|
-
//
|
|
589
|
+
// Safety timeout — if an extension never reports done, force the checklist
|
|
590
|
+
// to complete so the "press any key" prompt appears. Does NOT auto-dismiss;
|
|
591
|
+
// the user must press a key.
|
|
508
592
|
const safetyTimer = setTimeout(() => {
|
|
509
593
|
clearInterval(pollTimer);
|
|
510
594
|
splashDone();
|
|
511
|
-
},
|
|
595
|
+
}, 5000);
|
|
512
596
|
|
|
513
597
|
// Clean up on early session exit to prevent timer leaks
|
|
514
598
|
pi.on("session_shutdown", async () => {
|
|
515
599
|
clearInterval(pollTimer);
|
|
516
600
|
clearTimeout(safetyTimer);
|
|
601
|
+
if (activeSplash) dismissSplash();
|
|
517
602
|
});
|
|
518
603
|
});
|
|
519
604
|
}
|
|
@@ -470,11 +470,18 @@ function restartOmegon(): never {
|
|
|
470
470
|
' [ "$_w" -ge 50 ] && break',
|
|
471
471
|
"done",
|
|
472
472
|
// Extra grace period for fd/terminal release
|
|
473
|
-
"sleep 0.
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
473
|
+
"sleep 0.3",
|
|
474
|
+
// Hard terminal reset: RIS (Reset to Initial State) clears ALL protocol
|
|
475
|
+
// state — kitty keyboard protocol, bracketed paste, mouse tracking,
|
|
476
|
+
// modifyOtherKeys, SGR, scroll regions, alternate screen, everything.
|
|
477
|
+
// This is what `reset` does internally and has worked since the VT100.
|
|
478
|
+
"printf '\\033c' 2>/dev/null",
|
|
477
479
|
"stty sane 2>/dev/null",
|
|
480
|
+
// `reset` as belt-and-suspenders — reinitializes terminfo state.
|
|
481
|
+
// Some terminals (Kitty) maintain protocol state that RIS alone
|
|
482
|
+
// doesn't fully clear; reset queries terminfo and sends the full
|
|
483
|
+
// initialization sequence for the current TERM.
|
|
484
|
+
"reset 2>/dev/null",
|
|
478
485
|
// Clean up this script
|
|
479
486
|
`rm -f "${script}"`,
|
|
480
487
|
// Replace this shell with new omegon
|
|
@@ -484,26 +491,17 @@ function restartOmegon(): never {
|
|
|
484
491
|
// Reset terminal to cooked mode BEFORE exiting so the restart script
|
|
485
492
|
// (and the user) aren't stuck with raw-mode terminal if something goes wrong.
|
|
486
493
|
try {
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
process.stdout.write(
|
|
491
|
-
"\x1b[<u" + // Pop kitty keyboard protocol flags
|
|
492
|
-
"\x1b[>4;0m" + // Disable modifyOtherKeys
|
|
493
|
-
"\x1b[?2004l" + // Disable bracketed paste
|
|
494
|
-
"\x1b[?25h" + // Show cursor
|
|
495
|
-
"\x1b[0m" + // Reset all SGR attributes
|
|
496
|
-
"\x1b[r" // Reset scroll region to full screen
|
|
497
|
-
);
|
|
494
|
+
// RIS (Reset to Initial State) — the only reliable way to ensure ALL
|
|
495
|
+
// terminal protocol state is cleared. Selective escape sequences are
|
|
496
|
+
// fragile and miss features we don't know about.
|
|
497
|
+
process.stdout.write("\x1bc");
|
|
498
498
|
// Pause stdin to prevent buffered input from being re-interpreted
|
|
499
499
|
// after raw mode is disabled (prevents Ctrl+D from closing parent shell).
|
|
500
500
|
process.stdin.pause();
|
|
501
501
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
502
502
|
process.stdin.setRawMode(false);
|
|
503
503
|
}
|
|
504
|
-
//
|
|
505
|
-
// Use /dev/null for stdout/stderr to prevent any stray output (including
|
|
506
|
-
// terminal bells) from reaching the user's terminal during the transition.
|
|
504
|
+
// stty sane resets line discipline to known-good state.
|
|
507
505
|
spawnSync("stty", ["sane"], { stdio: ["inherit", "ignore", "ignore"], timeout: 2000 });
|
|
508
506
|
} catch { /* best-effort */ }
|
|
509
507
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omegon",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Omegon — an opinionated distribution of pi (by Mario Zechner) with extensions for lifecycle management, memory, orchestration, and visualization",
|
|
5
5
|
"bin": {
|
|
6
6
|
"omegon": "bin/omegon.mjs",
|