Skip to content

DRAFT: Prompt and the command wizard write colour escapes when NO_COLOR is set #8641

Closed

Description

@spencerbeggs

Thanks for #7573. Help, version and error output now follow NO_COLOR exactly as the spec describes. We noticed that Prompt and the command wizard don't consult it, and we aren't sure whether that's intended.

What we observed

With NO_COLOR=1 on a terminal, --help prints no escape sequences. In the same program and the same terminal, a Prompt and the --wizard flow both write colour and style codes. Even with every colour slot of Prompt.Theme set to "", a prompt still writes bold, underline and a reset after every styled fragment.

Environment: [email protected] (tag [email protected], ad61db8) and @effect/[email protected], on Node 26.10.0 and macOS. From reading the source, the same lines are unchanged on main at 8b0eac6 (packages/effect/src/cli/internal/ansi.ts:108-111, packages/effect/src/cli/internal/wizard.ts:250-270).

Reproduction

A Prompt.Select with an all-empty-colour theme, prompt.ts:

import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { Console, Effect } from "effect"
import { Prompt } from "effect/cli"

// Every colour slot of the theme emptied, on top of NO_COLOR=1.
const noColours = Prompt.makeTheme({
  primaryColor: "",
  mutedColor: "",
  successColor: "",
  errorColor: "",
  submittedColor: ""
})

const pick = Prompt.Select({
  message: "Pick one",
  choices: [{ title: "a", value: "a" }, { title: "b", value: "b" }]
})

Prompt.run(pick).pipe(
  Effect.flatMap((value) => Console.log(`selected: ${value}`)),
  Effect.provideService(Prompt.Theme, noColours),
  Effect.provide(NodeServices.layer),
  NodeRuntime.runMain
)

A one-flag command for --help and --wizard, wizard.ts:

import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { Effect } from "effect"
import { Command, Flag } from "effect/cli"

const app = Command.make("app", { name: Flag.String("name") })

Command.run(app, { version: "0.0.0" }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain)

We ran each under a pseudo-terminal with NO_COLOR=1 and captured the raw output live (escape bytes shown escaped; long lines trimmed with …).

The control, NO_COLOR=1 node wizard.ts --help, has no escapes:

USAGE\r\n  app [flags]\r\n\r\nFLAGS\r\n  --name string    \r\n\r\nGLOBAL FLAGS\r\n  --help, -h   …

NO_COLOR=1 node prompt.ts, then Enter:

\x1b[?25l?\x1b[0m \x1b[1mPick one\x1b[0m ›\x1b[0m \r\n❯\x1b[0m \x1b[4ma\x1b[0m \r\n  b \x1b[2K\x1b[1G\x1b[2K\x1b[G✔\x1b[0m \x1b[1mPick one\x1b[0m …\x1b[0m  a\x1b[0m\r\n\x1b[?25hselected: a\r\n

NO_COLOR=1 node wizard.ts --wizard, then Ctrl-C:

\x1b[1m\x1b[96mapp\x1b[0m \x1b[37mv0.0.0\x1b[0m \x1b[1m\x1b[37m· Command wizard\x1b[0m\r\n\x1b[90mBuild a command interactively. Press Ctrl+C to cancel.\x1b[0m\r\n\r\n\x1b[1m\x1b[37mCurrent command\x1b[0m\r\n\x1b[35m  $ app\x1b[0m\r\n\r\n\x1b[1m\x1b[96mROOT\x1b[0m \x1b[90m·\x1b[0m \x1b[1m\x1b[37mFLAGS\x1b[0m\r\n\x1b[96m?\x1b[0m \x1b[1mName (--name)\x1b[0m \x1b[90m›\x1b[0m \x07\x1b[?25h\r\n\x1b[31mWizard cancelled.\x1b[0m\r\n

In the prompt output, each \x1b[0m is a reset with nothing to reset once the theme is empty. \x1b[1m is bold and \x1b[4m is underline. The wizard's headings, command block and "cancelled" line use fixed colours (\x1b[96m, \x1b[37m, \x1b[35m, \x1b[31m).

The cursor codes (\x1b[?25l/\x1b[?25h, \x1b[2K, \x1b[1G) are a separate matter. NO_COLOR is about colour, and a prompt needs cursor movement to redraw, so we're not suggesting those change.

Where it happens

  • ansi.ts:108-111: annotate always appends reset, even when every style passed to it is empty.
  • Prompt.ts:623-641: the theme's colours are raw escape strings, and the prompt renderers add Ansi.bold and Ansi.underlined independently of the theme.
  • wizard.ts:250-270: the section headings, introduction and "Wizard cancelled." line are coloured unconditionally, and renderCommandBlock just below follows the same pattern.
  • By contrast, CliOutput.ts:308-319: defaultFormatter checks the TTY and NO_COLOR, which is why the help control above is clean.

Is this intended?

Maybe prompts are exempt on purpose, since they only run on a terminal anyway. If not, one possible direction is for annotate to skip the reset when no style was applied, and for the prompt renderers and wizard to take styling from the theme, or from the same colour decision the formatter makes. That would let an application follow NO_COLOR with a plain theme. #7556 and its PR #8350 propose an injectable colour decision for CliOutput and leave Prompt out of scope; this may be the other half of that.

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions