CLI Color Strategy¶
Reference for all ANSI color usage in tx. Read this before adding or changing colored output.
Palette¶
Every role keeps at least ~3.3:1 contrast on both dark (#1E1E1E) and light (#FFFFFF) terminal backgrounds — the practical ceiling for a single palette, since 4.5:1 (WCAG normal text) on both is mathematically impossible: it would require every color's luminance to sit in a near-zero-width band. Roles that appear in the same view are separated by lightness as well as hue, so the distinction survives for red-green color-blind readers (see Palette construction). The guarantees are enforced by PaletteTests.
| Role | Name | Hex | Example | Use |
|---|---|---|---|---|
| Title | Sage | #34897E |
MyCli |
App names, section headers |
| Command | Default | — | mycli build |
Commands (bold, no color) |
| Option | Lav | #8572AF |
--project |
Flags and options |
| Value | Terra | #966442 |
api-service |
IDs, names, literals |
| Path | Harbor | #4582AC |
./src/api-service |
Files and folders |
| Success | Moss | #408139 |
OK Project initialized |
Completed actions |
| Warning | Amber | #B07E2A |
WARN Config not found |
Recoverable issues |
| Error | Rose | #CC6766 |
ERROR Build failed |
Failures |
| Measures | Orchid | #CF67AC |
[Profit] in DAX |
Measure references in DAX |
| Muted | Slate | #757F88 |
(2.3s elapsed) |
Hints, timings, secondary |
Palette Implementation¶
Defined in src/Tomix.Cli/Output/Styling.cs:
using Spectre.Console;
namespace Tomix.Cli.Output;
internal static class Palette
{
public static readonly Color Sage = new(0x34, 0x89, 0x7E);
public static readonly Color Lav = new(0x85, 0x72, 0xAF);
public static readonly Color Terra = new(0x96, 0x64, 0x42);
public static readonly Color Harbor = new(0x45, 0x82, 0xAC);
public static readonly Color Moss = new(0x40, 0x81, 0x39);
public static readonly Color Amber = new(0xB0, 0x7E, 0x2A);
public static readonly Color Rose = new(0xCC, 0x67, 0x66);
public static readonly Color Orchid = new(0xCF, 0x67, 0xAC);
public static readonly Color Slate = new(0x75, 0x7F, 0x88);
}
Use Palette.Sage for Spectre widget styling (table borders, panel borders). Use the markup helpers below for inline text.
Palette Construction¶
The palette is derived, not hand-picked: hues and chroma come from the original design, and each color's CIELAB lightness is set deliberately.
- Contrast first. Lightness targets pull every role toward the luminance that maximizes its worst-case contrast against dark and light backgrounds, so no role drops below ~3.3:1 on either.
- Lightness as a second channel. Roles that appear side by side get a deliberate lightness gap in addition to their hue difference (columns darker than measures, variables darker than literals). Under red-green color vision deficiency the hue difference vanishes and the lightness gap carries the distinction: the columns/measures pair improves from ΔE 13 to 29 under deuteranopia simulation, variables/literals from 13 to 16.
- Enforced by tests.
PaletteTestscomputes WCAG contrast ratios and CIELAB ΔE76 for every role and fails when a color falls below 3.2:1 on either background or a same-view pair drifts under ΔE 25.
When changing a palette color, keep this contract: adjust lightness before hue, and let PaletteTests arbitrate.
Message Categories¶
| Category | Style | Example |
|---|---|---|
| Banner | [bold] on title |
[bold]tx doctor[/] |
| Section header | [bold] label |
[bold]Tables[/] (4) |
| Status progress | Sage | Validating... in Sage |
| Success | Moss | Saved: model.tmdl in Moss |
| Warning | Amber | Changes not saved. in Amber |
| Error | Rose + bold | Build failed in Rose bold |
| Key-value label | [bold] label, plain value |
[bold]Version:[/] 1.0.0 |
| Guidance hint | Slate | Run 'tx stage commit' to promote. in Slate |
| Diff added | Moss prefix + |
+ table Sales |
| Diff removed | Rose prefix - |
- table Sales |
| Diff modified | Amber prefix ~ |
~ table Sales |
| Table | Spectre Table().RoundedBorder().BorderColor(Palette.Slate) |
Already established in LsRenderer |
| Table row de-emphasis | Whole row in Slate (Styling.Muted per cell) |
Hidden-object rows in ls are muted end to end |
| Connection banner | Slate on stderr | Connected to: C:\models\Sales before the model opens |
| DAX highlighting | Role-mapped palette on text output only | get properties, ls expression cells, validate offending lines, set DAX before/after previews, and format inline/--path output: keywords Lav, functions Harbor, tables Sage, columns Moss, measures Orchid, variables Terra, literals Amber, comments Slate; JSON/CSV/TMDL/BIM stay markup-free; bpa run --fix stays plain because it carries no DAX today |
| M highlighting | Same roles, text output only | get properties, ls expression cells (partitions, shared expressions), and format --lang m output: keywords and type names Lav, library functions and #table-style constructors Harbor, step and field definitions Terra, field access ([Amount]) Moss, literals Amber, comments Slate. A lexical pass (MLanguage.Classify), not the parser, so it costs nothing per command |
| CI annotations | Plain text, no markup | ::error::... / ##vso[task.logissue...] |
NO_COLOR Compliance¶
tx follows the NO_COLOR convention:
- Environment variable. When
NO_COLORis set (any non-empty value),txsuppresses all ANSI color codes. - Config override.
noColor: truein~/.tomix/config.jsonalso disables color. - Implementation. Both mechanisms set
AnsiConsole.Profile.Capabilities.ColorSystem = ColorSystem.NoColorsat startup (seeProgram.cs). Spectre.Console automatically strips all markup and color from output when this is set. - Piped output. Spectre.Console also detects
Console.IsOutputRedirectedand degrades gracefully.
Do not bypass this by writing raw ANSI escape codes. Always use Spectre.Console APIs or the Styling helpers.
Styling Helpers¶
All output helpers live in src/Tomix.Cli/Output/Styling.cs. Use these instead of raw markup strings.
| Helper | Output |
|---|---|
Styling.Bold(text) |
Bold text |
Styling.Title(text) |
Sage bold |
Styling.Success(text) |
Moss |
Styling.Warning(text) |
Amber |
Styling.Error(text) |
Rose bold |
Styling.Muted(text) |
Slate |
Styling.Path(text) |
Harbor |
Styling.Value(text) |
Terra |
Styling.Option(text) |
Lav |
Styling.KeyValue(label, value) |
Bold label + plain value |
Styling.Guidance(text) |
Slate |
Styling.MarkupEscape(text) |
Escapes [ and ] for Spectre markup |
Styling.DaxMarkup(expression) |
Syntax-highlighted DAX as escaped markup (see Message Categories) |
Styling.MMarkup(expression) |
Syntax-highlighted M as escaped markup (see Message Categories) |
Styling.ExpressionMarkup(language, text) |
The shared entry point for expression text: ExpressionLanguage.Dax or .M highlights (decide with DaxExpressions.IsDaxValue/IsDaxExpression and MExpressions.IsMValue/IsMExpression), .Plain escapes; optional measureNames resolves DAX measure references to their own color, optional suffix (e.g. ... (+2 lines)) stays plain |
Styling.SeverityMarkup(severity) |
Colored severity label (Error/Warning/Info) |
Styling.NewTable(params columns) |
Rounded-border table with Slate border |
What NOT to Color¶
- JSON output (
--format json) — raw JSON, no markup. - CSV output (
--format csv) — raw CSV, no markup. - TMDL/BIM raw output (
--format tmdl,--format bim) — raw syntax. - CI annotations (
::error::,##vso[task.logissue...]) — plain-text CI protocols. - Completion scripts (
completion bash/zsh/fish) — shell script output.