Beautiful, interactive command-line prompts for Go β A Go port inspired by the TypeScript Clack library.
Building CLI applications shouldn't require wrestling with terminal complexities. Tap provides elegant, type-safe prompts with beautiful Unicode styling, letting you focus on your application logic instead of terminal management.
- π― Type-safe prompts with Go generics for strongly-typed selections
- π¨ Beautiful styling with consistent Unicode symbols and colors
- β‘ Zero-config terminal management with automatic cleanup
- π§ͺ Testing utilities with built-in mocks for reliable testing
- π¦ Minimal dependencies β only essential terminal libraries
- Text Input β Single-line input with validation, placeholders, and defaults
- Autocomplete β Text input with inline, navigable suggestions (Tab to accept)
- Password Input β Masked input for sensitive data
- Confirm β Yes/No prompts with customizable labels
- Select β Single selection from typed options with hints
- MultiSelect β Multiple selection with checkboxes
- Spinner β Loading indicators with dots, timer, or custom frames
- Progress Bar β Animated progress indicators (light, heavy, or block styles)
- Stream β Real-time output with a start/write/stop lifecycle
- Messages β Intro, outro, and styled message boxes
- Box β A flexible, styled box for surrounding content
- Table β A component for displaying data in a tabular format
- Context Support β All interactive prompts support context cancellation and timeouts
go get github.com/noojuno/tap@latest- Go 1.20+
- A TTY-capable terminal (ANSI escape sequences); Linux/macOS and modern Windows terminals supported
package main
import (
"context"
"fmt"
"github.com/noojuno/tap"
)
func main() {
ctx := context.Background()
tap.Intro("Welcome! π")
name := tap.Text(ctx, tap.TextOptions{
Message: "What's your name?",
Placeholder: "Enter your name...",
})
confirmed := tap.Confirm(ctx, tap.ConfirmOptions{
Message: fmt.Sprintf("Hello %s! Continue?", name),
})
if confirmed {
tap.Outro("Let's go! π")
}
}- Navigate: Arrow keys or
h/j/k/l - Submit:
Enter - Cancel:
Ctrl+CorEsc - Toggle (MultiSelect):
Space - Accept suggestion (Autocomplete):
Tab
email := tap.Text(ctx, tap.TextOptions{
Message: "Enter your email:",
Placeholder: "[email protected]",
DefaultValue: "[email protected]",
Validate: func(input string) error {
if !strings.Contains(input, "@") {
return errors.New("Please enter a valid email")
}
return nil
},
})// Define a simple suggest function
suggest := func(input string) []string {
all := []string{"Go", "Golang", "Python", "Rust", "Java"}
if input == "" { return all }
low := strings.ToLower(input)
var out []string
for _, s := range all {
if strings.Contains(strings.ToLower(s), low) {
out = append(out, s)
}
}
return out
}
lang := tap.Autocomplete(ctx, tap.AutocompleteOptions{
Message: "Search language:",
Placeholder: "Start typing...",
Suggest: suggest,
MaxResults: 6,
})password := tap.Password(ctx, tap.PasswordOptions{
Message: "Enter a new password:",
Validate: func(input string) error {
if len(input) < 8 {
return errors.New("Password must be at least 8 characters long")
}
return nil
},
})type Environment string
environments := []tap.SelectOption[Environment]{
{Value: "dev", Label: "Development", Hint: "Local development"},
{Value: "staging", Label: "Staging", Hint: "Pre-production testing"},
{Value: "production", Label: "Production", Hint: "Live environment"},
}
env := tap.Select(ctx, tap.SelectOptions[Environment]{
Message: "Choose deployment target:",
Options: environments,
})
// env is strongly typed as Environmentlanguages := []tap.SelectOption[string]{
{Value: "go", Label: "Go"},
{Value: "python", Label: "Python"},
{Value: "javascript", Label: "JavaScript"},
}
selected := tap.MultiSelect(ctx, tap.MultiSelectOptions[string]{
Message: "Which languages do you use?",
Options: languages,
})
fmt.Printf("You selected: %v
", selected)// Spinner
spinner := tap.NewSpinner(tap.SpinnerOptions{})
spinner.Start("Loading...")
// ... do work ...
spinner.Stop("Done!", 0)
// Progress Bar
progress := tap.NewProgress(tap.ProgressOptions{
Style: "heavy", // "light", "heavy", or "block"
Max: 100,
Size: 40,
})
progress.Start("Processing...")
for i := 0; i <= 100; i += 10 {
time.Sleep(200 * time.Millisecond)
progress.Advance(10, fmt.Sprintf("Step %d/10", i/10+1))
}
progress.Stop("Complete!", 0)stream := tap.NewStream(tap.StreamOptions{ShowTimer: true})
stream.Start("Streaming output...")
stream.WriteLine("First line of output.")
time.Sleep(500 * time.Millisecond)
stream.WriteLine("Second line of output.")
stream.Stop("Stream finished.", 0)headers := []string{"Field", "Value"}
rows := [][]string{
{"Name", "Alice"},
{"Languages", "Go, Python"},
}
tap.Table(headers, rows, tap.TableOptions{
ShowBorders: true,
IncludePrefix: true,
HeaderStyle: tap.TableStyleBold,
HeaderColor: tap.TableColorCyan,
})tap.Intro("Welcome! π")
tap.Message("Here's what's next:")
tap.Outro("Let's go! π")Produces:
β Welcome! π
β
β
β Here's what's next:
β
β Let's go! π
// Message box with custom styling
tap.Box("This is important information!", "β οΈ Warning", tap.BoxOptions{
Rounded: true,
FormatBorder: tap.CyanBorder, // also GrayBorder, GreenBorder, YellowBorder, RedBorder
TitleAlign: tap.BoxAlignCenter,
ContentAlign: tap.BoxAlignCenter,
})- Always call
StoponSpinner/Progress/Streamto restore the terminal state. - For
Select[T]/MultiSelect[T], you can omit the type parameter if it can be inferred from the options' type.
All interactive prompts support Go's context package for cancellation and timeouts:
// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
name := tap.Text(ctx, tap.TextOptions{
Message: "Enter your name (30s timeout):",
})
// With cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(5*time.Second)
cancel() // Cancel after 5 seconds
}()
result := tap.Confirm(ctx, tap.ConfirmOptions{
Message: "Quick decision needed:",
})
// On cancel or timeout, helpers return zero values
// (e.g., Text/Password β "", Confirm β false, Select[T] β zero T).Tap emits OSC 9;4 control sequences to signal progress/spinner state to compatible terminals. Unsupported terminals ignore these sequences (no-op), so visuals remain unchanged.
Whatβs emitted automatically:
- Spinner:
- Start β indeterminate:
ESC ] 9 ; 4 ; 3 ST - Stop β always clear:
ESC ] 9 ; 4 ; 0 ST
- Start β indeterminate:
- Progress:
- On render when percent changes β
ESC ] 9 ; 4 ; 1 ; <PCT> ST - Stop β always clear:
ESC ] 9 ; 4 ; 0 ST
- On render when percent changes β
Notes:
- Terminator: Tap uses ST (
ESC \) for robustness. Some terminals also accept BEL (οΏ½). - Throttling: Progress only emits a new percentage when it changes to avoid spam.
- Multiplexers: tmux/screen may swallow OSC sequences unless configured to passthrough.
Tap includes comprehensive testing utilities. Override terminal I/O in tests:
func TestYourPrompt(t *testing.T) {
// Create mock I/O
mockInput := tap.NewMockReadable()
mockOutput := tap.NewMockWritable()
// Override terminal I/O for testing
tap.SetTermIO(mockInput, mockOutput)
defer tap.SetTermIO(nil, nil)
// Simulate user input
go func() {
mockInput.EmitKeypress("test", tap.Key{Name: "t"})
mockInput.EmitKeypress("", tap.Key{Name: "return"})
}()
result := tap.Text(ctx, tap.TextOptions{Message: "Enter text:"})
assert.Equal(t, "test", result)
}Alternatively, pass I/O per call by setting Input and Output on options like TextOptions, ConfirmOptions, etc. This avoids using the global override when you need finer control.
Run tests:
go test ./...
go test -race ./... # with race detectionExplore working examples in the examples/ directory. Each example is self-contained and can be run directly.
# Basic prompts
go run examples/autocomplete/main.go
go run examples/text/main.go
go run examples/password/main.go
go run examples/confirm/main.go
go run examples/select/main.go
go run examples/multiselect/main.go
# Long-running tasks
go run examples/spinner/main.go
go run examples/progress/main.go
go run examples/stream/main.go
# Output and formatting
go run examples/table/main.go
# Complete workflow
go run examples/multiple/main.goTap uses an event-driven architecture with atomic state management for race-condition-free operation. The library automatically handles:
- Terminal raw mode setup/cleanup
- Keyboard input processing
- Cursor positioning and output formatting
- Cross-platform compatibility
The main package provides a clean API while internal packages handle terminal complexity.
Tap API is stable and production-ready. The library follows semantic versioning and maintains backward compatibility.
Contributions welcome! Please:
- Follow Go best practices and maintain test coverage
- Include examples for new features
- Update documentation as needed
MIT License - see LICENSE file for details.
- Clack β The original TypeScript library that inspired this project
- @eiannone/keyboard β Cross-platform keyboard input
- The Go community for excellent tooling and feedback
Built with β€οΈ for developers who value simplicity and speed.