@xynogen/pix-runtime 0.8.2 → 0.8.4
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/package.json +2 -1
- package/src/icon-catalog.ts +174 -0
- package/src/pix-command.ts +22 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"./config": "./src/runtime.ts",
|
|
26
26
|
"./sections": "./src/sections/index.ts",
|
|
27
27
|
"./collapse": "./src/collapse.ts",
|
|
28
|
+
"./icon-catalog": "./src/icon-catalog.ts",
|
|
28
29
|
"./io": "./src/io.ts",
|
|
29
30
|
"./once": "./src/once.ts",
|
|
30
31
|
"./testing": "./src/testing.ts"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* icon-catalog.ts — semantic icon catalog, treated like an l10n message table.
|
|
3
|
+
*
|
|
4
|
+
* Packages must NOT hardcode glyph codepoints. Instead they ask for an icon by
|
|
5
|
+
* its semantic role — `icon("cwd")`, `icon("paste.image")` — and this module
|
|
6
|
+
* resolves it against the single active icon mode, exactly like `t("key")`
|
|
7
|
+
* resolves a translation against the active locale.
|
|
8
|
+
*
|
|
9
|
+
* catalog: key -> { nerd, unicode, ascii } (the "messages")
|
|
10
|
+
* mode: "nerd" | "unicode" | "ascii" (the "locale")
|
|
11
|
+
* icon(k): catalog[k][mode] (the "t(key)")
|
|
12
|
+
*
|
|
13
|
+
* One global mode governs the whole stack. It is switched via the `/pix`
|
|
14
|
+
* settings command (in pix-data), persisted to `~/.pi/agent/pix.json`
|
|
15
|
+
* (`pretty.icons`), and seeded from the PRETTY_ICONS env var on first load.
|
|
16
|
+
*
|
|
17
|
+
* Why a catalog instead of per-package toggles: reskinning or fixing a
|
|
18
|
+
* missing-glyph ("tofu") problem becomes a one-file edit here, and there is
|
|
19
|
+
* exactly ONE knob (the mode) rather than one env var per package.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Presentation modes, in /pix settings cycle order. nerd = Nerd Font PUA glyphs. */
|
|
23
|
+
export type IconMode = "nerd" | "unicode" | "ascii";
|
|
24
|
+
|
|
25
|
+
/** All modes in cycle order. */
|
|
26
|
+
export const ICON_MODES: readonly IconMode[] = ["nerd", "unicode", "ascii"];
|
|
27
|
+
|
|
28
|
+
/** Force text (non-emoji) presentation for symbols that default to emoji. */
|
|
29
|
+
const VS = "\uFE0E";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The catalog. Each semantic key maps to one glyph per mode.
|
|
33
|
+
* - nerd: Nerd Font Private Use Area codepoint (needs a patched font).
|
|
34
|
+
* - unicode: standard BMP glyph that ships with virtually every monospace
|
|
35
|
+
* font (no Nerd Font required); +VS to force text presentation.
|
|
36
|
+
* - ascii: pure ASCII, renders on literally any terminal.
|
|
37
|
+
*
|
|
38
|
+
* Keys are SEMANTIC ROLES, never glyph names — consumers reference meaning.
|
|
39
|
+
*/
|
|
40
|
+
const CATALOG = {
|
|
41
|
+
// ── footer / status segments ──────────────────────────────────────────
|
|
42
|
+
model: { nerd: "\u{F06A9}", unicode: `\u25C8${VS}`, ascii: "M" },
|
|
43
|
+
lsp: { nerd: "\u{F0626}", unicode: `\u25C9${VS}`, ascii: "LSP" },
|
|
44
|
+
mcp: { nerd: "\u{F048D}", unicode: `\u25D0${VS}`, ascii: "MCP" },
|
|
45
|
+
cwd: { nerd: "\u{F024B}", unicode: `\u2302${VS}`, ascii: "~" },
|
|
46
|
+
folder: { nerd: "\u{F024B}", unicode: `\u2302${VS}`, ascii: "/" },
|
|
47
|
+
afk: { nerd: "\u{F0310}", unicode: `\u2328${VS}`, ascii: "kbd" },
|
|
48
|
+
|
|
49
|
+
// ── footer indicators (git status, score) ─────────────────────────────
|
|
50
|
+
"git.unstaged": { nerd: "\u2717", unicode: "\u2717", ascii: "x" },
|
|
51
|
+
"git.ahead": { nerd: "\u21E1", unicode: "\u21E1", ascii: "^" },
|
|
52
|
+
"git.behind": { nerd: "\u21E3", unicode: "\u21E3", ascii: "v" },
|
|
53
|
+
"net.in": { nerd: "\u21E1", unicode: "\u21E1", ascii: "in" },
|
|
54
|
+
"net.out": { nerd: "\u21E3", unicode: "\u21E3", ascii: "out" },
|
|
55
|
+
score: { nerd: "\u26A1", unicode: "\u26A1", ascii: "S" },
|
|
56
|
+
|
|
57
|
+
// ── misc ──────────────────────────────────────────────────────────────
|
|
58
|
+
ok: { nerd: "\u2713", unicode: "\u2713", ascii: "ok" },
|
|
59
|
+
warn: { nerd: "\u26A0", unicode: "\u26A0", ascii: "!" },
|
|
60
|
+
error: { nerd: "\u2717", unicode: "\u2717", ascii: "x" },
|
|
61
|
+
|
|
62
|
+
// ── permission / security modal titles ────────────────────────────────
|
|
63
|
+
// Semantic keys for the danger prompts (🔐 root, 🔑 secret). nerd = Nerd
|
|
64
|
+
// Font glyph, unicode = a widely-shipped BMP symbol forced to text
|
|
65
|
+
// presentation, ascii = tofu-free token.
|
|
66
|
+
lock: { nerd: "\u{F0341}", unicode: `\u{1F512}${VS}`, ascii: "[!]" },
|
|
67
|
+
secret: { nerd: "\u{F0306}", unicode: `\u{1F511}${VS}`, ascii: "[key]" },
|
|
68
|
+
settings: { nerd: "\u{F0493}", unicode: `\u2699${VS}`, ascii: "[*]" },
|
|
69
|
+
update: { nerd: "\u{F01DA}", unicode: `\u2193${VS}`, ascii: "[v]" },
|
|
70
|
+
|
|
71
|
+
// ── shared status glyphs (checklists, panels, markers) ────────────────
|
|
72
|
+
// nerd/unicode keep the historical literal so mixed-glyph rows stay
|
|
73
|
+
// aligned; ascii mode swaps in tofu-free tokens. `⚡` (energetic
|
|
74
|
+
// warning/killed/denied) intentionally stays a local literal — it is not
|
|
75
|
+
// part of this set.
|
|
76
|
+
"status.ok": { nerd: "\u2713", unicode: `\u2713${VS}`, ascii: "ok" },
|
|
77
|
+
"status.error": { nerd: "\u2717", unicode: `\u2717${VS}`, ascii: "x" },
|
|
78
|
+
// `⚠` is East-Asian wide (2 cells); consumers that place it in an aligned
|
|
79
|
+
// marker column must normalize width via `padIcon` (pix-pretty/utils).
|
|
80
|
+
"status.warn": { nerd: "\u26A0", unicode: `\u26A0${VS}`, ascii: "!" },
|
|
81
|
+
"status.pending": { nerd: "\u25CB", unicode: `\u25CB${VS}`, ascii: "o" },
|
|
82
|
+
"status.running": { nerd: "\u25D0", unicode: `\u25D0${VS}`, ascii: "*" },
|
|
83
|
+
"status.active": { nerd: "\u25CF", unicode: `\u25CF${VS}`, ascii: "*" },
|
|
84
|
+
"status.done": { nerd: "\u25CF", unicode: `\u25CF${VS}`, ascii: "x" },
|
|
85
|
+
"status.blocked": { nerd: "\u2298", unicode: `\u2298${VS}`, ascii: "!" },
|
|
86
|
+
|
|
87
|
+
// ── welcome banner ────────────────────────────────────────────────────
|
|
88
|
+
ready: { nerd: "\u{F0633}", unicode: `\u2713${VS}`, ascii: "ok" },
|
|
89
|
+
|
|
90
|
+
// ── paste chips (pix-display) ─────────────────────────────────────────
|
|
91
|
+
"paste.image": { nerd: "\u{F02E9}", unicode: `\u25A3${VS}`, ascii: "img" },
|
|
92
|
+
"paste.text": { nerd: "\u{F027F}", unicode: `\u25A4${VS}`, ascii: "txt" },
|
|
93
|
+
|
|
94
|
+
// ── model picker (pix-models) ─────────────────────────────────────────
|
|
95
|
+
"picker.model": { nerd: "\u{F0229}", unicode: `\u25C8${VS}`, ascii: "M" },
|
|
96
|
+
|
|
97
|
+
// ── optimizer suite (pix-optimizer) ───────────────────────────────────
|
|
98
|
+
"opt.caveman": { nerd: "\u{F0710}", unicode: `\u2664${VS}`, ascii: "Cv" },
|
|
99
|
+
"opt.rtk": { nerd: "\u{F04E5}", unicode: `\u2661${VS}`, ascii: "Rk" },
|
|
100
|
+
"opt.toon": { nerd: "\u{F05C0}", unicode: `\u2662${VS}`, ascii: "Tn" },
|
|
101
|
+
"opt.ponytail": { nerd: "\u{F0190}", unicode: `\u2667${VS}`, ascii: "Pt" },
|
|
102
|
+
"opt.title": { nerd: "\u{F0DAB}", unicode: `\u25C8${VS}`, ascii: "*" },
|
|
103
|
+
|
|
104
|
+
// ── subagent widget (pix-subagent) ────────────────────────────────────
|
|
105
|
+
agent: { nerd: "\u{F0BA0}", unicode: `\u2699${VS}`, ascii: "@" },
|
|
106
|
+
turns: { nerd: "\u{F006A}", unicode: `\u21BB${VS}`, ascii: "~" },
|
|
107
|
+
tools: { nerd: "\u{F1064}", unicode: `\u2692${VS}`, ascii: "T" },
|
|
108
|
+
tokens: { nerd: "\u{F027F}", unicode: `\u25A4${VS}`, ascii: "tk" },
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
/** Every valid semantic icon key. */
|
|
112
|
+
export type IconKey = keyof typeof CATALOG;
|
|
113
|
+
|
|
114
|
+
/** All catalog keys (useful for /pix previews and tests). */
|
|
115
|
+
export const ICON_KEYS = Object.keys(CATALOG) as IconKey[];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Active mode. Seeded from PRETTY_ICONS env (back-compat: none/off => ascii),
|
|
119
|
+
* then overridden by a persisted choice when the host loads pix.json.
|
|
120
|
+
*/
|
|
121
|
+
function envMode(): IconMode {
|
|
122
|
+
const raw = (process.env.PRETTY_ICONS ?? "").toLowerCase();
|
|
123
|
+
if (raw === "nerd" || raw === "unicode" || raw === "ascii") return raw;
|
|
124
|
+
if (raw === "none" || raw === "off") return "ascii";
|
|
125
|
+
return "nerd";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let activeMode: IconMode = envMode();
|
|
129
|
+
|
|
130
|
+
/** Current global icon mode. */
|
|
131
|
+
export function getIconMode(): IconMode {
|
|
132
|
+
return activeMode;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mode-change subscribers. Most consumers resolve icon() at render time and
|
|
137
|
+
* need no notification, but PUSHED-status consumers (e.g. the optimizer cell,
|
|
138
|
+
* drawn once via setStatus) must repaint when the mode flips. They subscribe
|
|
139
|
+
* here; setIconMode fires every callback on an actual change.
|
|
140
|
+
*/
|
|
141
|
+
type ModeListener = (mode: IconMode) => void;
|
|
142
|
+
const listeners = new Set<ModeListener>();
|
|
143
|
+
|
|
144
|
+
/** Subscribe to global icon-mode changes. Returns an unsubscribe fn. */
|
|
145
|
+
export function onIconModeChange(cb: ModeListener): () => void {
|
|
146
|
+
listeners.add(cb);
|
|
147
|
+
return () => listeners.delete(cb);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Set the global icon mode (does NOT persist — callers that want persistence
|
|
152
|
+
* use the /pix command, which writes pix.json then calls this). Fires
|
|
153
|
+
* subscribers only on an actual change (no-op re-sets are ignored).
|
|
154
|
+
*/
|
|
155
|
+
export function setIconMode(mode: IconMode): void {
|
|
156
|
+
if (!ICON_MODES.includes(mode) || mode === activeMode) return;
|
|
157
|
+
activeMode = mode;
|
|
158
|
+
for (const cb of listeners) cb(mode);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a semantic icon key to its glyph for the active mode — the `t(key)`
|
|
163
|
+
* of this module. Unknown keys return "" (fail soft: never throw mid-render).
|
|
164
|
+
*/
|
|
165
|
+
export function icon(key: IconKey): string {
|
|
166
|
+
const entry = CATALOG[key];
|
|
167
|
+
return entry ? entry[activeMode] : "";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Resolve a key for an explicit mode (used by /pix previews + tests). */
|
|
171
|
+
export function iconFor(key: IconKey, mode: IconMode): string {
|
|
172
|
+
const entry = CATALOG[key];
|
|
173
|
+
return entry ? entry[mode] : "";
|
|
174
|
+
}
|
package/src/pix-command.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
visibleWidth,
|
|
16
16
|
wrapTextWithAnsi,
|
|
17
17
|
} from "@earendil-works/pi-tui";
|
|
18
|
+
import { icon } from "./icon-catalog.ts";
|
|
18
19
|
import type { PixRuntime } from "./runtime.ts";
|
|
19
20
|
import type { DeepPartial, SectionHandle } from "./schema.ts";
|
|
20
21
|
import { collapseSection } from "./sections/collapse.ts";
|
|
@@ -184,7 +185,7 @@ const SETTINGS: SettingRow<unknown>[] = [
|
|
|
184
185
|
];
|
|
185
186
|
|
|
186
187
|
function buildSummary(runtime: PixRuntime): string {
|
|
187
|
-
const lines = [`
|
|
188
|
+
const lines = [`Pix Settings (${runtime.path})`, ""];
|
|
188
189
|
let lastSection = "";
|
|
189
190
|
for (const row of SETTINGS) {
|
|
190
191
|
if (row.section !== lastSection) {
|
|
@@ -299,7 +300,9 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
299
300
|
tui.terminal?.rows,
|
|
300
301
|
runtime.get(prettySection).maxRenderHeight,
|
|
301
302
|
),
|
|
302
|
-
|
|
303
|
+
title: `${icon("settings")} Pix Settings`,
|
|
304
|
+
titleColor: (s: string) => theme.fg("accent", theme.bold(s)),
|
|
305
|
+
header: [""],
|
|
303
306
|
body,
|
|
304
307
|
footer: [
|
|
305
308
|
"",
|
|
@@ -378,6 +381,10 @@ interface RuntimeModalOptions {
|
|
|
378
381
|
selectedBodyLine?: number;
|
|
379
382
|
color: (s: string) => string;
|
|
380
383
|
bg?: (s: string) => string;
|
|
384
|
+
/** Plain title embedded in the top border (`╭─ title ──╮`). */
|
|
385
|
+
title?: string;
|
|
386
|
+
/** Color for the embedded title. Defaults to `color`. */
|
|
387
|
+
titleColor?: (s: string) => string;
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
interface RuntimeModalResult {
|
|
@@ -451,11 +458,22 @@ function frameModal(opts: RuntimeModalOptions): RuntimeModalResult {
|
|
|
451
458
|
function frameRows(
|
|
452
459
|
width: number,
|
|
453
460
|
lines: string[],
|
|
454
|
-
opts: Pick<RuntimeModalOptions, "color" | "bg">,
|
|
461
|
+
opts: Pick<RuntimeModalOptions, "color" | "bg" | "title" | "titleColor">,
|
|
455
462
|
): string[] {
|
|
456
463
|
const bg = opts.bg ?? ((s: string) => s);
|
|
457
464
|
const inner = Math.max(1, width - 4);
|
|
458
465
|
const dashes = "─".repeat(width - 2);
|
|
466
|
+
// Top border: bare, or `╭─ title ──╮` when a title is given. Chrome around
|
|
467
|
+
// the label is 5 cols (2 corners, 1 lead dash, 2 pad spaces); tail fills the
|
|
468
|
+
// rest so the row stays exactly `width` visible cells.
|
|
469
|
+
const topBorder = ((): string => {
|
|
470
|
+
const span = width - 5;
|
|
471
|
+
if (!opts.title || span < 3) return opts.color(`╭${dashes}╮`);
|
|
472
|
+
const paint = opts.titleColor ?? opts.color;
|
|
473
|
+
const label = truncateToWidth(opts.title, span - 1, "…");
|
|
474
|
+
const tail = "─".repeat(Math.max(1, span - visibleWidth(label)));
|
|
475
|
+
return `${opts.color("╭─ ")}${paint(label)}${opts.color(` ${tail}╮`)}`;
|
|
476
|
+
})();
|
|
459
477
|
const SENTINEL = "\x00";
|
|
460
478
|
const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
|
|
461
479
|
const reassert = (s: string): string => {
|
|
@@ -470,5 +488,5 @@ function frameRows(
|
|
|
470
488
|
const padded = fitted + " ".repeat(Math.max(0, inner - visibleWidth(fitted)));
|
|
471
489
|
return bg(`${opts.color("│")} ${reassert(padded)} ${opts.color("│")}`);
|
|
472
490
|
};
|
|
473
|
-
return [bg(
|
|
491
|
+
return [bg(topBorder), ...lines.map(row), bg(opts.color(`╰${dashes}╯`))];
|
|
474
492
|
}
|