Skip to content

fix(render): emit colour only on a TTY, honour NO_COLOR; README: -C operates on the named directory - #197

Merged
JordanCoin merged 1 commit into
mainfrom
fix/no-ansi-when-piped
Oct 7, 2026
Merged

JordanCoin merged 1 commit into
mainfrom
fix/no-ansi-when-piped

Conversation

@JordanCoin

Copy link
Copy Markdown
Owner

What does this PR do?

Problem

codemap emitted ANSI colour escapes unconditionally. There was no isatty check anywhere and NO_COLOR / TERM=dumb were ignored. Measured on this repo at origin/main (e8e7e73) with stdout piped:

Command (stdout piped) Total bytes ANSI escape bytes
codemap . | wc -c 14,301 4,906 (34%)
codemap hook session-start (stdin {"session_id":"t","cwd":"<repo>"}) 16,829 5,397 (32%)

Hooks always run with stdout piped into an agent prompt, so every session paid roughly 1,500 tokens of pure escape codes.

Fix

  • render/colors.go: the colour names (Reset, Bold, Cyan, ...) are now package variables backed by a constant palette. Every renderer keeps concatenating them unconditionally; the variables are the escape sequence while colour is on and "" while it is off. skyline's buildingColors is derived from the same switch.
  • render/color_mode.go (new): ColorAuto | ColorAlways | ColorNever, SetColorMode, ParseColorMode, ColorEnabled. Auto resolves at package init and again when the flag is applied: colour only if golang.org/x/term.IsTerminal(stdout) and NO_COLOR is unset (https://no-color.org) and TERM != dumb. golang.org/x/term was already a direct dependency; go mod tidy is a no-op.
  • cmd/root.go: --color=auto|always|never (also --color <mode>; bare --color means always, like ls/grep) is parsed with the other global options, so it works before any subcommand. -C --color=never is rejected like the other missing-path cases.
  • main.go: applies the mode right after global options; the hook dispatcher forces ColorNever (hook stdout is never a terminal). Hooks exec the binary into a pipe, so auto already yields no colour there; the force is belt-and-braces.
  • --json is untouched and byte-identical (cmp of before/after output on this repo: identical; e2e test asserts auto/always/never produce the same JSON).
  • --skyline --animate keeps colour on a terminal and goes monochrome under --color=never/piped.

Before / after

Command (stdout piped) Before After After --color=always
codemap . 14,301 B / 4,906 ANSI 9,395 B / 0 ANSI 14,301 B / 4,906 ANSI
codemap hook session-start 16,829 B / 5,397 ANSI 11,432 B / 0 ANSI n/a (never colours)
codemap --json . 15,473 B / 0 15,473 B / 0, byte-identical identical

New or changed printed lines (docs/OUTPUT-STANDARD.md)

  1. Help: --color <mode> Colour output: auto (default: only on a terminal, off when NO_COLOR is set), always, never
    Sourced: codemap --help. Precise: names the three modes and the two conditions. Bounded: states the default and when auto is off. Useful: tells the human which flag keeps colour under less -R.
  2. Error: --color: unknown mode "sometimes" (want auto, always or never) (exit 2)
    Sourced: the rejected argument is echoed. Precise: lists the accepted values. Bounded: nothing else fails. Useful: names what to type instead.
  3. README flag row: `--color <mode>` | `auto` (default): ANSI colour only when stdout is a terminal and `NO_COLOR` is unset; `always` keeps colour when piping into `less -R`; `never` turns it off
  4. README -C wording (two lines): `-C`/`--project-root` operates on exactly the named directory, like `git -C`; it may be a subdirectory of a repository, and storage and config discovery still walk up to the git root. and the table row `-C, --project-root <dir>` | Operate on exactly `<dir>` (may be a subdirectory of a repository; storage and config discovery still walk up to the git root). Sourced: fix(cmd): -C operates on the given directory, not the Git root #194. Precise and bounded: says what -C does and the one thing that still walks up.

No other printed line changed; tree, skyline and hook text is identical minus the escapes.

Tests

  • render/color_mode_test.go (new): piped tree (stdoutIsTerminal stubbed false, auto) contains no \x1b[; ColorAlways restores \x1b[1;34m src/app/\x1b[0m and the bold project name even with NO_COLOR=1; ColorNever on a terminal is quiet; static skyline honours the mode and buildingColors is blanked/restored; autoColorEnabled table (tty, piped, NO_COLOR=1, NO_COLOR= set-but-empty, TERM=dumb); ParseColorMode round trip and rejects.
  • render/colors_test.go: TestGetFileColor* and TestANSIConstants now force ColorAlways for the test (with cleanup) so they keep asserting the real palette instead of becoming vacuous under go test's piped stdout.
  • cmd/root_test.go: --color=never, --color always, bare --color, --color=auto after a subcommand, no flag, --color=sometimes rejected, -C --color=never rejected, --color alone does not activate root options.
  • color_e2e_test.go (new, runs the built binary): piped codemap . has no escapes; --color=always with NO_COLOR=1 has them; --color=never, --color never, NO_COLOR=1, TERM=dumb are quiet; --json identical across modes; unknown mode exits non-zero with the error above.
  • gofmt -l ., go vet ./..., staticcheck ./...: clean. go mod tidy: no change.
  • go test ./...: all green except the three pre-existing root-only failures (TestRunSetupCreatesConfigAndHooks, TestSelectReportsInaccessiblePrimarySetup, TestListProjectsPreservesPerChildScanErrors). watch.TestEventDetection flaked once on a full run and passed 3/3 on rerun; this PR touches nothing under watch/.

Fixes part 1 of #154

Type of change

  • Bug fix
  • Documentation

Checklist

  • I've tested this locally with go build && ./codemap .
  • I've read CONTRIBUTING.md
  • I've updated documentation if needed

Additional notes

Rebased onto #194 (393b048) before pushing; the README -C lines were still the pre-#194 wording on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_0159tcanCgA3jxi2E7DdRVWJ


Generated by Claude Code

…perates on the named directory

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159tcanCgA3jxi2E7DdRVWJ
Copilot AI balanced review requested due to automatic review settings October 7, 2026 03:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants