tap

package module
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2025 License: MIT Imports: 15 Imported by: 4

README

Tap

Beautiful, interactive command-line prompts for Go — A Go port inspired by the TypeScript Clack library.

Tap Demo

Why Tap?

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.

Features

  • 🎯 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
Available Components
  • Text Input — Single-line input with validation, placeholders, and defaults
  • 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
  • Context Support — All interactive prompts support context cancellation and timeouts
  • Progress Bar — Animated progress indicators (light, heavy, block styles)
  • Spinner — Loading indicators with dots, timer, or custom frames
  • Stream — Real-time output with start/write/stop lifecycle
  • Messages — Intro, outro, and styled message boxes

Installation

go get github.com/yarlson/tap@latest

Quick Start

package main

import (
    "context"
    "fmt"
    "github.com/yarlson/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! 🎉")
    }
}

API Examples

Text Input with Validation
email := tap.Text(ctx, tap.TextOptions{
    Message:      "Enter your email:",
    Placeholder:  "user@example.com",
    DefaultValue: "anonymous@example.com",
    Validate: func(input string) error {
        if !strings.Contains(input, "@") {
            return errors.New("Please enter a valid email")
        }
        return nil
    },
})
Type-Safe Selection
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 Environment
Progress Indicators
// 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)

// Spinner
spinner := tap.NewSpinner(tap.SpinnerOptions{})
spinner.Start("Loading...")
// ... do work ...
spinner.Stop("Done!", 0)

OSC 9;4 Integration (Terminal Progress)

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
  • Progress:
    • On render when percent changes → ESC ] 9 ; 4 ; 1 ; <PCT> ST
    • Stop → always clear: ESC ] 9 ; 4 ; 0 ST

Notes:

  • Terminator: Tap uses ST (ESC \\) for robustness. Some terminals also accept BEL (\a).
  • Throttling: Progress only emits a new percentage when it changes to avoid spam.
  • Multiplexers: tmux/screen may swallow OSC sequences unless configured to passthrough.
Multiple Selection
languages := []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\n", selected)
Context Support and Cancellation

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:",
})
Styled Messages
// Message box with custom styling
tap.Box("This is important information!", "⚠️ Warning", tap.BoxOptions{
    Rounded:       true,
    FormatBorder:  tap.CyanBorder,
    TitleAlign:    tap.BoxAlignCenter,
    ContentAlign:  tap.BoxAlignCenter,
})

Testing

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)
}

Run tests:

go test ./...
go test -race ./...  # with race detection

Examples

Explore working examples in the examples/ directory:

go run examples/text/main.go      # Text input
go run examples/select/main.go    # Selection menus
go run examples/progress/main.go  # Progress bars
go run examples/multiple/main.go  # Complete workflow

Architecture

Tap 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.

Status

Tap API is stable and production-ready. The library follows semantic versioning and maintains backward compatibility.

Contributing

Contributions welcome! Please:

  • Follow Go best practices and maintain test coverage
  • Include examples for new features
  • Update documentation as needed

License

MIT License - see LICENSE file for details.

Acknowledgments

  • 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.

Documentation

Index

Constants

View Source
const (
	// Step symbols
	StepActive = "◆"
	StepCancel = "■"
	StepError  = "▲"
	StepSubmit = "◇"

	// Bar symbols
	Bar           = "│"
	BarH          = "─"
	BarStart      = "┌"
	BarStartRight = "┐"
	BarEnd        = "└"
	BarEndRight   = "┘"

	// Corner symbols (rounded)
	CornerTopLeft     = "╭"
	CornerTopRight    = "╮"
	CornerBottomLeft  = "╰"
	CornerBottomRight = "╯"

	// Radio symbols
	RadioActive   = "●"
	RadioInactive = "○"

	// Checkbox symbols for multiselect
	CheckboxChecked   = "◼"
	CheckboxUnchecked = "◻"
)

Unicode symbols for drawing styled prompts

