Skip to main content

Overview

Terminator provides utilities for detecting and interacting with terminal environments, including TTY detection, pager management, browser launching, and CI/CD environment detection.

Terminal Detection

IsTTY

Checks if the current output is a TTY (terminal) or Cygwin terminal.
func IsTTY() bool
result
bool
True if running in a terminal environment, false if output is redirected or piped
Use Cases:
  • Disable colored output when piping to files
  • Disable interactive prompts in non-interactive environments
  • Adjust output formatting based on terminal capabilities
Example:
import "github.com/raystack/salt/cli/terminator"

if terminator.IsTTY() {
    // Terminal output: use colors and spinners
    spinner := printer.Spin("Loading")
    // ...
    spinner.Stop()
} else {
    // Non-terminal: plain text output
    fmt.Println("Loading...")
}

IsColorDisabled

Checks if color output is disabled via the NO_COLOR environment variable.
func IsColorDisabled() bool
result
bool
True if NO_COLOR environment variable is set, false otherwise
Example:
if terminator.IsColorDisabled() {
    // Output plain text without ANSI color codes
    fmt.Println("Status: OK")
} else {
    // Use colored output
    printer.Successln("Status: OK")
}
Environment Variable:
# Disable colors
export NO_COLOR=1

# Enable colors (default)
unset NO_COLOR

IsCI

Checks if the code is running in a Continuous Integration environment.
func IsCI() bool
result
bool
True if running in a CI/CD environment, false otherwise
Detected CI Environments:
  • GitHub Actions
  • Travis CI
  • CircleCI
  • GitLab CI
  • Jenkins
  • TeamCity
  • AppVeyor
  • TaskCluster
  • And others (via standard CI environment variables)
Example:
if terminator.IsCI() {
    // CI environment: disable interactive prompts
    fmt.Println("Running in CI mode")
    useDefaults := true
} else {
    // Local development: allow interactive prompts
    prompter := prompter.New()
    confirmed, _ := prompter.Confirm("Continue?", true)
}

Pager Management

Manage pager processes for paginated output (like less or more).

Pager Type

type Pager struct {
    Out          io.Writer   // Output writer to pager
    ErrOut       io.Writer   // Error output writer
    // Private fields...
}

NewPager

Creates a new Pager instance with default settings.
func NewPager() *Pager
pager
*Pager
Configured Pager instance (uses PAGER env var or defaults to “more”)
Example:
pager := terminator.NewPager()

Set

Updates the pager command.
func (p *Pager) Set(cmd string)
cmd
string
required
Pager command (e.g., “less”, “more”, “cat”)
Example:
pager := terminator.NewPager()
pager.Set("less -R") // Enable color support in less

Get

Returns the current pager command.
func (p *Pager) Get() string
command
string
Current pager command

Start

Starts the pager process to display output.
func (p *Pager) Start() error
error
error
Error if pager fails to start or command is invalid
Behavior:
  • Does nothing if pager command is “cat” or empty
  • Sets LESS=FRX environment variable if not already set
  • Sets LV=-c environment variable if not already set
  • Redirects output to pager’s stdin
Example:
pager := terminator.NewPager()
if err := pager.Start(); err != nil {
    log.Fatal(err)
}

// Write to pager
fmt.Fprintln(pager.Out, "Line 1")
fmt.Fprintln(pager.Out, "Line 2")
// ... many more lines

// Clean up
pager.Stop()

Stop

Terminates the running pager process and cleans up resources.
func (p *Pager) Stop()
Example:
defer pager.Stop() // Ensure cleanup

// Use pager...

Complete Pager Example

package main

import (
    "fmt"
    "log"

    "github.com/raystack/salt/cli/terminator"
)

func main() {
    // Create and start pager
    pager := terminator.NewPager()
    if err := pager.Start(); err != nil {
        log.Fatal(err)
    }
    defer pager.Stop()

    // Generate long output
    for i := 1; i <= 100; i++ {
        fmt.Fprintf(pager.Out, "Line %d: Some content here\n", i)
    }
}

ErrClosedPagerPipe

Error type returned when writing to a closed pager pipe (user quit pager).
type ErrClosedPagerPipe struct {
    error
}
Handling:
_, err := fmt.Fprintln(pager.Out, "Content")
if err != nil {
    var closedPipe *terminator.ErrClosedPagerPipe
    if errors.As(err, &closedPipe) {
        // User closed pager, exit gracefully
        return nil
    }
    return err
}

Browser Management

OpenBrowser

Opens the default web browser at a specified URL.
func OpenBrowser(goos, url string) *exec.Cmd
goos
string
required
Operating system name (“darwin”, “windows”, “linux”)
url
string
required
URL to open in the browser
cmd
*exec.Cmd
Configured command (must call Run() or Start() to execute)
Platform Commands:
  • macOS: open <url>
  • Windows: cmd /c start <url>
  • Linux: xdg-open <url> (or wslview for WSL)
Example:
import (
    "runtime"
    
    "github.com/raystack/salt/cli/terminator"
)

func openDocs() error {
    url := "https://docs.example.com"
    cmd := terminator.OpenBrowser(runtime.GOOS, url)
    return cmd.Start()
}
With TTY Check:
if terminator.IsTTY() {
    fmt.Println("Opening browser...")
    cmd := terminator.OpenBrowser(runtime.GOOS, url)
    if err := cmd.Start(); err != nil {
        fmt.Printf("Failed to open browser: %v\n", err)
        fmt.Printf("Visit: %s\n", url)
    }
} else {
    fmt.Printf("Visit: %s\n", url)
}
Important: OpenBrowser will panic if called without a TTY. Always check with IsTTY() first.

Homebrew Detection

IsUnderHomebrew

Checks if a binary path is managed by Homebrew.
func IsUnderHomebrew(path string) bool
path
string
required
Full path to the binary
result
bool
True if binary is in Homebrew’s bin directory, false otherwise
Example:
import "os"

exePath, _ := os.Executable()
if terminator.IsUnderHomebrew(exePath) {
    fmt.Println("Installed via Homebrew")
    fmt.Println("Update with: brew upgrade mycli")
}

HasHomebrew

Checks if Homebrew is installed on the system.
func HasHomebrew() bool
result
bool
True if Homebrew is available, false otherwise
Example:
if terminator.HasHomebrew() {
    fmt.Println("Homebrew detected")
    fmt.Println("Install with: brew install mycli")
} else {
    fmt.Println("Download from: https://example.com/install")
}

Best Practices

Always verify TTY before using interactive features:
if terminator.IsTTY() {
    // Use interactive features
    spinner := printer.Spin("Loading")
    defer spinner.Stop()
} else {
    // Use simple output
    fmt.Println("Loading...")
}
Check color preferences before adding ANSI codes:
useColor := terminator.IsTTY() && !terminator.IsColorDisabled()

if useColor {
    printer.Successln("Done")
} else {
    fmt.Println("Done")
}
Adjust behavior for CI/CD pipelines:
if terminator.IsCI() {
    // CI: Non-interactive, verbose output
    fmt.Println("Step 1: Building...")
    fmt.Println("Step 2: Testing...")
} else {
    // Local: Interactive with progress bars
    bar := printer.Progress(100, "Building")
    // ...
}
Handle pager pipe closures gracefully:
for i := 0; i < 1000; i++ {
    _, err := fmt.Fprintf(pager.Out, "Line %d\n", i)
    if err != nil {
        var closedPipe *terminator.ErrClosedPagerPipe
        if errors.As(err, &closedPipe) {
            // User quit pager early
            break
        }
        return err
    }
}

Complete Example

package main

import (
    "fmt"
    "log"
    "os"
    "runtime"

    "github.com/raystack/salt/cli/printer"
    "github.com/raystack/salt/cli/prompter"
    "github.com/raystack/salt/cli/terminator"
)

func main() {
    // Detect environment
    isTTY := terminator.IsTTY()
    isCI := terminator.IsCI()
    colorDisabled := terminator.IsColorDisabled()

    fmt.Printf("Environment:\n")
    fmt.Printf("  TTY: %v\n", isTTY)
    fmt.Printf("  CI: %v\n", isCI)
    fmt.Printf("  Color: %v\n", !colorDisabled)
    fmt.Println()

    // Adjust behavior based on environment
    if isCI {
        runCIMode()
    } else if isTTY {
        runInteractiveMode()
    } else {
        runBatchMode()
    }

    // Show docs option
    if isTTY {
        showDocsOption()
    }
}

func runCIMode() {
    fmt.Println("Running in CI mode...")
    fmt.Println("Using default configuration")
    // Non-interactive execution
}

func runInteractiveMode() {
    fmt.Println("Running in interactive mode...")
    
    p := prompter.New()
    confirmed, err := p.Confirm("Continue?", true)
    if err != nil || !confirmed {
        fmt.Println("Cancelled")
        return
    }
    
    // Show progress
    spinner := printer.Spin("Processing")
    // ... do work
    spinner.Stop()
    printer.Successln("Complete")
}

func runBatchMode() {
    fmt.Println("Running in batch mode...")
    fmt.Println("Output can be piped or redirected")
    // Simple text output
}

func showDocsOption() {
    p := prompter.New()
    openDocs, err := p.Confirm("Open documentation?", false)
    if err != nil || !openDocs {
        return
    }

    url := "https://docs.example.com"
    cmd := terminator.OpenBrowser(runtime.GOOS, url)
    if err := cmd.Start(); err != nil {
        fmt.Printf("Failed to open browser: %v\n", err)
        fmt.Printf("Visit: %s\n", url)
    }
}

Build docs developers (and LLMs) love