util

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 11, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package util provides foundational utilities for the Aleutian CLI.

This package contains low-level utilities that have no dependencies on other internal packages. All utilities depend only on the Go standard library, making this a leaf package in the dependency graph.

Overview

The util package provides seven categories of utilities:

  • Timeout Management: Enforce minimum and default timeouts to prevent hangs
  • Environment Variables: Type-safe environment variable handling with validation
  • Command Errors: Rich error wrapping for command execution failures
  • Ring Buffer: Thread-safe circular buffer for bounded data collection
  • Progress Indicators: CLI spinners for long-running operations
  • Saga Pattern: Multi-step transactions with automatic rollback
  • Goroutine Safety: Panic recovery for background goroutines

Thread Safety

All types in this package are safe for concurrent use from multiple goroutines unless their documentation explicitly states otherwise. Specifically:

  • RingBuffer is fully thread-safe (protected by mutex)
  • Spinner is thread-safe for Start/Stop/SetMessage
  • [Saga] is NOT thread-safe (use from single goroutine)
  • EnvVars is NOT thread-safe (do not modify concurrently)

Key Types

Timeout utilities:

cfg := util.NewTimeoutConfig()
timeout := util.EnforceMinTimeout(requested, util.MinHTTPTimeout)

Environment variables:

envs, err := util.NewEnvVars(
    util.EnvVar{Key: "API_KEY", Value: "secret", Sensitive: true},
)
fmt.Println(envs.RedactedSlice()) // Safe for logging

Command errors:

err := util.NewCommandError("podman-compose up", 1, stderr, originalErr)
if cmdErr, ok := err.(*util.CommandError); ok {
    fmt.Println(cmdErr.Stderr)
}

Ring buffer:

buffer := util.NewRingBuffer[string](1000)
buffer.Push("log line")
items := buffer.Drain()

Progress spinner:

spinner := util.NewSpinner(util.SpinnerConfig{Message: "Loading..."})
spinner.Start()
defer spinner.Stop()

Saga transactions:

saga := util.NewSaga(util.DefaultSagaConfig())
saga.AddStep(util.SagaStep{
    Name:       "Create Network",
    Execute:    createNetwork,
    Compensate: deleteNetwork,
})
if err := saga.Execute(ctx); err != nil {
    // All completed steps have been rolled back
}

Safe goroutines:

util.SafeGo(func() {
    riskyOperation()
}, func(r util.SafeGoResult) {
    log.Printf("Panic recovered: %v\n%s", r.PanicValue, r.Stack)
})

File Mapping

This package was extracted from cmd/aleutian/ as part of the codebase reorganization (Phase 1). The original files map as follows:

Original                    → New Location
timeouts.go                 → internal/util/timeouts.go
env_vars.go                 → internal/util/env.go
command_error.go            → internal/util/errors.go
ring_buffer.go              → internal/util/ring_buffer.go
progress.go                 → internal/util/progress.go
saga.go                     → internal/util/saga.go
goroutine_safety.go         → internal/util/goroutine.go

Design Principles

All utilities in this package follow these principles:

  1. Single Responsibility: Each type/function does one thing well
  2. Interface First: Interfaces defined before implementations
  3. Defensive Defaults: Safe defaults that prevent common mistakes
  4. Explicit Over Implicit: No hidden behavior or magic
  5. Testable: All functionality is easily unit testable

Security Considerations

  • EnvVar supports sensitivity marking for safe logging
  • CommandError captures stderr without exposing to end users
  • SafeGoResult captures full stack traces for debugging

Performance Considerations

  • RingBuffer pre-allocates memory to avoid runtime allocations
  • Spinner uses efficient ticker-based animation
  • [Saga] executes steps sequentially (no parallel execution)

Package util provides UserPrompter for handling interactive user input.

UserPrompter abstracts all user interaction, enabling:

  • Interactive prompts in terminal environments
  • Non-interactive modes for CI/CD pipelines (--non-interactive)
  • Auto-approve modes for scripting (--yes)
  • Testability through mock implementations

Design Rationale

User prompts are scattered throughout command handlers. By abstracting them behind an interface, we can:

  • Test code that prompts users without real stdin
  • Support CI environments that cannot respond to prompts
  • Provide --yes flag to auto-approve all prompts

Index

Constants

View Source
const (
	// MinHTTPTimeout is the absolute minimum for any HTTP operation.
	// Prevents accidental infinite hangs from zero timeouts.
	MinHTTPTimeout = 1 * time.Second

	// MinTCPTimeout is the absolute minimum for TCP connection checks.
	MinTCPTimeout = 500 * time.Millisecond

	// MinProcessTimeout is the absolute minimum for process operations.
	MinProcessTimeout = 5 * time.Second

	// DefaultHTTPTimeout is the standard timeout for HTTP health checks.
	DefaultHTTPTimeout = 30 * time.Second

	// DefaultTCPTimeout is the standard timeout for TCP connectivity checks.
	DefaultTCPTimeout = 5 * time.Second

	// DefaultProcessTimeout is the standard timeout for process operations.
	DefaultProcessTimeout = 2 * time.Minute

	// DefaultComposeTimeout is the standard timeout for compose operations.
	DefaultComposeTimeout = 5 * time.Minute
)

Timeout constants define minimum and default values for various operations.

These constants prevent accidental infinite hangs by ensuring all operations have a reasonable timeout even if misconfigured.

Variables

View Source
var ErrCancelled = errors.New("operation cancelled by user")

ErrCancelled is returned when the user cancels a prompt.

View Source
var ErrInvalidEnvVarKey = fmt.Errorf("invalid environment variable key")

ErrInvalidEnvVarKey is returned when an environment variable key is invalid.

View Source
var ErrInvalidSelection = errors.New("invalid selection")

ErrInvalidSelection is returned when the user provides an invalid selection.

View Source
var ErrNonInteractive = errors.New("prompt not allowed in non-interactive mode")

ErrNonInteractive is returned when a prompt is attempted in non-interactive mode.

Functions

func EnforceDefaultTimeout

func EnforceDefaultTimeout(requested, defaultVal time.Duration) time.Duration

EnforceDefaultTimeout returns the default if the requested is zero or negative.

Description

Unlike EnforceMinTimeout, this only applies the default when the value is explicitly zero or negative. Useful when you want to allow any positive value but provide a sensible default.

Inputs

  • requested: The timeout value requested by the caller
  • defaultVal: The default timeout to use if requested is invalid

Outputs

  • time.Duration: The requested timeout if positive, otherwise the default

Example

// Use default only if not specified
timeout := util.EnforceDefaultTimeout(opts.Timeout, util.DefaultHTTPTimeout)

Limitations

  • Does not enforce minimum - caller should also check if needed
  • Does not validate that defaultVal is positive

Assumptions

  • defaultVal is a positive duration

func EnforceMinTimeout

func EnforceMinTimeout(requested, minimum time.Duration) time.Duration

EnforceMinTimeout returns at least the minimum timeout.

Description

Ensures a timeout is never below the specified minimum. If the requested timeout is zero, negative, or below the minimum, returns the minimum instead. This prevents misconfiguration from causing infinite hangs.

Inputs

  • requested: The timeout value requested by the caller
  • minimum: The absolute minimum acceptable timeout

Outputs

  • time.Duration: The requested timeout if valid, otherwise the minimum

Example

// User configured 0 timeout (infinite) - enforce 1s minimum
timeout := util.EnforceMinTimeout(config.Timeout, util.MinHTTPTimeout)
client := &http.Client{Timeout: timeout}

Limitations

  • Does not enforce maximum timeouts
  • Does not validate that minimum is positive

Assumptions

  • minimum is a positive duration

func ExtractStderr

func ExtractStderr(err error) string

ExtractStderr extracts stderr from an error chain.

Description

Walks the error chain looking for a CommandError with non-empty stderr. Returns the first stderr found, or empty string if none exists. This is useful for displaying command output to users.

Inputs

  • err: Error to extract stderr from (may be nil)

Outputs

  • string: Stderr content, or empty string if not found

Example

stderr := ExtractStderr(err)
if stderr != "" {
    fmt.Fprintf(os.Stderr, "Command output:\n%s\n", stderr)
}

Limitations

  • Only finds first stderr in chain (not all)
  • Requires error to implement Unwrap() for chain walking

Assumptions

  • Caller accepts empty string as "not found"

func RecoverPanic

func RecoverPanic(onPanic func(SafeGoResult)) func()

RecoverPanic returns a deferred function that recovers panics.

Description

Returns a function suitable for use with defer that recovers panics and passes them to the provided callback. Useful when you can't use SafeGo but still want panic recovery, such as in synchronous code or when you need to control the goroutine lifecycle yourself.

Inputs

  • onPanic: Callback invoked if a panic is recovered (may be nil)

Outputs

  • func(): A function to be deferred that performs panic recovery

Example

func riskyOperation() {
    defer RecoverPanic(func(r SafeGoResult) {
        log.Printf("Recovered: %v", r.PanicValue)
    })()
    // ... risky code that might panic
}

Limitations

  • Must be called with () after defer: defer RecoverPanic(handler)()
  • onPanic runs in the panicking goroutine
  • After recovery, the function returns normally (does not re-panic)

Assumptions

  • Called with defer statement
  • The trailing () is not forgotten

func SafeGo

func SafeGo(fn func(), onPanic func(SafeGoResult))

SafeGo runs a function in a goroutine with panic recovery.

Description

Wraps a function execution in a goroutine with deferred panic recovery. If the function panics, the panic is caught and passed to the onPanic callback instead of crashing the application. This is essential for background operations where a panic should be logged but not terminate the entire process.

Inputs

  • fn: The function to execute in the goroutine
  • onPanic: Callback invoked if fn panics (may be nil to silently recover)

Outputs

  • None (results passed via callbacks)

Example

var wg sync.WaitGroup
wg.Add(1)
SafeGo(func() {
    defer wg.Done()
    riskyOperation()
}, func(r SafeGoResult) {
    defer wg.Done()
    log.Printf("Operation failed: %v", r.PanicValue)
})
wg.Wait()

Limitations

  • onPanic is called synchronously in the recovered goroutine
  • If onPanic itself panics, the application will crash
  • No way to return values from fn; use channels if needed

Assumptions

  • fn is non-nil (will panic if nil)

func SafeGoWithContext

func SafeGoWithContext(ctx context.Context, fn func(), onPanic func(SafeGoResult))

SafeGoWithContext is like SafeGo but checks context before execution.

Description

Similar to SafeGo but performs an initial context check before executing the function. If the context is already cancelled, the function is not executed at all. This prevents unnecessary work when the operation has already been cancelled.

Inputs

  • ctx: Context to check for cancellation
  • fn: The function to execute if context is valid
  • onPanic: Callback invoked if fn panics (may be nil)

Outputs

  • None

Example

ctx, cancel := context.WithCancel(context.Background())
cancel() // Already cancelled
SafeGoWithContext(ctx, func() {
    // This will NOT execute
}, nil)

Limitations

  • Only checks context at start, not during execution
  • fn should check ctx.Done() itself for long operations
  • No notification when skipped due to cancellation

Assumptions

  • ctx and fn are non-nil (will panic if nil)

func SpinWhile

func SpinWhile(message string, fn func() error) error

SpinWhile runs a function with a spinner showing progress.

Description

Convenience function that starts a spinner, runs the provided function, and stops the spinner when done. Shows success or failure based on the function's return value.

Inputs

  • message: Message to display while running
  • fn: Function to execute

Outputs

  • error: Error from fn, or nil

Example

err := SpinWhile("Starting services...", func() error {
    return startAllServices()
})

Limitations

  • Cannot update message during execution
  • Uses default spinner configuration

Assumptions

  • fn is not nil

func SpinWhileContext

func SpinWhileContext(ctx context.Context, message string, fn func() error) error

SpinWhileContext runs a function with a spinner, respecting context.

Description

Like SpinWhile but stops the spinner if the context is cancelled. The function runs in a separate goroutine and may be abandoned if context is cancelled.

Inputs

  • ctx: Context for cancellation
  • message: Message to display
  • fn: Function to execute

Outputs

  • error: Error from fn, context error, or nil

Example

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := SpinWhileContext(ctx, "Waiting for health...", func() error {
    return waitForHealth(ctx)
})

Limitations

  • fn continues running even if context is cancelled
  • fn should check ctx.Done() for proper cancellation

Assumptions

  • ctx and fn are not nil

Types

type CommandError

type CommandError struct {
	// Command is the command that was executed.
	Command string

	// ExitCode is the process exit code (-1 if unknown).
	ExitCode int

	// Stderr contains the standard error output (trimmed).
	Stderr string

	// Wrapped is the underlying error (may be nil).
	Wrapped error
}

CommandError wraps a command execution failure with stderr context.

Description

Provides rich error context for command failures, including the command that failed, exit code, and stderr output. Implements the error interface and supports unwrapping via errors.Is/As.

Thread Safety

CommandError is immutable after creation and safe for concurrent reads.

Example

err := NewCommandError("podman-compose up", 1, "disk full", originalErr)
fmt.Println(err.Error()) // "podman-compose up (exit 1): disk full"

var cmdErr *CommandError
if errors.As(err, &cmdErr) {
    fmt.Println(cmdErr.Stderr) // "disk full"
}

Limitations

  • Stderr is stored as a single string, not streaming
  • Large stderr output consumes memory

func NewCommandError

func NewCommandError(cmd string, exitCode int, stderr string, wrapped error) *CommandError

NewCommandError creates a CommandError with full context.

Description

Creates a new CommandError with command name, exit code, stderr, and underlying error. Stderr is trimmed of leading/trailing whitespace to normalize output from various command sources.

Inputs

  • cmd: The command that was executed (e.g., "podman-compose up")
  • exitCode: Process exit code (-1 if unknown)
  • stderr: Standard error output (will be trimmed)
  • wrapped: Underlying error (may be nil)

Outputs

  • *CommandError: New error with full context

Example

if err := cmd.Run(); err != nil {
    return NewCommandError("podman-compose up", exitCode, stderr.String(), err)
}