View Source
const (
	Reset = "\033[0m"

	// Colors
	Gray   = "\033[90m"
	Red    = "\033[91m"
	Green  = "\033[92m"
	Yellow = "\033[93m"
	Cyan   = "\033[96m"

	// Text styles
	Dim           = "\033[2m"
	Inverse       = "\033[7m"
	Strikethrough = "\033[9m"
)

ANSI color codes

View Source
const (
	CursorHide = "\x1b[?25l"
	CursorShow = "\x1b[?25h"
	EraseLine  = "\x1b[K"
	CursorUp   = "\x1b[A"
	EraseDown  = "\x1b[J"
)

Variables

This section is empty.

Functions

func Box

func Box(message string, title string, opts BoxOptions)

Box renders a framed message with optional title.

func Cancel

func Cancel(message string, opts ...MessageOptions)

Cancel prints a cancel-styled message (bar end + red message).

func Confirm

func Confirm(ctx context.Context, opts ConfirmOptions) bool

Confirm creates a styled confirm prompt

func CyanBorder

func CyanBorder(s string) string

func GrayBorder

func GrayBorder(s string) string

func Intro

func Intro(title string, opts ...MessageOptions)

Intro prints an intro title (bar start + title).

func MultiSelect added in v0.5.0

func MultiSelect[T any](ctx context.Context, opts MultiSelectOptions[T]) []T

MultiSelect renders a styled multi-select and returns selected values.

func Outro

func Outro(message string, opts ...MessageOptions)

Outro prints a final outro (bar line, then bar end + message).

func Password

func Password(ctx context.Context, opts PasswordOptions) string

Password creates a styled password input prompt that masks user input

func Select

func Select[T any](ctx context.Context, opts SelectOptions[T]) T

Select creates a styled select prompt

func SetTermIO added in v0.4.0

func SetTermIO(in Reader, out Writer)

SetTermIO sets a custom reader and writer used by helpers. Pass nil values to restore default terminal behavior.

func Symbol added in v0.7.0

func Symbol(state ClackState) string

Symbol returns the appropriate symbol for a given state with color

func Text

func Text(ctx context.Context, opts TextOptions) string

Text creates a styled text input prompt

Types

type BoxAlignment

type BoxAlignment string
const (
	BoxAlignLeft   BoxAlignment = "left"
	BoxAlignCenter BoxAlignment = "center"
	BoxAlignRight  BoxAlignment = "right"
)

type BoxOptions

type BoxOptions struct {
	Output         Writer
	Columns        int          // terminal columns; if 0, default to 80
	WidthFraction  float64      // 0..1 fraction of Columns; ignored if WidthAuto
	WidthAuto      bool         // compute width to content automatically (capped by Columns)
	TitlePadding   int          // spaces padding inside borders around title
	ContentPadding int          // spaces padding inside borders around content lines
	TitleAlign     BoxAlignment // left|center|right
	ContentAlign   BoxAlignment // left|center|right
	Rounded        bool
	IncludePrefix  bool
	FormatBorder   func(string) string // formatter for border glyphs (e.g., color)
}

type ClackState added in v0.7.0

type ClackState string
const (
	StateInitial ClackState = "initial"
	StateActive  ClackState = "active"
	StateCancel  ClackState = "cancel"
	StateSubmit  ClackState = "submit"
	StateError   ClackState = "error"
)

type ConfirmOptions

type ConfirmOptions struct {
	Message      string
	Active       string
	Inactive     string
	InitialValue bool
	Input        Reader
	Output       Writer
}

ConfirmOptions defines options for styled confirm prompt

type EventHandler added in v0.7.0

type EventHandler any

type Key added in v0.7.0

type Key = terminal.Key

type MessageOptions added in v0.7.0

type MessageOptions struct {
	Output Writer
}

MessageOptions configures simple message helpers output. If Output is nil, the helper functions are no-ops.

type MockReadable added in v0.7.0

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

func NewMockReadable added in v0.7.0

func NewMockReadable() *MockReadable

func (*MockReadable) Close added in v0.7.0

func (m *MockReadable) Close() error

func (*MockReadable) EmitKeypress added in v0.7.0

func (m *MockReadable) EmitKeypress(char string, key Key)

