osdy-pi 0.1.8 → 1.2.0

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.
Files changed (39) hide show
  1. package/README.md +209 -161
  2. package/bin/osdy-pi.mjs +127 -0
  3. package/extensions/osdy-pi/account-profiles.test.ts +517 -0
  4. package/extensions/osdy-pi/account-profiles.ts +581 -0
  5. package/extensions/osdy-pi/animation.ts +47 -5
  6. package/extensions/osdy-pi/constants.ts +282 -103
  7. package/extensions/osdy-pi/diff-panel.ts +78 -24
  8. package/extensions/osdy-pi/editor-settings.test.ts +163 -0
  9. package/extensions/osdy-pi/editor-settings.ts +112 -0
  10. package/extensions/osdy-pi/metrics.ts +8 -135
  11. package/extensions/osdy-pi/plugin-events.ts +26 -0
  12. package/extensions/osdy-pi/profile-label.ts +29 -0
  13. package/extensions/osdy-pi/runtime-helpers.test.ts +26 -0
  14. package/extensions/osdy-pi/runtime-helpers.ts +87 -44
  15. package/extensions/osdy-pi/runtime.test.ts +62 -0
  16. package/extensions/osdy-pi/runtime.ts +256 -64
  17. package/extensions/osdy-pi/types.ts +33 -10
  18. package/extensions/osdy-pi/ui.test.ts +51 -0
  19. package/extensions/osdy-pi/ui.ts +298 -114
  20. package/extensions/osdy-pi/utils.ts +43 -1
  21. package/extensions/osdy-pi/working-tree.ts +25 -6
  22. package/package.json +22 -5
  23. package/scripts/osdy-pi-account-profiles.mjs +501 -0
  24. package/themes/osdy-pi-catppuccin-frappe.json +94 -0
  25. package/themes/osdy-pi-catppuccin-latte.json +94 -0
  26. package/themes/osdy-pi-catppuccin-macchiato.json +94 -0
  27. package/themes/osdy-pi-catppuccin-mocha.json +94 -0
  28. package/themes/osdy-pi-dark.json +0 -6
  29. package/themes/osdy-pi-dracula.json +83 -0
  30. package/themes/osdy-pi-kanagawa-dragon.json +91 -0
  31. package/themes/osdy-pi-kanagawa-lotus.json +92 -0
  32. package/themes/osdy-pi-kanagawa-wave.json +91 -0
  33. package/themes/osdy-pi-lucent-orange.json +80 -0
  34. package/themes/osdy-pi-matrix.json +83 -0
  35. package/themes/osdy-pi-new.json +91 -0
  36. package/themes/osdy-pi-sexy.json +84 -0
  37. package/themes/osdy-pi-tokyo-night.json +83 -0
  38. package/themes/osdy-pi-light.json +0 -84
  39. package/themes/osdy-pi-simple.json +0 -83
@@ -1,7 +1,6 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
2
  import type { HeaderVariant } from "./types.js";
3
3
 
4
- export const THEME_NAME = "osdy-pi-dark";
5
4
  export const ANIMATION_ENABLED = true;
6
5
  export const ANIMATION_INTERVAL_MS = 30;
7
6
  export const INTRO_ANIMATION_FRAMES = 28;
@@ -19,8 +18,11 @@ export const WORKING_SPINNER_FRAMES = [
19
18
  ];
20
19
  export const WORKING_WIDGET_KEY = "osdy-pi-working";
21
20
  export const WORKING_TREE_WIDGET_KEY = "osdy-pi-working-tree";
22
- export const MASCOT_MIN_ROWS = 34;
23
21
  export const MASCOT_GAP = 0;
22
+ export const COMPACT_HEADER_HIDDEN_COLUMNS = 72;
23
+ export const STACKED_CONTENT_MAX_ROWS = 32;
24
+ export const STACKED_HEADER_MAX_ROWS = 8;
25
+ export const STACKED_HEADER_MIN_ROWS = 4;
24
26
 
