@xynogen/pix-pretty 1.7.16 → 1.7.17

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 CHANGED
@@ -9,9 +9,10 @@ consume. It does not register user-facing tools itself — the tool renderers
9
9
  (`pix-read`, `pix-bash`, `pix-ls`, `pix-find`, `pix-grep`, `pix-edit`,
10
10
  `pix-write`) import from it. The extension entry point (`src/index.ts`) only
11
11
  initializes the syntax-highlight theme from Pi settings, clears the highlight
12
- cache, registers the `/pretty` icon-style switch, and registers two FFF slash
12
+ cache, seeds the icon mode from `pix.json`, and registers two FFF slash
13
13
  commands (`/fff-health`, `/fff-rescan`) once `pix-grep` has brought the FFF
14
- finder online. (Activated by `pix-core`; not a standalone extension.)
14
+ finder online. The `/pix` settings command lives in `pix-data`.
15
+ (Activated by `pix-core`; not a standalone extension.)
15
16
 
16
17
  ### Rendering
17
18
 
@@ -33,12 +34,12 @@ problem on terminals without a Nerd Font, becomes a one-file edit here.
33
34
  mode. Modes: `nerd` (Nerd Font PUA, default), `unicode` (standard BMP glyphs,
34
35
  no patched font needed), `ascii` (plain letters). Also `iconFor(key, mode)`,
35
36
  `getIconMode()`, `setIconMode()`, `ICON_KEYS`, `ICON_MODES`.
36
- - **`./icon-persist`** — stores the mode in `~/.pi/agent/pretty.json`;
37
- `initIconMode()` applies it on load.
38
- - **`/pretty`** the single switch: an overlay that previews each mode's
39
- glyphs live and persists the choice. One global knob governs every pix-*
40
- package (footer, paste chips, model picker, welcome banner, optimizer cell).
41
- Seeded from `PRETTY_ICONS` (`none`/`off` → `ascii`) when no choice is saved.
37
+ - **`./icon-persist`** — reads/writes the icon mode via `pix.json`
38
+ (`pretty.icons`); `initIconMode()` applies it on load.
39
+ - **`/pix`** (in `pix-data`) unified settings overlay that includes the icon
40
+ mode switch. One global knob governs every pix-* package (footer, paste
41
+ chips, model picker, welcome banner, optimizer cell). Seeded from
42
+ `PRETTY_ICONS` (`none`/`off` → `ascii`) when no choice is saved.
42
43
 
43
44
  ### Shared overlay
44
45
 
