osdy-pi 1.11.9 → 1.11.10
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/README.md +6 -4
- package/extensions/osdy-pi/animation.test.ts +29 -2
- package/extensions/osdy-pi/constants.ts +48 -0
- package/extensions/osdy-pi/control-center-preferences.test.ts +4 -2
- package/extensions/osdy-pi/control-center-preferences.ts +5 -1
- package/extensions/osdy-pi/control-center.test.ts +16 -2
- package/extensions/osdy-pi/editor-settings.test.ts +13 -1
- package/extensions/osdy-pi/runtime.test.ts +10 -2
- package/extensions/osdy-pi/runtime.ts +1 -0
- package/extensions/osdy-pi/types.ts +1 -1
- package/extensions/osdy-pi/ui.test.ts +47 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Run **`/osdyConfig`** in interactive Pi to open the Osdy Control Center. The ten
|
|
|
63
63
|
- TODO shows configured selection, current eligibility, and actually loaded SDK registration separately. Opt-in/out requires an idle session and a Cancel-first inline confirmation naming the target, owned Gentle filter effects, and reload. Successful selection closes/disposes the panel and releases its editor hold before reloading once; restart Pi if reload fails. Rejections stay in the panel. No installs or task-history changes occur; legacy `/osdy-pi todo on|off|status` remains available.
|
|
64
64
|
- Agents shows concise **Joker / Gentle** choices without activating anything. With detail focus, visible lowercase-only **j Joker / g Gentle** letters open the existing confirmation directly; only available, unambiguous rows get shortcuts. **↑/↓ + Enter** remains the fallback. Profile and Agent selections survive category navigation; unavailable choices fall back to the current row. Letters never bypass category focus, confirmation, path input, loading, querying, or saving. Selection is idle-only and Cancel-first confirmed, naming the real target, possible Joker installation, owned agent filters, and preserved unrelated resources. Normal personal Pi only: `pi:dev` and isolated/override sessions are blocked, never redirected to personal settings. Success closes/disposes the panel and releases its editor hold before reloading once; restart Pi if reload fails. Legacy `/osdy-pi agents setup|on|off|status` is unchanged.
|
|
65
65
|
- Theme lists Pi's available themes and marks the current theme. Explicit **Page x/N** pages contain at most **8 themes**, fewer in short viewports or with expanded help. **Page Up/Down** changes pages, **Home/End** reaches the first/last theme, and arrows cross page boundaries. Selection survives category navigation and resizing; resizing adjusts the page around the selected theme. A successful selection applies live and saves globally in the active Pi agent directory; project settings can override it at startup. Failures appear in the modal.
|
|
66
|
-
- Header offers **osdy-theme / neon**; Mascot offers **current / Bts**; Editor offers **auto / extended / simple**, with the current preference marked. Editor also shows the effective mode: small terminals stay simple/native even with extended selected.
|
|
66
|
+
- Header offers **osdy-theme / neon**; Mascot offers **current / Bts / Osdy-Halloween**; Editor offers **auto / extended / simple**, with the current preference marked. Editor also shows the effective mode: small terminals stay simple/native even with extended selected.
|
|
67
67
|
- Preferences save immediately through the same runtime and global Osdy settings store as `/osdy-pi` commands (`<agent-dir>/extensions/osdy-pi/settings.json`). Header/Mascot apply live without closing the panel. Editor replacement waits until all overlays close; if another overlay remains, the next existing responsive watcher tick applies the latest choice. The effective mode reports the actually mounted editor while queued. Opening a category only reads values; while Osdy is disabled, choices save for later without enabling it.
|
|
68
68
|
- A selection shows **Saving** until persistence finishes and **Saved globally** only on success. Failed saves retain the live preference but report **could not be saved**; select again to retry. Application errors report **Not saved**, without claiming the runtime change succeeded. Duplicate selections are ignored while saving.
|
|
69
69
|
- **Git** shows the current branch (or detached HEAD), changed-entry count/clean status, and the current widget position as a read-only session detail. Working-tree enablement applies and saves through the same owner as `/osdy-pi working-tree`; Git inspection never writes Git. `/osdy-pi diff` and position commands are unchanged.
|
|
@@ -300,7 +300,7 @@ Before rename or removal, close this Pi process when it uses the target and **ma
|
|
|
300
300
|
| Area | Included behavior |
|
|
301
301
|
| --- | --- |
|
|
302
302
|
| Themes | 23 built-in themes, including Osdy, Kanagawa, Dracula, Gruvbox, Nord, Rosé Pine, Daniela Cute, Halloween, Halloween Killer, Catppuccin, Matrix, Lucent Orange, and three Spider-Man-inspired palettes |
|
|
303
|
-
| Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts` mascots |
|
|
303
|
+
| Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts`/`osdy-halloween` mascots |
|
|
304
304
|
| Messages | Native colored user cards without emojis and plain assistant Markdown with a separate assistant `🦝` row |
|
|
305
305
|
| Input | Responsive auto editor by default, with selectable simple Pi-native or extended framed modes |
|
|
306
306
|
| Status | Six activity labels with compact per-state glyphs and a shared theme-color pulse, responsive footer metrics, dynamic extension statuses, and Codex subscription quota with low-capacity emphasis |
|
|
@@ -366,10 +366,12 @@ Header and mascot choices are independent:
|
|
|
366
366
|
| Element | Choices | Command |
|
|
367
367
|
| --- | --- | --- |
|
|
368
368
|
| Header | `osdy-theme` (default), `neon` | `/osdy-pi header osdy-theme|neon` |
|
|
369
|
-
| Mascot | `current` (default), `bts` | `/osdy-pi mascot current
|
|
369
|
+
| Mascot | `current` (default), `bts`, `osdy-halloween` | `/osdy-pi mascot current\|bts\|osdy-halloween` |
|
|
370
370
|
|
|
371
371
|
Both commands update the UI immediately. Add `status` instead of a choice to inspect the current selection. Choices persist in the global Osdy Pi settings across reloads and sessions. The old top-level style commands and `classic` header aliases are no longer registered.
|
|
372
372
|
|
|
373
|
+
Choose **Osdy-Halloween** in `/osdyConfig` → **Mascot**, or run `/osdy-pi mascot osdy-halloween`. It dresses the same Current raccoon in ivory fangs and a high-collared dark/crimson vampire cape. Its glow follows the active theme's `accent`, `mdHeading`, and `mdLink` colors; no Halloween theme is required. Current remains the default, and Current/Bts artwork and version-1 settings compatibility are unchanged.
|
|
374
|
+
|
|
373
375
|
The header also shows **`osdy-pi v<version>`** at the top right in the theme's accent color, read from the installed Osdy package—not Pi's host version or the workspace manifest. It uses an empty first row or adds a row without overwriting artwork; invalid/unreadable versions or labels wider than the terminal are omitted.
|
|
374
376
|
|
|
375
377
|
In normal mode, the selected header and mascot render side by side when both fit. The Neon header derives its highlights from the active theme rather than using one fixed palette. Each mascot keeps its own tone map while its animated edge glow can resolve through theme colors. Header and mascot scaling remain independent, so any combination follows the same responsive layout rules.
|
|
@@ -407,7 +409,7 @@ The enabled state, editor mode, working-tree visibility preference, header, and
|
|
|
407
409
|
| TODO selection | `/osdy-pi todo on\|off\|status` |
|
|
408
410
|
| Session TODO (opt-in, starting in 1.5.0) | `/todos` (read-only) |
|
|
409
411
|
| Header | `/osdy-pi header osdy-theme\|neon\|status` |
|
|
410
|
-
| Mascot | `/osdy-pi mascot current\|bts\|status` |
|
|
412
|
+
| Mascot | `/osdy-pi mascot current\|bts\|osdy-halloween\|status` |
|
|
411
413
|
| Editor | `/osdy-pi editor auto\|extended\|simple\|on\|off\|toggle\|status` |
|
|
412
414
|
| Working tree | `/osdy-pi working-tree on\|off\|toggle\|status` |
|
|
413
415
|
| Working tree | `/osdy-pi working-tree position top\|bottom\|status` |
|
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { registerHooks } from "node:module";
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
5
|
-
import { HEADER_VARIANTS } from "./constants.ts";
|
|
5
|
+
import { HEADER_VARIANTS, mascotForChoice } from "./constants.ts";
|
|
6
6
|
import type { SimpleTheme } from "./types.js";
|
|
7
7
|
|
|
8
8
|
registerHooks({
|
|
@@ -19,7 +19,7 @@ registerHooks({
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
22
|
-
const { colorAsciiCharacter } = await import("./animation.ts");
|
|
22
|
+
const { colorAsciiCharacter, animateAsciiLineWithToneMap } = await import("./animation.ts");
|
|
23
23
|
|
|
24
24
|
class RecordingTheme implements SimpleTheme {
|
|
25
25
|
readonly calls: Array<{ name: string; text: string }> = [];
|
|
@@ -74,6 +74,33 @@ void test("Neon renders main and dark pink through bounded accent styling", () =
|
|
|
74
74
|
assert.equal(`${main}${dark.join("")}`.includes("\u001B[48;"), false);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
void test("Osdy-Halloween resolves edge glow against the live theme in static and animated frames", () => {
|
|
78
|
+
const { art, tonePalette } = mascotForChoice("osdy-halloween");
|
|
79
|
+
assert.deepEqual([tonePalette.p, tonePalette.c, tonePalette.v], ["accent", "mdHeading", "mdLink"]);
|
|
80
|
+
let themeId = "first";
|
|
81
|
+
const calls: string[] = [];
|
|
82
|
+
const theme: SimpleTheme = { fg(name, text) { calls.push(name); return `<${themeId}:${name}:${text}>`; } };
|
|
83
|
+
for (const style of ["static", "animated"] as const) {
|
|
84
|
+
for (const frame of [0, 7, 28]) {
|
|
85
|
+
const render = () => art.mascot.map((line, row) =>
|
|
86
|
+
animateAsciiLineWithToneMap(line, art.toneMap[row] ?? "", row, frame, theme, tonePalette, style)).join("\n");
|
|
87
|
+
themeId = "first";
|
|
88
|
+
const first = render();
|
|
89
|
+
themeId = "second";
|
|
90
|
+
const second = render();
|
|
91
|
+
assert.notEqual(first, second);
|
|
92
|
+
assert.equal(first.replaceAll("first:", "second:"), second, "only theme-resolved colors change");
|
|
93
|
+
assert.ok(second.includes("\u001B[38;2;"), "fixed body/costume foreground colors remain");
|
|
94
|
+
assert.ok(!second.includes("\u001B[48;"), "transparent background");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
assert.deepEqual([...new Set(calls)].sort(), ["accent", "mdHeading", "mdLink"].sort());
|
|
98
|
+
for (let row = 0; row < art.mascot.length; row++) {
|
|
99
|
+
const occupied = Array.from(art.mascot[row] ?? "").flatMap((glyph, column) => glyph === " " ? [] : [column]);
|
|
100
|
+
assert.deepEqual(occupied.slice(-3).reverse().map((column) => art.toneMap[row]?.[column]), ["p", "c", "v"].slice(0, occupied.length));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
77
104
|
void test("Neon never maps a tone to mdHeading or mdLink", () => {
|
|
78
105
|
for (const color of Object.values(neonPalette())) {
|
|
79
106
|
if (typeof color === "string") {
|
|
@@ -457,6 +457,52 @@ function addRightEdgeGlow(art: MascotArt): MascotArt {
|
|
|
457
457
|
};
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// Transparent overlay at Current's mouth/shoulders (row 17 onward).
|
|
461
|
+
// ▓ = dark cape, ▒ = crimson lining, █/▼ = ivory fangs. Keep the face and tail.
|
|
462
|
+
const VAMPIRE_COSTUME = [
|
|
463
|
+
" ▓ ▓",
|
|
464
|
+
" ▓▒▓ █ █ ▓▒▒▓",
|
|
465
|
+
" ▓▒▒▒▓ ▼ ▼ ▓▒▒▒▒▓",
|
|
466
|
+
" ▓▒▒▒▒▒▒▓ ▓▒▒▒▒▒▒▓",
|
|
467
|
+
" ▓▓▒▒▒▒▒▓▓ ▓▓▒▒▒▒▒▓▓",
|
|
468
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▒▓▓▓",
|
|
469
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓▓",
|
|
470
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▒▓▓",
|
|
471
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
|
|
472
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
|
|
473
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
|
|
474
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
|
|
475
|
+
" ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
|
|
476
|
+
" ▓▓▓▓▒▒▓▓ ▓▓▒▒▓▓",
|
|
477
|
+
" ▓▓▓▓▒▒▓▓ ▓▓▒▓▓",
|
|
478
|
+
" ▓▓▓▓▓▓ ▓▓▓",
|
|
479
|
+
] as const;
|
|
480
|
+
|
|
481
|
+
function addVampireCostume(art: MascotArt): MascotArt {
|
|
482
|
+
const rows = art.mascot.map((line, row) => {
|
|
483
|
+
const glyphs = Array.from(line);
|
|
484
|
+
// Reserve b for lining; retain the few original eye glints in ivory h.
|
|
485
|
+
const tones = Array.from((art.toneMap[row] ?? "").replaceAll("b", "h"));
|
|
486
|
+
for (const [column, glyph] of Array.from(VAMPIRE_COSTUME[row - 17] ?? "").entries()) {
|
|
487
|
+
if (glyph === " ") continue;
|
|
488
|
+
glyphs[column] = glyph;
|
|
489
|
+
if (glyph === "▓") tones[column] = "d";
|
|
490
|
+
else if (glyph === "▒") tones[column] = "b";
|
|
491
|
+
else tones[column] = "h";
|
|
492
|
+
}
|
|
493
|
+
return { mascot: glyphs.join(""), toneMap: tones.join("") };
|
|
494
|
+
});
|
|
495
|
+
return { mascot: rows.map((row) => row.mascot), toneMap: rows.map((row) => row.toneMap) };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const HALLOWEEN_MASCOT_TONES: MascotTonePalette = {
|
|
499
|
+
...HTML_MASCOT_TONES,
|
|
500
|
+
b: "#9F2342",
|
|
501
|
+
};
|
|
502
|
+
const HALLOWEEN_MASCOT = addRightEdgeGlow(addVampireCostume({
|
|
503
|
+
mascot: HTML_MASCOT,
|
|
504
|
+
toneMap: HTML_MASCOT_MAP,
|
|
505
|
+
}));
|
|
460
506
|
const ROSE_MASCOT = addRightEdgeGlow({
|
|
461
507
|
mascot: HTML_MASCOT,
|
|
462
508
|
toneMap: HTML_MASCOT_MAP,
|
|
@@ -468,6 +514,7 @@ const BTS_MASCOT_ART = addRightEdgeGlow({
|
|
|
468
514
|
for (const [art, palette] of [
|
|
469
515
|
[ROSE_MASCOT, HTML_MASCOT_TONES],
|
|
470
516
|
[BTS_MASCOT_ART, BTS_MASCOT_TONES],
|
|
517
|
+
[HALLOWEEN_MASCOT, HALLOWEEN_MASCOT_TONES],
|
|
471
518
|
] as const) {
|
|
472
519
|
assertRawHexPalette(palette);
|
|
473
520
|
assertMascotStructure(art, palette);
|
|
@@ -481,6 +528,7 @@ export type MascotConfig = {
|
|
|
481
528
|
export const MASCOTS: Record<MascotChoice, MascotConfig> = {
|
|
482
529
|
current: { art: ROSE_MASCOT, tonePalette: HTML_MASCOT_TONES },
|
|
483
530
|
bts: { art: BTS_MASCOT_ART, tonePalette: BTS_MASCOT_TONES },
|
|
531
|
+
"osdy-halloween": { art: HALLOWEEN_MASCOT, tonePalette: HALLOWEEN_MASCOT_TONES },
|
|
484
532
|
};
|
|
485
533
|
|
|
486
534
|
export function mascotForChoice(choice: MascotChoice): MascotConfig {
|
|
@@ -21,7 +21,7 @@ const snapshot: VisualPreferenceSnapshot = { enabled: true, headerVariant: "osdy
|
|
|
21
21
|
void test("preference rows expose every catalog value and exactly one current indicator", () => {
|
|
22
22
|
for (const [category, field, values] of [
|
|
23
23
|
["Header", "headerVariant", ["osdy-theme", "neon"]],
|
|
24
|
-
["Mascot", "mascot", ["current", "bts"]],
|
|
24
|
+
["Mascot", "mascot", ["current", "bts", "osdy-halloween"]],
|
|
25
25
|
["Editor", "editorMode", ["auto", "extended", "simple"]],
|
|
26
26
|
] as const) {
|
|
27
27
|
for (const value of values) {
|
|
@@ -30,7 +30,9 @@ void test("preference rows expose every catalog value and exactly one current in
|
|
|
30
30
|
assert.deepEqual(detail.rows.map((row) => "value" in row.action ? row.action.value : undefined), values);
|
|
31
31
|
assert.equal(detail.rows.filter((row) => row.current).length, 1);
|
|
32
32
|
assert.equal(detail.rows.find((row) => row.current)?.action.kind, category.toLowerCase());
|
|
33
|
-
|
|
33
|
+
const label = value === "bts" ? "Bts" : value === "osdy-halloween" ? "Osdy-Halloween" : value;
|
|
34
|
+
assert.match(detail.summary, new RegExp(`Current: ${label}`));
|
|
35
|
+
assert.equal(detail.rows.find((row) => row.current)?.label, label);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
});
|
|
@@ -17,7 +17,11 @@ export interface ControlCenterPreferences {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function visualPreferenceLabel(action: VisualPreferenceAction): string {
|
|
20
|
-
|
|
20
|
+
if (action.kind === "mascot") {
|
|
21
|
+
if (action.value === "bts") return "Bts";
|
|
22
|
+
if (action.value === "osdy-halloween") return "Osdy-Halloween";
|
|
23
|
+
}
|
|
24
|
+
return action.value;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** Explicit view adaptation only; application and persistence belong to runtime. */
|
|
@@ -336,7 +336,7 @@ void test("inline preferences show all supported values and live current/effecti
|
|
|
336
336
|
const f = preferenceFixture();
|
|
337
337
|
for (const [category, choices, current] of [
|
|
338
338
|
["Header", ["osdy-theme", "neon"], "osdy-theme"],
|
|
339
|
-
["Mascot", ["current", "Bts"], "current"],
|
|
339
|
+
["Mascot", ["current", "Bts", "Osdy-Halloween"], "current"],
|
|
340
340
|
["Editor", ["auto", "extended", "simple"], "auto"],
|
|
341
341
|
] as const) {
|
|
342
342
|
f.panel.handleInput(down);
|
|
@@ -350,6 +350,20 @@ void test("inline preferences show all supported values and live current/effecti
|
|
|
350
350
|
assert.deepEqual(f.applied, []);
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
+
void test("Mascot selects Osdy-Halloween and reports its exact saved label", async () => {
|
|
354
|
+
const f = preferenceFixture();
|
|
355
|
+
f.panel.handleInput(down);
|
|
356
|
+
f.panel.handleInput(down);
|
|
357
|
+
f.panel.handleInput(right);
|
|
358
|
+
f.panel.handleInput("\x1b[F");
|
|
359
|
+
f.panel.handleInput("\r");
|
|
360
|
+
assert.deepEqual(f.applied, [{ kind: "mascot", value: "osdy-halloween" }]);
|
|
361
|
+
assert.match(f.panel.render(100).join("\n"), /Saving.*Osdy-Halloween/);
|
|
362
|
+
f.complete(true);
|
|
363
|
+
await Promise.resolve();
|
|
364
|
+
assert.match(f.panel.render(100).join("\n"), /Saved globally: Osdy-Halloween/);
|
|
365
|
+
});
|
|
366
|
+
|
|
353
367
|
void test("changing categories resets selection to their current row, even after a longer theme list", () => {
|
|
354
368
|
const f = preferenceFixture();
|
|
355
369
|
f.panel.handleInput(right);
|
|
@@ -427,7 +441,7 @@ void test("inline preference rendering stays bounded at narrow widths and differ
|
|
|
427
441
|
f.panel.handleInput(down);
|
|
428
442
|
f.panel.handleInput(right);
|
|
429
443
|
f.panel.handleInput("\x1b[F");
|
|
430
|
-
assert.match(f.panel.render(48).join("\n"), new RegExp(`> ${["neon", "
|
|
444
|
+
assert.match(f.panel.render(48).join("\n"), new RegExp(`> ${["neon", "Osdy-Halloween", "simple"][category]}`));
|
|
431
445
|
for (const height of [1, 4, 7, 9, 18]) {
|
|
432
446
|
f.resize(height);
|
|
433
447
|
for (const width of [1, 8, 24, 48, 80]) {
|
|
@@ -97,7 +97,7 @@ void test("editor settings normalize only supported versioned modes", () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
void test("mascot choices preserve the version-one current default, migrate raccoon to Bts, and persist Bts", async () => {
|
|
100
|
-
assert.deepEqual(MASCOT_CHOICES, ["current", "bts"]);
|
|
100
|
+
assert.deepEqual(MASCOT_CHOICES, ["current", "bts", "osdy-halloween"]);
|
|
101
101
|
assert.equal(
|
|
102
102
|
normalizeEditorSettings({ version: 1, mascot: "invalid" }).mascot,
|
|
103
103
|
"current",
|
|
@@ -129,6 +129,18 @@ void test("mascot choices preserve the version-one current default, migrate racc
|
|
|
129
129
|
assert.equal((await store.load()).mascot, "bts");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
void test("Osdy-Halloween round-trips through a fresh version-one settings store", async () => {
|
|
133
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
134
|
+
const path = "/agent/extensions/osdy-pi/settings.json";
|
|
135
|
+
const settings = normalizeEditorSettings({ version: 1, mascot: "osdy-halloween" });
|
|
136
|
+
assert.equal(settings.mascot, "osdy-halloween");
|
|
137
|
+
await createEditorSettingsStore(path, fileSystem).save(settings);
|
|
138
|
+
const persisted: unknown = JSON.parse(fileSystem.content ?? "{}");
|
|
139
|
+
assert.deepEqual(persisted, { ...settings, version: 1 });
|
|
140
|
+
assert.deepEqual(await createEditorSettingsStore(path, fileSystem).load(), settings);
|
|
141
|
+
assert.equal(normalizeEditorSettings({ version: 2, mascot: "osdy-halloween" }).mascot, "current");
|
|
142
|
+
});
|
|
143
|
+
|
|
132
144
|
void test("enabled defaults safely and persists globally", async () => {
|
|
133
145
|
assert.equal(
|
|
134
146
|
normalizeEditorSettings({ version: 1, editorMode: "simple" }).enabled,
|
|
@@ -152,6 +152,7 @@ void test("shared visual actions apply immediately, save the complete settings,
|
|
|
152
152
|
for (const enabled of [true, false]) {
|
|
153
153
|
for (const action of [
|
|
154
154
|
{ kind: "header", value: "neon" }, { kind: "mascot", value: "bts" },
|
|
155
|
+
{ kind: "mascot", value: "osdy-halloween" },
|
|
155
156
|
{ kind: "editor", value: "simple" }, { kind: "editor", value: "extended" },
|
|
156
157
|
{ kind: "editor", value: "auto" },
|
|
157
158
|
] as const satisfies readonly VisualPreferenceAction[]) {
|
|
@@ -903,6 +904,7 @@ void test("registered legacy commands and osdyConfig share live values and exist
|
|
|
903
904
|
for (const [args, expected, field, value] of [
|
|
904
905
|
["header neon", "osdy-pi header: neon", "headerVariant", "neon"],
|
|
905
906
|
["mascot bts", "osdy-pi mascot: Bts", "mascot", "bts"],
|
|
907
|
+
["mascot osdy-halloween", "osdy-pi mascot: Osdy-Halloween", "mascot", "osdy-halloween"],
|
|
906
908
|
["editor off", "osdy-pi editor mode: simple", "editorMode", "simple"],
|
|
907
909
|
["editor on", "osdy-pi editor mode: extended", "editorMode", "extended"],
|
|
908
910
|
["editor toggle", "osdy-pi editor mode: simple", "editorMode", "simple"],
|
|
@@ -916,7 +918,9 @@ void test("registered legacy commands and osdyConfig share live values and exist
|
|
|
916
918
|
assert.equal(writes.at(-1)?.workingTreeEnabled, false);
|
|
917
919
|
assert.equal(notices.at(-1)?.message, "osdy-pi working tree disabled");
|
|
918
920
|
const before = writes.length;
|
|
919
|
-
|
|
921
|
+
await legacy.handler("mascot status", ctx);
|
|
922
|
+
assert.deepEqual(notices.at(-1), { message: "osdy-pi mascot: Osdy-Halloween", level: "info" });
|
|
923
|
+
for (const args of ["working-tree status", "header status", "mascot status", "editor status", "header invalid", "mascot bts extra", "mascot osdy-halloween extra", "mascot invalid", "editor simple extra"]) await legacy.handler(args, ctx);
|
|
920
924
|
assert.equal(writes.length, before, "status and invalid inputs never save");
|
|
921
925
|
const beforeModalGitCalls = gitCalls.length;
|
|
922
926
|
const beforeModalNotices = notices.length;
|
|
@@ -1348,15 +1352,19 @@ void test("uninstall command is offered and wired through guarded Pi CLI removal
|
|
|
1348
1352
|
assert.match(source, /pi\.exec\("pi", \["remove", source/);
|
|
1349
1353
|
});
|
|
1350
1354
|
|
|
1351
|
-
void test("mascot command completes
|
|
1355
|
+
void test("mascot command completes all three choices and status while persisting with immediate refresh", () => {
|
|
1352
1356
|
assert.deepEqual(getOsdyCommandCompletions("mascot"), [
|
|
1353
1357
|
{ value: "mascot current", label: "mascot current" },
|
|
1354
1358
|
{ value: "mascot bts", label: "mascot bts" },
|
|
1359
|
+
{ value: "mascot osdy-halloween", label: "mascot osdy-halloween" },
|
|
1355
1360
|
{ value: "mascot status", label: "mascot status" },
|
|
1356
1361
|
]);
|
|
1357
1362
|
assert.deepEqual(getOsdyCommandCompletions("mascot b"), [
|
|
1358
1363
|
{ value: "mascot bts", label: "mascot bts" },
|
|
1359
1364
|
]);
|
|
1365
|
+
assert.deepEqual(getOsdyCommandCompletions("mascot o"), [
|
|
1366
|
+
{ value: "mascot osdy-halloween", label: "mascot osdy-halloween" },
|
|
1367
|
+
]);
|
|
1360
1368
|
assert.deepEqual(getOsdyCommandCompletions("mascot d"), []);
|
|
1361
1369
|
|
|
1362
1370
|
const source = readFileSync(new URL("./runtime.ts", import.meta.url), "utf8");
|
|
@@ -44,7 +44,7 @@ export const HEADER_VARIANT_CHOICES = ["osdy-theme", "neon"] as const;
|
|
|
44
44
|
|
|
45
45
|
export type HeaderVariant = (typeof HEADER_VARIANT_CHOICES)[number];
|
|
46
46
|
|
|
47
|
-
export const MASCOT_CHOICES = ["current", "bts"] as const;
|
|
47
|
+
export const MASCOT_CHOICES = ["current", "bts", "osdy-halloween"] as const;
|
|
48
48
|
|
|
49
49
|
export type MascotChoice = (typeof MASCOT_CHOICES)[number];
|
|
50
50
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { registerHooks } from "node:module";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -244,12 +245,57 @@ void test("selected headers render through foreground-only tone-map animation",
|
|
|
244
245
|
void test("mascot selection wires Bts art through the responsive renderer", () => {
|
|
245
246
|
const ui = readFileSync(new URL("./ui.ts", import.meta.url), "utf8");
|
|
246
247
|
|
|
247
|
-
assert.deepEqual(Object.keys(MASCOTS), ["current", "bts"]);
|
|
248
|
+
assert.deepEqual(Object.keys(MASCOTS), ["current", "bts", "osdy-halloween"]);
|
|
248
249
|
assert.ok(mascotForChoice("bts"));
|
|
249
250
|
assert.match(ui, /const mascot = mascotForChoice\(state\.mascot\);/);
|
|
250
251
|
assert.match(ui, /scaleMascot\(\s*mascot\.art,/);
|
|
251
252
|
});
|
|
252
253
|
|
|
254
|
+
void test("Current and Bts retain their original glyphs, tone maps, and palettes", () => {
|
|
255
|
+
for (const [choice, fingerprint] of [
|
|
256
|
+
["current", "5d399dc6d6cf7f88dce300ca224d157e23e21186b1991a4f57b65fe60c34d8ac"],
|
|
257
|
+
["bts", "e2040efe00cf709b799834d5183ddae3273cfb515b8dfa0f63ae1b9c5def71ec"],
|
|
258
|
+
] as const) {
|
|
259
|
+
assert.equal(createHash("sha256").update(JSON.stringify(MASCOTS[choice])).digest("hex"), fingerprint);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
void test("Osdy-Halloween preserves Current's face and tail beneath a bounded vampire costume", () => {
|
|
264
|
+
const { art, tonePalette } = mascotForChoice("osdy-halloween");
|
|
265
|
+
const current = MASCOTS.current.art;
|
|
266
|
+
assert.deepEqual(art.mascot.slice(0, 17), current.mascot.slice(0, 17), "same ears, mask, eyes and snout");
|
|
267
|
+
for (let row = 21; row < current.mascot.length; row++) {
|
|
268
|
+
assert.equal(art.mascot[row]?.slice(44), current.mascot[row]?.slice(44), "original striped tail");
|
|
269
|
+
}
|
|
270
|
+
assert.notDeepEqual(art.mascot.slice(17), current.mascot.slice(17));
|
|
271
|
+
const fangs = art.mascot.slice(17, 20).join("\n");
|
|
272
|
+
assert.match(fangs, /▼[^▼\n]+▼/, "two ivory fang tips separated by the original muzzle");
|
|
273
|
+
assert.equal([...fangs].filter((glyph) => glyph === "▼").length, 2, "one-column fang tips");
|
|
274
|
+
assert.equal([...fangs].filter((glyph) => glyph === "█").length, 2, "one-column stems keep fangs two rows tall");
|
|
275
|
+
assert.equal(art.mascot.length, current.mascot.length);
|
|
276
|
+
assert.deepEqual(art.mascot.map((row) => row.length), current.mascot.map((row) => row.length));
|
|
277
|
+
assert.equal(art.toneMap[17]?.[8], "d", "raised collar has a dark outer edge");
|
|
278
|
+
assert.equal(art.toneMap[20]?.[12], "b", "collar has crimson lining");
|
|
279
|
+
assert.equal(tonePalette.b, "#9F2342");
|
|
280
|
+
assert.equal(tonePalette.d, MASCOTS.current.tonePalette.d);
|
|
281
|
+
for (const [width, rows] of [[100, 100], [48, 28], [32, 20], [1, 1]] as const) {
|
|
282
|
+
const scaled = scaleMascot(art, width, rows);
|
|
283
|
+
assert.ok(scaled.mascot.length <= rows);
|
|
284
|
+
assert.equal(scaled.mascot.length, scaled.toneMap.length);
|
|
285
|
+
if (width >= 32) assert.match(scaled.mascot.join("\n"), /▼+[^▼\n]+▼+/, "both fangs survive responsive sampling");
|
|
286
|
+
for (let row = 0; row < scaled.mascot.length; row++) {
|
|
287
|
+
const glyphs = Array.from(scaled.mascot[row] ?? "");
|
|
288
|
+
const tones = Array.from(scaled.toneMap[row] ?? "");
|
|
289
|
+
assert.ok(visibleWidth(scaled.mascot[row] ?? "") <= width);
|
|
290
|
+
assert.equal(glyphs.length, tones.length);
|
|
291
|
+
for (let column = 0; column < glyphs.length; column++) {
|
|
292
|
+
if (glyphs[column] !== " ") assert.ok(Object.keys(tonePalette).includes(tones[column] ?? ""));
|
|
293
|
+
if (glyphs[column] === "▼") assert.equal(tones[column], "h", "fangs retain ivory rather than glow/lining tones");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
253
299
|
void test("Bts retains the 60 by 26 raccoon glyph structure and transparent tone cells", () => {
|
|
254
300
|
const art = MASCOTS.bts.art;
|
|
255
301
|
assert.equal(art.mascot.length, 26);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osdy-pi",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.10",
|
|
4
4
|
"description": "Osdy Pi package: themes, header, and custom editor for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"typescript": "^5.9.3",
|
|
84
84
|
"typescript-eslint": "^8.46.3"
|
|
85
85
|
},
|
|
86
|
-
"releaseSource": "
|
|
86
|
+
"releaseSource": "96f670c11788dfb1a336c664a266b8581546ff59",
|
|
87
87
|
"repository": {
|
|
88
88
|
"type": "git",
|
|
89
89
|
"url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"
|