Skip to main content

Ouro AX

ax-terminal

What a terminal shows a person, with a plain fallback off a terminal: colour, status marks, the time something took, next steps and a progress line for anything slow. Colour is on only for a terminal; NO_COLOR always turns it off, and FORCE_COLOR turns it on for a capture. Off a terminal, the marks are words and nothing is redrawn, so a pipe, a log or an agent gets plain text with no escape codes.

npx shadcn@latest add ourostack/ouro-ui/ax-terminal#v1.3.10

It installs lib/ouro/ax/terminal.ts, with ax-outcome, ax-voice. Its tests are ax-terminal-tests. Every item runs on Node 22.18 or later as it is copied: the files import each other as ./<file>.ts. The Ouro AX guide shows how they fit together.

Exports

createProgress

Progress for anything slow, on stderr. On a terminal, a spinner line with the step, its place and the time so far, drawn only once a step has run past a short delay, redrawn in place and wiped when it ends, with spaces rather than escape codes. Off a terminal or in CI, one plain line for a step still running after the slow mark, and nothing else. With off, nothing.

function createProgress(stream: ProgressStream, options: ProgressOptions): Progress

FRAMES

The spinner's frames, on a terminal.

const FRAMES: readonly string[]

hasAnsi

Whether text holds an escape character: what the X-ray looks for in piped output.

function hasAnsi(text: string): boolean

Mark

type Mark = "passed" | "failed" | "errored" | "warned" | "skipped" | "planned";

markOf

A state's mark: its symbol in its colour on a terminal, else its word, so a reader without colour still tells a failure from a pass.

function markOf(state: Mark, style: TerminalStyle): string

MARKS

A state's mark, as a symbol on a terminal and as a word elsewhere, and its colour.

const MARKS: Readonly<Record<Mark, { symbol: string; word: string; color: keyof Paint }>>

nextStepsText

The next steps, under a Next heading: each step's reason, then its command on the line below, indented and alone, so it can be copied whole. An empty string for an empty list.

function nextStepsText(steps: readonly NextStep[], style: TerminalStyle): string

paint

The colours, as functions that wrap text in their escape codes when the style has colour, and return it unchanged when it doesn't.

function paint(style: Pick<TerminalStyle, "color">): Paint

Paint

type Paint = Record<"bold" | "dim" | "red" | "green" | "yellow" | "cyan", (text: string) => string>;

Progress

One running step: start it, then stop it when it ends.

interface Progress {
  start(label: string, at?: { index: number; total: number }): void;
  stop(): void;
}

ProgressOptions

interface ProgressOptions {
  env: Readonly<Record<string, string | undefined>>;
  off?: boolean;
  slowMs?: number;
  delayMs?: number;
  frameMs?: number;
  timers?: Timers;
}

ProgressStream

Where progress goes: stderr, so stdout keeps only the result.

interface ProgressStream {
  write(text: string): unknown;
  isTTY?: boolean;
}

stripAnsi

The text with its ANSI escape sequences taken out.

function stripAnsi(text: string): string

terminalStyle

The style for one stream: a terminal gets colour and symbols; a pipe, or a terminal in CI (the CI variable set), gets words and no colour. NO_COLOR (set and not empty) always turns colour off, and FORCE_COLOR or CLICOLOR_FORCE turns colour and symbols on off a terminal and in CI, for a screenshot or a capture.

function terminalStyle(stream: { isTTY?: boolean | undefined; }, env: Readonly<Record<string, string | undefined>>): TerminalStyle

TerminalStyle

What a terminal shows a person, with a plain fallback off a terminal: colour, status marks, the time something took, next steps and a progress line for anything slow. Colour is on only for a terminal; NO_COLOR always turns it off, and FORCE_COLOR turns it on for a capture. Off a terminal, the marks are words and nothing is redrawn, so a pipe, a log or an agent gets plain text with no escape codes.

interface TerminalStyle {
  tty: boolean;
  color: boolean;
  symbols: boolean;
}

Timers

The clock and timers progress uses; tests pass held ones.

interface Timers {
  now(): number;
  setTimeout(fn: () => void, ms: number): unknown;
  clearTimeout(handle: unknown): void;
  setInterval(fn: () => void, ms: number): unknown;
  clearInterval(handle: unknown): void;
}

timeText

How long something took, for a person: tenths of a second under a minute (0.4s, 14.2s), then minutes and seconds (1m 05s).

function timeText(ms: number): string