Limitations

  • Stderr is stored entirely in memory
  • Does not validate cmd is non-empty

Assumptions

  • Caller wants stderr trimmed (whitespace normalization)

func WrapCommandError

func WrapCommandError(err error, cmd string, exitCode int, stderr string) *CommandError

WrapCommandError wraps an existing error into a CommandError if it isn't already.

Description

If the error is already a *CommandError, returns it as-is to prevent double-wrapping. Otherwise, creates a new CommandError wrapping the original. Returns nil if the input error is nil.

Inputs

  • err: Error to wrap (may be nil)
  • cmd: Command name for context
  • exitCode: Exit code (-1 if unknown)
  • stderr: Standard error output

Outputs

  • *CommandError: Wrapped error, or nil if err was nil

Example

result := WrapCommandError(err, "docker build", exitCode, stderr)
if result != nil {
    return result
}

Limitations

  • Only checks for direct *CommandError type, not wrapped

Assumptions

  • Caller provides accurate command context

func (*CommandError) Error

func (e *CommandError) Error() string

Error returns a formatted error message.

Description

Returns a human-readable error message that includes the command, exit code, and stderr output if available. Stderr takes priority over wrapped error in the message format.

Inputs

  • e: The CommandError receiver (must not be nil)

Outputs

  • string: Formatted error message

Example

err := &CommandError{Command: "ls", ExitCode: 2, Stderr: "not found"}
fmt.Println(err.Error()) // "ls (exit 2): not found"

Limitations

  • Format is fixed and cannot be customized

Assumptions

  • Receiver is not nil

func (*CommandError) HasStderr

func (e *CommandError) HasStderr() bool

HasStderr returns true if stderr output is available.

Description

Checks whether the CommandError has captured stderr content. Useful for conditional formatting or display.

Inputs

  • e: The CommandError receiver (must not be nil)

Outputs

  • bool: true if Stderr is non-empty

Example

if cmdErr.HasStderr() {
    fmt.Fprintf(os.Stderr, "Output: %s\n", cmdErr.Stderr)
}

Assumptions

  • Receiver is not nil

func (*CommandError) Unwrap

func (e *CommandError) Unwrap() error

Unwrap returns the underlying error.

Description

Enables errors.Is() and errors.As() to work through the error chain. Returns nil if there is no wrapped error.

Inputs

  • e: The CommandError receiver (must not be nil)

Outputs

  • error: The wrapped error, or nil

Example

original := errors.New("connection refused")
cmdErr := NewCommandError("docker", 1, "", original)
fmt.Println(errors.Is(cmdErr, original)) // true

Assumptions

  • Receiver is not nil

type EnvVar

type EnvVar struct {
	// Key is the environment variable name.
	// Must match pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$
	Key string

	// Value is the environment variable value.
	// May be empty string (valid in POSIX).
	Value string

	// Sensitive indicates this value should be redacted in logs.
	Sensitive bool
}

EnvVar represents a single environment variable.

Description

A typed representation of an environment variable with validation and sensitivity marking for secure logging. Keys are validated against POSIX naming conventions.

Thread Safety

EnvVar is safe for concurrent reads. Do not modify after creation.

Example

ev := EnvVar{Key: "API_TOKEN", Value: "secret123", Sensitive: true}
fmt.Println(ev.Redacted()) // API_TOKEN=[REDACTED]

Limitations

  • Value is not validated (can be empty or contain any characters)
  • Key validation only happens when Validate() is called explicitly

func (EnvVar) Redacted

func (e EnvVar) Redacted() string

Redacted returns KEY=[REDACTED] for sensitive vars, otherwise String().

Description

Returns a log-safe representation of the environment variable. Sensitive values are replaced with [REDACTED] to prevent accidental exposure in logs.

Inputs

  • e: The EnvVar receiver

Outputs

  • string: Formatted as "KEY=[REDACTED]" if sensitive, else "KEY=VALUE"

Example

secret := EnvVar{Key: "TOKEN", Value: "abc123", Sensitive: true}
fmt.Println(secret.Redacted()) // "TOKEN=[REDACTED]"

func (EnvVar) String

func (e EnvVar) String() string

String returns the KEY=VALUE format.

Description

Returns the environment variable in standard KEY=VALUE format suitable for shell usage or exec.Cmd.Env.

Inputs

  • e: The EnvVar receiver

Outputs

  • string: Formatted as "KEY=VALUE"

Example

ev := EnvVar{Key: "FOO", Value: "bar"}
fmt.Println(ev.String()) // "FOO=bar"

func (EnvVar) Validate

func (e EnvVar) Validate() error

Validate checks if the key is valid.

Description

Validates the environment variable key against POSIX naming conventions. Keys must start with a letter or underscore and contain only alphanumeric characters and underscores.

Inputs

  • e: The EnvVar receiver

Outputs

  • error: ErrInvalidEnvVarKey wrapped with details if key is invalid

Example

ev := EnvVar{Key: "invalid-key", Value: "test"}
if err := ev.Validate(); err != nil {
    // Handle invalid key
}

Limitations

  • Does not validate value content

type EnvVars

type EnvVars struct {
	// contains filtered or unexported fields
}

EnvVars is a validated collection of environment variables.

Description

Provides a type-safe container for environment variables with validation, merging, and redaction capabilities. Replaces raw map[string]string for better type safety and security.

Thread Safety

EnvVars is NOT thread-safe. Do not modify concurrently. For concurrent access, use external synchronization or Clone().

Example

envs, err := NewEnvVars(
    EnvVar{Key: "OLLAMA_MODEL", Value: "llama2"},
    EnvVar{Key: "API_TOKEN", Value: "secret", Sensitive: true},
)
if err != nil {
    return err
}
fmt.Println(envs.RedactedSlice()) // Safe for logging

Limitations

  • Duplicate keys are allowed (last wins in ToMap/Get)
  • Not thread-safe for concurrent modifications

func EmptyEnvVars

func EmptyEnvVars() *EnvVars

EmptyEnvVars returns an empty EnvVars.

Description

Creates an empty EnvVars collection that can be populated using Add() method calls.

Outputs

  • *EnvVars: Empty collection

Example

envs := EmptyEnvVars()
envs.Add("FOO", "bar", false)

func FromMap

func FromMap(m map[string]string, sensitiveKeys []string) (*EnvVars, error)

FromMap creates EnvVars from a map[string]string.

Description

Converts a map[string]string to EnvVars with validation. Keys containing common sensitive patterns (TOKEN, SECRET, KEY, PASSWORD, CREDENTIAL, API_KEY, AUTH) are automatically marked as sensitive. Additional sensitive keys can be specified.

Inputs

  • m: Map of environment variables (may be nil)
  • sensitiveKeys: Additional keys that should be marked as sensitive

Outputs

  • *EnvVars: Validated collection
  • error: Non-nil if any key is invalid

Example

m := map[string]string{"FOO": "bar", "SECRET": "hidden"}
envs, err := FromMap(m, []string{"SECRET"})

Limitations

  • Order of keys in result is not guaranteed (map iteration order)

func MustNewEnvVars

func MustNewEnvVars(vars ...EnvVar) *EnvVars

MustNewEnvVars creates EnvVars or panics.

Description

Like NewEnvVars but panics on validation error. Use only for compile-time constants where you're certain the keys are valid.

Inputs

  • vars: Environment variables to include

Outputs

  • *EnvVars: Validated collection

Example

var defaultEnvs = MustNewEnvVars(
    EnvVar{Key: "LOG_LEVEL", Value: "info"},
)

Assumptions

  • Keys are known valid at compile time

func NewEnvVars

func NewEnvVars(vars ...EnvVar) (*EnvVars, error)

NewEnvVars creates a validated EnvVars collection.

Description

Creates a new EnvVars after validating all provided variables. Returns an error if any key is invalid.

Inputs

  • vars: Environment variables to include

Outputs

  • *EnvVars: Validated collection
  • error: Non-nil if any key is invalid

Example

envs, err := NewEnvVars(
    EnvVar{Key: "FOO", Value: "bar"},
)

Limitations

  • Duplicate keys are allowed (last wins in ToMap)

Assumptions

  • Caller handles validation errors appropriately

func (*EnvVars) Add

func (e *EnvVars) Add(key, value string, sensitive bool) error

Add appends a validated environment variable.

Description

Adds a new environment variable to the collection after validation. The variable is appended to the end. If the key is invalid, returns an error and does not add the variable.

Inputs

  • key: Environment variable name
  • value: Environment variable value
  • sensitive: Whether to redact in logs

Outputs

  • error: Non-nil if key is invalid

Example

envs := EmptyEnvVars()
envs.Add("FOO", "bar", false)
envs.Add("SECRET", "hidden", true)

Assumptions

  • Receiver is not nil

func (*EnvVars) Clone

func (e *EnvVars) Clone() *EnvVars

Clone returns a deep copy.

Description

Creates a new EnvVars with copies of all variables. Modifications to the clone do not affect the original. Returns nil if receiver is nil.

Outputs

  • *EnvVars: Deep copy of the collection

func (*EnvVars) Get

func (e *EnvVars) Get(key string) string

Get returns the value for a key, or empty string if not found.

Description

Retrieves the value associated with a key. If there are duplicate keys, returns the last value (matching shell semantics). Returns empty string if the key is not found or receiver is nil.

Inputs

  • key: Environment variable name to look up

Outputs

  • string: Value if found, empty string otherwise

Example

value := envs.Get("FOO")
if value == "" {
    // Key not found or has empty value
}

func (*EnvVars) Has

func (e *EnvVars) Has(key string) bool

Has returns true if the key exists.

Description

Checks whether a key exists in the collection. Returns false if the receiver is nil.

Inputs

  • key: Environment variable name to check

Outputs

  • bool: true if key exists, false otherwise

func (*EnvVars) Len

func (e *EnvVars) Len() int

Len returns the number of environment variables.

Description

Returns the count of environment variables in the collection. Returns 0 if the receiver is nil.

Outputs

  • int: Number of variables

func (*EnvVars) Merge

func (e *EnvVars) Merge(other *EnvVars) *EnvVars

Merge combines two EnvVars, with other taking precedence.

Description

Creates a new EnvVars containing all variables from both collections. If the same key exists in both, the value from 'other' is used. Handles nil receivers and nil other gracefully.

Inputs

  • other: EnvVars to merge (takes precedence)

Outputs

  • *EnvVars: New merged collection

Example

defaults := MustNewEnvVars(EnvVar{Key: "LOG_LEVEL", Value: "info"})
overrides := MustNewEnvVars(EnvVar{Key: "LOG_LEVEL", Value: "debug"})
merged := defaults.Merge(overrides)
// merged.Get("LOG_LEVEL") == "debug"

Limitations

  • Order of keys in result is not guaranteed

func (*EnvVars) MustAdd

func (e *EnvVars) MustAdd(key, value string, sensitive bool)

MustAdd adds a variable or panics.

Description

Like Add but panics on validation error. Use only when you're certain the key is valid.

Inputs

  • key: Environment variable name
  • value: Environment variable value
  • sensitive: Whether to redact in logs

Assumptions

  • Receiver is not nil
  • Key is known valid

func (*EnvVars) RedactedSlice

func (e *EnvVars) RedactedSlice() []string

RedactedSlice returns []string with sensitive values masked.

Description

Like ToSlice but replaces sensitive values with [REDACTED]. Safe for logging. Returns nil if receiver is nil.

Outputs

  • []string: Variables with sensitive values redacted

Example

log.Printf("Starting with env: %v", envs.RedactedSlice())

func (*EnvVars) ToMap

func (e *EnvVars) ToMap() map[string]string

ToMap converts to map[string]string.

Description

Returns environment variables as a map. If there are duplicate keys, the last value wins. Useful for compatibility with code that expects map[string]string. Returns nil if receiver is nil.

Outputs

  • map[string]string: Variables as key-value map

func (*EnvVars) ToSlice

func (e *EnvVars) ToSlice() []string

ToSlice converts to []string format for exec.Cmd.Env.

Description

Returns environment variables in KEY=VALUE format suitable for passing to exec.Cmd.Env. Returns nil if receiver is nil.

Outputs

  • []string: Variables in KEY=VALUE format

Example

cmd.Env = envs.ToSlice()

type InteractivePrompter

type InteractivePrompter struct {
	// contains filtered or unexported fields
}

InteractivePrompter implements UserPrompter using stdin/stdout.

This is the default prompter for terminal environments. It reads user input from the configured reader (typically os.Stdin) and writes prompts to the configured writer (typically os.Stdout).

Thread Safety

Not safe for concurrent use. Prompts should be serialized.

Assumptions

  • reader and writer are valid and not nil
  • reader provides line-based input (newline-terminated)

func NewInteractivePrompter

func NewInteractivePrompter() *InteractivePrompter

NewInteractivePrompter creates a prompter that uses stdin/stdout.

Description

Creates a UserPrompter that displays prompts to stdout and reads responses from stdin. Use this in production terminal environments.

Outputs

  • *InteractivePrompter: Ready-to-use interactive prompter

Examples

prompter := NewInteractivePrompter()
confirmed, err := prompter.Confirm(ctx, "Continue?")

Assumptions

  • os.Stdin and os.Stdout are available

func NewInteractivePrompterWithIO

func NewInteractivePrompterWithIO(reader io.Reader, writer io.Writer) *InteractivePrompter

NewInteractivePrompterWithIO creates a prompter with custom I/O.

Description

Creates a UserPrompter with custom reader/writer. Useful for testing with mock I/O or for redirecting prompts to alternative streams.

Inputs

  • reader: Source for user input (must not be nil)
  • writer: Destination for prompts (must not be nil)

Outputs

  • *InteractivePrompter: Configured prompter

Examples

var buf bytes.Buffer
buf.WriteString("y\n")
prompter := NewInteractivePrompterWithIO(&buf, os.Stdout)

Assumptions

  • reader and writer are not nil
  • reader provides newline-terminated input

func (*InteractivePrompter) Confirm

func (p *InteractivePrompter) Confirm(ctx context.Context, prompt string) (bool, error)

Confirm asks a yes/no question and returns the answer.

func (*InteractivePrompter) IsInteractive

func (p *InteractivePrompter) IsInteractive() bool

IsInteractive returns true for InteractivePrompter.

func (*InteractivePrompter) Select

func (p *InteractivePrompter) Select(ctx context.Context, prompt string, options []string) (int, error)

Select presents options and returns the selected index.

type MockPrompter

type MockPrompter struct {
	// ConfirmFunc is called when Confirm is invoked
	ConfirmFunc func(ctx context.Context, prompt string) (bool, error)

	// SelectFunc is called when Select is invoked
	SelectFunc func(ctx context.Context, prompt string, options []string) (int, error)

	// IsInteractiveFunc is called when IsInteractive is invoked
	// If nil, defaults to returning true
	IsInteractiveFunc func() bool

	// Calls records all method invocations for verification
	Calls []PrompterCall
}

MockPrompter is a test double for UserPrompter.

Configure the mock by setting function fields before use. If a function field is nil and the corresponding method is called, it will panic.

Thread Safety

Not safe for concurrent use due to Calls slice mutation.

Examples

mock := &MockPrompter{
    ConfirmFunc: func(ctx context.Context, prompt string) (bool, error) {
        if prompt == "Delete all data?" {
            return true, nil
        }
        return false, nil
    },
}

Assumptions

  • ConfirmFunc/SelectFunc are set before calling respective methods
  • Panics if function fields are nil (fail-fast for test debugging)

func (*MockPrompter) Confirm

func (m *MockPrompter) Confirm(ctx context.Context, prompt string) (bool, error)

Confirm delegates to ConfirmFunc and records the call.

func (*MockPrompter) IsInteractive

func (m *MockPrompter) IsInteractive() bool

IsInteractive delegates to IsInteractiveFunc or returns true if nil.

func (*MockPrompter) Reset

func (m *MockPrompter) Reset()

Reset clears all recorded calls.

func (*MockPrompter) Select

func (m *MockPrompter) Select(ctx context.Context, prompt string, options []string) (int, error)

Select delegates to SelectFunc and records the call.

type NonInteractivePrompter

type NonInteractivePrompter struct {
	// contains filtered or unexported fields
}

NonInteractivePrompter implements UserPrompter for CI/scripting.

This prompter either auto-approves all prompts (for --yes flag) or returns errors for all prompts (for --non-interactive without --yes).

Thread Safety

Safe for concurrent use (stateless after construction).

Assumptions

  • Auto-approve mode always selects the first option for Select()

func NewAutoApprovePrompter

func NewAutoApprovePrompter() *NonInteractivePrompter

NewAutoApprovePrompter creates a prompter that auto-approves all prompts.

Description

Creates a UserPrompter that automatically approves confirmations and selects the first option. Use this for --yes flag in CI/scripting.

Outputs

  • *NonInteractivePrompter: Prompter that auto-approves

Examples

prompter := NewAutoApprovePrompter()
confirmed, err := prompter.Confirm(ctx, "Continue?")
// confirmed == true, err == nil

Assumptions

  • For Select(), the first option (index 0) is acceptable as default

func NewNonInteractivePrompter

func NewNonInteractivePrompter() *NonInteractivePrompter

NewNonInteractivePrompter creates a prompter that rejects all prompts.

Description

Creates a UserPrompter that returns ErrNonInteractive for all prompts. Use this for --non-interactive mode without --yes.

Outputs

  • *NonInteractivePrompter: Prompter that rejects prompts

Examples

prompter := NewNonInteractivePrompter()
_, err := prompter.Confirm(ctx, "Continue?")
// err == ErrNonInteractive

func (*NonInteractivePrompter) Confirm

func (p *NonInteractivePrompter) Confirm(ctx context.Context, prompt string) (bool, error)

Confirm either auto-approves or returns an error.

func (*NonInteractivePrompter) IsInteractive

func (p *NonInteractivePrompter) IsInteractive() bool

IsInteractive returns false for NonInteractivePrompter.

func (*NonInteractivePrompter) Select

func (p *NonInteractivePrompter) Select(ctx context.Context, prompt string, options []string) (int, error)

Select either returns the first option or an error.

type ProgressIndicator

type ProgressIndicator interface {
	// Start begins the progress indication.
	Start()

	// Stop halts the progress indication.
	Stop()

	// SetMessage updates the displayed message.
	SetMessage(message string)

	// IsRunning returns whether the indicator is active.
	IsRunning() bool
}

ProgressIndicator defines the interface for progress feedback.

Description

ProgressIndicator provides visual feedback during long-running operations to prevent users from thinking the application has frozen.

Thread Safety

Implementations must be safe for concurrent use from multiple goroutines.

Example

var indicator ProgressIndicator = NewSpinner(DefaultSpinnerConfig())
indicator.Start()
defer indicator.Stop()

Limitations

  • Implementations may vary in display capabilities
  • Some implementations may not work without TTY

Assumptions

  • Start() can be called before Stop()
  • SetMessage() can be called at any time

type PrompterCall

type PrompterCall struct {
	Method  string
	Prompt  string
	Options []string
}

PrompterCall records a single method invocation.

type RingBuffer

type RingBuffer[T any] struct {
	// contains filtered or unexported fields
}

RingBuffer is a thread-safe, fixed-size circular buffer.

Description

RingBuffer implements a circular buffer (ring buffer) that automatically drops the oldest items when full. This provides backpressure handling for producer-consumer scenarios where unbounded growth would cause OOM.

Use Cases

  • Log collection where recent logs matter most
  • Metrics buffering before flushing to disk
  • Event queues with bounded memory
  • Sliding window calculations

How It Works

  1. Items are added at the tail position
  2. Items are removed from the head position
  3. When full, Push overwrites the oldest item
  4. DroppedCount tracks how many items were dropped

Thread Safety

RingBuffer is safe for concurrent use from multiple goroutines. All operations are protected by a mutex.

Example

buffer := NewRingBuffer[string](100)

// Producer
if dropped := buffer.Push("log line"); dropped {
    // An old log was dropped to make room
}

// Consumer
items := buffer.PopN(10)
for _, item := range items {
    process(item)
}

Limitations

  • Fixed capacity (cannot grow)
  • Drops oldest items when full (no backpressure signal)
  • Memory is pre-allocated for full capacity

Assumptions

  • Capacity is known and fixed at creation time
  • Dropping old items is acceptable
  • Items can be copied (stored by value)

func NewRingBuffer

func NewRingBuffer[T any](capacity int) *RingBuffer[T]

NewRingBuffer creates a new ring buffer with the specified capacity.

Description

Creates a ring buffer that can hold up to `capacity` items. The buffer is initially empty. Memory is pre-allocated for the full capacity to avoid runtime allocations during Push.

Inputs

  • capacity: Maximum number of items to hold (must be > 0)

Outputs

  • *RingBuffer[T]: New empty ring buffer

Example

// Create buffer for 1000 metric points
metrics := NewRingBuffer[MetricPoint](1000)

// Create buffer for log lines
logs := NewRingBuffer[string](500)

Limitations

  • Capacity cannot be changed after creation
  • Memory is allocated immediately, not lazily

Assumptions

  • Caller provides positive capacity

Panics

Panics if capacity <= 0.

func (*RingBuffer[T]) Capacity

func (r *RingBuffer[T]) Capacity() int

Capacity returns the maximum capacity.

Description

Returns the maximum number of items the buffer can hold. This value is immutable after creation.

Inputs

  • None (receiver only)

Outputs

  • int: Maximum capacity

Example

fmt.Printf("Buffer can hold %d items\n", buffer.Capacity())

Limitations

  • Cannot be changed after creation

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) Clear

func (r *RingBuffer[T]) Clear()

Clear removes all items and resets dropped count.

Description

Removes all items from the buffer and resets the dropped count to zero. The capacity remains unchanged. All internal references are cleared to allow GC.

Inputs

  • None (receiver only)

Outputs

  • None

Example

buffer.Clear()
// buffer.Size() == 0
// buffer.DroppedCount() == 0

Limitations

  • Cannot recover cleared items

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) Drain

func (r *RingBuffer[T]) Drain() []T

Drain removes and returns all items.

Description

Removes all items from the buffer and returns them. The buffer is empty after this call. Returns nil if the buffer is already empty.

Inputs

  • None (receiver only)

Outputs

  • []T: All items (oldest first), or nil if empty

Example

// Flush all buffered items on shutdown
items := buffer.Drain()
for _, item := range items {
    flush(item)
}

Limitations

  • Does not reset DroppedCount (use Clear for full reset)

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) DroppedCount

func (r *RingBuffer[T]) DroppedCount() int64

DroppedCount returns total items dropped due to capacity.

Description

Returns the total number of items that have been dropped since the buffer was created (or since Clear was called). Uses atomic operations for lock-free reading.

Inputs

  • None (receiver only)

Outputs

  • int64: Total dropped count (never negative)

Example

if buffer.DroppedCount() > 0 {
    log.Printf("WARNING: %d items dropped", buffer.DroppedCount())
}

Limitations

  • Counter can overflow (after 9 quintillion drops)

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) IsEmpty

func (r *RingBuffer[T]) IsEmpty() bool

IsEmpty returns true if buffer has no items.

Description

Returns whether the buffer contains no items.

Inputs

  • None (receiver only)

Outputs

  • bool: true if Size is zero

Example

if buffer.IsEmpty() {
    log.Println("No items to process")
}

Limitations

  • May be stale in concurrent scenarios

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) IsFull

func (r *RingBuffer[T]) IsFull() bool

IsFull returns true if buffer is at capacity.

Description

Returns whether the buffer is completely full. The next Push will cause an item to be dropped.

Inputs

  • None (receiver only)

Outputs

  • bool: true if Size equals Capacity

Example

if buffer.IsFull() {
    log.Println("Buffer is full, consider increasing capacity")
}

Limitations

  • May be stale in concurrent scenarios

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) Peek

func (r *RingBuffer[T]) Peek() (T, bool)

Peek returns the oldest item without removing it.

Description

Returns a copy of the oldest item without modifying the buffer. Useful for inspection without consumption.

Inputs

  • None (receiver only)

Outputs

  • T: The oldest item (or zero value if empty)
  • bool: true if an item exists, false if empty

Example

if oldest, ok := buffer.Peek(); ok {
    fmt.Printf("Next item will be: %v\n", oldest)
}

Limitations

  • Returns a copy, not a reference to the stored item

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) Pop

func (r *RingBuffer[T]) Pop() (T, bool)

Pop removes and returns the oldest item.

Description

Removes the oldest item from the buffer and returns it. Returns the zero value and false if the buffer is empty. Clears the internal reference to allow GC of the removed item.

Inputs

  • None (receiver only)

Outputs

  • T: The oldest item (or zero value if empty)
  • bool: true if an item was returned, false if empty

Example

for {
    item, ok := buffer.Pop()
    if !ok {
        break // Buffer empty
    }
    process(item)
}

Limitations

  • Cannot block; returns immediately if empty

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) PopN

func (r *RingBuffer[T]) PopN(n int) []T

PopN removes and returns up to n oldest items.

Description

Removes and returns the oldest n items from the buffer. Returns fewer items if the buffer contains less than n items. Returns nil if n <= 0 or buffer is empty.

Inputs

  • n: Maximum number of items to pop

Outputs

  • []T: Slice of items (oldest first), may be shorter than n

Example

// Batch process up to 100 items
batch := buffer.PopN(100)
if len(batch) > 0 {
    writeToDisk(batch)
}

Limitations

  • Returns nil (not empty slice) when nothing to return

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) Push

func (r *RingBuffer[T]) Push(item T) bool

Push adds an item to the buffer.

Description

Adds the item to the tail of the buffer. If the buffer is full, the oldest item is dropped and DroppedCount is incremented.

Inputs

  • item: Item to add

Outputs

  • bool: true if an item was dropped to make room

Example

if dropped := buffer.Push(logLine); dropped {
    if buffer.DroppedCount() % 1000 == 0 {
        log.Printf("WARNING: Dropped %d items", buffer.DroppedCount())
    }
}

Limitations

  • Cannot block; always succeeds immediately
  • Dropped items cannot be recovered

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) Size

func (r *RingBuffer[T]) Size() int

Size returns the current number of items.

Description

Returns the number of items currently in the buffer. This is a point-in-time snapshot and may change immediately after returning in concurrent scenarios.

Inputs

  • None (receiver only)

Outputs

  • int: Current item count (0 to Capacity)

Example

if buffer.Size() > buffer.Capacity()/2 {
    log.Println("Buffer over 50% full")
}

Limitations

  • Value may be stale in concurrent scenarios

Assumptions

  • Receiver is not nil

func (*RingBuffer[T]) ToSlice

func (r *RingBuffer[T]) ToSlice() []T

ToSlice returns a copy of all items without removing them.

Description

Returns a snapshot of all items in the buffer. The buffer is not modified. Items are returned oldest-first. Returns nil if buffer is empty.

Inputs

  • None (receiver only)

Outputs

  • []T: Copy of all items, or nil if empty

Example

// Inspect buffer contents
items := buffer.ToSlice()
for i, item := range items {
    fmt.Printf("[%d] %v\n", i, item)
}

Limitations

  • Allocates new slice every call
  • Snapshot may be stale immediately after returning

Assumptions

  • Receiver is not nil

type RingBufferable

type RingBufferable[T any] interface {
	// Push adds an item to the buffer. Returns true if an item was dropped.
	Push(item T) bool

	// Pop removes and returns the oldest item. Returns zero value and false if empty.
	Pop() (T, bool)

	// Peek returns the oldest item without removing it.
	Peek() (T, bool)

	// PopN removes and returns up to n oldest items.
	PopN(n int) []T

	// Drain removes and returns all items.
	Drain() []T

	// Size returns the current number of items.
	Size() int

	// Capacity returns the maximum capacity.
	Capacity() int

	// IsFull returns true if buffer is at capacity.
	IsFull() bool

	// IsEmpty returns true if buffer has no items.
	IsEmpty() bool

	// DroppedCount returns total items dropped due to capacity.
	DroppedCount() int64

	// Clear removes all items and resets dropped count.
	Clear()
}

RingBufferable defines the interface for bounded ring buffer operations.

Description

RingBufferable provides a fixed-size buffer that drops oldest items when full, preventing unbounded memory growth. Ideal for logging, metrics collection, and any producer-consumer scenario where dropping old data is acceptable.

Thread Safety

Implementations must be safe for concurrent use from multiple goroutines.

Example

var buffer RingBufferable[string] = NewRingBuffer[string](100)
buffer.Push("log line")
items := buffer.Drain()

Limitations

  • Implementations may have varying performance characteristics
  • No blocking operations; drops silently when full

Assumptions

  • Items can be copied by value
  • Dropping old items is acceptable

type SafeGoResult

type SafeGoResult struct {
	// PanicValue is the value passed to panic().
	// Can be any type (string, error, int, struct, etc.).
	PanicValue interface{}

	// Stack is the full stack trace at panic time.
	// Formatted by runtime/debug.Stack().
	Stack string
}

SafeGoResult captures the result of a safe goroutine execution.

Description

Contains information about a panic that occurred in a goroutine, including the panic value and full stack trace for debugging. This struct is passed to panic handlers to provide complete diagnostic information.

Thread Safety

SafeGoResult is immutable after creation and safe for concurrent reads.

Example

SafeGo(func() {
    panic("something went wrong")
}, func(r SafeGoResult) {
    log.Printf("Panic: %v\nStack:\n%s", r.PanicValue, r.Stack)
})

Limitations

  • Stack trace format depends on Go runtime version
  • PanicValue may be any type, requiring type assertion for specific handling

type Spinner

type Spinner struct {
	// contains filtered or unexported fields
}

Spinner provides animated progress feedback for CLI operations.

Description

Spinner displays an animated character sequence with a message to indicate that a long-running operation is in progress. This prevents users from thinking the application has frozen.

Use Cases

  • Waiting for containers to start
  • Downloading files
  • Health check waits
  • Any operation > 1 second

Thread Safety

Spinner is safe for concurrent use. Start/Stop can be called from different goroutines.

Example

spinner := NewSpinner(SpinnerConfig{Message: "Starting services..."})
spinner.Start()
defer spinner.Stop()

// ... long operation

spinner.SetMessage("Waiting for health checks...")
// ... more work

Limitations

  • Requires TTY-compatible terminal for proper display
  • ANSI escape codes may not work on all terminals
  • Concurrent writes to same Writer may cause garbled output

Assumptions

  • Terminal supports ANSI escape codes
  • Only one spinner writes to Writer at a time

func NewSpinner

func NewSpinner(config SpinnerConfig) *Spinner

NewSpinner creates a new spinner with the given configuration.

Description

Creates a spinner ready to be started. The spinner will not display anything until Start() is called. Zero values in config are replaced with sensible defaults.

Inputs

  • config: Configuration for spinner behavior

Outputs

  • *Spinner: New spinner (not yet started)

Example

spinner := NewSpinner(SpinnerConfig{
    Message: "Downloading model...",
    Frames:  []string{"|", "/", "-", "\\"},
})

Limitations

  • Does not validate Writer supports ANSI codes

Assumptions

  • Caller will call Start() when ready

func (*Spinner) IsRunning

func (s *Spinner) IsRunning() bool

IsRunning returns whether the spinner is active.

Description

Returns whether the spinner is currently animating. Thread-safe snapshot; value may change immediately after return.

Inputs

  • None (receiver only)

Outputs

  • bool: true if spinner is running

Example

if spinner.IsRunning() {
    spinner.Stop()
}

Limitations

  • Result is a point-in-time snapshot

Assumptions

  • Receiver is not nil

func (*Spinner) SetMessage

func (s *Spinner) SetMessage(message string)

SetMessage updates the displayed message.

Description

Changes the message shown next to the spinner. Safe to call while the spinner is running. Thread-safe.

Inputs

  • message: New message to display

Outputs

  • None

Example

spinner.SetMessage("Downloading... 50%")

Limitations

  • Long messages may wrap on narrow terminals

Assumptions

  • Receiver is not nil

func (*Spinner) Start

func (s *Spinner) Start()

Start begins the spinner animation.

Description

Starts the background goroutine that animates the spinner. Safe to call multiple times (subsequent calls are no-ops). Hides the cursor if HideCursor is enabled.

Inputs

  • None (receiver only)

Outputs

  • None

Example

spinner.Start()
defer spinner.Stop()

Limitations

  • Creates a goroutine that must be cleaned up with Stop()

Assumptions

  • Receiver is not nil

func (*Spinner) Stop

func (s *Spinner) Stop()

Stop halts the spinner animation.

Description

Stops the spinner and optionally clears the line. Blocks until the spinner goroutine has fully stopped. Safe to call multiple times (subsequent calls are no-ops). Restores cursor if it was hidden.

Inputs

  • None (receiver only)

Outputs

  • None

Example

spinner.Stop()

Limitations

  • Blocks until background goroutine exits

Assumptions

  • Receiver is not nil

func (*Spinner) StopFailure

func (s *Spinner) StopFailure(message string)

StopFailure stops and displays a failure message.

Description

Stops the spinner and displays a failure indicator with the configured or provided message. Shows an X mark (✗) prefix.

Inputs

  • message: Optional message (uses FailureMessage if empty)

Outputs

  • None

Example

spinner.StopFailure("Connection timed out")

Limitations

  • Requires terminal to display ✗ character

Assumptions

  • Receiver is not nil

func (*Spinner) StopSuccess

func (s *Spinner) StopSuccess(message string)

StopSuccess stops and displays a success message.

Description

Stops the spinner and displays a success indicator with the configured or provided message. Shows a checkmark (✓) prefix.

Inputs

  • message: Optional message (uses SuccessMessage if empty)

Outputs

  • None

Example

spinner.StopSuccess("Upload complete!")

Limitations

  • Requires terminal to display ✓ character

Assumptions

  • Receiver is not nil

type SpinnerConfig

type SpinnerConfig struct {
	// Message is the text displayed next to the spinner.
	Message string

	// Interval is the time between frame updates.
	// Default: 100ms
	Interval time.Duration

	// Frames are the animation characters.
	// Default: Braille dots (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
	Frames []string

	// Writer is where output is written.
	// Default: os.Stderr
	Writer io.Writer

	// HideCursor hides the terminal cursor while spinning.
	// Default: true
	HideCursor bool

	// ClearOnStop clears the spinner line when stopped.
	// Default: true
	ClearOnStop bool

	// SuccessMessage shown when StopSuccess is called.
	SuccessMessage string

	// FailureMessage shown when StopFailure is called.
	FailureMessage string
}

SpinnerConfig configures spinner behavior.

Description

Controls the spinner's appearance, speed, and output destination. All fields have sensible defaults that can be overridden.

Example

config := SpinnerConfig{
    Message:  "Loading...",
    Interval: 100 * time.Millisecond,
    Writer:   os.Stderr,
}

Limitations

  • Custom Frames must contain at least one element
  • Writer must support ANSI escape codes for full functionality

func DefaultSpinnerConfig

func DefaultSpinnerConfig() SpinnerConfig

DefaultSpinnerConfig returns sensible defaults.

Description

Returns a configuration with Braille dot animation, 100ms interval, writing to stderr. Suitable for most CLI use cases.

Inputs

  • None

Outputs

  • SpinnerConfig: Configuration with default values

Example

config := DefaultSpinnerConfig()
config.Message = "Custom message..."
spinner := NewSpinner(config)

Limitations

  • Braille characters may not display on all terminals

Assumptions

  • os.Stderr is available for writing

type TimeoutConfig

type TimeoutConfig struct {
	// HTTP is the timeout for HTTP operations.
	HTTP time.Duration

	// TCP is the timeout for TCP connection checks.
	TCP time.Duration

	// Process is the timeout for process operations.
	Process time.Duration

	// Compose is the timeout for compose operations.
	Compose time.Duration
}

TimeoutConfig holds timeout settings with validation.

Description

Provides a validated set of timeout configurations for various operations. Use NewTimeoutConfig to create with proper defaults.

Thread Safety

TimeoutConfig is safe for concurrent reads. For concurrent modifications, external synchronization is required.

Example

cfg := util.NewTimeoutConfig()
cfg.HTTP = 60 * time.Second  // Custom HTTP timeout
validCfg := cfg.Validated()  // Ensures minimums are met

Limitations

  • Does not enforce maximum timeouts
  • No built-in validation on field assignment

Assumptions

  • Consumers call Validated() before using values in production

func NewTimeoutConfig

func NewTimeoutConfig() TimeoutConfig

NewTimeoutConfig creates a TimeoutConfig with sensible defaults.

Description

Returns a TimeoutConfig initialized with the default timeout values. All values are guaranteed to be at least the minimum.

Inputs

  • None

Outputs

  • TimeoutConfig: Configuration with default timeouts

Example

cfg := util.NewTimeoutConfig()
// cfg.HTTP == util.DefaultHTTPTimeout

Limitations

  • Returns value type, not pointer

Assumptions

  • Default constants are defined and positive

func (*TimeoutConfig) Validated

func (c *TimeoutConfig) Validated() TimeoutConfig

Validated returns a copy with all timeouts at least at their minimums.

Description

Returns a new TimeoutConfig where any value below its minimum has been raised to the minimum. The original config is not modified.

Inputs

  • c: The TimeoutConfig to validate (receiver)

Outputs

  • TimeoutConfig: A validated copy with enforced minimums

Example

cfg := &util.TimeoutConfig{HTTP: 0}  // Invalid
valid := cfg.Validated()
// valid.HTTP == util.MinHTTPTimeout

Limitations

  • Does not enforce maximum timeouts

Assumptions

  • The receiver is not nil
  • Minimum constants are positive durations

type TimeoutValidator

type TimeoutValidator interface {
	// Validated returns a copy with all timeouts at least at their minimums.
	//
	// # Description
	//
	// Returns a new TimeoutConfig where any value below its minimum has been
	// raised to the minimum. The original config is not modified.
	//
	// # Outputs
	//
	//   - TimeoutConfig: A validated copy with enforced minimums
	//
	// # Assumptions
	//
	//   - The receiver is not nil
	Validated() TimeoutConfig
}

TimeoutValidator defines the contract for timeout configuration validation.

Description

TimeoutValidator provides a standard interface for validating timeout configurations. Implementations should ensure all timeout values meet their respective minimums to prevent infinite hangs.

Thread Safety

Implementations should be safe for concurrent use from multiple goroutines.

Example

func configureClient(v util.TimeoutValidator) {
    validated := v.Validated()
    client.Timeout = validated.HTTP
}

type UserPrompter

type UserPrompter interface {
	// Confirm asks a yes/no question and returns the answer.
	//
	// # Description
	//
	// Displays a yes/no prompt to the user and waits for their response.
	// Accepts various forms of yes (y, yes, Y, YES) and no (n, no, N, NO).
	//
	// # Inputs
	//
	//   - ctx: Context for cancellation
	//   - prompt: The question to display (e.g., "Continue?")
	//
	// # Outputs
	//
	//   - bool: True if user confirmed, false if declined
	//   - error: Non-nil if cancelled, non-interactive, or I/O error
	//
	// # Examples
	//
	//   confirmed, err := p.Confirm(ctx, "Delete all data?")
	//   if err != nil {
	//       return fmt.Errorf("prompt failed: %w", err)
	//   }
	//   if !confirmed {
	//       return ErrCancelled
	//   }
	//
	// # Limitations
	//
	//   - May not immediately respond to context cancellation while blocking on read
	//   - Empty input defaults to 'no' for safety
	//
	// # Assumptions
	//
	//   - prompt string does not contain control characters
	Confirm(ctx context.Context, prompt string) (bool, error)

	// Select presents options and returns the selected index.
	//
	// # Description
	//
	// Displays a numbered list of options and waits for the user to
	// select one by entering its number (1-based).
	//
	// # Inputs
	//
	//   - ctx: Context for cancellation
	//   - prompt: Header text to display above options
	//   - options: List of option strings to display
	//
	// # Outputs
	//
	//   - int: Zero-based index of selected option
	//   - error: Non-nil if cancelled, invalid selection, or I/O error
	//
	// # Examples
	//
	//   idx, err := p.Select(ctx, "Choose machine action:", []string{
	//       "Fix automatically",
	//       "Skip",
	//       "Abort",
	//   })
	//   if err != nil {
	//       return fmt.Errorf("selection failed: %w", err)
	//   }
	//
	// # Limitations
	//
	//   - User must enter 1-based number, not option text
	//   - Options slice must not be empty
	//   - Cannot interrupt blocking read once started
	//
	// # Assumptions
	//
	//   - options slice is non-empty (returns error if empty)
	//   - option strings do not contain newlines
	Select(ctx context.Context, prompt string, options []string) (int, error)

	// IsInteractive returns true if prompts are enabled.
	//
	// # Description
	//
	// Returns whether the prompter is configured for interactive use.
	// Non-interactive prompters will return errors from Confirm/Select.
	//
	// # Outputs
	//
	//   - bool: True if prompts will be displayed, false otherwise
	//
	// # Examples
	//
	//   if !p.IsInteractive() {
	//       log.Warn("Running in non-interactive mode, using defaults")
	//   }
	IsInteractive() bool
}

UserPrompter handles interactive user prompts.

This interface abstracts user interaction, enabling non-interactive modes for CI/scripting while maintaining UX for interactive terminal use.

Thread Safety

Implementations should be safe for sequential use but are not designed for concurrent prompt handling from multiple goroutines.

Context Handling

Methods accept context for cancellation support, though interactive prompts may not immediately respond to cancellation while waiting for input.

Assumptions

  • Callers handle the case where context is cancelled before prompt completes
  • For interactive use, stdin/stdout are available and functional

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL