ui

package
v0.6.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package ui owns every piece of chrome the ctx CLI prints: colour, layout, live progress, and interactive prompts.

Colours are the Contexo brand tokens from contexo-landing/packages/brand/tokens.css, converted oklch->hex. Light variants are the same hue at lowered lightness.

IMPORTANT: this package never queries the terminal. lipgloss.AdaptiveColor and Renderer.HasDarkBackground() resolve by issuing an OSC 11 query, which writes an escape to the output AND consumes the reply from stdin. `ctx mcp` owns both streams for JSON-RPC, so a query there corrupts the protocol in both directions. Background is an explicit bool instead (see Mode.Dark).

Index

Constants

This section is empty.

Variables

View Source
var (
	FG     = tone{Light: "#1c1a18", Dark: "#f4efe7"} // --fg      cream
	FG2    = tone{Light: "#3c3a34", Dark: "#bbb7af"} // --fg-2    body
	FG3    = tone{Light: "#58554f", Dark: "#83807a"} // --fg-3    labels
	FG4    = tone{Light: "#83807a", Dark: "#5a5853"} // --fg-4    rules, SHAs
	Accent = tone{Light: "#437a00", Dark: "#a1ea5a"} // --accent  acid green
	Warm   = tone{Light: "#a05000", Dark: "#f0995b"} // --warm    warnings
	AddInk = tone{Light: "#297d22", Dark: "#92f388"} // --diff-add-ink
	RemInk = tone{Light: "#ba2b2e", Dark: "#ff9b92"} // --diff-rem-ink
)

Brand tokens. Keep in sync with contexo-landing/packages/brand/tokens.css.

View Source
var ErrAborted = errors.New("cancelled")

ErrAborted is returned when the user cancels a prompt (Ctrl-C / Esc).

Functions

func Suggest

func Suggest(err error, cmds ...string) error

Suggest attaches actionable commands to err. Calling it twice on the same error appends rather than replacing, so a wrapper can add context.

Types

type CLIError

type CLIError struct {
	Msg    string
	Advice []string
	// contains filtered or unexported fields
}

CLIError carries a failure plus the commands that fix it, so advice stops being baked into message strings.

Before this existed, 18+ error sites hardcoded their advice, which drifted: "not authenticated" had five different phrasings naming three different commands, and two sites advised `ctx remote add`, which does not exist.

func (*CLIError) Error

func (e *CLIError) Error() string

func (*CLIError) Unwrap

func (e *CLIError) Unwrap() error

type Mode

type Mode struct {
	// Machine means the output is consumed by a program, not a human:
	// JSON-RPC, hook payloads, shell completion, --json, version --short.
	// In this mode the UI emits nothing and spinners/forms are disabled.
	Machine bool
	// TTY reports whether the DESTINATION STREAM is a terminal. Probe the fd
	// you actually write to -- ctx's update nudge probes stdout but writes to
	// stderr, and flipping that silently turns the nudge on for
	// `ctx completion bash > file`.
	TTY bool
	// Dark is explicit, never queried. See the package doc.
	Dark bool
}

Mode is resolved ONCE per process, by the root command, before any subcommand runs. Resolving it per call site is how machine output gets corrupted: every new print site would have to remember the rule.

func DetectMode

func DetectMode(cmd *cobra.Command, w io.Writer) Mode

DetectMode resolves the mode for cmd writing to w.

type Prompter

type Prompter struct {
	// contains filtered or unexported fields
}

Prompter is the single interactive-input implementation for the CLI. It replaces five separate y/N readers and the survey picker.

It holds ONE bufio.Reader for its whole life. Two readers over the same stdin let the first buffer past its newline and swallow the next answer -- a real bug previously memorialized by a comment in cli/migrate.go. Create one Prompter per command run and reuse it for every prompt.

func (*Prompter) Confirm

func (p *Prompter) Confirm(question string) (bool, error)

Confirm asks a y/N question. Defaults to No.

An unterminated final line still counts: "y" with no trailing newline is a yes. That matches how the pre-existing push confirmation behaved, and scripts that pipe a bare "y" rely on it.

func (*Prompter) ConfirmDestructive

func (p *Prompter) ConfirmDestructive(question string) (bool, error)

ConfirmDestructive is Confirm for irreversible actions. It additionally requires a COMPLETE line: if the reader hits EOF mid-answer, it declines even when the partial text says yes.

This is not pedantry. `ctx detach` performs os.RemoveAll on the knowledge directory, and a truncated pipe should never be read as consent. The two prompts genuinely differed before they were unified here, and the strict rule is the one the destructive path had.

func (*Prompter) Line

func (p *Prompter) Line(question string) (string, error)

Line asks a free-text question and returns the raw answer line, newline included, matching bufio.Reader.ReadString semantics that callers expect.

func (*Prompter) Select

func (p *Prompter) Select(title string, labels []string) (int, error)

Select returns the 0-based index of the chosen option.

Off a TTY it prints a numbered list and reads an index. Constructing a huh form there would read the stream as KEY EVENTS, not lines -- "all\n" becomes keystrokes a,l,l,Enter, which is type-ahead filtering, not a selection.

type Table

type Table struct {
	// contains filtered or unexported fields
}