func (*MockReadable) On added in v0.7.0

func (m *MockReadable) On(event string, handler func(string, Key))

func (*MockReadable) Read added in v0.7.0

func (m *MockReadable) Read(p []byte) (int, error)

func (*MockReadable) SendKey added in v0.7.0

func (m *MockReadable) SendKey(char string, key Key)

SendKey is a convenience method for testing

type MockWritable added in v0.7.0

type MockWritable struct {
	Buffer []string
	// contains filtered or unexported fields
}

func NewMockWritable added in v0.7.0

func NewMockWritable() *MockWritable

func (*MockWritable) Emit added in v0.7.0

func (m *MockWritable) Emit(event string)

func (*MockWritable) GetFrames added in v0.7.0

func (m *MockWritable) GetFrames() []string

GetFrames returns all written frames for testing

func (*MockWritable) On added in v0.7.0

func (m *MockWritable) On(event string, handler func())

func (*MockWritable) Write added in v0.7.0

func (m *MockWritable) Write(p []byte) (int, error)

type MultiSelectOptions added in v0.5.0

type MultiSelectOptions[T any] struct {
	Message       string
	Options       []SelectOption[T]
	InitialValues []T
	MaxItems      *int
	Input         Reader
	Output        Writer
}

MultiSelectOptions defines options for styled multi-select prompt

type PasswordOptions

type PasswordOptions struct {
	Message      string
	DefaultValue string
	InitialValue string
	Validate     func(string) error
	Input        Reader
	Output       Writer
}

PasswordOptions defines options for styled password prompt

type Progress added in v0.4.0

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

Progress represents a progress bar that wraps spinner functionality

func NewProgress

func NewProgress(opts ProgressOptions) *Progress

NewProgress creates a new progress bar

func (*Progress) Advance added in v0.4.0

func (p *Progress) Advance(step int, msg string)

Advance updates progress by the given step and optionally updates message

func (*Progress) Message added in v0.4.0

func (p *Progress) Message(msg string)

Message updates the message without advancing progress

func (*Progress) Start added in v0.4.0

func (p *Progress) Start(msg string)

Start begins the progress bar animation

func (*Progress) Stop added in v0.4.0

func (p *Progress) Stop(msg string, code int)

Stop halts the progress bar and shows final state

type ProgressOptions

type ProgressOptions struct {
	Style  string // "light", "heavy", "block"
	Max    int    // maximum value (default 100)
	Size   int    // bar width in characters (default 40)
	Output Writer
}

ProgressOptions configures the progress bar

type Prompt added in v0.7.0

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

func NewPrompt added in v0.7.0

func NewPrompt(options PromptOptions) *Prompt

NewPrompt creates a new prompt instance with default tracking

func NewPromptWithTracking added in v0.7.0

func NewPromptWithTracking(options PromptOptions, trackValue bool) *Prompt

NewPromptWithTracking creates a new prompt instance with specified tracking

func (*Prompt) CursorSnapshot added in v0.7.0

func (p *Prompt) CursorSnapshot() int

func (*Prompt) Emit added in v0.7.0

func (p *Prompt) Emit(event string, args ...any)

Emit emits an event to all subscribers

func (*Prompt) ErrorSnapshot added in v0.7.0

func (p *Prompt) ErrorSnapshot() string

func (*Prompt) On added in v0.7.0

func (p *Prompt) On(event string, handler any)

On subscribes to an event

func (*Prompt) Prompt added in v0.7.0

func (p *Prompt) Prompt(ctx context.Context) any

Prompt starts the prompt and returns the result

func (*Prompt) SetImmediateValue added in v0.7.0

func (p *Prompt) SetImmediateValue(v any)

SetImmediateValue updates the value in the current event-loop tick if possible. Falls back to enqueuing when called outside the loop.

func (*Prompt) SetValue added in v0.7.0

func (p *Prompt) SetValue(v any)

SetValue schedules a value update (for tests or programmatic flows). In the event-loop refactor, this will post to the loop; for now, set under lock.

func (*Prompt) StateSnapshot added in v0.7.0

