bullets

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Dec 1, 2025 License: MIT Imports: 11 Imported by: 0

README

Bullets

Go Report Card GitHub release linter coverage tests vulnerability-scan Release Build License

A colorful terminal logger for Go with bullet-style output, inspired by goreleaser's beautiful CLI output.

⚠️ Pre-v1.0 Notice: This library is currently in v0.x and under active development. The API may undergo breaking changes between minor versions until v1.0 is released. Please vendor your dependencies or pin to a specific version if you need API stability.

Features

  • 🎨 Colorful terminal output with ANSI colors
  • 🔘 Configurable bullet symbols (default circles, optional special symbols, custom icons)
  • 📊 Support for log levels (Debug, Info, Warn, Error, Fatal)
  • 📝 Structured logging with fields
  • ⏱️ Timing information for long-running operations
  • 🔄 Indentation/padding support for nested operations
  • ⏳ Animated spinners with multiple styles (Braille, Circle, Bounce)
  • 🔄 Updatable bullets - Update previously rendered bullets in real-time
  • 📊 Progress indicators - Show progress bars within bullets
  • 🎯 Batch operations - Update multiple bullets simultaneously
  • 🧵 Thread-safe operations
  • 🚀 Minimal dependencies (only golang.org/x/term for TTY detection)

Demo

demo

Installation

go get github.com/sgaunet/bullets

Quick Start

package main

import (
    "os"
    "github.com/sgaunet/bullets"
)

func main() {
    logger := bullets.New(os.Stdout)

    logger.Info("building")
    logger.IncreasePadding()
    logger.Info("binary=dist/app_linux_amd64")
    logger.Info("binary=dist/app_darwin_amd64")
    logger.DecreasePadding()

    logger.Success("build succeeded")
}

Usage

Basic Logging
logger := bullets.New(os.Stdout)

// By default, all levels use colored bullets (•)
logger.Debug("debug message")    // ○ debug message (dim)
logger.Info("info message")      // • info message (cyan)
logger.Warn("warning message")   // • warning message (yellow)
logger.Error("error message")    // • error message (red)
logger.Success("success!")       // • success! (green)
Formatted Messages
logger.Infof("processing %d items", count)
logger.Warnf("retry %d/%d", current, total)
Structured Logging
// Single field
logger.WithField("user", "john").Info("logged in")

// Multiple fields
logger.WithFields(map[string]interface{}{
    "version": "1.2.3",
    "arch":    "amd64",
}).Info("building package")

// Error field
err := errors.New("connection timeout")
logger.WithError(err).Error("upload failed")
Indentation
logger.Info("main task")
logger.IncreasePadding()
    logger.Info("subtask 1")
    logger.Info("subtask 2")
    logger.IncreasePadding()
        logger.Info("nested subtask")
    logger.DecreasePadding()
logger.DecreasePadding()
Step Function with Timing

The Step function is useful for tracking operations with automatic timing:

done := logger.Step("running tests")
// ... do work ...
done() // Automatically logs completion with duration if > 10s
Spinners

Animated spinners for long-running operations:

// Default Braille spinner (smooth dots)
spinner := logger.Spinner("downloading files")
time.Sleep(3 * time.Second)
spinner.Success("downloaded 10 files")

// Circle spinner (rotating circle)
spinner = logger.SpinnerCircle("connecting to database")
time.Sleep(2 * time.Second)
spinner.Error("connection failed")

// Bounce spinner (bouncing dots)
spinner = logger.SpinnerBounce("processing data")
time.Sleep(2 * time.Second)
spinner.Replace("processed 1000 records")

