Skip to main content
The terminator package provides utilities for terminal detection, pager management, browser launching, and Homebrew integration.

Installation

go get github.com/raystack/salt/cli/terminator

Terminal Detection

TTY Detection

Check if the application is running in a terminal:
import "github.com/raystack/salt/cli/terminator"

if terminator.IsTTY() {
    // Running in terminal - can use colors, prompts, etc.
    fmt.Println("Running in terminal")
} else {
    // Output is redirected or piped - use plain text
    fmt.Println("Output redirected")
}

Use Cases

func printOutput(data interface{}) {
    if terminator.IsTTY() {
        // Interactive terminal - use colors and formatting
        printer.PrettyJSON(data)
    } else {
        // Piped or redirected - use machine-readable format
        printer.JSON(data)
    }
}

Color Detection

Check if color output is disabled:
if terminator.IsColorDisabled() {
    // User set NO_COLOR environment variable
    // Use plain text without colors
    fmt.Println("Colors disabled")
} else {
    // Colors are allowed
    printer.Success("Colors enabled")
}

CI Detection

Detect if running in a Continuous Integration environment:
if terminator.IsCI() {
    // Running in CI - disable interactive features
    fmt.Println("CI environment detected")
    
    // Skip interactive prompts
    // Use default values
    // Disable spinners and animations
} else {
    // Local development - enable interactive features
    p := prompter.New()
    value, _ := p.Input("Enter value:", "default")
}
Detects these CI environments:
  • GitHub Actions
  • Travis CI
  • CircleCI
  • Cirrus CI
  • GitLab CI
  • AppVeyor
  • CodeShip
  • Jenkins
  • TeamCity
  • TaskCluster

Pager Management

Display long output in a paginated format using a pager like less or more.

Basic Usage

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

pager := terminator.NewPager()

// Start the pager
if err := pager.Start(); err != nil {
    log.Fatal(err)
}
defer pager.Stop()

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

Pager Configuration

The pager respects the PAGER environment variable:
# Use less
export PAGER="less"

# Use less with options
export PAGER="less -R"

# Use more
export PAGER="more"

# Disable pager
export PAGER="cat"

Custom Pager

pager := terminator.NewPager()
pager.Set("less -R")  // Set custom pager command

if err := pager.Start(); err != nil {
    log.Fatal(err)
}
defer pager.Stop()

fmt.Fprintln(pager.Out, "Output goes here")

Pager Example

func displayLogs(logs []string) error {
    // Only use pager if in TTY
    if !terminator.IsTTY() {
        for _, log := range logs {
            fmt.Println(log)
        }
        return nil
    }

    // Use pager for long output
    pager := terminator.NewPager()
    if err := pager.Start(); err != nil {
        return err
    }
    defer pager.Stop()

    for _, log := range logs {
        fmt.Fprintln(pager.Out, log)
    }

    return nil
}

Pager Type

type Pager struct {
    Out    io.Writer   // Writer to send output to pager
    ErrOut io.Writer   // Writer for error output
}

Pager Methods

// Create new pager (uses PAGER env var or "more")
pager := terminator.NewPager()

// Set custom pager command
pager.Set("less -R")

// Get current pager command
cmd := pager.Get()

// Start pager process
err := pager.Start()

// Stop pager process and cleanup
pager.Stop()

Browser Integration

Open URLs in the default web browser.

Basic Usage

import (
    "runtime"
    "github.com/raystack/salt/cli/terminator"
)

url := "https://example.com"
cmd := terminator.OpenBrowser(runtime.GOOS, url)

if err := cmd.Run(); err != nil {
    log.Printf("Failed to open browser: %v", err)
}

Cross-Platform Support

func openDocs() error {
    if !terminator.IsTTY() {
        return fmt.Errorf("cannot open browser: not a TTY")
    }

    url := "https://docs.example.com"
    cmd := terminator.OpenBrowser(runtime.GOOS, url)
    
    return cmd.Run()
}

Supported Platforms

  • macOS: Uses open command
  • Windows: Uses cmd /c start command
  • Linux: Uses xdg-open or wslview (for WSL)

Non-blocking Browser Launch

func openBrowserAsync(url string) {
    cmd := terminator.OpenBrowser(runtime.GOOS, url)
    
    // Start in background
    if err := cmd.Start(); err != nil {
        log.Printf("Failed to open browser: %v", err)
        return
    }
    
    // Don't wait for browser to close
    go func() {
        cmd.Wait()
    }()
    
    fmt.Println("Opening browser...")
}

Interactive Browser Prompt