func (p *Prompt) StateSnapshot() ClackState

func (*Prompt) UserInputSnapshot added in v0.7.0

func (p *Prompt) UserInputSnapshot() string

func (*Prompt) ValueSnapshot added in v0.7.0

func (p *Prompt) ValueSnapshot() any

type PromptOptions added in v0.7.0

type PromptOptions struct {
	Render           func(*Prompt) string
	InitialValue     any
	InitialUserInput string
	Validate         func(any) error
	Input            Reader
	Output           Writer
	Debug            bool
}

type Reader added in v0.7.0

type Reader interface {
	io.Reader
	On(event string, handler func(string, Key))
}

type SelectOption

type SelectOption[T any] struct {
	Value T
	Label string
	Hint  string
}

SelectOption represents an option in a styled select prompt

type SelectOptions

type SelectOptions[T any] struct {
	Message      string
	Options      []SelectOption[T]
	InitialValue *T
	MaxItems     *int
	Input        Reader
	Output       Writer
}

SelectOptions defines options for styled select prompt

type Spinner added in v0.4.0

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

Spinner represents an animated spinner

func NewSpinner

func NewSpinner(opts SpinnerOptions) *Spinner

NewSpinner creates a new Spinner with defaults

func (*Spinner) IsCancelled added in v0.4.0

func (s *Spinner) IsCancelled() bool

IsCancelled reports whether Stop was called with cancel code (1)

func (*Spinner) Message added in v0.4.0

func (s *Spinner) Message(msg string)

Message updates the spinner message for next frame

func (*Spinner) Start added in v0.4.0

func (s *Spinner) Start(msg string)

Start begins the spinner animation

func (*Spinner) Stop added in v0.4.0

func (s *Spinner) Stop(msg string, code int)

Stop halts the spinner and prints a final line with a status symbol code: 0 submit, 1 cancel, >1 error

type SpinnerOptions

type SpinnerOptions struct {
	Indicator     string   // "dots" (default) or "timer"
	Frames        []string // custom frames; defaults to unicode spinner frames
	Delay         time.Duration
	Output        Writer
	CancelMessage string
	ErrorMessage  string
}

SpinnerOptions configures the spinner behavior

type Stream added in v0.6.0

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

Stream renders a live stream area with clack-like styling Use Start to begin, WriteLine/Pipe to add content, and Stop to finalize.

func NewStream added in v0.6.0

func NewStream(opts StreamOptions) *Stream

NewStream creates a Stream

func (*Stream) Pipe added in v0.6.0

func (s *Stream) Pipe(r io.Reader)

Pipe reads from r line-by-line and writes to the stream area

func (*Stream) Start added in v0.6.0

func (s *Stream) Start(message string)

Start prints the header and prepares to receive lines

func (*Stream) Stop added in v0.6.0

func (s *Stream) Stop(finalMessage string, code int)

Stop finalizes the stream with a status symbol and optional timer code: 0 submit, 1 cancel, >1 error

func (*Stream) WriteLine added in v0.6.0

func (s *Stream) WriteLine(line string)

WriteLine appends a single line into the stream area

type StreamOptions added in v0.6.0

type StreamOptions struct {
	Output Writer
	// If true, show elapsed time on finalize line
	ShowTimer bool
}

StreamOptions configure the styled stream renderer

type TextOptions

type TextOptions struct {
	Message      string
	Placeholder  string
	DefaultValue string
	InitialValue string
	Validate     func(string) error
	Input        Reader
	Output       Writer
}

TextOptions defines options for styled text prompt

type ValidationError added in v0.7.0

type ValidationError struct {
	Message string
}

func NewValidationError added in v0.7.0

func NewValidationError(message string) *ValidationError

func (*ValidationError) Error added in v0.7.0

func (e *ValidationError) Error() string

type Writer added in v0.7.0

type Writer interface {
	io.Writer
	On(event string, handler func())
	Emit(event string)
}

Directories

Path Synopsis
examples
confirm command
multiple command
multiselect command
password command
progress command
select command
spinner command
stream command
text command
internal

Jump to

Keyboard shortcuts

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