// Custom frames
spinner = logger.SpinnerWithFrames("compiling", []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"})
spinner.Stop() // or spinner.Success(), spinner.Error(), spinner.Replace()
Concurrent Spinners

Multiple spinners can run simultaneously with automatic coordination:

logger := bullets.New(os.Stdout)

// Start multiple operations in parallel
dbSpinner := logger.SpinnerCircle("Connecting to database")
apiSpinner := logger.SpinnerDots("Fetching API data")
fileSpinner := logger.SpinnerBounce("Processing files")

// Each spinner operates independently
go func() {
    time.Sleep(2 * time.Second)
    dbSpinner.Success("Database connected")
}()

go func() {
    time.Sleep(3 * time.Second)
    apiSpinner.Success("API data fetched")
}()

go func() {
    time.Sleep(1 * time.Second)
    fileSpinner.Error("File processing failed")
}()

time.Sleep(4 * time.Second)

Features:

  • Automatic Coordination: SpinnerCoordinator manages all active spinners
  • Thread-Safe: Safe to start/stop spinners from multiple goroutines
  • Smart Line Management: Each spinner gets its own line in TTY mode
  • No Timing Issues: Central animation loop prevents flickering/conflicts
  • Graceful Degradation: Falls back to simple logging in non-TTY environments

How It Works:

  • All spinners share a single coordinator instance
  • Coordinator uses a central ticker (80ms) for smooth animations
  • Channel-based communication ensures thread safety
  • Line numbers automatically recalculated when spinners complete
  • Set BULLETS_FORCE_TTY=1 for reliable TTY detection in go run

Important: Spinner Groups

When using spinners in groups, all spinners in a group must be completed (via Success(), Error(), Stop(), or Replace()) before creating new spinners or using other logger functions. This ensures proper line management and prevents visual artifacts:

// First group of spinners
s1 := logger.Spinner("Task 1")
s2 := logger.Spinner("Task 2")
// ... work ...
s1.Success("Task 1 done")
s2.Success("Task 2 done")  // Complete ALL spinners in the group

// Now safe to create a new group or use regular logging
logger.Info("Starting next phase")

// Second group of spinners
s3 := logger.Spinner("Task 3")
s4 := logger.Spinner("Task 4")
// ... work ...
s3.Success("Task 3 done")
s4.Success("Task 4 done")

The coordinator tracks spinner mode sessions - completing all spinners properly exits the session and resets line tracking for the next group.

Updatable Bullets

Create bullets that can be updated after rendering - perfect for showing progress, updating status, and creating dynamic terminal UIs.

⚠️ Important Terminal Requirements:

The updatable feature requires ANSI escape code support and proper TTY detection. If bullets are not updating in-place (appearing as new lines instead):

  1. Force TTY mode by setting an environment variable:

    export BULLETS_FORCE_TTY=1
    go run your-program.go
    
  2. Why this is needed:

    • go run often doesn't properly detect terminal capabilities
    • Some terminal emulators don't report as TTY correctly
    • IDE integrated terminals may not support ANSI codes
  3. Fallback behavior:

    • When TTY is not detected, updates print as new lines (safe fallback)
    • This ensures your program works in all environments (logs, CI/CD, etc.)
// Create an updatable logger
logger := bullets.NewUpdatable(os.Stdout)

// Create bullets that return handles
handle1 := logger.InfoHandle("Downloading package...")
handle2 := logger.InfoHandle("Installing dependencies...")
handle3 := logger.InfoHandle("Running tests...")

// Update them later
handle1.Success("Package downloaded ✓")
handle2.Error("Dependencies failed ✗")
handle3.Warning("Tests completed with warnings ⚠")

Progress indicators:

download := logger.InfoHandle("Downloading file...")

// Show progress (updates message with progress bar)
for i := 0; i <= 100; i += 10 {
    download.Progress(i, 100)
    time.Sleep(100 * time.Millisecond)
}
download.Success("Download complete!")

Batch operations:

// Group handles for batch updates
h1 := logger.InfoHandle("Service 1")
h2 := logger.InfoHandle("Service 2")
h3 := logger.InfoHandle("Service 3")

group := bullets.NewHandleGroup(h1, h2, h3)
group.SuccessAll("All services running")

// Or use chains
bullets.Chain(h1, h2, h3).
    WithField("status", "active").
    Success("All systems operational")

Adding fields dynamically:

handle := logger.InfoHandle("Building project")

// Add fields as the operation progresses
handle.WithField("version", "1.2.3")
handle.WithFields(map[string]interface{}{
    "arch": "amd64",
    "os": "linux",
})
Customizing Bullets
logger := bullets.New(os.Stdout)

// Enable special bullet symbols (✓, ✗, ⚠, ○)
logger.SetUseSpecialBullets(true)

// Set custom bullet for a specific level
logger.SetBullet(bullets.InfoLevel, "→")
logger.SetBullet(bullets.ErrorLevel, "💥")

// Set multiple custom bullets at once
logger.SetBullets(map[bullets.Level]string{
    bullets.WarnLevel:  "⚡",
    bullets.DebugLevel: "🔍",
})
Log Levels
logger := bullets.New(os.Stdout)
logger.SetLevel(bullets.WarnLevel) // Only warn, error, and fatal will be logged

logger.Debug("not shown")
logger.Info("not shown")
logger.Warn("this is shown")
logger.Error("this is shown")

Available levels:

  • DebugLevel
  • InfoLevel (default)
  • WarnLevel
  • ErrorLevel
  • FatalLevel

Example Output

Default (bullets only):

• building
  • binary=dist/app_linux_amd64
  • binary=dist/app_darwin_amd64
  • binary=dist/app_windows_amd64
• archiving
  • binary=app name=app_0.2.1_linux_amd64
  • binary=app name=app_0.2.1_darwin_amd64
• calculating checksums
• release succeeded

With special bullets enabled:

• building
  • binary=dist/app_linux_amd64
  • binary=dist/app_darwin_amd64
  • binary=dist/app_windows_amd64
• archiving
  • binary=app name=app_0.2.1_linux_amd64
  • binary=app name=app_0.2.1_darwin_amd64
• calculating checksums
✓ release succeeded

With spinners:

⠹ downloading files...    (animating)
• downloaded 10 files     (completed)

Running the Examples

Basic example:

cd examples/basic
go run main.go

Spinner example (including concurrent spinners):

# Recommended: Set environment variable for proper TTY detection
export BULLETS_FORCE_TTY=1
go run examples/spinner/main.go

Updatable bullets example:

# REQUIRED: Set this environment variable for the updates to work properly
export BULLETS_FORCE_TTY=1
go run examples/updatable/main.go

Note: The spinner and updatable features use ANSI escape codes to update lines in place. For best results:

  1. Run in a terminal that supports ANSI codes (most modern terminals)
  2. Set BULLETS_FORCE_TTY=1 environment variable
  3. Run directly in the terminal (not through pipes or output redirection)

These examples demonstrate:

  • basic: Simple logging with bullets and indentation
  • spinner: Animated spinners including concurrent multi-spinner usage
  • updatable: Status updates, progress tracking, batch operations, and parallel operations

API Reference

Logger Methods

Core logging:

  • Debug(msg), Debugf(format, args...)
  • Info(msg), Infof(format, args...)
  • Warn(msg), Warnf(format, args...)
  • Error(msg), Errorf(format, args...)
  • Fatal(msg), Fatalf(format, args...) - logs and exits
  • Success(msg), Successf(format, args...)

Spinners:

  • Spinner(msg) - Default Braille dots spinner
  • SpinnerDots(msg) - Braille dots (same as default)
  • SpinnerCircle(msg) - Rotating circle
  • SpinnerBounce(msg) - Bouncing dots
  • SpinnerWithFrames(msg, frames) - Custom animation

Spinner Control:

  • spinner.Stop() - Stop and clear
  • spinner.Success(msg) - Complete with success
  • spinner.Error(msg) / spinner.Fail(msg) - Complete with error
  • spinner.Replace(msg) - Complete with custom message

Configuration:

  • SetLevel(level), GetLevel()
  • SetUseSpecialBullets(bool) - Enable/disable special symbols
  • SetBullet(level, symbol) - Set custom bullet for a level
  • SetBullets(map[Level]string) - Set multiple custom bullets

Structured logging:

  • WithField(key, value) - Add single field
  • WithFields(map[string]interface{}) - Add multiple fields
  • WithError(err) - Add error field

Indentation:

  • IncreasePadding(), DecreasePadding(), ResetPadding()

Utilities:

  • Step(msg) - Returns cleanup function with timing
UpdatableLogger Methods

Create updatable logger:

  • NewUpdatable(w io.Writer) - Create new updatable logger

Create handle-returning bullets:

  • InfoHandle(msg string) *BulletHandle - Log info and return handle
  • DebugHandle(msg string) *BulletHandle - Log debug and return handle
  • WarnHandle(msg string) *BulletHandle - Log warning and return handle
  • ErrorHandle(msg string) *BulletHandle - Log error and return handle
BulletHandle Methods

Update operations:

  • Update(level Level, msg string) - Update level and message
  • UpdateMessage(msg string) - Update message only
  • UpdateLevel(level Level) - Update level only
  • UpdateColor(color string) - Update color only
  • UpdateBullet(bullet string) - Update bullet symbol only

State transitions:

  • Success(msg string) - Mark as success with message
  • Error(msg string) - Mark as error with message
  • Warning(msg string) - Mark as warning with message

Fields and metadata:

  • WithField(key, value) - Add a field
  • WithFields(fields) - Add multiple fields

Progress tracking:

  • Progress(current, total int) - Show progress bar

State management:

  • GetState() HandleState - Get current state
  • SetState(state HandleState) - Set complete state
HandleGroup Methods
  • NewHandleGroup(handles...) - Create handle group
  • Add(handle) - Add handle to group
  • UpdateAll(level, msg) - Update all handles
  • SuccessAll(msg) - Mark all as success
  • ErrorAll(msg) - Mark all as error
HandleChain Methods
  • Chain(handles...) - Create handle chain
  • Update(level, msg) - Chain update operation
  • Success(msg) - Chain success operation
  • Error(msg) - Chain error operation
  • WithField(key, value) - Chain field addition

Comparison with Other Loggers

This library is designed specifically for CLI applications that need beautiful, human-readable output. It's inspired by:

Unlike general-purpose loggers, bullets focuses on:

  • Visual appeal for terminal output
  • Animated spinners for long operations
  • Customizable bullet symbols
  • Simple API for CLI applications
  • Zero configuration needed
  • Minimal dependencies (only golang.org/x/term)

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Acknowledgments

Inspired by the beautiful CLI output of goreleaser and caarlos0/log.

Development

This project has been extensively developed with AI assistance, leveraging Claude Code and other AI tools for implementation, testing, documentation, and bug fixes. The AI-assisted development approach enabled rapid iteration on complex features like the spinner coordination system and comprehensive test coverage.

Documentation

Overview

Package bullets provides a colorful terminal logger with bullet-style output. Inspired by goreleaser's logging output.

Index

Constants

View Source
const (
	Reset = reset // Exported version

	// Exported color constants for public use.
	ColorBlack   = black
	ColorRed     = red
	ColorGreen   = green
	ColorYellow  = yellow
	ColorBlue    = blue
	ColorMagenta = magenta
	ColorCyan    = cyan
	ColorWhite   = white

	// Exported bright color constants.
	ColorBrightBlack   = brightBlack
	ColorBrightRed     = brightRed
	ColorBrightGreen   = brightGreen
	ColorBrightYellow  = brightYellow
	ColorBrightBlue    = brightBlue
	ColorBrightMagenta = brightMagenta
	ColorBrightCyan    = brightCyan
	ColorBrightWhite   = brightWhite

	// Exported style constants.
	StyleBold      = bold
	StyleDim       = dim
	StyleItalic    = italic
	StyleUnderline = underline
)

ANSI color codes for terminal output.

Variables

View Source
var ErrInvalidLevel = errors.New("invalid log level")

ErrInvalidLevel is returned when parsing an invalid level string.

Functions

func BatchUpdate

func BatchUpdate(_ []*BulletHandle, updates map[*BulletHandle]struct {
	Level   Level
	Message string
})

BatchUpdate allows updating multiple handles at once. Note: The handles parameter is currently unused but kept for API compatibility.

func Colorize

func Colorize(color, text string) string

Colorize wraps text in ANSI color codes (exported for public use).

Types

type ANSIEvent added in v0.3.0

type ANSIEvent struct {
	Timestamp time.Time
	Raw       string
	Type      ANSIEventType
	Value     int    // For move operations
	Text      string // For text content
}

ANSIEvent represents a parsed ANSI escape sequence or text event.

type ANSIEventType added in v0.3.0

type ANSIEventType string

ANSIEventType categorizes ANSI events.

const (
	EventMoveUp    ANSIEventType = "moveUp"
	EventMoveDown  ANSIEventType = "moveDown"
	EventClearLine ANSIEventType = "clearLine"
	EventMoveToCol ANSIEventType = "moveToCol"
	EventText      ANSIEventType = "text"
	EventNewline   ANSIEventType = "newline"
	EventUnknown   ANSIEventType = "unknown"
)

ANSI event type constants.

type AnimationFrame added in v0.3.0

type AnimationFrame struct {
	Timestamp     time.Time
	SpinnerStates []SpinnerState
	CursorPos     CursorPosition
}

AnimationFrame represents a complete animation frame with all spinner states.

type BulletHandle

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

BulletHandle represents a handle to an updatable bullet.

func (*BulletHandle) Error

func (h *BulletHandle) Error(msg string) *BulletHandle

Error updates the bullet to show an error.

func (*BulletHandle) GetState

func (h *BulletHandle) GetState() HandleState

GetState returns the current state of the handle.

func (*BulletHandle) Progress

func (h *BulletHandle) Progress(current, total int) *BulletHandle

Progress updates the bullet to show progress.

func (*BulletHandle) Pulse

func (h *BulletHandle) Pulse(duration time.Duration, alternateMsg string)

Pulse creates a pulsing effect by alternating between two states.

func (*BulletHandle) SetState

func (h *BulletHandle) SetState(state HandleState) *BulletHandle

SetState sets the complete state of the handle.

func (*BulletHandle) Success

func (h *BulletHandle) Success(msg string) *BulletHandle

Success updates the bullet to show success.

func (*BulletHandle) Update

func (h *BulletHandle) Update(level Level, msg string) *BulletHandle

Update updates the bullet with a new message and level.

func (*BulletHandle) UpdateBullet

func (h *BulletHandle) UpdateBullet(bullet string) *BulletHandle

UpdateBullet updates just the bullet symbol.

func (*BulletHandle) UpdateColor

func (h *BulletHandle) UpdateColor(color string) *BulletHandle

UpdateColor updates just the color of the bullet.

func (*BulletHandle) UpdateLevel

func (h *BulletHandle) UpdateLevel(level Level) *BulletHandle

UpdateLevel updates just the level (and thus color/bullet).

func (*BulletHandle) UpdateMessage

func (h *BulletHandle) UpdateMessage(msg string) *BulletHandle

UpdateMessage updates just the message text.

func (*BulletHandle) Warning

func (h *BulletHandle) Warning(msg string) *BulletHandle

Warning updates the bullet to show a warning.

func (*BulletHandle) WithField

func (h *BulletHandle) WithField(key string, value interface{}) *BulletHandle

WithField adds a field to this bullet.

func (*BulletHandle) WithFields

func (h *BulletHandle) WithFields(fields map[string]interface{}) *BulletHandle

WithFields adds multiple fields to this bullet.

type CursorPosition added in v0.3.0

type CursorPosition struct {
	Line   int
	Column int
}

CursorPosition tracks the current cursor position.

type HandleChain

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

HandleChain allows chaining updates to multiple handles.

func Chain

func Chain(handles ...*BulletHandle) *HandleChain

Chain creates a new handle chain.

func (*HandleChain) Error

func (hc *HandleChain) Error(msg string) *HandleChain

Error marks all handles in the chain as error.

func (*HandleChain) Success

func (hc *HandleChain) Success(msg string) *HandleChain

Success marks all handles in the chain as success.

func (*HandleChain) Update

func (hc *HandleChain) Update(level Level, msg string) *HandleChain

Update updates all handles in the chain.

func (*HandleChain) WithField

func (hc *HandleChain) WithField(key string, value interface{}) *HandleChain

WithField adds a field to all handles in the chain.

type HandleGroup

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

HandleGroup manages a group of related handles.

func NewHandleGroup

func NewHandleGroup(handles ...*BulletHandle) *HandleGroup

NewHandleGroup creates a new handle group.

func (*HandleGroup) Add

func (hg *HandleGroup) Add(handle *BulletHandle)

Add adds a handle to the group.

func (*HandleGroup) Clear

func (hg *HandleGroup) Clear()

Clear removes all handles from the group.

func (*HandleGroup) ErrorAll

func (hg *HandleGroup) ErrorAll(msg string)

ErrorAll marks all handles as error.

func (*HandleGroup) Get

func (hg *HandleGroup) Get(index int) *BulletHandle

Get returns the handle at the specified index.

func (*HandleGroup) Size

func (hg *HandleGroup) Size() int

Size returns the number of handles in the group.

func (*HandleGroup) SuccessAll

func (hg *HandleGroup) SuccessAll(msg string)

SuccessAll marks all handles as success.

func (*HandleGroup) UpdateAll

func (hg *HandleGroup) UpdateAll(level Level, msg string)

UpdateAll updates all handles in the group.

func (*HandleGroup) UpdateEach

func (hg *HandleGroup) UpdateEach(updates map[int]struct {
	Level   Level
	Message string
})

UpdateEach updates each handle with a different message.

type HandleState

type HandleState struct {
	Level   Level
	Message string
	Color   string
	Bullet  string
	Fields  map[string]interface{}
}

HandleState represents the state of a bullet handle.

type Level

type Level uint32

Level represents a log level.

const (
	// DebugLevel is the debug level.
	DebugLevel Level = iota
	// InfoLevel is the info level.
	InfoLevel
	// WarnLevel is the warn level.
	WarnLevel
	// ErrorLevel is the error level.
	ErrorLevel
	// FatalLevel is the fatal level.
	FatalLevel
)

func MustParseLevel

func MustParseLevel(s string) Level

MustParseLevel parses a level string or panics.

func ParseLevel

func ParseLevel(s string) (Level, error)

ParseLevel parses a level string into a Level.

func (Level) String

func (l Level) String() string

String returns the string representation of the level.

type Logger

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

Logger represents a logger with configurable level and output.

func Default

func Default() *Logger

Default returns a logger that writes to stderr.

func New

func New(w io.Writer) *Logger

New creates a new logger that writes to the given writer.

func (*Logger) Debug

func (l *Logger) Debug(msg string)

Debug logs a debug message.

func (*Logger) Debugf

func (l *Logger) Debugf(format string, args ...interface{})

Debugf logs a formatted debug message.

func (*Logger) DecreasePadding

func (l *Logger) DecreasePadding()

DecreasePadding decreases the indentation level.

func (*Logger) Error

func (l *Logger) Error(msg string)

Error logs an error message.

func (*Logger) Errorf

func (l *Logger) Errorf(format string, args ...interface{})

Errorf logs a formatted error message.

func (*Logger) Fatal

func (l *Logger) Fatal(msg string)

Fatal logs a fatal message and exits.

func (*Logger) Fatalf

func (l *Logger) Fatalf(format string, args ...interface{})

Fatalf logs a formatted fatal message and exits.

func (*Logger) GetLevel

func (l *Logger) GetLevel() Level

GetLevel returns the current log level.

func (*Logger) IncreasePadding

func (l *Logger) IncreasePadding()

IncreasePadding increases the indentation level.

func (*Logger) Info

func (l *Logger) Info(msg string)

Info logs an info message.

func (*Logger) Infof

func (l *Logger) Infof(format string, args ...interface{})

Infof logs a formatted info message.

func (*Logger) Ln added in v0.3.0

func (l *Logger) Ln()

Ln prints a blank line without indentation. This method always outputs regardless of the log level.

func (*Logger) ResetPadding

func (l *Logger) ResetPadding()

ResetPadding resets the indentation to zero.

func (*Logger) SetBullet

func (l *Logger) SetBullet(level Level, bullet string)

SetBullet sets a custom bullet symbol for a specific log level. Custom bullets take priority over special bullets.

func (*Logger) SetBullets

func (l *Logger) SetBullets(bullets map[Level]string)

SetBullets sets custom bullet symbols for multiple log levels. Custom bullets take priority over special bullets.

func (*Logger) SetLevel

func (l *Logger) SetLevel(level Level)

SetLevel sets the minimum log level.

func (*Logger) SetUseSpecialBullets

func (l *Logger) SetUseSpecialBullets(use bool)

SetUseSpecialBullets enables or disables special bullet symbols (✓, ✗, ⚠). When disabled (default), all levels use the circle bullet (●) with level-specific colors.

func (*Logger) Spinner

func (l *Logger) Spinner(msg string) *Spinner

Spinner creates and starts a spinner with default Braille dots animation.

Multiple spinners can run concurrently with automatic coordination. The spinner animates until stopped with Stop(), Success(), Error(), or Replace().

Example:

spinner := logger.Spinner("Processing data")
// ... do work ...
spinner.Success("Processing complete")

In TTY mode, the spinner animates in-place. In non-TTY mode (logs, CI/CD), it displays as a static message.

Thread-safe: Multiple spinners can be created from different goroutines.

func (*Logger) SpinnerBounce

func (l *Logger) SpinnerBounce(msg string) *Spinner

SpinnerBounce creates a spinner with bouncing dot pattern.

Creates a smooth bouncing animation effect using Braille dots that appear to bounce vertically. The animation uses the default speed.

Thread-safe: Multiple spinners can be created from different goroutines.

func (*Logger) SpinnerCircle

func (l *Logger) SpinnerCircle(msg string) *Spinner

SpinnerCircle creates a spinner with growing/shrinking circle pattern.

Creates a glassy circular rotation effect using quarter-circle characters. The animation is slower than the default Braille dots for a more relaxed feel.

Thread-safe: Multiple spinners can be created from different goroutines.

func (*Logger) SpinnerDots

func (l *Logger) SpinnerDots(msg string) *Spinner

SpinnerDots creates a spinner with rotating Braille dots pattern.

This is the default spinner style with smooth dot transitions. Identical to Spinner().

Thread-safe: Multiple spinners can be created from different goroutines.

func (*Logger) SpinnerWithFrames

func (l *Logger) SpinnerWithFrames(msg string, frames []string) *Spinner

SpinnerWithFrames creates and starts a spinner with custom animation frames.

Frames will cycle through the provided slice of strings. If frames is empty, defaults to the standard Braille dots pattern.

Example:

frames := []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}
spinner := logger.SpinnerWithFrames("Compiling", frames)
// ... do work ...
spinner.Success("Compilation complete")

Thread-safe: Multiple spinners can be created from different goroutines.

func (*Logger) Step

func (l *Logger) Step(msg string) func()

Step logs a step message with timing information. It returns a function that should be called when the step is complete.

func (*Logger) Success

func (l *Logger) Success(msg string)

Success logs a success message (using info level with success bullet).

func (*Logger) Successf

func (l *Logger) Successf(format string, args ...interface{})

Successf logs a formatted success message.

func (*Logger) Warn

func (l *Logger) Warn(msg string)

Warn logs a warning message.

func (*Logger) Warnf

func (l *Logger) Warnf(format string, args ...interface{})

Warnf logs a formatted warning message.

func (*Logger) WithError

func (l *Logger) WithError(err error) *Logger

WithError returns a new logger with an error field.

func (*Logger) WithField

func (l *Logger) WithField(key string, value interface{}) *Logger

WithField returns a new logger with the given field added.

func (*Logger) WithFields

func (l *Logger) WithFields(fields map[string]interface{}) *Logger

WithFields returns a new logger with the given fields added.

type Spinner

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

Spinner represents an animated spinner that can be stopped and replaced.

Spinners provide visual feedback for long-running operations. Multiple spinners can run concurrently with automatic coordination via SpinnerCoordinator.

In TTY mode, spinners animate in-place using ANSI escape codes. In non-TTY mode, they display as static messages for compatibility with logs and CI/CD systems.

Example usage:

logger := bullets.New(os.Stdout)
spinner := logger.SpinnerCircle("Processing data")
// ... do work ...
spinner.Success("Processing complete")

Thread Safety:

All spinner methods are thread-safe and can be called from multiple goroutines. The coordinator ensures proper serialization of terminal updates.

func (*Spinner) Error

func (s *Spinner) Error(msg string)

Error stops the spinner and replaces it with an error message.

The completion message is displayed with an error bullet (red color). The bullet symbol respects custom bullets and special bullet settings.

In TTY mode, the spinner line is overwritten with the final message. In non-TTY mode, a new line is printed with the completion message.

Example:

spinner := logger.SpinnerCircle("Connecting")
// ... do work ...
spinner.Error("Connection failed: timeout")

Thread-safe: Can be called from any goroutine.

func (*Spinner) Fail

func (s *Spinner) Fail(msg string)

Fail is an alias for Error.

This method behaves identically to Error() and is provided for convenience. Thread-safe: Can be called from any goroutine.

func (*Spinner) Replace

func (s *Spinner) Replace(msg string)

Replace stops the spinner and replaces it with a custom message at info level.

The completion message is displayed with an info bullet (cyan color). The bullet symbol respects custom bullets and special bullet settings.

Use Replace when the operation completes but you want to show a custom message that doesn't imply success or failure.

Example:

spinner := logger.SpinnerCircle("Processing")
// ... do work ...
spinner.Replace("Processed 1000 records in 5.2s")

Thread-safe: Can be called from any goroutine.

func (*Spinner) Stop

func (s *Spinner) Stop()

Stop stops the spinner and clears the line without displaying a completion message.

This method immediately halts the animation and unregisters the spinner from the coordinator. The spinner cannot be reused after calling Stop().

Thread-safe: Can be called from any goroutine.

func (*Spinner) Success

func (s *Spinner) Success(msg string)

Success stops the spinner and replaces it with a success message.

The completion message is displayed with a success bullet (green color). The bullet symbol respects custom bullets and special bullet settings.

In TTY mode, the spinner line is overwritten with the final message. In non-TTY mode, a new line is printed with the completion message.

Example:

spinner := logger.SpinnerCircle("Connecting")
// ... do work ...
spinner.Success("Connected successfully")

Thread-safe: Can be called from any goroutine.

func (*Spinner) UpdateText added in v0.4.0

func (s *Spinner) UpdateText(msg string)

UpdateText updates the spinner's message text while keeping it running.

The new message will be displayed on the next animation frame. In TTY mode, the spinner line is updated in-place. In non-TTY mode, the update is silently ignored (no additional output).

Calling UpdateText() on a stopped spinner is safe (idempotent). Multiple calls to UpdateText() simply override the message with the latest value.

Example:

spinner := logger.Spinner("Processing items")
for i := 1; i <= 100; i++ {
    spinner.UpdateText(fmt.Sprintf("Processing items (%d/100)", i))
    time.Sleep(10 * time.Millisecond)
}
spinner.Success("All items processed")

Thread-safe: Can be called from any goroutine.

type SpinnerCoordinator added in v0.2.0

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

SpinnerCoordinator manages all spinner instances and coordinates their output.

The coordinator implements a centralized pattern where a single goroutine handles all spinner animations and updates. This eliminates timing issues and ensures smooth, flicker-free animations even with multiple concurrent spinners.

Architecture:

  • Central ticker goroutine updates all active spinners (80ms interval)
  • Channel-based communication for thread-safe spinner updates
  • Automatic line number allocation and recalculation
  • Unified TTY detection for consistent behavior

The coordinator is automatically created by Logger and managed internally. Users don't need to interact with it directly.

Thread Safety:

All coordinator methods are thread-safe and can be called from multiple goroutines. Internal state is protected by mutexes and channel synchronization.

type SpinnerState added in v0.3.0

type SpinnerState struct {
	Line    int
	Content string
}

SpinnerState represents the state of a single spinner at a point in time.

type SpinnerTestCapture added in v0.3.0

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

SpinnerTestCapture extends ansiCapture with comprehensive testing utilities.

func NewSpinnerTestCapture added in v0.3.0

func NewSpinnerTestCapture() *SpinnerTestCapture

NewSpinnerTestCapture creates a new test capture utility.

func (*SpinnerTestCapture) CountEventType added in v0.3.0

func (s *SpinnerTestCapture) CountEventType(eventType ANSIEventType) int

CountEventType counts occurrences of a specific event type.

func (*SpinnerTestCapture) DumpEvents added in v0.3.0

func (s *SpinnerTestCapture) DumpEvents(t *testing.T, maxEvents int)

DumpEvents prints all captured events for debugging.

func (*SpinnerTestCapture) ExtractFrames added in v0.3.0

func (s *SpinnerTestCapture) ExtractFrames() []AnimationFrame

ExtractFrames analyzes events to identify distinct animation frames.

func (*SpinnerTestCapture) GetCursorHistory added in v0.3.0

func (s *SpinnerTestCapture) GetCursorHistory() []CursorPosition

GetCursorHistory returns the complete cursor movement history.

func (*SpinnerTestCapture) GetEvents added in v0.3.0

func (s *SpinnerTestCapture) GetEvents() []ANSIEvent

