Zero-Configuration Terminal Output: Write Once, Works Everywhere
Atmos now features intelligent terminal output that adapts to any environment automatically. Developers can write code assuming a full-featured terminal, and Atmos handles the rest - capability detection, color adaptation, and secret masking happen transparently. No more capability checking, manual color detection, or masking code. Just write clean, simple output code and it works everywhere.
The Problem with Traditional CLI Outputโ
Most CLI tools force developers to make painful choices:
// Traditional approach - painful!
if isatty.IsTerminal(os.Stdout.Fd()) {
// Using Charm Bracelet's lipgloss for styling
successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
fmt.Println(successStyle.Render("Success!"))
} else {
fmt.Println("Success!") // Plain for pipes
}
// And don't forget to mask secrets!
if containsSecret(output) {
output = maskSecrets(output)
}
fmt.Println(output)
What Existing Solutions Don't Solveโ
While Charm Bracelet's Lip Gloss and similar libraries handle rendering beautifully (styled components, layouts, colors), they don't solve critical infrastructure CLI challenges:
- Secret Masking: No automatic redaction of sensitive data across all output channels
- Centralized I/O Control: Output scattered across stdout/stderr without unified masking
- Security-First Design: Secrets can leak through unmasked channels or error messages
- Atmos-Specific Requirements: Infrastructure tools handle AWS keys, API tokens, and sensitive configs that must never appear in logs
This leads to:
- ๐ซ Duplicated capability checking throughout the codebase
- ๐ซ Inconsistent output behavior across commands
- ๐ซ Secrets accidentally leaked to logs (the primary driver for this work)
- ๐ซ Broken pipelines when output assumptions change
- ๐ซ Difficult testing (mocking TTY detection is painful)
The Atmos Solution: Write Once, Works Everywhereโ
Atmos's I/O system complements Charm Bracelet by adding the infrastructure-critical layer that rendering libraries don't provide: centralized I/O control with automatic secret masking. Lip Gloss handles the beautiful rendering; Atmos ensures that rendering never exposes sensitive data.
With Atmos's new I/O system, developers write code once:
// Atmos approach - simple!
ui.Success("Deployment complete!")
That's it. No capability checking, no color detection, no TTY handling. The system automatically:
๐จ Color Degradationโ
- TrueColor terminal (iTerm2, Windows Terminal): Full 24-bit colors
- 256-color terminal: 256-color palette
- 16-color terminal (basic xterm): ANSI colors
- No color (CI,
NO_COLOR=1, pipes): Plain text
๐ Width Adaptationโ
- Wide terminal (120+ cols): Uses full width with proper wrapping
- Narrow terminal (80 cols): Wraps at 80 characters
- Config override: Respects
atmos.yamlsettings.terminal.max_width - Unknown width: Sensible defaults
๐ TTY Detectionโ
- Interactive terminal: Full styling, colors, icons, formatting
- Piped (
atmos deploy | tee): Plain text automatically - Redirected (
atmos > file): Plain text automatically - CI environment: Detects CI and disables interactivity
๐ญ Markdown Renderingโ
ui.Markdown("# Deployment Report\n\n**Status:** Success")
- Color terminal: Styled markdown with colors, bold, headers
- No-color terminal: Plain text formatting (notty style)
- Render failure: Gracefully falls back to plain content
๐ Automatic Secret Maskingโ
data.WriteJSON(config) // Contains AWS_SECRET_ACCESS_KEY
Output automatically masked:
{
"aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
"aws_secret_access_key": "***MASKED***"
}
No manual redaction needed. The system automatically detects and masks:
- AWS access keys and secrets (AKIA*, ASIA*)
- Sensitive environment variable patterns
- Common token formats
- JSON/YAML quoted variants
๐ฏ Channel Separationโ
// Data to stdout (pipeable)
data.WriteJSON(result)
// Messages to stderr (human-readable)
ui.Info("Processing components...")
ui.Success("Deployment complete!")
Users can now safely pipe data while seeing status:
atmos terraform output | jq .vpc_id
# Still sees progress on stderr:
# โน Loading configuration...
# โ Output retrieved!
๐ Logging vs Terminal Outputโ
Important distinction: This I/O system is for terminal output (user-facing data and messages), not logging (system events and debugging).
-
Terminal Output (
ui.*,data.*): User-facing messages, status updates, command results- Goes to stdout/stderr
- Formatted for humans
- Respects TTY detection and color settings
- Automatically masked for secrets
-
Logging (
log.*): System events, debugging, internal state- Goes to log files (or
/dev/stderrif configured) - Machine-readable format
- Controlled by
--logs-levelflag - Not affected by terminal capabilities
- Goes to log files (or
Read more in the CLI Configuration documentation (see logs section) and Global Flags for --logs-level and --logs-file options.
Real-World Examplesโ
Before: Manual Everythingโ
func deploy(cmd *cobra.Command, args []string) error {
// Capability checking
isTTY := isatty.IsTerminal(os.Stderr.Fd())
// Using Charm Bracelet for styling
var infoStyle, errorStyle, successStyle lipgloss.Style
if isTTY {
infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
}
// Choose output format
if isTTY {
fmt.Fprintf(os.Stderr, "%s\n", infoStyle.Render("โน Starting deployment..."))
} else {
fmt.Fprintf(os.Stderr, "Starting deployment...\n")
}
// Do deployment
result, err := performDeploy()
if err != nil {
if isTTY {
fmt.Fprintf(os.Stderr, "%s\n", errorStyle.Render("โ Deployment failed"))
} else {
fmt.Fprintf(os.Stderr, "Deployment failed\n")
}
return err
}
// Mask secrets before output
sanitized := maskSecrets(result)
// Output data
json.NewEncoder(os.Stdout).Encode(sanitized)
if isTTY {
fmt.Fprintf(os.Stderr, "%s\n", successStyle.Render("โ Deployment complete!"))
} else {
fmt.Fprintf(os.Stderr, "Deployment complete!\n")
}
return nil
}
After: Clean and Simpleโ
func deploy(cmd *cobra.Command, args []string) error {
ui.Info("Starting deployment...")
result, err := performDeploy()
if err != nil {
ui.Error("Deployment failed")
return err
}
data.WriteJSON(result) // Secrets automatically masked
ui.Success("Deployment complete!")
return nil
}
Result: Dramatically less code, zero capability checking, automatic secret masking, perfect degradation.
Environment Supportโ
The system automatically respects all standard conventions:
Environment Variablesโ
NO_COLOR=1- Disables all colorsCLICOLOR=0- Disables colorsFORCE_COLOR=1- Forces color even when pipedTERM=dumb- Uses plain text outputCI=true- Detects CI environmentATMOS_FORCE_TTY=true- Forces TTY mode with sane defaults (for screenshots)ATMOS_FORCE_COLOR=true- Forces TrueColor even for non-TTY (for screenshots)
CLI Flagsโ
--no-color- Disables colors--color- Enables color (only if TTY)--force-color- Forces TrueColor even for non-TTY (for screenshots)--force-tty- Forces TTY mode with sane defaults (for screenshots)--redirect-stderr- Redirects UI to stdout
Terminal Detectionโ
- TTY/PTY detection via
isatty - Color profile via
termenv - Width via
ioctl TIOCGWINSZ - CI detection via standard env vars
Testing Benefitsโ
Testing becomes trivial:
func TestDeployCommand(t *testing.T) {
// Setup test I/O with buffers
stdout, stderr, cleanup := setupTestUI(t)
defer cleanup()
// Run command
err := deploy(cmd, args)
// Verify output went to correct channels
assert.Contains(t, stderr.String(), "Deployment complete")
assert.Contains(t, stdout.String(), `"status":"success"`)
}
No TTY mocking, no color detection stubbing, no complex test fixtures.
Migration Guideโ
Old Pattern (Atmos main branch before this PR)โ
// Old: Direct fmt.Fprintf with explicit stream access
fmt.Fprintf(os.Stderr, "Starting...\n")
fmt.Fprintf(os.Stdout, "%s\n", jsonOutput)
// Or with context retrieval
ioCtx, _ := io.NewContext()
fmt.Fprintf(ioCtx.UI(), "Starting...\n")
fmt.Fprintf(ioCtx.Data(), "%s\n", jsonOutput)
New Patternโ
// New: Package-level functions with automatic I/O setup
ui.Writeln("Starting...")
data.Writeln(jsonOutput)
Available Functionsโ
Data Output (stdout):
data.Write(text) // Plain text
data.Writef(fmt, ...) // Formatted
data.Writeln(text) // With newline
data.WriteJSON(v) // JSON
data.WriteYAML(v) // YAML
UI Output (stderr):
ui.Write(text) // Plain (no icon/color)
ui.Writef(fmt, ...) // Plain formatted
ui.Writeln(text) // Plain with newline
ui.Success(text) // โ in green
ui.Error(text) // โ in red
ui.Warning(text) // โ in yellow
ui.Info(text) // โน in cyan
ui.Markdown(content) // Rendered โ stdout
ui.MarkdownMessage(content)// Rendered โ stderr
Architectureโ
The magic happens through clean separation of concerns:
Developer Code
โ
Package Functions (data.*, ui.*)
โ
Formatter (color/style selection)
โ
Terminal (capability detection)
โ
I/O Layer (masking + routing)
โ
stdout/stderr
Each layer handles one responsibility:
- Package functions - Simple API for developers
- Formatter - Returns styled strings (pure, no I/O)
- Terminal - Detects capabilities (TTY, color, width)
- I/O Layer - Masks secrets, routes to correct stream
Performanceโ
Zero overhead for capability detection:
- Capabilities detected once at startup
- Results cached for lifetime of command
- No per-call TTY checks
- No per-call color detection
What's Nextโ
This foundation enables exciting future enhancements:
- Progress bars - Automatic for TTY, plain for pipes
- Interactive prompts - Automatic TTY detection
- Spinner animations - Show in TTY, silent in CI
Try It Nowโ
Update to the latest Atmos version and start using the new I/O system:
// Replace manual TTY checking and Lip Gloss styling
- if isatty.IsTerminal(os.Stderr.Fd()) {
- style := lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
- fmt.Fprintf(os.Stderr, "%s\n", style.Render("โ Done"))
- } else {
- fmt.Fprintf(os.Stderr, "Done\n")
- }
+ ui.Success("Done")
// Replace manual JSON output
- json.NewEncoder(os.Stdout).Encode(data)
+ data.WriteJSON(data)
// Replace manual secret masking
- fmt.Println(maskSecrets(output))
+ data.Writeln(output) // Automatic masking
Feedbackโ
We'd love to hear your feedback on the new I/O system! Open an issue on GitHub or join the conversation in Slack.
Tags: #feature #enhancement #contributors
