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.
- 🎨 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)
go get github.com/sgaunet/bulletspackage 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")
}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)logger.Infof("processing %d items", count)
logger.Warnf("retry %d/%d", current, total)// 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")logger.Info("main task")
logger.IncreasePadding()
logger.Info("subtask 1")
logger.Info("subtask 2")
logger.IncreasePadding()
logger.Info("nested subtask")
logger.DecreasePadding()
logger.DecreasePadding()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 > 10sAnimated 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()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=1for reliable TTY detection ingo 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.
Create bullets that can be updated after rendering - perfect for showing progress, updating status, and creating dynamic terminal UIs.
The updatable feature requires ANSI escape code support and proper TTY detection. If bullets are not updating in-place (appearing as new lines instead):
-
Force TTY mode by setting an environment variable:
export BULLETS_FORCE_TTY=1 go run your-program.go -
Why this is needed:
go runoften doesn't properly detect terminal capabilities- Some terminal emulators don't report as TTY correctly
- IDE integrated terminals may not support ANSI codes
-
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",
})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: "🔍",
})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:
DebugLevelInfoLevel(default)WarnLevelErrorLevelFatalLevel
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)
Basic example:
cd examples/basic
go run main.goSpinner example (including concurrent spinners):
# Recommended: Set environment variable for proper TTY detection
export BULLETS_FORCE_TTY=1
go run examples/spinner/main.goUpdatable bullets example:
# REQUIRED: Set this environment variable for the updates to work properly
export BULLETS_FORCE_TTY=1
go run examples/updatable/main.goNote: The spinner and updatable features use ANSI escape codes to update lines in place. For best results:
- Run in a terminal that supports ANSI codes (most modern terminals)
- Set
BULLETS_FORCE_TTY=1environment variable - 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
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 exitsSuccess(msg),Successf(format, args...)
Spinners:
Spinner(msg)- Default Braille dots spinnerSpinnerDots(msg)- Braille dots (same as default)SpinnerCircle(msg)- Rotating circleSpinnerBounce(msg)- Bouncing dotsSpinnerWithFrames(msg, frames)- Custom animation
Spinner Control:
spinner.Stop()- Stop and clearspinner.Success(msg)- Complete with successspinner.Error(msg)/spinner.Fail(msg)- Complete with errorspinner.Replace(msg)- Complete with custom message
Configuration:
SetLevel(level),GetLevel()SetUseSpecialBullets(bool)- Enable/disable special symbolsSetBullet(level, symbol)- Set custom bullet for a levelSetBullets(map[Level]string)- Set multiple custom bullets
Structured logging:
WithField(key, value)- Add single fieldWithFields(map[string]interface{})- Add multiple fieldsWithError(err)- Add error field
Indentation:
IncreasePadding(),DecreasePadding(),ResetPadding()
Utilities:
Step(msg)- Returns cleanup function with timing
Create updatable logger:
NewUpdatable(w io.Writer)- Create new updatable logger
Create handle-returning bullets:
InfoHandle(msg string) *BulletHandle- Log info and return handleDebugHandle(msg string) *BulletHandle- Log debug and return handleWarnHandle(msg string) *BulletHandle- Log warning and return handleErrorHandle(msg string) *BulletHandle- Log error and return handle
Update operations:
Update(level Level, msg string)- Update level and messageUpdateMessage(msg string)- Update message onlyUpdateLevel(level Level)- Update level onlyUpdateColor(color string)- Update color onlyUpdateBullet(bullet string)- Update bullet symbol only
State transitions:
Success(msg string)- Mark as success with messageError(msg string)- Mark as error with messageWarning(msg string)- Mark as warning with message
Fields and metadata:
WithField(key, value)- Add a fieldWithFields(fields)- Add multiple fields
Progress tracking:
Progress(current, total int)- Show progress bar
State management:
GetState() HandleState- Get current stateSetState(state HandleState)- Set complete state
NewHandleGroup(handles...)- Create handle groupAdd(handle)- Add handle to groupUpdateAll(level, msg)- Update all handlesSuccessAll(msg)- Mark all as successErrorAll(msg)- Mark all as error
Chain(handles...)- Create handle chainUpdate(level, msg)- Chain update operationSuccess(msg)- Chain success operationError(msg)- Chain error operationWithField(key, value)- Chain field addition
This library is designed specifically for CLI applications that need beautiful, human-readable output. It's inspired by:
- caarlos0/log - The logger used by goreleaser
- apex/log - The original apex logger
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)
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
Inspired by the beautiful CLI output of goreleaser and caarlos0/log.
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.