GetEvents returns a thread-safe copy of captured events.

func (*SpinnerTestCapture) GetMoveDownValues added in v0.3.0

func (s *SpinnerTestCapture) GetMoveDownValues() []int

GetMoveDownValues extracts all moveDown values for pattern analysis.

func (*SpinnerTestCapture) GetMoveUpValues added in v0.3.0

func (s *SpinnerTestCapture) GetMoveUpValues() []int

GetMoveUpValues extracts all moveUp values for pattern analysis.

func (*SpinnerTestCapture) GetRawOutput added in v0.3.0

func (s *SpinnerTestCapture) GetRawOutput() string

GetRawOutput returns the complete raw output.

func (*SpinnerTestCapture) ValidateCursorStability added in v0.3.0

func (s *SpinnerTestCapture) ValidateCursorStability(t *testing.T) bool

ValidateCursorStability checks that cursor movements are consistent.

func (*SpinnerTestCapture) ValidateNoBlankLines added in v0.3.0

func (s *SpinnerTestCapture) ValidateNoBlankLines(t *testing.T) bool

ValidateNoBlankLines checks that no extra blank lines exist in output.

func (*SpinnerTestCapture) ValidateNoPositionDrift added in v0.3.0

func (s *SpinnerTestCapture) ValidateNoPositionDrift(t *testing.T) bool

ValidateNoPositionDrift checks that line positions remain stable over time.

func (*SpinnerTestCapture) Write added in v0.3.0

func (s *SpinnerTestCapture) Write(p []byte) (int, error)

Write implements io.Writer and captures output.

type UpdatableLogger

type UpdatableLogger struct {
	*Logger
	// contains filtered or unexported fields
}

UpdatableLogger wraps a regular Logger and provides updatable bullet functionality.

func NewUpdatable

func NewUpdatable(w io.Writer) *UpdatableLogger

NewUpdatable creates a new updatable logger.

func (*UpdatableLogger) Debug

func (ul *UpdatableLogger) Debug(msg string)

Debug logs a debug message and increments line count.

func (*UpdatableLogger) DebugHandle

func (ul *UpdatableLogger) DebugHandle(msg string) *BulletHandle

DebugHandle logs a debug message and returns a handle for updates.

func (*UpdatableLogger) Error

func (ul *UpdatableLogger) Error(msg string)

Error logs an error message and increments line count.

func (*UpdatableLogger) ErrorHandle

func (ul *UpdatableLogger) ErrorHandle(msg string) *BulletHandle

ErrorHandle logs an error message and returns a handle for updates.

func (*UpdatableLogger) IncrementLineCount

func (ul *UpdatableLogger) IncrementLineCount()

IncrementLineCount increments the line count (called by regular log methods).

func (*UpdatableLogger) Info

func (ul *UpdatableLogger) Info(msg string)

Info logs an info message and increments line count.

func (*UpdatableLogger) InfoHandle

func (ul *UpdatableLogger) InfoHandle(msg string) *BulletHandle

InfoHandle logs an info message and returns a handle for updates.

func (*UpdatableLogger) Success

func (ul *UpdatableLogger) Success(msg string)

Success logs a success message and increments line count.

func (*UpdatableLogger) Warn

func (ul *UpdatableLogger) Warn(msg string)

Warn logs a warning message and increments line count.

func (*UpdatableLogger) WarnHandle

func (ul *UpdatableLogger) WarnHandle(msg string) *BulletHandle

WarnHandle logs a warning message and returns a handle for updates.

Directories

Path Synopsis
examples
basic command
Package main demonstrates the basic usage of the bullets logger.
Package main demonstrates the basic usage of the bullets logger.
spinner command
Package main demonstrates concurrent spinner usage with the bullets logger.
Package main demonstrates concurrent spinner usage with the bullets logger.
spinner2 command
Package main demonstrates the basic usage of the bullets logger.
Package main demonstrates the basic usage of the bullets logger.
spinner_stress command
Package main demonstrates high-concurrency spinner usage with the bullets logger.
Package main demonstrates high-concurrency spinner usage with the bullets logger.
updatable command
Package main demonstrates updatable bullet functionality with real-time updates.
Package main demonstrates updatable bullet functionality with real-time updates.

Jump to

Keyboard shortcuts

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