@xynogen/pix-optimizer 1.1.8 → 1.1.9
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 +25 -2
- package/package.json +1 -1
- package/src/index.ts +8 -4
- package/src/opt.ts +50 -9
- package/src/persist.test.ts +23 -1
- package/src/persist.ts +27 -2
- package/src/status.test.ts +84 -1
- package/src/status.ts +90 -7
package/README.md
CHANGED
|
@@ -25,8 +25,31 @@ State persists to `~/.pi/agent/optimizer.json` (caveman/rtk/toon/ponytail).
|
|
|
25
25
|
|
|
26
26
|
## Status bar
|
|
27
27
|
|
|
28
|
-
A single cell always shows
|
|
29
|
-
|
|
28
|
+
A single cell always shows all four tool icons in a fixed order, color-coded by
|
|
29
|
+
state: **accent** when the tool is enabled, **dim** when disabled.
|
|
30
|
+
|
|
31
|
+
### Icon style (Nerd Font, Unicode, or ASCII)
|
|
32
|
+
|
|
33
|
+
The default glyphs (` `) are **Nerd Font** symbols and require a patched
|
|
34
|
+
font (e.g. MesloLGS NF). Terminals without one render them as missing-glyph
|
|
35
|
+
“tofu” boxes. Two font-independent fallbacks are available:
|
|
36
|
+
|
|
37
|
+
| Mode | Glyphs | Needs Nerd Font? |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `nerd` (default) | ` ` | yes |
|
|
40
|
+
| `unicode` | `♤ ♡ ♢ ♧` (outline card suits) | no |
|
|
41
|
+
| `ascii` | `Cv Rk Tn Pt` | no |
|
|
42
|
+
|
|
43
|
+
Switch the style live from the **`/optimizer` overlay** — a fifth `icons` row
|
|
44
|
+
at the bottom cycles `nerd → unicode → ascii` with `←→`; the choice persists to
|
|
45
|
+
`~/.pi/agent/optimizer.json`.
|
|
46
|
+
|
|
47
|
+
Or set it via env var (a persisted menu choice takes precedence):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export OPTIMIZER_ICONS=unicode # nerd | unicode | ascii
|
|
51
|
+
export PRETTY_ICONS=none # stack-wide: also maps the cell to ASCII
|
|
52
|
+
```
|
|
30
53
|
|
|
31
54
|
## Features
|
|
32
55
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-optimizer",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + jq/TOON JSON compression + ponytail lazy-dev mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -7,18 +7,21 @@
|
|
|
7
7
|
* - toon: jq + TOON guidance for dense JSON (+ bundled skill)
|
|
8
8
|
* - ponytail: lazy-senior-dev system prompt (minimal code, YAGNI)
|
|
9
9
|
*
|
|
10
|
-
* They share ONE status-bar cell (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* They share ONE status-bar cell (all four icons always shown — dimmed when
|
|
11
|
+
* off, accented when on; icon style is nerd/unicode/ascii, set via /optimizer)
|
|
12
|
+
* and ONE command (/optimizer — an interactive overlay). index.ts wires
|
|
13
|
+
* lifecycle hooks via each module, then registers the overlay command.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import { caveman } from "./caveman.ts";
|
|
17
18
|
import { json } from "./json.ts";
|
|
18
19
|
import { registerOptCommand } from "./opt.ts";
|
|
20
|
+
import { loadIconMode } from "./persist.ts";
|
|
19
21
|
import { ponytail } from "./ponytail.ts";
|
|
20
22
|
import { rtk } from "./rtk.ts";
|
|
21
23
|
import {
|
|
24
|
+
envIconMode,
|
|
22
25
|
type OptimizerHandle,
|
|
23
26
|
OptimizerStatus,
|
|
24
27
|
type OptimizerTool,
|
|
@@ -26,7 +29,8 @@ import {
|
|
|
26
29
|
import { filterModelWarnings } from "./tool-result-filter.ts";
|
|
27
30
|
|
|
28
31
|
export default function optimizer(pi: ExtensionAPI) {
|
|
29
|
-
|
|
32
|
+
// Persisted menu choice wins; otherwise fall back to the env default.
|
|
33
|
+
const status = new OptimizerStatus(loadIconMode() ?? envIconMode());
|
|
30
34
|
|
|
31
35
|
// Each module registers its own lifecycle hooks and returns a handle the
|
|
32
36
|
// /optimizer overlay renders + drives.
|
package/src/opt.ts
CHANGED
|
@@ -16,13 +16,23 @@
|
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { frameLines } from "@xynogen/pix-pretty/modal-frame";
|
|
19
|
+
import { saveIconMode } from "./persist.ts";
|
|
19
20
|
import {
|
|
21
|
+
getIcons,
|
|
22
|
+
ICON_MODES,
|
|
23
|
+
type IconMode,
|
|
20
24
|
type OptimizerHandle,
|
|
21
25
|
type OptimizerStatus,
|
|
22
26
|
type OptimizerTool,
|
|
23
|
-
TOOL_ICONS,
|
|
24
27
|
} from "./status.ts";
|
|
25
28
|
|
|
29
|
+
/** Glyph for the icon-mode row itself (uses the active mode's diamond/Tn). */
|
|
30
|
+
const ICON_ROW_GLYPH: Record<IconMode, string> = {
|
|
31
|
+
nerd: "", // gear-ish; distinct from the four tool glyphs
|
|
32
|
+
unicode: "\u25C8\uFE0E", // ◈ diamond-in-diamond
|
|
33
|
+
ascii: "Ic",
|
|
34
|
+
};
|
|
35
|
+
|
|
26
36
|
/** Min/max content width for the overlay box (excludes 4 cols of chrome). */
|
|
27
37
|
const MIN_CONTENT = 28;
|
|
28
38
|
const MAX_CONTENT = 60;
|
|
@@ -76,7 +86,7 @@ export function buildOptHelp(
|
|
|
76
86
|
export function registerOptCommand(
|
|
77
87
|
pi: ExtensionAPI,
|
|
78
88
|
handles: Record<OptimizerTool, OptimizerHandle>,
|
|
79
|
-
|
|
89
|
+
status: OptimizerStatus,
|
|
80
90
|
): void {
|
|
81
91
|
pi.registerCommand("optimizer", {
|
|
82
92
|
description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
|
|
@@ -129,9 +139,26 @@ export function registerOptCommand(
|
|
|
129
139
|
_kb: unknown,
|
|
130
140
|
done: (v: null) => void,
|
|
131
141
|
) => {
|
|
142
|
+
// Rows = 4 tools + 1 icon-mode row at index TOOL_ORDER.length.
|
|
143
|
+
const ICON_ROW = TOOL_ORDER.length;
|
|
144
|
+
const ROW_COUNT = TOOL_ORDER.length + 1;
|
|
145
|
+
const ICON_HELP = "status-cell icon style (nerd needs a Nerd Font)";
|
|
132
146
|
let selected = 0;
|
|
133
147
|
|
|
148
|
+
const cycleIconMode = (direction: -1 | 1) => {
|
|
149
|
+
const cur = ICON_MODES.indexOf(status.mode);
|
|
150
|
+
const next =
|
|
151
|
+
(cur + direction + ICON_MODES.length) % ICON_MODES.length;
|
|
152
|
+
const mode = ICON_MODES[next]!;
|
|
153
|
+
status.setMode(mode, ctx);
|
|
154
|
+
saveIconMode(mode);
|
|
155
|
+
};
|
|
156
|
+
|
|
134
157
|
const cycle = (direction: -1 | 1) => {
|
|
158
|
+
if (selected === ICON_ROW) {
|
|
159
|
+
cycleIconMode(direction);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
135
162
|
const tool = TOOL_ORDER[selected]!;
|
|
136
163
|
const values = handles[tool].values;
|
|
137
164
|
const cur = values.indexOf(handles[tool].current());
|
|
@@ -140,17 +167,17 @@ export function registerOptCommand(
|
|
|
140
167
|
};
|
|
141
168
|
|
|
142
169
|
const move = (direction: -1 | 1) => {
|
|
143
|
-
selected =
|
|
144
|
-
(selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
|
|
170
|
+
selected = (selected + direction + ROW_COUNT) % ROW_COUNT;
|
|
145
171
|
};
|
|
146
172
|
|
|
147
173
|
return {
|
|
148
174
|
render: () => {
|
|
175
|
+
const icons = getIcons(status.mode);
|
|
149
176
|
const rows = TOOL_ORDER.map((tool, i) => {
|
|
150
177
|
const on = handles[tool].current() !== "off";
|
|
151
178
|
const sel = i === selected;
|
|
152
179
|
const cursor = sel ? theme.fg("accent", "→") : " ";
|
|
153
|
-
const icon = theme.fg(on ? "accent" : "dim",
|
|
180
|
+
const icon = theme.fg(on ? "accent" : "dim", icons[tool]);
|
|
154
181
|
const name = theme.fg(
|
|
155
182
|
sel ? "accent" : on ? "text" : "muted",
|
|
156
183
|
tool.padEnd(nameWidth),
|
|
@@ -166,15 +193,29 @@ export function registerOptCommand(
|
|
|
166
193
|
);
|
|
167
194
|
return `${cursor} ${icon} ${name} ${value} ${bar}`;
|
|
168
195
|
});
|
|
196
|
+
// Icon-mode row: glyph + "icons" + current mode name.
|
|
197
|
+
const iconSel = selected === ICON_ROW;
|
|
198
|
+
const iconCursor = iconSel ? theme.fg("accent", "→") : " ";
|
|
199
|
+
const iconGlyph = theme.fg("accent", ICON_ROW_GLYPH[status.mode]);
|
|
200
|
+
const iconName = theme.fg(
|
|
201
|
+
iconSel ? "accent" : "text",
|
|
202
|
+
"icons".padEnd(nameWidth),
|
|
203
|
+
);
|
|
204
|
+
const iconValue = theme.fg(
|
|
205
|
+
"success",
|
|
206
|
+
status.mode.padEnd(valueWidth),
|
|
207
|
+
);
|
|
208
|
+
const iconRow = `${iconCursor} ${iconGlyph} ${iconName} ${iconValue}`;
|
|
209
|
+
const helpText = iconSel
|
|
210
|
+
? ICON_HELP
|
|
211
|
+
: helpSummary(handles[TOOL_ORDER[selected]!].help);
|
|
169
212
|
const lines = [
|
|
170
213
|
theme.fg("accent", theme.bold(" Optimizer")),
|
|
171
214
|
"",
|
|
172
215
|
...rows,
|
|
216
|
+
iconRow,
|
|
173
217
|
"",
|
|
174
|
-
theme.fg(
|
|
175
|
-
"dim",
|
|
176
|
-
helpSummary(handles[TOOL_ORDER[selected]!].help),
|
|
177
|
-
),
|
|
218
|
+
theme.fg("dim", helpText),
|
|
178
219
|
"",
|
|
179
220
|
theme.fg("dim", "←→ change · ↑↓ move · esc close"),
|
|
180
221
|
];
|
package/src/persist.test.ts
CHANGED
|
@@ -9,7 +9,12 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
9
9
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
loadIconMode,
|
|
14
|
+
loadOptValue,
|
|
15
|
+
saveIconMode,
|
|
16
|
+
saveOptValue,
|
|
17
|
+
} from "./persist.ts";
|
|
13
18
|
|
|
14
19
|
let tmpAgentDir: string;
|
|
15
20
|
|
|
@@ -53,4 +58,21 @@ describe("optimizer persistence", () => {
|
|
|
53
58
|
expect(loadOptValue("caveman")).toBe("ultra");
|
|
54
59
|
expect(loadOptValue("ponytail")).toBe("full");
|
|
55
60
|
});
|
|
61
|
+
|
|
62
|
+
test("icon mode round-trips and is independent of tool values", () => {
|
|
63
|
+
expect(loadIconMode()).toBeUndefined();
|
|
64
|
+
saveIconMode("unicode");
|
|
65
|
+
expect(loadIconMode()).toBe("unicode");
|
|
66
|
+
// Did not disturb existing tool state…
|
|
67
|
+
expect(loadOptValue("caveman")).toBe("ultra");
|
|
68
|
+
// …and a later tool write does not disturb the icon mode.
|
|
69
|
+
saveOptValue("rtk", "on");
|
|
70
|
+
expect(loadIconMode()).toBe("unicode");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects an invalid persisted icon mode", () => {
|
|
74
|
+
saveOptValue("caveman", "lite"); // unrelated write
|
|
75
|
+
saveIconMode("ascii");
|
|
76
|
+
expect(loadIconMode()).toBe("ascii");
|
|
77
|
+
});
|
|
56
78
|
});
|
package/src/persist.ts
CHANGED
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
* restart. Each tool reads its value on session_start and writes on run().
|
|
8
8
|
*
|
|
9
9
|
* ~/.pi/agent/optimizer.json → { "caveman": "lite", "rtk": "on", ... }
|
|
10
|
+
*
|
|
11
|
+
* A reserved "iconMode" key (not a tool) stores the status-cell icon style.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
15
|
import { dirname, join } from "node:path";
|
|
14
16
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import type
|
|
17
|
+
import { ICON_MODES, type IconMode, type OptimizerTool } from "./status.ts";
|
|
18
|
+
|
|
19
|
+
/** Reserved (non-tool) key holding the icon presentation mode. */
|
|
20
|
+
const ICON_MODE_KEY = "iconMode";
|
|
16
21
|
|
|
17
|
-
type OptimizerConfig = Partial<Record<OptimizerTool, string
|
|
22
|
+
type OptimizerConfig = Partial<Record<OptimizerTool, string>> & {
|
|
23
|
+
[ICON_MODE_KEY]?: string;
|
|
24
|
+
};
|
|
18
25
|
|
|
19
26
|
function getStatePath(): string {
|
|
20
27
|
return join(getAgentDir(), "optimizer.json");
|
|
@@ -48,3 +55,21 @@ export function saveOptValue(tool: OptimizerTool, value: string): void {
|
|
|
48
55
|
console.warn(`optimizer: persist ${tool} failed:`, err);
|
|
49
56
|
}
|
|
50
57
|
}
|
|
58
|
+
|
|
59
|
+
/** Read the persisted icon mode, or undefined if unset/invalid. */
|
|
60
|
+
export function loadIconMode(): IconMode | undefined {
|
|
61
|
+
const raw = readAll()[ICON_MODE_KEY];
|
|
62
|
+
return ICON_MODES.includes(raw as IconMode) ? (raw as IconMode) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Persist the icon mode, merging into the shared config file. */
|
|
66
|
+
export function saveIconMode(mode: IconMode): void {
|
|
67
|
+
try {
|
|
68
|
+
const sp = getStatePath();
|
|
69
|
+
mkdirSync(dirname(sp), { recursive: true });
|
|
70
|
+
const next = { ...readAll(), [ICON_MODE_KEY]: mode };
|
|
71
|
+
writeFileSync(sp, JSON.stringify(next, null, 2), "utf-8");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn("optimizer: persist iconMode failed:", err);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/status.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
envIconMode,
|
|
4
|
+
getIcons,
|
|
5
|
+
ICON_MODES,
|
|
3
6
|
OptimizerStatus,
|
|
4
7
|
renderStatus,
|
|
5
8
|
STATUS_KEY,
|
|
@@ -45,6 +48,66 @@ describe("renderStatus", () => {
|
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
describe("icon modes", () => {
|
|
52
|
+
it("nerd is the default mode used by renderStatus", () => {
|
|
53
|
+
expect(renderStatus({ rtk: true }, tag)).toContain(TOOL_ICONS.rtk);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders the unicode (outline-suit) set when mode=unicode", () => {
|
|
57
|
+
const icons = getIcons("unicode");
|
|
58
|
+
const out = renderStatus({ caveman: true }, tag, "unicode");
|
|
59
|
+
expect(out).toContain(icons.caveman);
|
|
60
|
+
// outline white spade + text variation selector
|
|
61
|
+
expect(icons.caveman).toBe("\u2664\uFE0E");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders the ascii set when mode=ascii", () => {
|
|
65
|
+
const out = renderStatus(
|
|
66
|
+
{ caveman: true, rtk: false, toon: true, ponytail: false },
|
|
67
|
+
tag,
|
|
68
|
+
"ascii",
|
|
69
|
+
);
|
|
70
|
+
expect(out).toBe(
|
|
71
|
+
`<accent>Cv</accent> <dim>Rk</dim> <accent>Tn</accent> <dim>Pt</dim> `,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("falls back to nerd for an unknown mode", () => {
|
|
76
|
+
// @ts-expect-error exercising the runtime guard in getIcons
|
|
77
|
+
expect(getIcons("bogus")).toBe(getIcons("nerd"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("exposes exactly nerd/unicode/ascii in cycle order", () => {
|
|
81
|
+
expect([...ICON_MODES]).toEqual(["nerd", "unicode", "ascii"]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("envIconMode", () => {
|
|
86
|
+
const saved = { ...process.env };
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
process.env.OPTIMIZER_ICONS = saved.OPTIMIZER_ICONS;
|
|
89
|
+
process.env.PRETTY_ICONS = saved.PRETTY_ICONS;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("defaults to nerd when nothing set", () => {
|
|
93
|
+
process.env.OPTIMIZER_ICONS = undefined;
|
|
94
|
+
process.env.PRETTY_ICONS = undefined;
|
|
95
|
+
expect(envIconMode()).toBe("nerd");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("OPTIMIZER_ICONS wins over PRETTY_ICONS", () => {
|
|
99
|
+
process.env.OPTIMIZER_ICONS = "unicode";
|
|
100
|
+
process.env.PRETTY_ICONS = "none";
|
|
101
|
+
expect(envIconMode()).toBe("unicode");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("PRETTY_ICONS=none maps to ascii", () => {
|
|
105
|
+
process.env.OPTIMIZER_ICONS = undefined;
|
|
106
|
+
process.env.PRETTY_ICONS = "none";
|
|
107
|
+
expect(envIconMode()).toBe("ascii");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
48
111
|
describe("OptimizerStatus", () => {
|
|
49
112
|
/** Minimal ui stub capturing setStatus calls. */
|
|
50
113
|
function fakeCtx() {
|
|
@@ -92,4 +155,24 @@ describe("OptimizerStatus", () => {
|
|
|
92
155
|
`<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
93
156
|
);
|
|
94
157
|
});
|
|
158
|
+
|
|
159
|
+
it("seeds icon mode from the constructor", () => {
|
|
160
|
+
const status = new OptimizerStatus("ascii");
|
|
161
|
+
expect(status.mode).toBe("ascii");
|
|
162
|
+
const ctx = fakeCtx();
|
|
163
|
+
status.set("caveman", true, ctx as never);
|
|
164
|
+
expect(ctx.calls.at(-1)!.text).toContain("Cv");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("setMode switches the rendered glyph set and repaints", () => {
|
|
168
|
+
const status = new OptimizerStatus("nerd");
|
|
169
|
+
const ctx = fakeCtx();
|
|
170
|
+
status.set("rtk", true, ctx as never);
|
|
171
|
+
expect(ctx.calls.at(-1)!.text).toContain(TOOL_ICONS.rtk);
|
|
172
|
+
status.setMode("ascii", ctx as never);
|
|
173
|
+
const last = ctx.calls.at(-1)!;
|
|
174
|
+
expect(last.key).toBe(STATUS_KEY);
|
|
175
|
+
expect(last.text).toContain("Rk");
|
|
176
|
+
expect(last.text).not.toContain(TOOL_ICONS.rtk);
|
|
177
|
+
});
|
|
95
178
|
});
|
package/src/status.ts
CHANGED
|
@@ -42,13 +42,72 @@ export const STATUS_KEY = "pix-optimizer";
|
|
|
42
42
|
/** Tools that participate in the shared indicator, in render order. */
|
|
43
43
|
export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Icon presentation modes. The optimizer cell defaults to Nerd Font glyphs,
|
|
47
|
+
* which require a patched font (e.g. MesloLGS NF). Terminals without one show
|
|
48
|
+
* missing-glyph tofu, so two font-independent fallbacks are offered:
|
|
49
|
+
* - `unicode`: outline playing-card suits (standard BMP, ships with virtually
|
|
50
|
+
* every monospace font) + U+FE0E to force text (non-emoji) presentation.
|
|
51
|
+
* - `ascii`: two-letter labels, renders on literally any terminal.
|
|
52
|
+
* Mode is chosen via the /optimizer menu (persisted) or the OPTIMIZER_ICONS /
|
|
53
|
+
* PRETTY_ICONS env vars.
|
|
54
|
+
*/
|
|
55
|
+
export type IconMode = "nerd" | "unicode" | "ascii";
|
|
56
|
+
|
|
57
|
+
/** All selectable icon modes, in /optimizer cycle order. */
|
|
58
|
+
export const ICON_MODES: readonly IconMode[] = ["nerd", "unicode", "ascii"];
|
|
59
|
+
|
|
60
|
+
/** Force text (non-emoji) presentation for symbols that default to emoji. */
|
|
61
|
+
const VS_TEXT = "\uFE0E";
|
|
62
|
+
|
|
63
|
+
/** Per-mode glyph table for each tool. */
|
|
64
|
+
const ICON_SETS: Record<IconMode, Record<OptimizerTool, string>> = {
|
|
65
|
+
nerd: {
|
|
66
|
+
caveman: "",
|
|
67
|
+
rtk: "",
|
|
68
|
+
toon: "",
|
|
69
|
+
ponytail: "",
|
|
70
|
+
},
|
|
71
|
+
unicode: {
|
|
72
|
+
caveman: `\u2664${VS_TEXT}`, // ♤ white spade
|
|
73
|
+
rtk: `\u2661${VS_TEXT}`, // ♡ white heart
|
|
74
|
+
toon: `\u2662${VS_TEXT}`, // ♢ white diamond
|
|
75
|
+
ponytail: `\u2667${VS_TEXT}`, // ♧ white club
|
|
76
|
+
},
|
|
77
|
+
ascii: {
|
|
78
|
+
caveman: "Cv",
|
|
79
|
+
rtk: "Rk",
|
|
80
|
+
toon: "Tn",
|
|
81
|
+
ponytail: "Pt",
|
|
82
|
+
},
|
|
50
83
|
};
|
|
51
84
|
|
|
85
|
+
/** Resolve the glyph table for a mode. */
|
|
86
|
+
export function getIcons(mode: IconMode): Record<OptimizerTool, string> {
|
|
87
|
+
return ICON_SETS[mode] ?? ICON_SETS.nerd;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Default icon mode from the environment. OPTIMIZER_ICONS takes precedence;
|
|
92
|
+
* PRETTY_ICONS=none|off maps to ascii so one stack-wide var disables glyphs.
|
|
93
|
+
* Anything unrecognized (or unset) falls back to `nerd`. A persisted menu
|
|
94
|
+
* choice overrides this at load time (see persist.loadIconMode).
|
|
95
|
+
*/
|
|
96
|
+
export function envIconMode(): IconMode {
|
|
97
|
+
const raw = (process.env.OPTIMIZER_ICONS ?? "").toLowerCase();
|
|
98
|
+
if (raw === "nerd" || raw === "unicode" || raw === "ascii") return raw;
|
|
99
|
+
const pretty = (process.env.PRETTY_ICONS ?? "").toLowerCase();
|
|
100
|
+
if (pretty === "none" || pretty === "off") return "ascii";
|
|
101
|
+
return "nerd";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default Nerd Font glyph table. Kept as a named export for back-compat with
|
|
106
|
+
* callers/tests that reference the nerd set directly; equivalent to
|
|
107
|
+
* getIcons("nerd").
|
|
108
|
+
*/
|
|
109
|
+
export const TOOL_ICONS: Record<OptimizerTool, string> = ICON_SETS.nerd;
|
|
110
|
+
|
|
52
111
|
/** Fixed left-to-right order of icons in the cell. */
|
|
53
112
|
const TOOL_ORDER: readonly OptimizerTool[] = [
|
|
54
113
|
"caveman",
|
|
@@ -69,6 +128,7 @@ export type Colorize = (color: ThemeColor, text: string) => string;
|
|
|
69
128
|
* Build the colored status string for a set of tool states. ALL tool icons are
|
|
70
129
|
* always shown, in TOOL_ORDER; each is accent-colored when its tool is enabled
|
|
71
130
|
* and dim when disabled. `color` applies the theme color (e.g. theme.fg).
|
|
131
|
+
* `mode` selects the glyph table (nerd / unicode / ascii); defaults to nerd.
|
|
72
132
|
*
|
|
73
133
|
* Pure + exported for tests (pass a tagging colorizer to assert per-icon color).
|
|
74
134
|
* A trailing space separates the cell from the next status segment.
|
|
@@ -76,9 +136,11 @@ export type Colorize = (color: ThemeColor, text: string) => string;
|
|
|
76
136
|
export function renderStatus(
|
|
77
137
|
states: Partial<Record<OptimizerTool, boolean>>,
|
|
78
138
|
color: Colorize,
|
|
139
|
+
mode: IconMode = "nerd",
|
|
79
140
|
): string {
|
|
141
|
+
const icons = getIcons(mode);
|
|
80
142
|
return `${TOOL_ORDER.map((t) =>
|
|
81
|
-
color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR,
|
|
143
|
+
color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR, icons[t]),
|
|
82
144
|
).join(" ")} `;
|
|
83
145
|
}
|
|
84
146
|
|
|
@@ -89,12 +151,29 @@ export function renderStatus(
|
|
|
89
151
|
*/
|
|
90
152
|
export class OptimizerStatus {
|
|
91
153
|
private states: Partial<Record<OptimizerTool, boolean>> = {};
|
|
154
|
+
private iconMode: IconMode;
|
|
155
|
+
|
|
156
|
+
/** `mode` seeds the icon set (persisted choice or env default). */
|
|
157
|
+
constructor(mode: IconMode = "nerd") {
|
|
158
|
+
this.iconMode = mode;
|
|
159
|
+
}
|
|
92
160
|
|
|
93
161
|
/** Current enabled state for one tool (undefined until first set). */
|
|
94
162
|
get(tool: OptimizerTool): boolean | undefined {
|
|
95
163
|
return this.states[tool];
|
|
96
164
|
}
|
|
97
165
|
|
|
166
|
+
/** Current icon presentation mode. */
|
|
167
|
+
get mode(): IconMode {
|
|
168
|
+
return this.iconMode;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Switch icon mode and repaint the shared cell. */
|
|
172
|
+
setMode(mode: IconMode, ctx: Pick<ExtensionContext, "ui">): void {
|
|
173
|
+
this.iconMode = mode;
|
|
174
|
+
this.paint(ctx);
|
|
175
|
+
}
|
|
176
|
+
|
|
98
177
|
/** Update one tool's enabled state and repaint the shared cell. */
|
|
99
178
|
set(
|
|
100
179
|
tool: OptimizerTool,
|
|
@@ -107,7 +186,11 @@ export class OptimizerStatus {
|
|
|
107
186
|
|
|
108
187
|
/** Repaint the shared cell from current states. */
|
|
109
188
|
paint(ctx: Pick<ExtensionContext, "ui">): void {
|
|
110
|
-
const text = renderStatus(
|
|
189
|
+
const text = renderStatus(
|
|
190
|
+
this.states,
|
|
191
|
+
(c, t) => ctx.ui.theme.fg(c, t),
|
|
192
|
+
this.iconMode,
|
|
193
|
+
);
|
|
111
194
|
ctx.ui.setStatus(STATUS_KEY, text);
|
|
112
195
|
}
|
|
113
196
|
}
|