diff --git a/docs/USAGE.md b/docs/USAGE.md index 98ddafbe..36cb8435 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -60,6 +60,14 @@ These settings affect where long lines are truncated, and where right-alignment - **Full width minus 40** - Reserves 40 characters for auto-compact message to prevent wrapping (default) - **Full width until compact** - Dynamically switches between full width and minus 40 based on context percentage threshold (configurable, default 60%) +If ccstatusline cannot detect your terminal width, set `CCSTATUSLINE_WIDTH` to a positive integer to override probing: + +```bash +CCSTATUSLINE_WIDTH=160 ccstatusline +``` + +The override is checked before automatic width detection, so it also works in wrapper processes, IDE integrations, nested PTYs, and Windows environments where probing may be unavailable. Invalid values such as `0`, negative numbers, or non-numeric strings are ignored and ccstatusline falls back to normal detection. + ## Global Options Configure global formatting preferences that apply to all widgets: diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index 4ca55085..9af9bd56 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -106,6 +106,16 @@ winget install DEVCOM.JetBrainsMonoNerdFont ### Common Issues & Solutions +**Issue**: Status lines wrap because terminal width cannot be detected + +```powershell +# Set an explicit width before launching Claude Code +$env:CCSTATUSLINE_WIDTH="160" +claude +``` + +`CCSTATUSLINE_WIDTH` accepts a positive integer column width and is checked before automatic width detection. Set it in the same environment that starts Claude Code so the status line command inherits it. This is useful on Windows because native width probing is disabled when ccstatusline runs outside WSL. + **Issue**: Powerline symbols showing as question marks or boxes ```powershell diff --git a/src/utils/__tests__/terminal.test.ts b/src/utils/__tests__/terminal.test.ts index 0fe783b3..da5db8a4 100644 --- a/src/utils/__tests__/terminal.test.ts +++ b/src/utils/__tests__/terminal.test.ts @@ -30,10 +30,12 @@ describe('terminal utils', () => { beforeEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); + delete process.env.CCSTATUSLINE_WIDTH; }); afterEach(() => { vi.restoreAllMocks(); + delete process.env.CCSTATUSLINE_WIDTH; }); it('returns width from the immediate parent tty when available', () => { @@ -188,6 +190,66 @@ describe('terminal utils', () => { expect(canDetectTerminalWidth()).toBe(false); }); + it('honors CCSTATUSLINE_WIDTH override before probing', () => { + process.env.CCSTATUSLINE_WIDTH = '220'; + + expect(getTerminalWidth()).toBe(220); + expect(mockExecSync.mock.calls.length).toBe(0); + }); + + it('ignores a non-positive CCSTATUSLINE_WIDTH and falls back to probing', () => { + process.env.CCSTATUSLINE_WIDTH = '0'; + + mockExecSync.mockImplementation((command: string) => { + if (command === `ps -o ppid= -p ${process.pid}`) { + return '1234\n'; + } + + if (command === 'ps -o tty= -p 1234') { + return 'ttys001\n'; + } + + if (command === `stty -F /dev/ttys001 size 2>/dev/null | awk '{print $2}'`) { + return '160\n'; + } + + throw new Error(`Unexpected command: ${command}`); + }); + + expect(getTerminalWidth()).toBe(160); + }); + + it('ignores a non-numeric CCSTATUSLINE_WIDTH and falls back to probing', () => { + process.env.CCSTATUSLINE_WIDTH = 'wide'; + + mockExecSync.mockImplementation((command: string) => { + if (command === `ps -o ppid= -p ${process.pid}`) { + return '1234\n'; + } + + if (command === 'ps -o tty= -p 1234') { + return 'ttys001\n'; + } + + if (command === `stty -F /dev/ttys001 size 2>/dev/null | awk '{print $2}'`) { + return '160\n'; + } + + throw new Error(`Unexpected command: ${command}`); + }); + + expect(getTerminalWidth()).toBe(160); + }); + + it('CCSTATUSLINE_WIDTH override applies on Windows where probing is disabled', () => { + vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); + process.env.CCSTATUSLINE_WIDTH = '180'; + + expect(getTerminalWidth()).toBe(180); + expect(canDetectTerminalWidth()).toBe(true); + expect(mockExecSync.mock.calls.length).toBe(0); + }); + it('disables width detection on Windows', () => { vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); diff --git a/src/utils/terminal.ts b/src/utils/terminal.ts index f6ef73e2..57429ba5 100644 --- a/src/utils/terminal.ts +++ b/src/utils/terminal.ts @@ -33,6 +33,20 @@ export function getPackageVersion(): string { } function probeTerminalWidth(): number | null { + // Explicit override. Useful when ccstatusline is spawned in a context where + // no ancestor process owns a TTY at all — e.g. some Claude Code >= 2.1.139 + // spawn paths, IDE integrations, or nested-shell scenarios where both the + // ancestor-walk probe and `tput cols` return nothing usable. Users can set + // CCSTATUSLINE_WIDTH on the statusLine command (e.g. + // `CCSTATUSLINE_WIDTH=200 ccstatusline ...`) to bypass probing entirely. + const overrideRaw = process.env.CCSTATUSLINE_WIDTH; + if (overrideRaw) { + const override = parsePositiveInteger(overrideRaw); + if (override !== null) { + return override; + } + } + // Preserve historical behavior on Windows: width detection is unavailable. // This avoids Unix fallback command behavior (e.g. 2>/dev/null) on Windows. if (process.platform === 'win32') {