@@ -70,7 +71,7 @@ Configuration is read from **`~/.pi/agent/pix.json`** (the unified config file h
70
71
  ```jsonc
71
72
  {
72
73
  "pretty": {
73
- "theme": "monokai", // syntax-highlight theme
74
+ "syntaxTheme": "monokai", // syntax-highlight theme
74
75
  "icons": "nerd", // nerd | unicode | ascii
75
76
  "maxPreviewLines": 50,
76
77
  "diffColors": true
@@ -85,8 +86,9 @@ Configuration is read from **`~/.pi/agent/pix.json`** (the unified config file h
85
86
  - `PRETTY_MAX_PREVIEW_LINES` — max lines in preview output
86
87
  - `PRETTY_CACHE_LIMIT` — FFF cache size limit
87
88
  - `PRETTY_ICONS` — default icon mode when none is persisted: `nerd` (default),
88
- `unicode`, `ascii`, or `none`/`off` (→ `ascii`). Overridden by `/pretty`.
89
+ `unicode`, `ascii`, or `none`/`off` (→ `ascii`).
89
90
  Note: this seeds the file-icon helpers AND the semantic icon catalog.
91
+ Overridden by the `/pix` settings command.
90
92
  - `PRETTY_MAX_RENDER_LINES` — max lines in edit/write diff render (default: 150)
91
93
  - `PRETTY_FFF_DIR` — override FFF state dir (default: `~/.cache/pi/fff`)
92
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.7.16",
3
+ "version": "1.7.17",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/config.ts CHANGED
@@ -1,47 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
1
  import { pixConfig } from "@xynogen/pix-data/pix-config";
5
- import type { BundledTheme } from "./types.js";
6
-
7
- const DEFAULT_THEME: BundledTheme = "github-dark";
8
-
9
- export function getDefaultAgentDir(): string | undefined {
10
- const home = process.env.HOME ?? "";
11
- return home ? join(home, ".pi/agent") : undefined;
12
- }
13
-
14
- function readThemeFromSettings(agentDir?: string): BundledTheme | undefined {
15
- const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
16
- if (!resolvedAgentDir) return undefined;
17
-
18
- try {
19
- const settings = JSON.parse(readFileSync(join(resolvedAgentDir, "settings.json"), "utf8")) as {
20
- theme?: unknown;
21
- };
22
- return typeof settings.theme === "string" ? (settings.theme as BundledTheme) : undefined;
23
- } catch {
24
- return undefined;
25
- }
26
- }
27
-
28
- function resolvePrettyTheme(agentDir?: string): BundledTheme {
29
- // Precedence: env → pix.json → settings.json → default
30
- return (
31
- (process.env.PRETTY_THEME as BundledTheme | undefined) ??
32
- (pixConfig().pretty.theme as BundledTheme) ??
33
- readThemeFromSettings(agentDir) ??
34
- DEFAULT_THEME
35
- );
36
- }
37
-
38
- export let THEME: BundledTheme = resolvePrettyTheme();
39
-
40
- export function setPrettyTheme(agentDir?: string): void {
41
- const resolvedTheme = resolvePrettyTheme(agentDir);
42
- if (resolvedTheme === THEME) return;
43
- THEME = resolvedTheme;
44
- }
45
2
 
46
3
  export function envInt(name: string, fallback: number): number {
47
4
  const v = Number.parseInt(process.env[name] ?? "", 10);
package/src/highlight.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { normalizeShikiContrast } from "./ansi.js";
2
- import { CACHE_LIMIT, MAX_HL_CHARS, THEME } from "./config.js";
2
+ import { CACHE_LIMIT, MAX_HL_CHARS } from "./config.js";
3
3
  import type { BundledLanguage } from "./types.js";
4
4
 
5
5
  // Engine: cli-highlight (highlight.js-backed, synchronous ANSI output).
@@ -88,7 +88,7 @@ export async function hlBlock(
88
88
  const hljsLang = toHljsLang(language);
89
89
  if (!hljsLang) return code.split("\n");
90
90
 
91
- const k = `${THEME}\0${hljsLang}\0${code}`;
91
+ const k = `${hljsLang}\0${code}`;
92
92
  const hit = _cache.get(k);
93
93
  if (hit) return _touch(k, hit);
94
94
 
@@ -10,16 +10,16 @@
10
10
  * mode: "nerd" | "unicode" | "ascii" (the "locale")
11
11
  * icon(k): catalog[k][mode] (the "t(key)")
12
12
  *
13
- * One global mode governs the whole stack. It is switched via the `/pretty`
14
- * command (see pretty-command.ts), persisted to ~/.pi/agent/pretty.json, and
15
- * seeded from the PRETTY_ICONS env var on first load.
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
16
  *
17
17
  * Why a catalog instead of per-package toggles: reskinning or fixing a
18
18
  * missing-glyph ("tofu") problem becomes a one-file edit here, and there is
19
19
  * exactly ONE knob (the mode) rather than one env var per package.
20
20
  */
21
21
 
22
- /** Presentation modes, in /pretty cycle order. nerd = Nerd Font PUA glyphs. */
22
+ /** Presentation modes, in /pix settings cycle order. nerd = Nerd Font PUA glyphs. */
23
23
  export type IconMode = "nerd" | "unicode" | "ascii";
24
24
 
25
25
  /** All modes in cycle order. */
@@ -85,12 +85,12 @@ const CATALOG = {
85
85
  /** Every valid semantic icon key. */
86
86
  export type IconKey = keyof typeof CATALOG;
87
87
 
88
- /** All catalog keys (useful for /pretty previews and tests). */
88
+ /** All catalog keys (useful for /pix previews and tests). */
89
89
  export const ICON_KEYS = Object.keys(CATALOG) as IconKey[];
90
90
 
91
91
  /**
92
92
  * Active mode. Seeded from PRETTY_ICONS env (back-compat: none/off => ascii),
93
- * then overridden by a persisted choice when the host loads pretty.json.
93
+ * then overridden by a persisted choice when the host loads pix.json.
94
94
  */
95
95
  function envMode(): IconMode {
96
96
  const raw = (process.env.PRETTY_ICONS ?? "").toLowerCase();
@@ -123,7 +123,7 @@ export function onIconModeChange(cb: ModeListener): () => void {
123
123
 
124
124
  /**
125
125
  * Set the global icon mode (does NOT persist — callers that want persistence
126
- * use pretty-command.ts, which writes pretty.json then calls this). Fires
126
+ * use the /pix command, which writes pix.json then calls this). Fires
127
127
  * subscribers only on an actual change (no-op re-sets are ignored).
128
128
  */
129
129
  export function setIconMode(mode: IconMode): void {
@@ -141,7 +141,7 @@ export function icon(key: IconKey): string {
141
141
  return entry ? entry[activeMode] : "";
142
142
  }
143
143
 
144
- /** Resolve a key for an explicit mode (used by /pretty previews + tests). */
144
+ /** Resolve a key for an explicit mode (used by /pix previews + tests). */
145
145
  export function iconFor(key: IconKey, mode: IconMode): string {
146
146
  const entry = CATALOG[key];
147
147
  return entry ? entry[mode] : "";
@@ -2,17 +2,25 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { reloadPixConfig } from "@xynogen/pix-data/pix-config";
5
6
  import { getIconMode, setIconMode } from "./icon-catalog.ts";
6
7
  import { initIconMode, loadIconMode, saveIconMode } from "./icon-persist.ts";
7
8
 
8
9
  let tmpAgentDir: string;
10
+ let origHome: string | undefined;
9
11
 
10
12
  beforeAll(() => {
11
13
  tmpAgentDir = mkdtempSync(join(tmpdir(), "pretty-persist-test-"));
14
+ origHome = process.env.HOME;
15
+ // Point HOME at the temp dir so pixConfig() reads from there, not the real ~/.pi/agent/pix.json
16
+ process.env.HOME = tmpAgentDir;
12
17
  process.env.PI_CODING_AGENT_DIR = tmpAgentDir;
18
+ // Force pix-config to re-read from the temp HOME (clears cached real config).
19
+ reloadPixConfig();
13
20
  });
14
21
 
15
22
  afterAll(() => {
23
+ process.env.HOME = origHome;
16
24
  delete process.env.PI_CODING_AGENT_DIR;
17
25
  try {
18
26
  rmSync(tmpAgentDir, { recursive: true });
@@ -24,8 +32,8 @@ afterAll(() => {
24
32
  describe("icon-persist", () => {
25
33
  afterEach(() => setIconMode("nerd"));
26
34
 
27
- it("returns undefined before anything is saved", () => {
28
- expect(loadIconMode()).toBeUndefined();
35
+ it("returns default (nerd) in a fresh config", () => {
36
+ expect(loadIconMode()).toBe("nerd");
29
37
  });
30
38
 
31
39
  it("round-trips a mode across save/load (new-session sim)", () => {
@@ -1,37 +1,25 @@
1
1
  /**
2
2
  * icon-persist.ts — disk persistence for the global icon mode.
3
3
  *
4
- * Stores the single icon mode in ~/.pi/agent/pretty.json so a choice made via
5
- * /pretty survives quit/restart. Kept separate from icon-catalog.ts so the
6
- * catalog resolver stays pure (no fs) and trivially testable.
4
+ * Reads/writes the icon mode via the unified pix.json config
5
+ * (`~/.pi/agent/pix.json` `pretty.icons`). Kept separate from
6
+ * icon-catalog.ts so the catalog resolver stays pure (no fs) and trivially
7
+ * testable.
7
8
  *
8
- * ~/.pi/agent/pretty.json -> { "icons": "unicode" }
9
- *
10
- * Precedence: a persisted value wins; otherwise the catalog's env-seeded
11
- * default (PRETTY_ICONS) stands.
9
+ * Precedence: env PRETTY_ICONS → pix.json pretty.icons → default ("nerd")
12
10
  */
13
11
 
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
- import { dirname, join } from "node:path";
16
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
- import { pixConfig } from "@xynogen/pix-data/pix-config";
12
+ import { onPixConfigChange, pixConfig, savePixConfig } from "@xynogen/pix-data/pix-config";
18
13
  import { ICON_MODES, type IconMode, setIconMode } from "./icon-catalog.js";
19
14
 
20
15
  function isIconMode(m: string): m is IconMode {
21
16
  return (ICON_MODES as readonly string[]).includes(m);
22
17
  }
23
18
 
24
- function statePath(): string {
25
- return join(getAgentDir(), "pretty.json");
26
- }
27
-
28
- /** Read the persisted icon mode, or undefined if unset/invalid/missing. */
19
+ /** Read the persisted icon mode from pix.json, or undefined if unset/invalid. */
29
20
  export function loadIconMode(): IconMode | undefined {
30
21
  try {
31
- const p = statePath();
32
- if (!existsSync(p)) return undefined;
33
- const raw = JSON.parse(readFileSync(p, "utf-8")) as { icons?: string };
34
- const mode = raw?.icons;
22
+ const mode = pixConfig().pretty.icons;
35
23
  if (mode == null) return undefined;
36
24
  return isIconMode(mode) ? mode : undefined;
37
25
  } catch {
@@ -39,38 +27,28 @@ export function loadIconMode(): IconMode | undefined {
39
27
  }
40
28
  }
41
29
 
42
- /** Persist the icon mode, merging into pretty.json. */
30
+ /** Persist the icon mode to pix.json (`pretty.icons`). */
43
31
  export function saveIconMode(mode: IconMode): void {
44
32
  try {
45
- const p = statePath();
46
- mkdirSync(dirname(p), { recursive: true });
47
- let existing: Record<string, unknown> = {};
48
- if (existsSync(p)) {
49
- try {
50
- existing = JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>;
51
- } catch {
52
- existing = {};
53
- }
54
- }
55
- writeFileSync(p, JSON.stringify({ ...existing, icons: mode }, null, 2));
33
+ savePixConfig({ pretty: { icons: mode } });
56
34
  } catch (err) {
57
35
  console.warn("pix-pretty: persist icon mode failed:", err);
58
36
  }
59
37
  }
60
38
 
61
39
  /**
62
- * Apply the persisted mode (if any) to the catalog. Called once at extension
63
- * load so the env-seeded default is overridden by the user's saved choice.
40
+ * Apply the persisted mode (if any) to the catalog and subscribe to live
41
+ * changes from the /pix settings command. Called once at extension load.
64
42
  *
65
- * Precedence: env PRETTY_ICONS → pretty.json → pix.json pretty.icons → default ("nerd")
43
+ * Precedence: env PRETTY_ICONS → pix.json pretty.icons → default ("nerd")
66
44
  */
67
45
  export function initIconMode(): void {
68
- const saved = loadIconMode();
69
- if (saved) {
70
- setIconMode(saved);
71
- return;
72
- }
73
- // No persisted choice — try pix.json
74
46
  const pixIcons = pixConfig().pretty.icons;
75
47
  if (pixIcons && isIconMode(pixIcons)) setIconMode(pixIcons);
48
+
49
+ // Keep the in-memory icon mode in sync when /pix changes pretty.icons.
50
+ onPixConfigChange((cfg) => {
51
+ const mode = cfg.pretty.icons;
52
+ if (mode && isIconMode(mode)) setIconMode(mode);
53
+ });
76
54
  }
package/src/index.ts CHANGED
@@ -8,33 +8,19 @@
8
8
  * UI features (paste chips, thinking blocks) live in pix-display.
9
9
  */
10
10
 
11
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
12
11
  import { registerFffCommands } from "./commands/fff.js";
13
- import { registerPrettyCommand } from "./commands/pretty.js";
14
- import { getDefaultAgentDir, setPrettyTheme } from "./config.js";
15
12
  import { fffState } from "./fff.js";
16
13
  import { clearHighlightCache } from "./highlight.js";
17
14
  import { initIconMode } from "./icon-persist.js";
18
15
  import type { PiPrettyApi } from "./types.js";
19
16
 
20
17
  export default function piPrettyExtension(pi: PiPrettyApi): void {
21
- // ── Theme init ──────────────────────────────────────────────────────
22
- setPrettyTheme(
23
- (() => {
24
- try {
25
- return getAgentDir?.() ?? getDefaultAgentDir();
26
- } catch {
27
- return getDefaultAgentDir();
28
- }
29
- })(),
30
- );
31
18
  clearHighlightCache();
32
19
 
33
20
  // ── Icon mode ───────────────────────────────────────────
34
- // Seed the global icon mode from pretty.json (overrides env default), then
35
- // register the single /pretty switch.
21
+ // Seed the global icon mode from pix.json (overrides env default).
22
+ // The /pix settings command lives in pix-data.
36
23
  initIconMode();
37
- registerPrettyCommand(pi);
38
24
 
39
25
  // ── FFF slash commands ──────────────────────────────────────────────
40
26
  // fffState is a module-level singleton shared with pix-grep/pix-find.
package/src/renderers.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { truncateToWidth } from "@earendil-works/pi-tui";
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { getLsStyle } from "@xynogen/pix-data/pix-config";
2
3
 
3
4
  import { BOLD, FG_BLUE, FG_DIM, FG_GREEN, FG_RED, FG_RULE, FG_YELLOW, RST } from "./ansi.js";
4
5
  import { MAX_PREVIEW_LINES } from "./config.js";
@@ -71,8 +72,13 @@ export function renderBashOutput(
71
72
  return { summary: codeStr, body };
72
73
  }
73
74
 
74
- /** Render ls output as a tree view with icons. */
75
- export function renderTree(text: string, _basePath: string): string {
75
+ /** Render ls output using the configured style (grid or tree). */
76
+ export function renderTree(text: string, basePath: string): string {
77
+ return getLsStyle() === "tree" ? renderLsTree(text, basePath) : renderLsGrid(text, basePath);
78
+ }
79
+
80
+ /** Vertical tree view with connectors and icons. */
81
+ function renderLsTree(text: string, _basePath: string): string {
76
82
  const lines = text.trim().split("\n").filter(Boolean);
77
83
  if (!lines.length) return `${FG_DIM}(empty directory)${RST}`;
78
84
 
@@ -86,7 +92,6 @@ export function renderTree(text: string, _basePath: string): string {
86
92
  const prefix = isLast ? "└── " : "├── ";
87
93
  const connector = `${FG_RULE}${prefix}${RST}`;
88
94
 
89
- // Detect directories (entries ending with /)
90
95
  const isDir = entry.endsWith("/");
91
96
  const name = isDir ? entry.slice(0, -1) : entry;
92
97
  const icon = isDir ? dirIcon() : fileIcon(name);
@@ -105,6 +110,109 @@ export function renderTree(text: string, _basePath: string): string {
105
110
  return out.join("\n");
106
111
  }
107
112
 
113
+ /** Horizontal grid with icons (like eza/ls). */
114
+ function renderLsGrid(text: string, _basePath: string): string {
115
+ const lines = text.trim().split("\n").filter(Boolean);
116
+ if (!lines.length) return `${FG_DIM}(empty directory)${RST}`;
117
+
118
+ const total = lines.length;
119
+ const show = lines.slice(0, MAX_PREVIEW_LINES);
120
+
121
+ // Build styled cells + measure their visible widths
122
+ const cells: string[] = [];
123
+ const cellWidths: number[] = [];
124
+
125
+ for (const raw of show) {
126
+ const entry = raw.trim();
127
+ const isDir = entry.endsWith("/");
128
+ const name = isDir ? entry.slice(0, -1) : entry;
129
+ const icon = isDir ? dirIcon() : fileIcon(name);
130
+ const fg = isDir ? FG_BLUE + BOLD : "";
131
+ const reset = isDir ? RST : "";
132
+ const cell = `${icon}${fg}${name}${reset}`;
133
+ cells.push(cell);
134
+ cellWidths.push(visibleWidth(cell));
135
+ }
136
+
137
+ // Layout into columns that fit the terminal width
138
+ const tw = termW();
139
+ const GAP = 3; // spaces between columns
140
+ const rows = layoutGrid(cells, cellWidths, tw, GAP);
141
+
142
+ if (total > MAX_PREVIEW_LINES) {
143
+ rows.push(
144
+ `${FG_DIM}… ${pluralize(total - MAX_PREVIEW_LINES, "more entry", "more entries")}${RST}`,
145
+ );
146
+ }
147
+
148
+ return rows.join("\n");
149
+ }
150
+
151
+ /**
152
+ * Lay out styled cells into a grid that fills rows left-to-right,
153
+ * using as many columns as fit within `maxWidth`.
154
+ */
155
+ function layoutGrid(cells: string[], widths: number[], maxWidth: number, gap: number): string[] {
156
+ const n = cells.length;
157
+ if (n === 0) return [];
158
+
159
+ // Try increasing column counts to find the maximum that fits
160
+ let bestCols = 1;
161
+ for (let cols = 2; cols <= n; cols++) {
162
+ const numRows = Math.ceil(n / cols);
163
+ let totalW = 0;
164
+ let fits = true;
165
+ for (let c = 0; c < cols; c++) {
166
+ // Find max width in this column
167
+ let colW = 0;
168
+ for (let r = 0; r < numRows; r++) {
169
+ const idx = r * cols + c;
170
+ if (idx < n && (widths[idx] ?? 0) > colW) colW = widths[idx] ?? 0;
171
+ }
172
+ totalW += colW + (c < cols - 1 ? gap : 0);
173
+ if (totalW > maxWidth) {
174
+ fits = false;
175
+ break;
176
+ }
177
+ }
178
+ if (fits) bestCols = cols;
179
+ else break;
180
+ }
181
+
182
+ const cols = bestCols;
183
+ const numRows = Math.ceil(n / cols);
184
+
185
+ // Compute column widths
186
+ const colWidths: number[] = [];
187
+ for (let c = 0; c < cols; c++) {
188
+ let colW = 0;
189
+ for (let r = 0; r < numRows; r++) {
190
+ const idx = r * cols + c;
191
+ if (idx < n && (widths[idx] ?? 0) > colW) colW = widths[idx] ?? 0;
192
+ }
193
+ colWidths.push(colW);
194
+ }
195
+
196
+ // Render rows
197
+ const out: string[] = [];
198
+ for (let r = 0; r < numRows; r++) {
199
+ const parts: string[] = [];
200
+ for (let c = 0; c < cols; c++) {
201
+ const idx = r * cols + c;
202
+ if (idx >= n) break;
203
+ const cell = cells[idx] ?? "";
204
+ const w = widths[idx] ?? 0;
205
+ const target = colWidths[c] ?? 0;
206
+ // Pad to column width, except for the last column in a row
207
+ const pad = c < cols - 1 ? " ".repeat(Math.max(0, target - w + gap)) : "";
208
+ parts.push(cell + pad);
209
+ }
210
+ out.push(parts.join(""));
211
+ }
212
+
213
+ return out;
214
+ }
215
+
108
216
  // ---------------------------------------------------------------------------
109
217
  // FFF integration (optional) — Fast File Finder with frecency & SIMD search
110
218
  //
@@ -1,132 +0,0 @@
1
- /**
2
- * pretty.ts — the `/pretty` command: switch the global icon mode.
3
- *
4
- * Treats icons like an l10n locale: one global mode (nerd/unicode/ascii)
5
- * governs every pix-* package that renders via the icon catalog. This command
6
- * is the single switch — an overlay that previews each mode's glyphs live and
7
- * persists the choice to ~/.pi/agent/pretty.json. Headless hosts get a notify
8
- * fallback that cycles the mode without UI.
9
- */
10
-
11
- import {
12
- getIconMode,
13
- ICON_MODES,
14
- type IconKey,
15
- type IconMode,
16
- icon,
17
- setIconMode,
18
- } from "../icon-catalog.js";
19
- import { saveIconMode } from "../icon-persist.js";
20
- import { frameLines } from "../modal-frame.js";
21
- import type { CommandContextLike, PiPrettyApi } from "../types.js";
22
-
23
- /** Sample keys shown in the preview, one row per representative role. */
24
- const PREVIEW: { key: IconKey; label: string }[] = [
25
- { key: "model", label: "model" },
26
- { key: "cwd", label: "cwd" },
27
- { key: "lsp", label: "lsp" },
28
- { key: "paste.image", label: "paste" },
29
- { key: "opt.caveman", label: "optimizer" },
30
- ];
31
-
32
- /** Apply + persist a mode in one step. */
33
- function applyMode(mode: IconMode): void {
34
- setIconMode(mode);
35
- saveIconMode(mode);
36
- }
37
-
38
- export function registerPrettyCommand(pi: PiPrettyApi): void {
39
- pi.registerCommand("pretty", {
40
- description: "pix-pretty: switch icon style (nerd / unicode / ascii)",
41
- handler: async (_args: string, ctx: CommandContextLike) => {
42
- const ui = ctx.ui as unknown as {
43
- theme: {
44
- fg(c: string, t: string): string;
45
- bg(c: string, t: string): string;
46
- bold(t: string): string;
47
- };
48
- custom?: <T>(
49
- f: unknown,
50
- opts?: {
51
- overlay?: boolean;
52
- overlayOptions?: {
53
- anchor?: string;
54
- width?: number;
55
- maxHeight?: string;
56
- };
57
- },
58
- ) => Promise<T>;
59
- notify(m: string, t?: "info" | "warning" | "error"): void;
60
- };
61
-
62
- // Headless / no custom-UI host: cycle to the next mode + notify.
63
- if (typeof ui.custom !== "function") {
64
- const cur = ICON_MODES.indexOf(getIconMode());
65
- const next = ICON_MODES[(cur + 1) % ICON_MODES.length] ?? "nerd";
66
- applyMode(next);
67
- ui.notify(`pix-pretty icons: ${next}`, "info");
68
- return;
69
- }
70
-
71
- const boxW = 40;
72
-
73
- await ui.custom<null>(
74
- (
75
- tui: { requestRender(): void },
76
- theme: typeof ui.theme,
77
- _kb: unknown,
78
- done: (v: null) => void,
79
- ) => {
80
- let selected = ICON_MODES.indexOf(getIconMode());
81
- if (selected < 0) selected = 0;
82
-
83
- const choose = (i: number) => {
84
- selected = (i + ICON_MODES.length) % ICON_MODES.length;
85
- applyMode(ICON_MODES[selected] ?? "nerd");
86
- };
87
-
88
- return {
89
- render: () => {
90
- const rows = ICON_MODES.map((mode, i) => {
91
- const sel = i === selected;
92
- const cursor = sel ? theme.fg("accent", "→") : " ";
93
- const name = theme.fg(sel ? "accent" : "text", mode.padEnd(8));
94
- // Live preview: render the sample glyphs in THIS mode.
95
- const prev = ICON_MODES[i] === getIconMode();
96
- const samples = prev ? PREVIEW.map((p) => icon(p.key)).join(" ") : "";
97
- return `${cursor} ${name} ${theme.fg("dim", samples)}`;
98
- });
99
- const lines = [
100
- theme.fg("accent", theme.bold(" Icon style")),
101
- "",
102
- ...rows,
103
- "",
104
- theme.fg("dim", "↑↓ select · esc close"),
105
- ];
106
- return frameLines({
107
- width: boxW,
108
- lines,
109
- color: (s) => theme.fg("accent", s),
110
- bg: (s) => theme.bg("customMessageBg", s),
111
- });
112
- },
113
- invalidate: () => {},
114
- handleInput: (data: string) => {
115
- if (data === "k" || data === "\u001b[A") choose(selected - 1);
116
- else if (data === "j" || data === "\u001b[B") choose(selected + 1);
117
- else if (data === "\u001b" || data === "q" || data === "\r") {
118
- done(null);
119
- return;
120
- } else return;
121
- tui.requestRender();
122
- },
123
- };
124
- },
125
- {
126
- overlay: true,
127
- overlayOptions: { anchor: "center", width: boxW, maxHeight: "60%" },
128
- },
129
- );
130
- },
131
- });
132
- }