25
27
  const HTML_MASCOT = [
26
28
  " ▓▓▓▓▒ ",
@@ -81,11 +83,11 @@ const HTML_MASCOT_MAP = [
81
83
  " llllmddmddddddddhhhhhhhhhhlhhhhhhhlllmmmm ",
82
84
  " hllllmddddddmhhhhhhhhhhhhhllllllmmmh ",
83
85
  " dmmmmmmmmlllllllllllllmdmmm ",
84
- " mmmmmmmmmmmmmmmlllmm ",
86
+ " mmmmmmmmmmmmmmmlllmm m v c p ",
85
87
  " dlmmmmllllmmlmmllllllm dmllllmmmmmmmm ",
86
88
  " lllllmmlllllllllllllmmm lmllllllllhlmmmmm ",
87
89
  " mlllllmdmlllllllllmmmd mddmmmmlhhhlllmmmmm ",
88
- " hmmmmmmdmdmmmmmmmmmmm dddmmmmmmmmlllllmmdmm",
90
+ " hhmmmmmmdmdmmmmmmmmmmm dddmmmmmmmmlllllmmdmm",
89
91
  " mmmlllllhllllmmmmmmmm dmmllllmmmmmmmlllmdm ",
90
92
  " mlllhhhhhhhhlllllllmmdddmlllhllmmmmmmlmml ",
91
93
  " llllllhhhhhhhllllllllmmdddmmlllllmmmmdmmd ",
@@ -132,7 +134,19 @@ type HeaderPalette = {
132
134
  trailColor: string;
133
135
  };
134
136
 
135
- export type MascotToneKey = "b" | "h" | "l" | "m" | "d" | "p" | "c" | "v";
137
+ export const MASCOT_TONE_KEYS = {
138
+ background: "b",
139
+ bright: "h",
140
+ light: "l",
141
+ mid: "m",
142
+ dark: "d",
143
+ rightEdgeCyan: "p",
144
+ rightEdgeViolet: "c",
145
+ rightEdgeSilver: "v",
146
+ } as const;
147
+
148
+ export type MascotToneKey =
149
+ (typeof MASCOT_TONE_KEYS)[keyof typeof MASCOT_TONE_KEYS];
136
150
 
137
151
  export type MascotTonePalette = Record<MascotToneKey, string>;
138
152
 
@@ -141,9 +155,6 @@ type HeaderVariantConfig = {
141
155
  header: readonly string[];
142
156
  headerMap?: readonly string[];
143
157
  headerTonePalette?: MascotTonePalette;
144
- minRowsForFull?: number;
145
- fallbackHeader?: readonly string[];
146
- fallbackLinePalette?: (lineIndex: number) => HeaderPalette;
147
158
  mascot?: readonly string[];
148
159
  mascotMap?: readonly string[];
149
160
  linePalette: (lineIndex: number) => HeaderPalette;
@@ -160,22 +171,22 @@ const CLASSIC_PALETTE: HeaderPalette = {
160
171
  const CLASSIC_LINK_PALETTE: HeaderPalette = {
161
172
  baseColor: "mdLink",
162
173
  highlightColor: "mdHeading",
163
- trailColor: "mdLink",
174
+ trailColor: "accent",
164
175
  };
165
176
 
166
- const OSDY_THEME_PINK_PALETTE: HeaderPalette = {
177
+ const OSDY_THEME_CYAN_PALETTE: HeaderPalette = {
167
178
  baseColor: "accent",
168
179
  highlightColor: "mdHeading",
169
180
  trailColor: "mdLink",
170
181
  };
171
182
 
172
- const OSDY_THEME_CYAN_PALETTE: HeaderPalette = {
183
+ const OSDY_THEME_SILVER_PALETTE: HeaderPalette = {
173
184
  baseColor: "mdLink",
174
185
  highlightColor: "accent",
175
186
  trailColor: "mdHeading",
176
187
  };
177
188
 
178
- const OSDY_THEME_PURPLE_PALETTE: HeaderPalette = {
189
+ const OSDY_THEME_VIOLET_PALETTE: HeaderPalette = {
179
190
  baseColor: "mdHeading",
180
191
  highlightColor: "mdLink",
181
192
  trailColor: "accent",
@@ -188,21 +199,137 @@ const OSDY_THEME_MASCOT_PALETTE: HeaderPalette = {
188
199
  };
189
200
 
190
201
  const HTML_MASCOT_TONES: MascotTonePalette = {
191
- b: "mascotBg",
192
- h: "mascotBright",
193
- l: "mascotLight",
194
- m: "mascotMid",
195
- d: "mascotDark",
196
- p: "htmlPink",
197
- c: "mdLink",
198
- v: "mdHeading",
202
+ b: "#F9F7F2",
203
+ h: "#F0E5D7",
204
+ l: "#C7B4A1",
205
+ m: "#7D6F67",
206
+ d: "#2A2321",
207
+ p: "accent",
208
+ c: "mdHeading",
209
+ v: "mdLink",
199
210
  };
200
211
 
201
- type MascotArt = {
212
+ const RAW_BODY_TONE_KEYS = [
213
+ MASCOT_TONE_KEYS.background,
214
+ MASCOT_TONE_KEYS.bright,
215
+ MASCOT_TONE_KEYS.light,
216
+ MASCOT_TONE_KEYS.mid,
217
+ MASCOT_TONE_KEYS.dark,
218
+ ] as const;
219
+ const RAW_HEX_COLOR = /^#[0-9A-F]{6}$/;
220
+
221
+ function assertRawHexPalette(palette: MascotTonePalette): void {
222
+ for (const tone of RAW_BODY_TONE_KEYS) {
223
+ if (!RAW_HEX_COLOR.test(palette[tone])) {
224
+ throw new Error(`Mascot tone ${tone} must be a #RRGGBB color.`);
225
+ }
226
+ }
227
+ }
228
+
229
+ export type MascotArt = {
202
230
  mascot: readonly string[];
203
231
  toneMap: readonly string[];
204
232
  };
205
233
 
234
+ function mascotCodePointWidth(mascot: readonly string[]): number {
235
+ return Math.max(...mascot.map((line) => Array.from(line).length));
236
+ }
237
+
238
+ function trimMascotMargins(art: MascotArt): MascotArt {
239
+ const rowWidth = Math.max(
240
+ mascotCodePointWidth(art.mascot),
241
+ mascotCodePointWidth(art.toneMap),
242
+ );
243
+ const rows = art.mascot.map((line, index) => ({
244
+ mascot: Array.from(line.padEnd(rowWidth, " ")),
245
+ toneMap: Array.from((art.toneMap[index] ?? "").padEnd(rowWidth, " ")),
246
+ }));
247
+ const occupied = (rowIndex: number, columnIndex: number): boolean => {
248
+ const row = rows[rowIndex];
249
+ return (
250
+ row?.mascot[columnIndex] !== " " || row?.toneMap[columnIndex] !== " "
251
+ );
252
+ };
253
+ const rowIndexes = rows
254
+ .map((_row, rowIndex) => rowIndex)
255
+ .filter((rowIndex) =>
256
+ Array.from({ length: rowWidth }, (_value, columnIndex) =>
257
+ occupied(rowIndex, columnIndex),
258
+ ).some(Boolean),
259
+ );
260
+ const columnIndexes = Array.from(
261
+ { length: rowWidth },
262
+ (_value, columnIndex) =>
263
+ rows.some((_row, rowIndex) => occupied(rowIndex, columnIndex)),
264
+ );
265
+ const firstRow = rowIndexes[0] ?? 0;
266
+ const lastRow = rowIndexes.at(-1) ?? firstRow;
267
+ const firstColumn = columnIndexes.findIndex(Boolean);
268
+ const lastColumn = columnIndexes.lastIndexOf(true);
269
+ if (firstColumn < 0 || lastColumn < firstColumn) return art;
270
+ return {
271
+ mascot: rows
272
+ .slice(firstRow, lastRow + 1)
273
+ .map((row) => row.mascot.slice(firstColumn, lastColumn + 1).join("")),
274
+ toneMap: rows
275
+ .slice(firstRow, lastRow + 1)
276
+ .map((row) => row.toneMap.slice(firstColumn, lastColumn + 1).join("")),
277
+ };
278
+ }
279
+
280
+ function isMascotToneKey(value: string): value is MascotToneKey {
281
+ return (
282
+ value === MASCOT_TONE_KEYS.background ||
283
+ value === MASCOT_TONE_KEYS.bright ||
284
+ value === MASCOT_TONE_KEYS.light ||
285
+ value === MASCOT_TONE_KEYS.mid ||
286
+ value === MASCOT_TONE_KEYS.dark ||
287
+ value === MASCOT_TONE_KEYS.rightEdgeCyan ||
288
+ value === MASCOT_TONE_KEYS.rightEdgeViolet ||
289
+ value === MASCOT_TONE_KEYS.rightEdgeSilver
290
+ );
291
+ }
292
+
293
+ function assertMascotStructure(
294
+ art: MascotArt,
295
+ palette: MascotTonePalette,
296
+ ): void {
297
+ if (art.mascot.length === 0) {
298
+ throw new Error("Mascot art must contain at least one row.");
299
+ }
300
+ if (art.mascot.length !== art.toneMap.length) {
301
+ throw new Error("Mascot art and tone map must have the same row count.");
302
+ }
303
+
304
+ for (let rowIndex = 0; rowIndex < art.mascot.length; rowIndex += 1) {
305
+ const mascotLine = art.mascot[rowIndex] ?? "";
306
+ const toneLine = art.toneMap[rowIndex] ?? "";
307
+ const mascotCharacters = Array.from(mascotLine);
308
+ const toneCharacters = Array.from(toneLine);
309
+ if (mascotCharacters.length !== toneCharacters.length) {
310
+ throw new Error(`Mascot row ${rowIndex} and its tone map width differ.`);
311
+ }
312
+ for (
313
+ let columnIndex = 0;
314
+ columnIndex < toneCharacters.length;
315
+ columnIndex += 1
316
+ ) {
317
+ const tone = toneCharacters[columnIndex] ?? " ";
318
+ const character = mascotCharacters[columnIndex] ?? " ";
319
+ if (tone !== " " && !isMascotToneKey(tone)) {
320
+ throw new Error(
321
+ `Mascot row ${rowIndex} has an invalid tone key: ${tone}.`,
322
+ );
323
+ }
324
+ if (character !== " " && (!isMascotToneKey(tone) || !palette[tone])) {
325
+ throw new Error(
326
+ `Mascot row ${rowIndex} column ${columnIndex} has no usable tone.`,
327
+ );
328
+ }
329
+ }
330
+ }
331
+ }
332
+
206
333
  function sampleIndex(
207
334
  outputIndex: number,
208
335
  outputCount: number,
@@ -215,94 +342,113 @@ function sampleIndex(
215
342
  );
216
343
  }
217
344
 
218
- function sampleLine(
219
- line: string,
220
- outputWidth: number,
221
- inputWidth: number,
222
- ): string {
223
- const chars = Array.from(line.padEnd(inputWidth, " "));
345
+ function scaleLine(line: string, outputWidth: number): string {
346
+ const characters = Array.from(line);
224
347
  return Array.from({ length: outputWidth }, (_value, outputIndex) => {
225
- const inputIndex = sampleIndex(outputIndex, outputWidth, inputWidth);
226
- return chars[inputIndex] ?? " ";
348
+ const inputIndex = sampleIndex(outputIndex, outputWidth, characters.length);
349
+ return characters[inputIndex] ?? " ";
227
350
  }).join("");
228
351
  }
229
352
 
230
- function scaleMascotArt(
231
- mascot: readonly string[],
232
- toneMap: readonly string[],
233
- scale: number,
234
- ): MascotArt {
235
- const inputRows = Math.min(mascot.length, toneMap.length);
236
- const inputWidth = Math.max(
237
- ...mascot.slice(0, inputRows).map((line) => Array.from(line).length),
238
- ...toneMap.slice(0, inputRows).map((line) => Array.from(line).length),
239
- );
240
- const outputRows = Math.max(1, Math.round(inputRows * scale));
241
- const outputWidth = Math.max(1, Math.round(inputWidth * scale));
242
-
243
- return Array.from({ length: outputRows }, (_value, outputIndex) => {
244
- const inputIndex = sampleIndex(outputIndex, outputRows, inputRows);
245
- return {
246
- mascot: sampleLine(mascot[inputIndex] ?? "", outputWidth, inputWidth),
247
- toneMap: sampleLine(toneMap[inputIndex] ?? "", outputWidth, inputWidth),
248
- };
249
- }).reduce<MascotArt>(
250
- (accumulator, line) => ({
251
- mascot: [...accumulator.mascot, line.mascot],
252
- toneMap: [...accumulator.toneMap, line.toneMap],
353
+ function addRightEdgeGlow(art: MascotArt): MascotArt {
354
+ return {
355
+ mascot: art.mascot,
356
+ toneMap: art.toneMap.map((lineMap, lineIndex) => {
357
+ const tones = Array.from(lineMap);
358
+ const mascotLine = Array.from(art.mascot[lineIndex] ?? "");
359
+ let glowIndex = 0;
360
+ for (
361
+ let index = mascotLine.length - 1;
362
+ index >= 0 && glowIndex < 3;
363
+ index -= 1
364
+ ) {
365
+ if (mascotLine[index] !== " ") {
366
+ const glowTone = ["p", "c", "v"] as const;
367
+ tones[index] =
368
+ glowTone[glowIndex] ?? MASCOT_TONE_KEYS.rightEdgeSilver;
369
+ glowIndex += 1;
370
+ }
371
+ }
372
+ return tones.join("");
253
373
  }),
254
- { mascot: [], toneMap: [] },
255
- );
374
+ };
256
375
  }
257
376
 
258
- function addRightEdgeGlow(
259
- mascot: readonly string[],
260
- toneMap: readonly string[],
261
- ): readonly string[] {
262
- return toneMap.map((lineMap, lineIndex) => {
263
- const chars = Array.from(lineMap);
264
- const mascotLine = mascot[lineIndex] ?? "";
265
- const rightEdgeIndex = Math.max(
266
- ...Array.from(mascotLine).map((char, index) =>
267
- char === " " ? -1 : index,
268
- ),
269
- );
270
- if (rightEdgeIndex < 0) return lineMap;
271
- const glowTones = ["p", "c", "v"] as const;
272
- for (let offset = 0; offset < glowTones.length; offset += 1) {
273
- const glowTone = glowTones[offset];
274
- const index = rightEdgeIndex - offset;
275
- if (
276
- glowTone &&
277
- index >= 0 &&
278
- index < chars.length &&
279
- chars[index] !== " "
280
- ) {
281
- chars[index] = glowTone;
282
- }
283
- }
284
- return chars.join("");
285
- });
377
+ const ROSE_MASCOT = addRightEdgeGlow({
378
+ mascot: HTML_MASCOT,
379
+ toneMap: HTML_MASCOT_MAP,
380
+ });
381
+
382
+ assertRawHexPalette(HTML_MASCOT_TONES);
383
+ assertMascotStructure(ROSE_MASCOT, HTML_MASCOT_TONES);
384
+
385
+ type PreparedMascotArt = {
386
+ art: MascotArt;
387
+ width: number;
388
+ rows: number;
389
+ };
390
+
391
+ function prepareMascotArt(art: MascotArt): PreparedMascotArt {
392
+ assertMascotStructure(art, HTML_MASCOT_TONES);
393
+ const trimmed = trimMascotMargins(art);
394
+ return {
395
+ art: trimmed,
396
+ width: mascotCodePointWidth(trimmed.mascot),
397
+ rows: trimmed.mascot.length,
398
+ };
286
399
  }
287
400
 
288
- const OSDY_THEME_MASCOT_ART = scaleMascotArt(HTML_MASCOT, HTML_MASCOT_MAP, 0.9);
289
- const OSDY_THEME_MASCOT_MAP = addRightEdgeGlow(
290
- OSDY_THEME_MASCOT_ART.mascot,
291
- OSDY_THEME_MASCOT_ART.toneMap,
292
- );
401
+ export function mascotWidthForRows(
402
+ art: MascotArt,
403
+ maximumRows: number,
404
+ ): number {
405
+ const prepared = prepareMascotArt(art);
406
+ const heightScale = Math.min(
407
+ 1,
408
+ Math.max(1, Math.floor(maximumRows)) / prepared.rows,
409
+ );
410
+ return Math.max(1, Math.floor(prepared.width * heightScale));
411
+ }
412
+
413
+ export function scaleMascot(
414
+ art: MascotArt,
415
+ maximumWidth: number,
416
+ maximumRows: number,
417
+ ): MascotArt {
418
+ const prepared = prepareMascotArt(art);
419
+ const scale = Math.min(
420
+ 1,
421
+ Math.max(1, Math.floor(maximumWidth)) / prepared.width,
422
+ Math.max(1, Math.floor(maximumRows)) / prepared.rows,
423
+ );
424
+ const outputWidth = Math.max(1, Math.floor(prepared.width * scale));
425
+ const outputRows = Math.max(1, Math.floor(prepared.rows * scale));
426
+ const scaled = {
427
+ mascot: Array.from({ length: outputRows }, (_value, outputIndex) => {
428
+ const inputIndex = sampleIndex(outputIndex, outputRows, prepared.rows);
429
+ return scaleLine(prepared.art.mascot[inputIndex] ?? "", outputWidth);
430
+ }),
431
+ toneMap: Array.from({ length: outputRows }, (_value, outputIndex) => {
432
+ const inputIndex = sampleIndex(outputIndex, outputRows, prepared.rows);
433
+ return scaleLine(prepared.art.toneMap[inputIndex] ?? "", outputWidth);
434
+ }),
435
+ };
436
+ assertMascotStructure(scaled, HTML_MASCOT_TONES);
437
+ return scaled;
438
+ }
293
439
 
294
440
  function osdyThemePalette(lineIndex: number): HeaderPalette {
295
- if (lineIndex < 4) return OSDY_THEME_PINK_PALETTE;
296
- if (lineIndex < 8) return OSDY_THEME_CYAN_PALETTE;
297
- return OSDY_THEME_PURPLE_PALETTE;
441
+ if (lineIndex < 4) return OSDY_THEME_CYAN_PALETTE;
442
+ if (lineIndex < 8) return OSDY_THEME_SILVER_PALETTE;
443
+ return OSDY_THEME_VIOLET_PALETTE;
298
444
  }
299
445
 
300
446
  export const HEADER_VARIANTS: Record<HeaderVariant, HeaderVariantConfig> = {
301
447
  "osdy-theme": {
302
448
  label: "OsdyTheme",
303
449
  header: HEADER_SIMPLE,
304
- mascot: OSDY_THEME_MASCOT_ART.mascot,
305
- mascotMap: OSDY_THEME_MASCOT_MAP,
450
+ mascot: ROSE_MASCOT.mascot,
451
+ mascotMap: ROSE_MASCOT.toneMap,
306
452
  linePalette: osdyThemePalette,
307
453
  mascotPalette: OSDY_THEME_MASCOT_PALETTE,
308
454
  mascotTonePalette: HTML_MASCOT_TONES,
@@ -310,8 +456,8 @@ export const HEADER_VARIANTS: Record<HeaderVariant, HeaderVariantConfig> = {
310
456
  classic: {
311
457
  label: "Classic",
312
458
  header: HEADER_CLASSIC,
313
- mascot: HTML_MASCOT,
314
- mascotMap: HTML_MASCOT_MAP,
459
+ mascot: ROSE_MASCOT.mascot,
460
+ mascotMap: ROSE_MASCOT.toneMap,
315
461
  linePalette: (lineIndex) =>
316
462
  lineIndex >= 6 ? CLASSIC_LINK_PALETTE : CLASSIC_PALETTE,
317
463
  mascotPalette: OSDY_THEME_MASCOT_PALETTE,
@@ -319,6 +465,12 @@ export const HEADER_VARIANTS: Record<HeaderVariant, HeaderVariantConfig> = {
319
465
  },
320
466
  };
321
467
 
468
+ export type ScaledHeaderArt = {
469
+ header: readonly string[];
470
+ toneMap: readonly string[] | undefined;
471
+ sourceIndexes: number[];
472
+ };
473
+
322
474
  export function headerWidth(variant: HeaderVariant): number {
323
475
  return HEADER_VARIANTS[variant].header.reduce(
324
476
  (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
@@ -326,14 +478,41 @@ export function headerWidth(variant: HeaderVariant): number {
326
478
  );
327
479
  }
328
480
 
329
- export function mascotWidth(variant: HeaderVariant): number {
330
- return (HEADER_VARIANTS[variant].mascot ?? []).reduce(
331
- (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
332
- 0,
481
+ export function scaleHeader(
482
+ variant: HeaderVariant,
483
+ maximumWidth: number,
484
+ maximumRows: number,
485
+ ): ScaledHeaderArt {
486
+ const source = HEADER_VARIANTS[variant];
487
+ const inputWidth = headerWidth(variant);
488
+ const inputRows = source.header.length;
489
+ if (maximumWidth >= inputWidth && maximumRows >= inputRows) {
490
+ return {
491
+ header: source.header,
492
+ toneMap: source.headerMap,
493
+ sourceIndexes: source.header.map((_line, index) => index),
494
+ };
495
+ }
496
+ const scale = Math.min(
497
+ 1,
498
+ Math.max(1, Math.floor(maximumWidth)) / inputWidth,
499
+ Math.max(1, Math.floor(maximumRows)) / inputRows,
333
500
  );
501
+ const outputWidth = Math.max(1, Math.floor(inputWidth * scale));
502
+ const outputRows = Math.max(1, Math.floor(inputRows * scale));
503
+ const sourceIndexes = Array.from(
504
+ { length: outputRows },
505
+ (_value, outputIndex) => sampleIndex(outputIndex, outputRows, inputRows),
506
+ );
507
+ return {
508
+ header: sourceIndexes.map((sourceIndex) =>
509
+ scaleLine(source.header[sourceIndex] ?? "", outputWidth),
510
+ ),
511
+ toneMap: source.headerMap
512
+ ? sourceIndexes.map((sourceIndex) =>
513
+ scaleLine(source.headerMap?.[sourceIndex] ?? "", outputWidth),
514
+ )
515
+ : undefined,
516
+ sourceIndexes,
517
+ };
334
518
  }
335
-
336
- export const HEADER_FALLBACK = [
337
- "OSDY - PI",
338
- "</> Compilador de ideas",
339
- ] as const;