Skip to main content

Zero-Configuration Terminal Output: Write Once, Works Everywhere

ยท 7 min read
Erik Osterman
Founder @ Cloud Posse

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.yaml settings.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/stderr if configured)
    • Machine-readable format
    • Controlled by --logs-level flag
    • Not affected by terminal capabilities

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 colors
  • CLICOLOR=0 - Disables colors
  • FORCE_COLOR=1 - Forces color even when piped
  • TERM=dumb - Uses plain text output
  • CI=true - Detects CI environment
  • ATMOS_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