Skip to content

TTY-aware color (--color, FORCE_COLOR) and OSC 8 clickable rule ids and paths #415

Description

@not-stbenjam

skillsaw decides whether to emit ANSI color solely by checking for the NO_COLOR environment variable — there is no isatty() check on stdout anywhere. skillsaw lint | less or redirecting to a file leaks raw ^[[91m escapes, which is the one place a polished CLI looks broken. The fix is the standard cascade every mature tool ships (ruff, pip, gh): --color always|never|auto > FORCE_COLOR > NO_COLOR > stream.isatty().

Once capability detection exists, the same gate enables OSC 8 hyperlinks: rule ids link to their skillsaw.org docs page and file paths become clickable file:// links, which lets the redundant "Rule docs" footer collapse in interactive terminals.

Why

Verified against main (5d2dbf9):

  • Color is gated only on NO_COLOR presence, in exactly two places: src/skillsaw/formatters/text.py:35 and _ansi_colors() at src/skillsaw/cli/_helpers.py:147.
  • isatty() appears only for the stderr progress indicator (_helpers.py:27) and stdin prompts in marketplace code — never stdout.
  • No --color flag in src/skillsaw/cli/_parser.py; zero FORCE_COLOR or OSC 8 (\x1b]8;;) references anywhere in src/.
  • Live repro: skillsaw lint | cat -v shows literal ^[[91m escapes in the piped output.
  • The "Rule docs" footer (text.py:80) repeats one docs URL per rule that OSC 8 links would make redundant interactively.

Proposed implementation

  1. Add color_enabled(stream) to src/skillsaw/cli/_helpers.py implementing the cascade: explicit --color always|never|auto (new flag in src/skillsaw/cli/_parser.py) > FORCE_COLOR > NO_COLOR > stream.isatty(). Document the chosen precedence in docs/cli.md.
  2. Thread the resolved bool through format_output/format_text (src/skillsaw/formatters/__init__.py, src/skillsaw/formatters/text.py) and the four ANSI-emitting subcommands — _lint.py, _fix.py, _explain.py, _badge.py all call _ansi_colors() today. Gate stderr notices (_helpers.py:180) on sys.stderr.isatty() separately from stdout.
  3. Emit OSC 8 hyperlinks only when color is enabled and TERM != dumb: rule ids → their docs page; paths → file:// URLs. Note: #L<line> fragments on file:// are nonstandard and ignored by many terminals — harmless, but don't advertise line jumping.
  4. Collapse the "Rule docs" footer only in hyperlink-capable (TTY) mode; piped output keeps the footer, so anything parsing text output today is unaffected.
  5. Gotcha (main regression risk): action.yml must set FORCE_COLOR=1. GitHub Actions stdout is not a TTY, so without it every Actions user silently loses colored logs the release this lands.
  6. Gotcha: existing tests assert color-by-default through non-TTY capture and must be updated: tests/test_formatters.py test_text_includes_ansi_by_default (~line 194) and tests/test_integration.py test_custom_rule_warning_colors_respect_no_color (~line 2562, a subprocess-pipe assert of \x1b[ on stderr with NO_COLOR unset). Convert them to FORCE_COLOR=1 or invert the expectation.

Files to touch: src/skillsaw/cli/_parser.py, src/skillsaw/cli/_helpers.py, src/skillsaw/formatters/text.py, src/skillsaw/formatters/__init__.py, src/skillsaw/cli/_lint.py, src/skillsaw/cli/_fix.py, src/skillsaw/cli/_explain.py, src/skillsaw/cli/_badge.py, action.yml, tests/test_formatters.py, tests/test_integration.py, docs/cli.md, README.md.

Scope

Effort: M — the mechanics are simple, but the bool threads through four subcommands plus formatters, and the action.yml + test updates are mandatory, not optional polish.

Out of scope:

  • Color themes or new color choices — same palette, just correct gating.
  • ANSI/OSC 8 in JSON, SARIF, HTML, or the new github/markdown formatters — those stay byte-identical.
  • Windows legacy console handling (modern Windows terminals enable VT processing by default).

Interactions with open PRs:


Surfaced and adversarially verified by a multi-agent codebase exploration. 🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    fable-5PR's/Issues filed by Fable

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions