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 ¶
- Variables
- func Suggest(err error, cmds ...string) error
- type CLIError
- type Mode
- type Prompter
- type Table
- type Task
- type UI
- func (u *UI) Brand(state string, healthy bool)
- func (u *UI) Eyebrow(label string)
- func (u *UI) Fail(format string, a ...any)
- func (u *UI) Field(name, value string)
- func (u *UI) Glyph() glyphSet
- func (u *UI) Hint(verb, command string)
- func (u *UI) Line(format string, a ...any)
- func (u *UI) Mark(on bool) string
- func (u *UI) Mode() Mode
- func (u *UI) Out() io.Writer
- func (u *UI) Prompter(in io.Reader) *Prompter
- func (u *UI) RenderError(err error)
- func (u *UI) Step(format string, a ...any)
- func (u *UI) Style(t tone, s string) string
- func (u *UI) Success(format string, a ...any)
- func (u *UI) Table(headers ...string) *Table
- func (u *UI) Task(label string) *Task
- func (u *UI) Warn(format string, a ...any)
Constants ¶
This section is empty.
Variables ¶
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.
var ErrAborted = errors.New("cancelled")
ErrAborted is returned when the user cancels a prompt (Ctrl-C / Esc).
Functions ¶
Types ¶
type CLIError ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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.
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 New ¶
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 ¶
Plain builds a UI for w with no cobra context. Used by helpers that only receive an io.Writer.
func (*UI) Brand ¶
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) 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) Line ¶
Line writes an unstyled line. Convenience for data rows so callers do not need to reach for the writer.
func (*UI) Mark ¶
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) RenderError ¶
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 ¶
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.