tap

package module
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2025 License: MIT Imports: 17 Imported by: 0

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

Installation

go get github.com/noojuno/tap@latest
Requirements
  • Go 1.20+
  • A TTY-capable terminal (ANSI escape sequences); Linux/macOS and modern Windows terminals supported

Quick Start

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! 🎉")
    }
}
Controls
  • Navigate: Arrow keys or h/j/k/l
  • Submit: Enter
  • Cancel: Ctrl+C or Esc
  • Toggle (MultiSelect): Space
  • Accept suggestion (Autocomplete): Tab

API Examples

Text Input with Validation
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
    },
})
Autocomplete
// 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 Input
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-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
MultiSelect
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
", selected)
Progress Indicators
// 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
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)
Table
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,
})
Message Helpers
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! 🎉
Styled Messages (Box)
// 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,
})
Usage Tips
  • Always call Stop on Spinner/Progress/Stream to restore the terminal state.
  • For Select[T]/MultiSelect[T], you can omit the type parameter if it can be inferred from the options' type.
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:",
})

// On cancel or timeout, helpers return zero values
// (e.g., Text/Password → "", Confirm → false, Select[T] → zero T).

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 ().
  • Throttling: Progress only emits a new percentage when it changes to avoid spam.
  • Multiplexers: tmux/screen may swallow OSC sequences unless configured to passthrough.

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

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 detection

Examples

Explore 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.go

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"
	Bold          = "\033[1m"
	Inverse       = "\033[7m"
	Strikethrough = "\033[9m"
)

ANSI color codes

View Source
const (
	// Table border symbols
	TableTopLeft     = "┌"
	TableTopRight    = "┐"
	TableBottomLeft  = "└"
	TableBottomRight = "┘"
	TableTopTee      = "┬"
	TableBottomTee   = "┴"
	TableLeftTee     = "├"
	TableRightTee    = "┤"
	TableCross       = "┼"
	TableHorizontal  = "─"
	TableVertical    = "│"
)

Table symbols

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 Autocomplete

func Autocomplete(ctx context.Context, opts AutocompleteOptions) string

Autocomplete renders a text prompt with inline suggestions.

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 GreenBorder

func GreenBorder(s string) string

func Intro

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

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

func Message

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

func MultiSelect

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 RedBorder

func RedBorder(s string) string

func Select

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

Select creates a styled select prompt

func SetTermIO

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

func Symbol(state ClackState) string

Symbol returns the appropriate symbol for a given state with color

func Table

func Table(headers []string, rows [][]string, opts TableOptions)

Table renders a formatted table with headers and rows

func Text

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

Text creates a styled text input prompt

func YellowBorder

func YellowBorder(s string) string

Types

type AutocompleteOptions

type AutocompleteOptions struct {
	Message      string
	Placeholder  string
	DefaultValue string
	InitialValue string
	Validate     func(string) error
	Suggest      func(string) []string // returns suggestion list for current input
	MaxResults   int                   // maximum suggestions to show (default 5)
	Input        Reader
	Output       Writer
}

AutocompleteOptions defines options for styled autocomplete text prompt

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

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

type EventHandler any

type Key

type Key = terminal.Key

type MessageOptions

type MessageOptions struct {
	Output Writer
}

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

type MockReadable

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

func NewMockReadable

func NewMockReadable() *MockReadable

func (*MockReadable) Close

func (m *MockReadable) Close() error

func (*MockReadable) EmitKeypress

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

func (*MockReadable) On

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

func (*MockReadable) Read

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

func (*MockReadable) SendKey

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

SendKey is a convenience method for testing

type MockWritable

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

func NewMockWritable

func NewMockWritable() *MockWritable

func (*MockWritable) Emit

func (m *MockWritable) Emit(event string)

func (*MockWritable) GetFrames

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

GetFrames returns all written frames for testing

func (*MockWritable) On

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

func (*MockWritable) Write

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

type MultiSelectOptions

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

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

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

Advance updates progress by the given step and optionally updates message

func (*Progress) Message

func (p *Progress) Message(msg string)

Message updates the message without advancing progress

func (*Progress) Start

func (p *Progress) Start(msg string)

Start begins the progress bar animation

func (*Progress) Stop

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

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

func NewPrompt

func NewPrompt(options PromptOptions) *Prompt

NewPrompt creates a new prompt instance with default tracking

func NewPromptWithTracking

func NewPromptWithTracking(options PromptOptions, trackValue bool) *Prompt

NewPromptWithTracking creates a new prompt instance with specified tracking

func (*Prompt) CursorSnapshot

func (p *Prompt) CursorSnapshot() int

func (*Prompt) Emit

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

Emit emits an event to all subscribers

func (*Prompt) ErrorSnapshot

func (p *Prompt) ErrorSnapshot() string

func (*Prompt) On

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

On subscribes to an event

func (*Prompt) Prompt

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

Prompt starts the prompt and returns the result

func (*Prompt) SetImmediateValue

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

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

func (p *Prompt) StateSnapshot() ClackState

func (*Prompt) UserInputSnapshot

func (p *Prompt) UserInputSnapshot() string

func (*Prompt) ValueSnapshot

func (p *Prompt) ValueSnapshot() any

type PromptOptions

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

type Reader

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

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

func (s *Spinner) IsCancelled() bool

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

func (*Spinner) Message

func (s *Spinner) Message(msg string)

Message updates the spinner message for next frame

func (*Spinner) Start

func (s *Spinner) Start(msg string)

Start begins the spinner animation

func (*Spinner) Stop

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

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

func NewStream(opts StreamOptions) *Stream

NewStream creates a Stream

func (*Stream) Pipe

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

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

func (*Stream) Start

func (s *Stream) Start(message string)

Start prints the header and prepares to receive lines

func (*Stream) Stop

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

func (s *Stream) WriteLine(line string)

WriteLine appends a single line into the stream area

type StreamOptions

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

StreamOptions configure the styled stream renderer

type TableAlignment

type TableAlignment string
const (
	TableAlignLeft   TableAlignment = "left"
	TableAlignCenter TableAlignment = "center"
	TableAlignRight  TableAlignment = "right"
)

type TableColor

type TableColor string
const (
	TableColorDefault TableColor = "default"
	TableColorGray    TableColor = "gray"
	TableColorRed     TableColor = "red"
	TableColorGreen   TableColor = "green"
	TableColorYellow  TableColor = "yellow"
	TableColorCyan    TableColor = "cyan"
)

type TableOptions

type TableOptions struct {
	Output           Writer
	ShowBorders      bool
	IncludePrefix    bool
	MaxWidth         int
	ColumnAlignments []TableAlignment
	HeaderStyle      TableStyle
	HeaderColor      TableColor
	FormatBorder     func(string) string
}

TableOptions defines options for styled table rendering

type TableStyle

type TableStyle string
const (
	TableStyleNormal TableStyle = "normal"
	TableStyleBold   TableStyle = "bold"
	TableStyleDim    TableStyle = "dim"
)

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

type ValidationError struct {
	Message string
}

func NewValidationError

func NewValidationError(message string) *ValidationError

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Writer

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

Directories

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

Jump to

Keyboard shortcuts

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