From e8a248f80e859335b27282e1e01bc1f874393f8c Mon Sep 17 00:00:00 2001 From: Daniel Naves de Carvalho Date: Thu, 14 May 2026 16:53:12 -0300 Subject: [PATCH 1/2] feat(terminal): honor CCSTATUSLINE_WIDTH env var to override probe Provide an explicit width override so users can bypass the TTY probe entirely when both ancestor-walk and `tput cols` fall through. This is the fallback case the existing probe cannot solve on its own: Claude Code >= 2.1.139 spawns statusline/hooks without terminal access, and in some configurations (IDE integrations, nested shells, certain agent-mode spawn paths) no ancestor process owns a TTY either. The ancestor walk fails, `tput cols` returns 80, and the multi-line layout truncates regardless of the actual iTerm2/terminal width. PR #377 (`stty -F`/`stty -f`) covers the case where an ancestor does hold a TTY but the legacy `< /dev/tty` form errors with ENOTTY. This patch is complementary -- it handles the case where the ancestor walk finds no TTY at all -- and gives users a knob today while upstream work on passing `terminalWidth` via stdin JSON (#308) lands. Change ------ `src/utils/terminal.ts` -- `probeTerminalWidth` now reads `CCSTATUSLINE_WIDTH` before any platform check or probe. A valid positive integer short-circuits with that value; anything else (missing, empty, non-numeric, zero, negative) falls through to the existing probe logic. Usage ----- Set the env var on the statusLine command in `~/.claude/settings.json`: ```json "statusLine": { "type": "command", "command": "CCSTATUSLINE_WIDTH=200 ccstatusline" } ``` Tests ----- Four new cases in `src/utils/__tests__/terminal.test.ts`: - override short-circuits probing entirely - non-positive override (`0`) falls back to probing - non-numeric override falls back to probing - override applies on Windows where probing is otherwise disabled `bunx vitest run src/utils/__tests__/terminal.test.ts` -> 12/12 pass. Full-suite delta vs `main`: 4 new passing tests, zero new failures (the pre-existing 105 env-specific failures in other suites are unchanged on both branches). `bun run lint` -> clean. --- src/utils/__tests__/terminal.test.ts | 62 ++++++++++++++++++++++++++++ src/utils/terminal.ts | 14 +++++++ 2 files changed, 76 insertions(+) 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 582c8aa1..4b8df82c 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') { From 90e912b7de4b30d9521f9ac3163835c84310ff21 Mon Sep 17 00:00:00 2001 From: Matthew Breedlove Date: Sun, 17 May 2026 02:56:27 -0400 Subject: [PATCH 2/2] docs: document terminal width override --- docs/USAGE.md | 8 ++++++++ docs/WINDOWS.md | 10 ++++++++++ 2 files changed, 18 insertions(+) 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