https://github.com/yarlson/tap
Beautiful, interactive command-line prompts for Go
https://github.com/yarlson/tap
cli command-line-app command-line-tool go golang golang-library prompt prompts
Last synced: about 1 month ago
JSON representation
Beautiful, interactive command-line prompts for Go
- Host: GitHub
- URL: https://github.com/yarlson/tap
- Owner: yarlson
- License: mit
- Created: 2025-08-19T05:26:00.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2025-09-01T07:49:41.000Z (about 1 month ago)
- Last Synced: 2025-09-01T09:58:40.659Z (about 1 month ago)
- Topics: cli, command-line-app, command-line-tool, go, golang, golang-library, prompt, prompts
- Language: Go
- Homepage:
- Size: 6.01 MB
- Stars: 28
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.LLM.md
- License: LICENSE
Awesome Lists containing this project
README
# Tap: LLM Usage Guide
This document gives language models a compact, precise map of Tap’s public API so you can confidently generate working code that uses this library.
Tap is a Go library for building interactive, clack-style terminal prompts (text/password/confirm/select/multiselect), spinners, progress bars, streams, and message boxes.
- Module path: `github.com/yarlson/tap`
- Runtime model: each helper opens a terminal internally, runs, and restores the TTY
- Return values are typed (not `any`) and cancel maps to a sensible zero value
- A small test-only override is available to inject I/O without a real terminal## Import
```go
import (
"context"
"fmt"
"github.com/yarlson/tap"
)
```## Core helpers and types
All helpers create and close a terminal per call, unless I/O is overridden in tests.
- `func tap.Text(ctx context.Context, opts tap.TextOptions) string`
- Options:
- `Message string`
- `Placeholder string`
- `DefaultValue string`
- `InitialValue string`
- `Validate func(string) error` (return non-nil to block submission and show error)- `func tap.Password(ctx context.Context, opts tap.PasswordOptions) string`
- Same options as `TextOptions` (input is masked in the UI)
- `func tap.Confirm(ctx context.Context, opts tap.ConfirmOptions) bool`
- Options:
- `Message string`
- `Active string` (label for true)
- `Inactive string` (label for false)
- `InitialValue bool`- `type tap.SelectOption[T any] struct { Value T; Label, Hint string }`
- `type tap.SelectOptions[T any] struct { Message string; Options []tap.SelectOption[T]; InitialValue *T; MaxItems *int }`
- `func tap.Select[T any](ctx context.Context, opts tap.SelectOptions[T]) T`- `type tap.MultiSelectOptions[T any] struct { Message string; Options []tap.SelectOption[T]; InitialValues []T; MaxItems *int }`
- `func tap.MultiSelect[T any](ctx context.Context, opts tap.MultiSelectOptions[T]) []T`- Spinner
- `type tap.SpinnerOptions struct { Indicator string; Frames []string; Delay time.Duration; CancelMessage, ErrorMessage string }`
- `type tap.Spinner struct { /* unexported */ }`
- `func tap.NewSpinner(opts tap.SpinnerOptions) *tap.Spinner`
- `func (s *tap.Spinner) Start(msg string)`
- `func (s *tap.Spinner) Message(msg string)`
- `func (s *tap.Spinner) Stop(msg string, code int)` // 0=success, 1=cancel, >1=error
- `func (s *tap.Spinner) IsCanceled() bool` (idiomatic) — `IsCancelled()` is kept for backward compat- Progress
- Stream (live output)- `type tap.StreamOptions struct { ShowTimer bool }`
- `type tap.Stream struct { /* unexported */ }`
- `func tap.NewStream(opts tap.StreamOptions) *tap.Stream`
- `func (s *tap.Stream) Start(msg string)`
- `func (s *tap.Stream) WriteLine(line string)`
- `func (s *tap.Stream) Pipe(r io.Reader)`
- `func (s *tap.Stream) Stop(msg string, code int)` // 0=success, 1=cancel, >1=error- `type tap.ProgressOptions struct { Style string; Max, Size int }`
- `type tap.Progress struct { /* unexported */ }`
- `func tap.NewProgress(opts tap.ProgressOptions) *tap.Progress`
- `func (p *tap.Progress) Start(msg string)`
- `func (p *tap.Progress) Advance(step int, msg string)`
- `func (p *tap.Progress) Message(msg string)`
- `func (p *tap.Progress) Stop(msg string, code int)` // 0=success, 1=cancel, >1=error- Messages and Box
- `func tap.Intro(title string)`
- `func tap.Outro(message string)`
- `type tap.BoxOptions struct { Columns int; WidthFraction float64; WidthAuto bool; TitlePadding, ContentPadding int; TitleAlign, ContentAlign tap.BoxAlignment; Rounded, IncludePrefix bool; FormatBorder func(string) string }`
- `func tap.Box(message string, title string, opts tap.BoxOptions)`
- `func tap.GrayBorder(s string) string`
- `func tap.CyanBorder(s string) string`## Behavior and conventions
- **Context Support**
- All interactive prompt functions (Text, Password, Confirm, Select, MultiSelect) require a `context.Context` as the first parameter
- Use `context.Background()` for basic usage, or pass custom contexts for cancellation/timeouts
- If context is cancelled, prompts return zero values (empty string, false, etc.)- **Typed returns**
- `Text`/`Password` → `string`
- `Confirm` → `bool`
- `Select[T]` → `T`
- `MultiSelect[T]` → `[]T`
- If the user cancels, helpers return a reasonable zero value (`""`, `false`, `var zero T`).- **Validation errors** (Text/Password)
- Provide `Validate: func(string) error { ... }`
- If non-nil error is returned on submit, the prompt stays active and shows a yellow error line below the input:
- Yellow left bar for the input line
- Yellow bottom-left corner (└) for the error line and a yellow error message- **Lifecycle**
- Helpers open and close a terminal per call; you do not need to manage sessions
- `Spinner`/`Progress` create a terminal under the hood and close it on `Stop` (unless I/O is overridden in tests)## Basic usage
```go
ctx := context.Background()
name := tap.Text(ctx, tap.TextOptions{Message: "What is your name?"})
lang := tap.Text(ctx, tap.TextOptions{Message: fmt.Sprintf("Hi %s! Favorite language?", name)})
proceed := tap.Confirm(ctx, tap.ConfirmOptions{Message: "Proceed?", InitialValue: true})
if proceed {
tap.Outro("Let's go! 🎉")
}
```### Validation
```go
email := tap.Text(ctx, tap.TextOptions{
Message: "Enter email:",
Validate: func(s string) error {
if !strings.Contains(s, "@") { return errors.New("please enter a valid email") }
return nil
},
})
```### Select with typed values
```go
type Env string
env := tap.Select(ctx, tap.SelectOptions[Env]{
Message: "Choose environment:",
Options: []tap.SelectOption[Env]{
{Value: "dev", Label: "Development", Hint: "Local"},
{Value: "prod", Label: "Production", Hint: "Live"},
},
})
```### Multi-Select with typed values
```go
langs := tap.MultiSelect(ctx, tap.MultiSelectOptions[string]{
Message: "Choose languages:",
Options: []tap.SelectOption[string]{
{Value: "go", Label: "Go"},
{Value: "py", Label: "Python"},
{Value: "js", Label: "JavaScript"},
},
InitialValues: []string{"go"},
})
fmt.Println(langs) // []string{"go", ...}
```### Spinner/Progress/Stream
```go
sp := tap.NewSpinner(tap.SpinnerOptions{Indicator: "dots"})
sp.Start("Connecting")
// ... do work ...
sp.Stop("Connected", 0)pr := tap.NewProgress(tap.ProgressOptions{Style: "heavy", Max: 100, Size: 40})
pr.Start("Processing…")
for i := 0; i < 10; i++ { pr.Advance(10, fmt.Sprintf("%d/10", i+1)) }
pr.Stop("Complete", 0)st := tap.NewStream(tap.StreamOptions{ShowTimer: true})
st.Start("Building project")
st.WriteLine("step 1: deps")
st.WriteLine("step 2: compile")
st.Stop("Done", 0)
```## Testing (override terminal I/O)
In tests, route I/O to mocks:
```go
in := core.NewMockReadable()
out := core.NewMockWritable()tap.SetTermIO(in, out)
defer tap.SetTermIO(nil, nil)_ = tap.Text(ctx, tap.TextOptions{Message: "Your name:"})
// feed input
in.EmitKeypress("A", core.Key{Name: "a"})
in.EmitKeypress("", core.Key{Name: "return"})
// assert frames in out.Buffer
```## Gotchas and guidance for code generation
- For `Select[T]`, supply `[]tap.SelectOption[T]`. Labels/hints are optional.
- Always call `Stop` on `Spinner`/`Progress` to restore the terminal when used.
- For validation, return `error` (not `bool`) from the provided function; non-nil blocks submit and shows an error line.
- The library uses ANSI; ensure output is sent to a TTY when running examples.
- Use `context.Background()` for basic usage, or create custom contexts for cancellation/timeout behavior.## Minimal end-to-end example
```go
package mainimport (
"context"
"fmt"
"strings"
"errors"
"github.com/yarlson/tap"
)func main() {
ctx := context.Background()
name := tap.Text(ctx, tap.TextOptions{Message: "Name:"})
email := tap.Text(ctx, tap.TextOptions{
Message: "Email:",
Validate: func(s string) error {
if !strings.Contains(s, "@") { return errors.New("please enter a valid email") }
return nil
},
})
ok := tap.Confirm(ctx, tap.ConfirmOptions{Message: fmt.Sprintf("Submit for %s?", name)})
if ok { tap.Outro(fmt.Sprintf("Saved %s", email)) }
}
```