Table renders aligned columns.

Widths are measured with lipgloss.Width, which counts DISPLAY width and ignores ANSI escape sequences. That is the whole point: a plain text/tabwriter counts escape bytes toward the column width, so the moment any cell is styled the columns drift -- and the drift is invisible to tests, which see the Ascii profile where the escapes are absent, while being obvious to a user on a colour terminal. Measuring display width lets callers pass pre-toned cells (via UI.Style or UI.Mark) and still get alignment.

func (*Table) Render

func (t *Table) Render()

Render writes the table. No-op in machine mode.

func (*Table) Row

func (t *Table) Row(cells ...string) *Table

Row appends a row. Cell count need not match the header count; short rows simply end early.

type Task

type Task struct {
	// contains filtered or unexported fields
}

Task reports progress on a long operation.

Three behaviours, chosen by Mode:

  • machine: silent. Never construct a bubbletea program on a machine path; it writes cursor-control escapes unconditionally, which the colour profile does NOT gate, and it would corrupt JSON-RPC or a hook payload.
  • tty: an inline (not full-screen) spinner bound explicitly to the UI's writer via tea.WithOutput.
  • non-tty: exactly two plain lines. The docs capture harness records this branch (stdout to a file, stdin from /dev/null), and tests assert on it.

func (*Task) Detail

func (t *Task) Detail(s string)

Detail updates the trailing context on the live line. No-op off a TTY -- the non-TTY contract is exactly two lines, and a third would break the docs captures and the tests that assert on them.

func (*Task) Done

func (t *Task) Done(format string, a ...any)

Done ends the task with a success mark.

func (*Task) Fail

func (t *Task) Fail(format string, a ...any)

Fail ends the task with a failure mark.

type UI

type UI struct {
	// contains filtered or unexported fields
}

UI owns chrome only. Pure data lines keep using fmt.Fprintf -- routing everything through here would produce a second, worse fmt.

func ForStderr

func ForStderr(cmd *cobra.Command) *UI

ForStderr builds a UI for cmd's stderr, probing that stream for TTY-ness.

func From

func From(cmd *cobra.Command) *UI

From builds a UI for cmd's stdout.

func New

func New(w io.Writer, m Mode) *UI

New binds a renderer to THIS writer. The sink stays io.Writer so existing render helpers and their test call sites compile unchanged.

func Plain

func Plain(w io.Writer) *UI

Plain builds a UI for w with no cobra context. Used by helpers that only receive an io.Writer.

func (*UI) Brand

func (u *UI) Brand(state string, healthy bool)

Brand renders the wordmark plus a status dot: "contexo ● connected".

healthy drives the dot. It is a parameter rather than always-accent because a green status light beside the words "not authenticated" is exactly the misuse the palette rules out: accent means brand or success, nothing else.

func (*UI) Eyebrow

func (u *UI) Eyebrow(label string)

Eyebrow renders a section label: "── REMOTE".

func (*UI) Fail

func (u *UI) Fail(format string, a ...any)

func (*UI) Field

func (u *UI) Field(name, value string)

Field renders an aligned label/value pair.

func (*UI) Glyph

func (u *UI) Glyph() glyphSet

Glyph exposes a symbol so callers can build their own rows in the same vocabulary without reaching for a literal.

func (*UI) Hint

func (u *UI) Hint(verb, command string)

Hint renders an actionable next step: "try ctx auth login".

func (*UI) Line

func (u *UI) Line(format string, a ...any)

Line writes an unstyled line. Convenience for data rows so callers do not need to reach for the writer.

func (*UI) Mark

func (u *UI) Mark(on bool) string

Mark returns a status dot: accent when on, dim when off.

"Off" is deliberately FG4, not Warm. An unwired integration is a neutral fact, not a warning -- on a fresh project every row would otherwise light up as a caution. It matters more than it looks: on a 16-colour terminal Warm (#f0995b) and RemInk (#ba2b2e) both degrade to ANSI 91, so a "not wired" dot would render in the same red as an irreversible DELETE.

func (*UI) Mode

func (u *UI) Mode() Mode

func (*UI) Out

func (u *UI) Out() io.Writer

func (*UI) Prompter

func (u *UI) Prompter(in io.Reader) *Prompter

func (*UI) RenderError

func (u *UI) RenderError(err error)

RenderError prints a failure. In machine mode it degrades to a single bare line so scripts still see the reason on stderr.

func (*UI) Step

func (u *UI) Step(format string, a ...any)

Step renders one line of running commentary: dim and indented, no glyph.

It exists so a multi-step command reads as a single column. A ✔ marks a RESULT; the individual actions leading to it are log lines. Mixing the two weights for semantically identical steps is what makes a wiring log look arbitrary.

func (*UI) Style

func (u *UI) Style(t tone, s string) string

Style renders s in the given tone. Exported for callers that compose their own lines (table cells, preview rows) but still want brand colour.

func (*UI) Success

func (u *UI) Success(format string, a ...any)

func (*UI) Table

func (u *UI) Table(headers ...string) *Table

func (*UI) Task

func (u *UI) Task(label string) *Task

func (*UI) Warn

func (u *UI) Warn(format string, a ...any)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL