From f809ef35ad82ea878294d66339fb5089415be1d6 Mon Sep 17 00:00:00 2001 From: RECTOR Date: Mon, 20 Jul 2026 17:46:51 +0700 Subject: [PATCH] fix(v0.2.2): use real Theme instance for cursor colors, not EditorTheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editor factory registered via ctx.ui.setEditorComponent receives an EditorTheme ({ borderColor, selectList }) — NOT the full Theme instance. v0.2.0 assumed that param had getFgAnsi/getColorMode and called theme.getFgAnsi("accent") at render time, crashing pi on startup with "theme.getFgAnsi is not a function" on the first render tick. Resolve the live Theme from ctx.ui.theme (readonly getter on ExtensionUIContext that always returns the current theme, so theme switches are reflected) and pass it into CursorEditor via a new deps.getTheme accessor. The EditorTheme factory arg is still forwarded to super() for border/autocomplete styling. Tests updated to supply getTheme; 122 pass, typecheck clean. --- extensions/cursor.ts | 2 +- lib/editor.ts | 11 +++++++---- package.json | 2 +- tests/editor.test.ts | 8 ++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/extensions/cursor.ts b/extensions/cursor.ts index 3172df9..5e8357e 100644 --- a/extensions/cursor.ts +++ b/extensions/cursor.ts @@ -127,7 +127,7 @@ export default function (pi: ExtensionAPI): void { blink = blinkController; ctx.ui.setEditorComponent((tui: any, theme: any, keybindings: any) => { const wrapped = prevEditorFactory ? prevEditorFactory(tui, theme, keybindings) : null; - const ed = new CursorEditor(tui, theme, keybindings, { wrapped, blink: blinkController }); + const ed = new CursorEditor(tui, theme, keybindings, { wrapped, blink: blinkController, getTheme: () => ctx.ui.theme }); editor = ed; ed.updateConfig(cfg); // In hardware mode the native DECSCUSR blink drives the cursor; don't run the fake blink. diff --git a/lib/editor.ts b/lib/editor.ts index 64f3931..962d37d 100644 --- a/lib/editor.ts +++ b/lib/editor.ts @@ -8,6 +8,11 @@ type Theme = { getFgAnsi(color: string): string; getColorMode(): "truecolor" | " export interface CursorEditorDeps { wrapped: { render(width: number): string[]; handleInput(data: string): void } | null; blink: BlinkController; + /** Live accessor for the active pi `Theme` instance (NOT the EditorTheme + * passed to the editor factory, which only has `borderColor`/`selectList`). + * The real Theme exposes `getFgAnsi`/`getColorMode` for truecolor cursor + * colors and must be read live so theme switches are reflected. */ + getTheme: () => Theme; } export function composeRender( @@ -28,12 +33,10 @@ export class CursorEditor extends CustomEditor { private cfg: CursorConfig; private paneFocused = true; private deps: CursorEditorDeps; - private cursorTheme: Theme; constructor(tui: any, theme: any, keybindings: any, deps: CursorEditorDeps) { super(tui, theme, keybindings, {}); this.deps = deps; - this.cursorTheme = theme as Theme; this.cfg = { enabled: true, focusedStyle: "block", @@ -73,7 +76,7 @@ export class CursorEditor extends CustomEditor { this.cfg.focusedStyle === "underline" ? "underline" : this.cfg.focusedStyle === "bar" ? "bar" : "block"; this.writeTerm(decscusr(shape, this.cfg.blink)); - const hex = this.cfg.cursorColor === "accent" ? themeAccentHex(this.cursorTheme) : this.cfg.cursorColor; + const hex = this.cfg.cursorColor === "accent" ? themeAccentHex(this.deps.getTheme()) : this.cfg.cursorColor; const osc = osc12(hex); if (osc) this.writeTerm(osc); } else { @@ -112,6 +115,6 @@ export class CursorEditor extends CustomEditor { render(width: number): string[] { const lines = this.deps.wrapped ? this.deps.wrapped.render(width) : super.render(width); - return composeRender(lines, this.paneFocused, this.cfg, this.cursorTheme, this.deps.blink.visible, this.cfg.cursorMode); + return composeRender(lines, this.paneFocused, this.cfg, this.deps.getTheme(), this.deps.blink.visible, this.cfg.cursorMode); } } \ No newline at end of file diff --git a/package.json b/package.json index c5738e2..c06b92d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getpipher/cursor", - "version": "0.2.1", + "version": "0.2.2", "description": "Focus-aware, customizable editor cursor for the pi coding agent (truecolor + Ghostty/tmux deep; tmux + cmux + herdr; static fallback).", "keywords": [ "pi-package", diff --git a/tests/editor.test.ts b/tests/editor.test.ts index cb71c80..209dc65 100644 --- a/tests/editor.test.ts +++ b/tests/editor.test.ts @@ -37,7 +37,7 @@ function makeEditor(wrappedRender: (w: number) => string[]) { const wrapped = { render: wrappedRender, handleInput: (_d: string) => {} }; const tui = { requestRender: () => {} }; const theme = THEME; - const ed = new CursorEditor(tui as any, theme as any, {} as any, { wrapped: wrapped as any, blink }); + const ed = new CursorEditor(tui as any, theme as any, {} as any, { wrapped: wrapped as any, blink, getTheme: () => theme }); ed.updateConfig(DEFAULT_CONFIG); return { ed, wrapped }; } @@ -116,7 +116,7 @@ function makeHwEditor(cfg: Partial = {}) { const m = mockTui(); const blink = new BlinkController(noopScheduler); const wrapped = { render: () => [""], handleInput: (_d: string) => {} }; - const ed = new CursorEditor(m.tui as any, HW_THEME as any, {} as any, { wrapped: wrapped as any, blink }); + const ed = new CursorEditor(m.tui as any, HW_THEME as any, {} as any, { wrapped: wrapped as any, blink, getTheme: () => HW_THEME }); ed.updateConfig({ ...DEFAULT_CONFIG, ...cfg }); return { ed, m, blink }; } @@ -135,7 +135,7 @@ test("hardware mode: blink on → blinking DECSCUSR + BlinkController stopped", const origStop = blink.stop.bind(blink); blink.stop = () => { stopped++; origStop(); }; const wrapped = { render: () => [""], handleInput: (_d: string) => {} }; - const ed = new CursorEditor(m.tui as any, HW_THEME as any, {} as any, { wrapped: wrapped as any, blink }); + const ed = new CursorEditor(m.tui as any, HW_THEME as any, {} as any, { wrapped: wrapped as any, blink, getTheme: () => HW_THEME }); ed.updateConfig({ ...DEFAULT_CONFIG, cursorMode: "hardware", blink: true, focusedStyle: "underline" }); assert.ok(m.writes.some((w) => w.includes("\x1b[3 q")), "blinking underline DECSCUSR"); assert.ok(stopped >= 1, "blink.stop called (native blink replaces fake blink)"); @@ -151,7 +151,7 @@ test("hardware mode: 256-color theme → OSC 12 skipped (no exact hex)", () => { const blink = new BlinkController(noopScheduler); const wrapped = { render: () => [""], handleInput: (_d: string) => {} }; const theme256 = { getFgAnsi: () => "\x1b[38;5;7m", getColorMode: () => "256color" as const }; - const ed = new CursorEditor(m.tui as any, theme256 as any, {} as any, { wrapped: wrapped as any, blink }); + const ed = new CursorEditor(m.tui as any, theme256 as any, {} as any, { wrapped: wrapped as any, blink, getTheme: () => theme256 }); ed.updateConfig({ ...DEFAULT_CONFIG, cursorMode: "hardware" }); assert.ok(!m.writes.some((w) => w.includes("\x1b]12;")), "OSC 12 skipped in 256 mode"); });