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"); });