func openDocumentation() error {
    url := "https://docs.example.com"
    
    fmt.Printf("Documentation: %s\n", url)
    
    if terminator.IsTTY() {
        p := prompter.New()
        open, err := p.Confirm("Open in browser?", true)
        if err != nil {
            return err
        }
        
        if open {
            cmd := terminator.OpenBrowser(runtime.GOOS, url)
            return cmd.Run()
        }
    }
    
    return nil
}

Homebrew Integration

Utilities for detecting and working with Homebrew on macOS/Linux.

Check Homebrew Installation

if terminator.HasHomebrew() {
    fmt.Println("Homebrew is installed")
} else {
    fmt.Println("Homebrew is not installed")
}

Check if Binary is from Homebrew

import "os"

func checkInstallation() {
    execPath, err := os.Executable()
    if err != nil {
        log.Fatal(err)
    }
    
    if terminator.IsUnderHomebrew(execPath) {
        fmt.Println("This tool was installed via Homebrew")
    } else {
        fmt.Println("This tool was not installed via Homebrew")
    }
}

Update Check Example

func checkForUpdates() {
    execPath, _ := os.Executable()
    
    if terminator.IsUnderHomebrew(execPath) {
        fmt.Println("To update, run: brew upgrade mytool")
    } else {
        fmt.Println("Download latest version from GitHub releases")
    }
}

Complete Example

Here’s a complete example combining multiple terminal utilities:
package main

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

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

func main() {
    // Detect environment
    if terminator.IsCI() {
        fmt.Println("Running in CI - using defaults")
        runNonInteractive()
        return
    }

    if !terminator.IsTTY() {
        fmt.Println("Not a TTY - using machine-readable output")
        runNonInteractive()
        return
    }

    // Interactive mode
    runInteractive()
}

func runInteractive() {
    // Check colors
    if terminator.IsColorDisabled() {
        fmt.Println("Colors disabled by user")
    } else {
        printer.Boldln("Interactive Mode")
    }

    // Get logs
    logs := generateLogs(100)

    // Use pager for long output
    if err := displayWithPager(logs); err != nil {
        log.Fatal(err)
    }

    // Offer to open documentation
    p := prompter.New()
    openDocs, err := p.Confirm("Open documentation?", false)
    if err != nil {
        log.Fatal(err)
    }

    if openDocs {
        cmd := terminator.OpenBrowser(runtime.GOOS, "https://docs.example.com")
        if err := cmd.Run(); err != nil {
            printer.Warningf("Failed to open browser: %v\n", err)
        }
    }

    // Check installation method
    execPath, _ := os.Executable()
    if terminator.IsUnderHomebrew(execPath) {
        printer.Infoln("Installed via Homebrew")
        printer.Infoln("Update with: brew upgrade mytool")
    }
}

func runNonInteractive() {
    logs := generateLogs(100)
    for _, log := range logs {
        fmt.Println(log)
    }
}

func displayWithPager(logs []string) error {
    pager := terminator.NewPager()
    
    if err := pager.Start(); err != nil {
        return err
    }
    defer pager.Stop()

    // Write header
    fmt.Fprintln(pager.Out, "Application Logs")
    fmt.Fprintln(pager.Out, strings.Repeat("=", 50))
    fmt.Fprintln(pager.Out)

    // Write logs
    for i, log := range logs {
        fmt.Fprintf(pager.Out, "%3d: %s\n", i+1, log)
    }

    return nil
}

func generateLogs(count int) []string {
    logs := make([]string, count)
    for i := 0; i < count; i++ {
        logs[i] = fmt.Sprintf("Log entry %d", i+1)
    }
    return logs
}

Best Practices

  1. Always check IsTTY - Before using interactive features or colors
  2. Respect NO_COLOR - Check IsColorDisabled() before using colors
  3. Handle CI environments - Use IsCI() to skip interactive prompts
  4. Use pagers for long output - Improve UX for viewing large amounts of data
  5. Defer pager cleanup - Always use defer pager.Stop()
  6. Check TTY before opening browser - Browser commands require a terminal
  7. Provide fallbacks - Always offer non-interactive alternatives
  8. Test in different environments - Test with TTY, piped output, and CI

Environment Variables

PAGER

Controls which pager to use:
export PAGER="less -R"    # Use less with color support
export PAGER="more"       # Use more
export PAGER="cat"        # Disable pager

NO_COLOR

Disables color output:
export NO_COLOR=1         # Disable colors

CI

Indicates CI environment:
export CI=true            # Mark as CI environment

Build docs developers (and LLMs) love