@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 +12 -10
- package/package.json +1 -1
- package/src/config.ts +0 -43
- package/src/highlight.ts +2 -2
- package/src/icon-catalog.ts +8 -8
- package/src/icon-persist.test.ts +10 -2
- package/src/icon-persist.ts +19 -41
- package/src/index.ts +2 -16
- package/src/renderers.ts +112 -4
- package/src/commands/pretty.ts +0 -132
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,
|
|
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.
|
|
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`** —
|
|
37
|
-
`initIconMode()` applies it on load.
|
|
38
|
-
- **`/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
"
|
|
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`).
|
|
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
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
|
|
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 = `${
|
|
91
|
+
const k = `${hljsLang}\0${code}`;
|
|
92
92
|
const hit = _cache.get(k);
|
|
93
93
|
if (hit) return _touch(k, hit);
|
|
94
94
|
|
package/src/icon-catalog.ts
CHANGED
|
@@ -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 `/
|
|
14
|
-
* command (
|
|
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 /
|
|
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 /
|
|
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
|
|
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
|
|
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 /
|
|
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] : "";
|
package/src/icon-persist.test.ts
CHANGED
|
@@ -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
|
|
28
|
-
expect(loadIconMode()).
|
|
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)", () => {
|
package/src/icon-persist.ts
CHANGED
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* icon-persist.ts — disk persistence for the global icon mode.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* /
|
|
6
|
-
* catalog resolver stays pure (no fs) and trivially
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
30
|
+
/** Persist the icon mode to pix.json (`pretty.icons`). */
|
|
43
31
|
export function saveIconMode(mode: IconMode): void {
|
|
44
32
|
try {
|
|
45
|
-
|
|
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
|
|
63
|
-
*
|
|
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 →
|
|
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
|
|
35
|
-
//
|
|
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
|
|
75
|
-
export function renderTree(text: 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
|
//
|
package/src/commands/pretty.ts
DELETED
|
@@ -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
|
-
}
|