omegon 0.7.1 → 0.7.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.
@@ -14,7 +14,11 @@ import type { Component, TUI } from "@styrene-lab/pi-tui";
14
14
  import { truncateToWidth } from "@styrene-lab/pi-tui";
15
15
  import {
16
16
  LOGO_LINES,
17
+ WORDMARK_LINES,
17
18
  LINE_WIDTH,
19
+ COMPACT_LOGO_LINES,
20
+ COMPACT_LINE_WIDTH,
21
+ COMPACT_MARK_ROWS,
18
22
  FRAME_INTERVAL_MS,
19
23
  TOTAL_FRAMES,
20
24
  HOLD_FRAMES,
@@ -95,8 +99,9 @@ const PENDING_GLYPH = "· ";
95
99
  // ---------------------------------------------------------------------------
96
100
  class SplashHeader implements Component {
97
101
  private tui: TUI;
102
+ private lines: string[];
98
103
  private frame = 0;
99
- private frameMap = assignUnlockFrames(LOGO_LINES, TOTAL_FRAMES, Date.now() & 0xffff);
104
+ private frameMap: ReturnType<typeof assignUnlockFrames>;
100
105
  private noiseSeed = (Date.now() * 7) & 0x7fffffff;
101
106
  private timer: ReturnType<typeof setInterval> | null = null;
102
107
  private scanFrame = 0;
@@ -107,9 +112,16 @@ class SplashHeader implements Component {
107
112
  private cachedLines: string[] | undefined;
108
113
  private cachedWidth: number | undefined;
109
114
 
110
- constructor(tui: TUI, onTransition: () => void) {
115
+ private markRows: number;
116
+ private logoWidth: number;
117
+
118
+ constructor(tui: TUI, onTransition: () => void, lines: string[], markRows: number, logoWidth: number) {
111
119
  this.tui = tui;
112
120
  this.onTransition = onTransition;
121
+ this.lines = lines;
122
+ this.markRows = markRows;
123
+ this.logoWidth = logoWidth;
124
+ this.frameMap = assignUnlockFrames(lines, TOTAL_FRAMES, Date.now() & 0xffff);
113
125
  }
114
126
 
115
127
  start(): void {
@@ -147,14 +159,14 @@ class SplashHeader implements Component {
147
159
  const lines: string[] = [];
148
160
 
149
161
  // Centre the logo horizontally
150
- const logoW = LINE_WIDTH;
162
+ const logoW = this.logoWidth;
151
163
  const pad = Math.max(0, Math.floor((width - logoW) / 2));
152
164
  const padStr = " ".repeat(pad);
153
165
 
154
166
  // Render logo frame
155
167
  const logoFrame = this.transitioned
156
- ? renderFrame(TOTAL_FRAMES + 1, LOGO_LINES, this.frameMap, this.noiseSeed)
157
- : renderFrame(Math.min(this.frame, TOTAL_FRAMES), LOGO_LINES, this.frameMap, this.noiseSeed);
168
+ ? renderFrame(TOTAL_FRAMES + 1, this.lines, this.frameMap, this.noiseSeed, this.markRows)
169
+ : renderFrame(Math.min(this.frame, TOTAL_FRAMES), this.lines, this.frameMap, this.noiseSeed, this.markRows);
158
170
 
159
171
  lines.push(""); // top spacer
160
172
  for (const row of logoFrame) {
@@ -267,6 +279,112 @@ class BrandedHeader implements Component {
267
279
  }
268
280
  }
269
281
 
282
+ // ---------------------------------------------------------------------------
283
+ // Extension entry point
284
+ // ---------------------------------------------------------------------------
285
+ // ---------------------------------------------------------------------------
286
+ // Fullscreen splash replay component (easter egg)
287
+ // ---------------------------------------------------------------------------
288
+ class SplashReplay implements Component {
289
+ private tui: TUI;
290
+ private lines: string[];
291
+ private frame = 0;
292
+ private frameMap: ReturnType<typeof assignUnlockFrames>;
293
+ private noiseSeed = (Date.now() * 7) & 0x7fffffff;
294
+ private timer: ReturnType<typeof setInterval> | null = null;
295
+ private holdCount = 0;
296
+ private done: () => void;
297
+ private markRows: number;
298
+ private logoWidth: number;
299
+ private cachedLines: string[] | undefined;
300
+ private cachedWidth: number | undefined;
301
+
302
+ constructor(tui: TUI, done: () => void, lines: string[], markRows: number, logoWidth: number) {
303
+ this.tui = tui;
304
+ this.done = done;
305
+ this.lines = lines;
306
+ this.markRows = markRows;
307
+ this.logoWidth = logoWidth;
308
+ this.frameMap = assignUnlockFrames(lines, TOTAL_FRAMES, Date.now() & 0xffff);
309
+ }
310
+
311
+ start(): void {
312
+ this.timer = setInterval(() => this.tick(), FRAME_INTERVAL_MS);
313
+ }
314
+
315
+ private tick(): void {
316
+ this.frame++;
317
+ this.cachedLines = undefined;
318
+
319
+ if (this.frame >= TOTAL_FRAMES) {
320
+ this.holdCount++;
321
+ if (this.holdCount >= HOLD_FRAMES + 12) {
322
+ this.dispose();
323
+ this.done();
324
+ return;
325
+ }
326
+ }
327
+
328
+ this.tui.requestRender();
329
+ }
330
+
331
+ render(width: number): string[] {
332
+ if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
333
+
334
+ const height = process.stdout.rows ?? 24;
335
+ const lines: string[] = [];
336
+
337
+ const logoFrame = renderFrame(
338
+ Math.min(this.frame, TOTAL_FRAMES),
339
+ this.lines,
340
+ this.frameMap,
341
+ this.noiseSeed,
342
+ this.markRows,
343
+ );
344
+
345
+ // Vertically centre
346
+ const topPad = Math.max(0, Math.floor((height - logoFrame.length) / 2));
347
+ for (let i = 0; i < topPad; i++) lines.push("");
348
+
349
+ // Horizontally centre
350
+ const pad = Math.max(0, Math.floor((width - this.logoWidth) / 2));
351
+ const padStr = " ".repeat(pad);
352
+ for (const row of logoFrame) {
353
+ lines.push(truncateToWidth(padStr + row, width));
354
+ }
355
+
356
+ // Fill remaining
357
+ const remaining = height - lines.length;
358
+ for (let i = 0; i < remaining; i++) lines.push("");
359
+
360
+ this.cachedLines = lines;
361
+ this.cachedWidth = width;
362
+ return lines;
363
+ }
364
+
365
+ handleInput(input: string): boolean {
366
+ // Any key dismisses early
367
+ if (input) {
368
+ this.dispose();
369
+ this.done();
370
+ return true;
371
+ }
372
+ return false;
373
+ }
374
+
375
+ invalidate(): void {
376
+ this.cachedLines = undefined;
377
+ this.cachedWidth = undefined;
378
+ }
379
+
380
+ dispose(): void {
381
+ if (this.timer) {
382
+ clearInterval(this.timer);
383
+ this.timer = null;
384
+ }
385
+ }
386
+ }
387
+
270
388
  // ---------------------------------------------------------------------------
271
389
  // Extension entry point
272
390
  // ---------------------------------------------------------------------------
@@ -274,6 +392,42 @@ export default function splashExtension(pi: ExtensionAPI): void {
274
392
  // Initialise shared state immediately so other extensions can write to it
275
393
  getSharedState();
276
394
 
395
+ // Easter egg: /splash replays the animation fullscreen
396
+ pi.registerCommand("splash", {
397
+ description: "Replay the Omegon splash animation",
398
+ handler: async (_args, ctx) => {
399
+ if (!ctx.hasUI) return;
400
+ const termWidth = process.stdout.columns ?? 80;
401
+ const termRows = process.stdout.rows ?? 24;
402
+
403
+ // Pick the best art that fits
404
+ let artLines: string[];
405
+ let markRows: number;
406
+ let logoWidth: number;
407
+ const canFitFull = termWidth >= LINE_WIDTH + 4 && termRows >= LOGO_LINES.length + 4;
408
+ const canFitCompact = termWidth >= COMPACT_LINE_WIDTH + 4 && termRows >= COMPACT_LOGO_LINES.length + 4;
409
+ if (canFitFull) {
410
+ artLines = LOGO_LINES;
411
+ markRows = 31;
412
+ logoWidth = LINE_WIDTH;
413
+ } else if (canFitCompact) {
414
+ artLines = COMPACT_LOGO_LINES;
415
+ markRows = COMPACT_MARK_ROWS;
416
+ logoWidth = COMPACT_LINE_WIDTH;
417
+ } else {
418
+ artLines = WORDMARK_LINES;
419
+ markRows = 0;
420
+ logoWidth = LINE_WIDTH;
421
+ }
422
+
423
+ await ctx.ui.custom<void>((tui, _theme, _kb, done) => {
424
+ const replay = new SplashReplay(tui, () => done(undefined), artLines, markRows, logoWidth);
425
+ replay.start();
426
+ return replay;
427
+ });
428
+ },
429
+ });
430
+
277
431
  let version = "0.0.0";
278
432
 
279
433
  pi.on("session_start", async (_event, ctx) => {
@@ -295,15 +449,42 @@ export default function splashExtension(pi: ExtensionAPI): void {
295
449
  // after a version update), it renders as a one-liner below the splash.
296
450
  // This is acceptable — it only appears once per update.
297
451
  const termWidth = process.stdout.columns ?? 80;
298
- if (termWidth < LINE_WIDTH + 4) {
299
- // Too narrow for the ASCII art — use minimal header immediately
452
+ const termRows = process.stdout.rows ?? 24;
453
+
454
+ // Four tiers based on terminal size:
455
+ // Full (sigil + wordmark): needs ~46 rows and LINE_WIDTH+4 cols (~84 cols)
456
+ // Compact (smaller sigil + wordmark): needs ~34 rows and COMPACT_LINE_WIDTH+4 cols (~58 cols)
457
+ // Wordmark only: needs ~14 rows and LINE_WIDTH+4 cols
458
+ // Minimal (no animation): everything else
459
+ const canFitFull = termWidth >= LINE_WIDTH + 4 && termRows >= LOGO_LINES.length + 6;
460
+ const canFitCompact = termWidth >= COMPACT_LINE_WIDTH + 4 && termRows >= COMPACT_LOGO_LINES.length + 6;
461
+ const canFitWordmark = termWidth >= LINE_WIDTH + 4 && termRows >= WORDMARK_LINES.length + 6;
462
+
463
+ if (!canFitCompact && !canFitWordmark) {
464
+ // Too small for any animation — minimal branded header
300
465
  ctx.ui.setHeader(() => new BrandedHeader(version));
301
466
  } else {
467
+ let artLines: string[];
468
+ let markRows: number;
469
+ let logoWidth: number;
470
+ if (canFitFull) {
471
+ artLines = LOGO_LINES;
472
+ markRows = 31; // MARK_ROWS
473
+ logoWidth = LINE_WIDTH;
474
+ } else if (canFitCompact) {
475
+ artLines = COMPACT_LOGO_LINES;
476
+ markRows = COMPACT_MARK_ROWS;
477
+ logoWidth = COMPACT_LINE_WIDTH;
478
+ } else {
479
+ artLines = WORDMARK_LINES;
480
+ markRows = 0; // all wordmark
481
+ logoWidth = LINE_WIDTH;
482
+ }
302
483
  ctx.ui.setHeader((tui, _theme) => {
303
484
  const splash = new SplashHeader(tui, () => {
304
485
  // Transition to minimal branded header
305
486
  ctx.ui.setHeader((_, _t) => new BrandedHeader(version));
306
- });
487
+ }, artLines, markRows, logoWidth);
307
488
  splash.start();
308
489
  return splash;
309
490
  });
@@ -139,6 +139,7 @@ export function renderFrame(
139
139
  lines: string[],
140
140
  frameMap: FrameMap,
141
141
  noiseSeed: number,
142
+ markRows: number = MARK_ROWS,
142
143
  ): string[] {
143
144
  const rng = new SimpleRNG(noiseSeed + frame * 997);
144
145
  const output: string[] = [];
@@ -162,7 +163,7 @@ export function renderFrame(
162
163
  buf += " ";
163
164
  } else if (frame >= unlock) {
164
165
  // Resolved — final glyph
165
- const color = y >= MARK_ROWS + 2 ? `${BOLD}${BRIGHT}` : PRIMARY;
166
+ const color = y >= markRows + 1 ? `${BOLD}${BRIGHT}` : PRIMARY;
166
167
  if (color !== lastColor) { buf += color; lastColor = color; }
167
168
  buf += ch;
168
169
  } else {
@@ -190,5 +191,56 @@ export function renderFrame(
190
191
  // ---------------------------------------------------------------------------
191
192
  // Pre-computed data for the default logo
192
193
  // ---------------------------------------------------------------------------
193
- export { LOGO_LINES, LINE_WIDTH, MARK_ROWS };
194
+ /** Wordmark-only lines (spacer + 7 wordmark rows) for compact terminals. */
195
+ const WORDMARK_LINES: string[] = LOGO_LINES.slice(MARK_ROWS + 1); // skip sigil + first spacer, keep second spacer + wordmark
196
+ // Pad to same width
197
+ for (let i = 0; i < WORDMARK_LINES.length; i++) {
198
+ WORDMARK_LINES[i] = WORDMARK_LINES[i].padEnd(LINE_WIDTH);
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Compact logo — sigil + wordmark for mid-size terminals (~56 cols)
203
+ // ---------------------------------------------------------------------------
204
+ const COMPACT_MARK_ROWS = 23;
205
+
206
+ const COMPACT_LOGO_LINES: string[] = [
207
+ " * ``` #` ",
208
+ " ` ```##` ``````##` .#` ",
209
+ "````##`######### `############`##` ",
210
+ "*`*############## `##################` ",
211
+ "##:````*` `####` `#########` *#######:## ",
212
+ "` ##### ``####### ######`#` ",
213
+ " `##### #######. #########` ",
214
+ " ####`` ``*@@@@@@@@@@`* `## #### ",
215
+ " ##### `@@@@@@@@@@@@@@@@@@@ `#@ :` ",
216
+ " #####`@@@@@@@@@@@@@@@@@@@@@@@@` `#` ",
217
+ " ##*@@@@@@@@@@@@@@@@@@@@@@@@@@@` ",
218
+ " :@@@@@@@@@@@``##```@@@@@@@@@@@`` ",
219
+ " @@@@@@@@*#:` `#######`@@@@@@@@` ` ` ",
220
+ " @@@@@@@#####` `########`@@@@@@@`####`#` ",
221
+ " @@@@@@ ###### `#`#####`@@@@@@########` ",
222
+ " @@@@@ ###### `::``#*@@@@@`##` #### ",
223
+ " `@@@@@####### `@@@@@`###` `*## ",
224
+ " ``#` .@@@@`##### `@@@@@` ``###` `**",
225
+ " ``:######```@@@@@#` `.@@@@. `#.##` ",
226
+ " ######`####`* `@@@@@@ ``@@@@@` ``#####` ",
227
+ " #* .@@@@@@@@@@@@@@ :@@@@@@@@@@@@@@## ",
228
+ " ` .@@@@@@@@@@@@@@ :@@@@@@@@@@@@@@` ",
229
+ " .@ ` ` ",
230
+ // spacer
231
+ " ",
232
+ // wordmark (4 rows)
233
+ " @@@@@@@ @@@` `@@@ @@@@@@``@@@@@@ `@@@@@@@`@@@` @@ ",
234
+ " @@ @@ @@@@`@@@@ @@```` `@@` `@@ @@ @@@@ @@ ",
235
+ " @@ @@ @@ @*@`@@ @@@@` `@@`@@@ `@@ @@ @@ *@@@ ",
236
+ " @@@@@@@ @@ `@``@@ @@@@@@``@@@@@@ `@@@@@@@`@@ `@@ ",
237
+ ];
238
+
239
+ const COMPACT_LINE_WIDTH = Math.max(...COMPACT_LOGO_LINES.map(l => l.length));
240
+ for (let i = 0; i < COMPACT_LOGO_LINES.length; i++) {
241
+ COMPACT_LOGO_LINES[i] = COMPACT_LOGO_LINES[i].padEnd(COMPACT_LINE_WIDTH);
242
+ }
243
+
244
+ export { LOGO_LINES, WORDMARK_LINES, LINE_WIDTH, MARK_ROWS };
245
+ export { COMPACT_LOGO_LINES, COMPACT_LINE_WIDTH, COMPACT_MARK_ROWS };
194
246
  export { PRIMARY, PRIMARY_DIM, DIM, BRIGHT, SUCCESS, ERROR_CLR, RESET, BOLD };
@@ -471,9 +471,9 @@ function restartOmegon(): never {
471
471
  "done",
472
472
  // Extra grace period for fd/terminal release
473
473
  "sleep 0.2",
474
- // Pop kitty keyboard protocol and bracketed paste before resetting
475
- // stty sane only resets line discipline, not terminal protocol state
476
- "printf '\\033[<u\\033[>4;0m\\033[?2004l' 2>/dev/null",
474
+ // Full terminal protocol reset stty sane only resets line discipline,
475
+ // not terminal protocol state (kitty keyboard, bracketed paste, cursor, SGR)
476
+ "printf '\\033[<u\\033[>4;0m\\033[?2004l\\033[?25h\\033[0m\\033[r' 2>/dev/null",
477
477
  "stty sane 2>/dev/null",
478
478
  // Clean up this script
479
479
  `rm -f "${script}"`,
@@ -484,15 +484,20 @@ function restartOmegon(): never {
484
484
  // Reset terminal to cooked mode BEFORE exiting so the restart script
485
485
  // (and the user) aren't stuck with raw-mode terminal if something goes wrong.
486
486
  try {
487
- // Pop kitty keyboard protocol and disable bracketed paste BEFORE
488
- // releasing raw mode the terminal needs these escape sequences to
489
- // stop encoding keystrokes in CSI-u format. Without this, the restart
490
- // script (and any keystrokes during the wait) dump raw kitty sequences.
487
+ // Full terminal protocol teardown: pop kitty keyboard protocol,
488
+ // disable modifyOtherKeys, disable bracketed paste, show cursor,
489
+ // reset SGR attributes, and clear any pending scroll region.
491
490
  process.stdout.write(
492
491
  "\x1b[<u" + // Pop kitty keyboard protocol flags
493
492
  "\x1b[>4;0m" + // Disable modifyOtherKeys
494
- "\x1b[?2004l" // Disable bracketed paste
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
495
497
  );
498
+ // Pause stdin to prevent buffered input from being re-interpreted
499
+ // after raw mode is disabled (prevents Ctrl+D from closing parent shell).
500
+ process.stdin.pause();
496
501
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
497
502
  process.stdin.setRawMode(false);
498
503
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omegon",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",