Skip to content

fix(cli): --color auto styles terminals that do not render ANSI (TERM=dumb) #3120

Description

@mrunalp

User Story

As someone running openshell from a terminal that does not render ANSI — an
editor shell, a serial console, or any environment that sets TERM=dumb
I want output to be plain text,
so that I read status and error messages instead of the escape sequences meant
to color them.

Problem Statement

--color auto, the default, treats any terminal as able to render ANSI. It
checks only whether the stream is a terminal, not whether that terminal
interprets escape sequences. TERM=dumb is still a terminal, so output is
styled and the escapes are printed literally.

An unset TERM behaves the same way, and nothing identifies a capable terminal
in that case either.

Part of this is a regression introduced by #3026. Two of the four styling paths
were doing the right thing before that change:

  • console, which draws indicatif progress bars and dialoguer prompts,
    refuses to colorize when TERM is dumb or unset
    (console-0.15.11/src/unix_term.rs).
  • miette applies the same check for error rendering, through
    supports-color.

#3026 overrides both with a single process-wide switch so that --color could
govern them, and that switch had no capability check. So it replaced two working
checks rather than only failing to add one. The remaining two paths — the
tracing formatter and the owo-colors wrapper — never had detection, so those
are a gap rather than a regression.

Impact / Why This Matters

On a dumb terminal every styled line is now harder to read than the plain text
it replaced. STATUS in forward list reads ^[[31mdead^[[0m, error messages
and -v log lines carry escapes, and progress spinners and prompts do too.

This affects environments where the user cannot simply switch terminals: Emacs
M-x shell and comint buffers set TERM=dumb by design, as do some CI shells,
serial consoles, and build-tool subshells. In those, the escapes are noise
around exactly the text the user is trying to read.

The workaround is to pass --color never or set NO_COLOR on every invocation,
or export NO_COLOR in the shell profile. That works but is not discoverable —
nothing in the output suggests why it looks wrong, and a user seeing ^[[31m
has no obvious reason to reach for a color flag. It also means opting out of
color entirely rather than getting the correct automatic answer.

For the two regressed paths, users who had correct behavior before #3026 now
have to add a workaround they did not previously need.

Acceptance Criteria

  • On a terminal with TERM=dumb, default output contains no ANSI escape
    sequences.
  • The same holds when TERM is unset.
  • Output is still styled on a terminal that reports ANSI support, so normal
    interactive use is unchanged.
  • --color always and FORCE_COLOR still force styling on a dumb
    terminal, for callers who know better than the detection.
  • --color never and NO_COLOR still suppress styling on a capable
    terminal.
  • The rule covers every styled surface — tables, -v log lines, progress
    spinners, prompts, and error rendering — not only the ones that regressed.
  • Piped and redirected output is unaffected; it is already plain.
  • Documented alongside --color and the color environment variables.

Reproduction Steps

This reproduces only on a real terminal. Piping will not show it, because color
is already suppressed for non-terminals.

  1. In a terminal, run:

    TERM=dumb openshell forward list

    (any command with styled output works — openshell gateway list needs no
    gateway configured)

  2. Observe raw escape sequences in the output rather than color, for example
    ^[[31mdead^[[0m in the STATUS column.

  3. The same with TERM unset:

    env -u TERM openshell forward list

To capture it non-interactively, allocate a pseudo-terminal so the CLI still
sees a terminal, then inspect the bytes:

TERM=dumb script -qec "openshell forward list" /dev/null | cat -A

Environment

Logs

# TERM=dumb on a pseudo-terminal, bytes as received
$ TERM=dumb script -qec "openshell forward list" /dev/null | cat -A
SANDBOX BIND    PORT     PID        STATUS$
demo    0.0.0.0 8443     4000000    ^[[31mdead^[[0m$

# The same terminal, opting out by hand
$ TERM=dumb script -qec "openshell forward list --color never" /dev/null | cat -A
SANDBOX BIND    PORT     PID        STATUS$
demo    0.0.0.0 8443     4000000    dead$

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:cliCLI-related work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions