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:
- Single Responsibility: Each type/function does one thing well
- Interface First: Interfaces defined before implementations
- Defensive Defaults: Safe defaults that prevent common mistakes
- Explicit Over Implicit: No hidden behavior or magic
- 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
- Variables
- func EnforceDefaultTimeout(requested, defaultVal time.Duration) time.Duration
- func EnforceMinTimeout(requested, minimum time.Duration) time.Duration
- func ExtractStderr(err error) string
- func RecoverPanic(onPanic func(SafeGoResult)) func()
- func SafeGo(fn func(), onPanic func(SafeGoResult))
- func SafeGoWithContext(ctx context.Context, fn func(), onPanic func(SafeGoResult))
- func SpinWhile(message string, fn func() error) error
- func SpinWhileContext(ctx context.Context, message string, fn func() error) error
- type CommandError
- type EnvVar
- type EnvVars
- func (e *EnvVars) Add(key, value string, sensitive bool) error
- func (e *EnvVars) Clone() *EnvVars
- func (e *EnvVars) Get(key string) string
- func (e *EnvVars) Has(key string) bool
- func (e *EnvVars) Len() int
- func (e *EnvVars) Merge(other *EnvVars) *EnvVars
- func (e *EnvVars) MustAdd(key, value string, sensitive bool)
- func (e *EnvVars) RedactedSlice() []string
- func (e *EnvVars) ToMap() map[string]string
- func (e *EnvVars) ToSlice() []string
- type InteractivePrompter
- type MockPrompter
- type NonInteractivePrompter
- type ProgressIndicator
- type PrompterCall
- type RingBuffer
- func (r *RingBuffer[T]) Capacity() int
- func (r *RingBuffer[T]) Clear()
- func (r *RingBuffer[T]) Drain() []T
- func (r *RingBuffer[T]) DroppedCount() int64
- func (r *RingBuffer[T]) IsEmpty() bool
- func (r *RingBuffer[T]) IsFull() bool
- func (r *RingBuffer[T]) Peek() (T, bool)
- func (r *RingBuffer[T]) Pop() (T, bool)
- func (r *RingBuffer[T]) PopN(n int) []T
- func (r *RingBuffer[T]) Push(item T) bool
- func (r *RingBuffer[T]) Size() int
- func (r *RingBuffer[T]) ToSlice() []T
- type RingBufferable
- type SafeGoResult
- type Spinner
- type SpinnerConfig
- type TimeoutConfig
- type TimeoutValidator
- type UserPrompter
Constants ¶
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 ¶
var ErrCancelled = errors.New("operation cancelled by user")
ErrCancelled is returned when the user cancels a prompt.
var ErrInvalidEnvVarKey = fmt.Errorf("invalid environment variable key")
ErrInvalidEnvVarKey is returned when an environment variable key is invalid.
var ErrInvalidSelection = errors.New("invalid selection")
ErrInvalidSelection is returned when the user provides an invalid selection.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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) IsInteractive ¶
func (p *InteractivePrompter) IsInteractive() bool
IsInteractive returns true for InteractivePrompter.
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) IsInteractive ¶
func (m *MockPrompter) IsInteractive() bool
IsInteractive delegates to IsInteractiveFunc or returns true if nil.
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) IsInteractive ¶
func (p *NonInteractivePrompter) IsInteractive() bool
IsInteractive returns false for NonInteractivePrompter.
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 ¶
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 ¶
- Items are added at the tail position
- Items are removed from the head position
- When full, Push overwrites the oldest item
- 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 ¶
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 ¶
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 ¶
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 ¶
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