tui

package
v0.260507.1 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: MPL-2.0 Imports: 16 Imported by: 0

README

internal/command/tui

Interactive terminal prompts for the tingly-box CLI. A small, self-contained package: a handful of single-shot prompts plus a generic Wizard runner that strings them together with a shared header, breadcrumb, and help line.


Structure

internal/command/tui/
  tui.go          core types, shared theme, run() helper
  prompts.go      Confirm, Input, Select, MultiSelect
  wizard.go       Step[S], RunWizard[S], WithSpinner[T]
  quickstart.go   Quickstart wizard — the only consumer

Everything is in one flat package. The package is a leaf: only internal/command/quickstart.go imports it. To avoid a cycle, the QuickstartManager interface is defined here; AppManager satisfies it implicitly.


Prompts

Each prompt is a self-contained bubbletea program. All share the same Header string option — the wizard passes its rendered breadcrumb down so the step counter and progress crumbs appear above every question inside the same tea frame, not as a preceding fmt.Println.

After a prompt resolves it collapses to a single trace line that stays in the terminal scrollback:

✓ Pick a provider: OpenAI

Users can scroll up and see every answer they gave.

Confirm

Two visible buttons (Yes / No) that the user can toggle with /, Tab, or y/n. Enter confirms the highlighted button. The current selection is always visible, so there is no ambiguity about what pressing Enter will do.

Input

Wraps bubbles/textinput with an optional Validate func(string) error. Validation runs on every keystroke; errors appear inline below the field. There is no submit-and-fail cycle.

Select

A flat list with a cursor and an instant fzf-style filter.

Filter: typing any printable character narrows the list in real-time. A filter: hint appears above the list while active. The list is never mutated — a visible []int slice (indices into the immutable items) is rebuilt on each keystroke. Moving the cursor moves through visible.

Esc is two-stage: the first Esc clears the filter; a second Esc on an empty filter triggers back navigation. This gives users a clear escape hatch without accidentally leaving the prompt mid-search.

No vim aliases in Select. Navigation uses tea.KeyMsg.Type directly (KeyUp, KeyDown, KeyEnter, KeyEsc) rather than bubbles/key.Matches. This means tea.KeyRunes always feeds the filter — pressing k to search for "kubernetes" works as expected and does not move the cursor up. The k/j aliases are intentionally absent here.

MultiSelect

Like Select but with / checkboxes. Space toggles the item under the cursor; Enter confirms the full selection. k/j vim aliases are kept because there is no freeform filter input.

Spinner

WithSpinner[T](message, fn) runs fn in a goroutine and renders an animated spinner.Points while it blocks. On completion: ✓ message on success, ✗ message on failure. The result line stays in the scrollback like other prompt traces.


Wizard

Generic state
type Step[S any] struct {
    Name    string
    Skip    func(state S) bool
    Execute func(ctx StepContext, state S) (S, StepResult, error)
}

func RunWizard[S any](title string, initial S, steps []Step[S]) (S, error)

Each step receives the current state, returns a (possibly mutated) copy, and signals what the wizard should do next: StepContinue, StepBack, StepSkip, StepDone, or StepCancel. The wizard carries arbitrary application state S without interface boxing or type assertions.

Back navigation

A history []int stack records step indices. advance() pushes before moving forward; retreat() pops. Back always returns to wherever the user actually came from, even when some steps were auto-skipped, with no special-casing.

Breadcrumb

The header rendered above every prompt:

Tingly Box · Quickstart   Step 3/7
✓ Welcome › ✓ Credential › ❯ Provider › · API Style › · Model › · Rules › · Agent › · Done

done, active, · upcoming. Named steps tell users what is coming, not just how far along they are.


Theme

All colours are lipgloss.AdaptiveColor with separate light/dark values:

colAccent  = lipgloss.AdaptiveColor{Light: "#7C3AED", Dark: "#B4BEFE"}
colSuccess = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#A6E3A1"}
colDanger  = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#F38BA8"}
colText    = lipgloss.AdaptiveColor{Light: "#1F2937", Dark: "#CDD6F4"}
colMuted   = lipgloss.AdaptiveColor{Light: "#6B7280", Dark: "#9099B0"}
colSubtle  = lipgloss.AdaptiveColor{Light: "#9CA3AF", Dark: "#585B70"}

No colour is hard-coded outside tui.go. Every prompt renders its help footer through the same helpLine function, producing a uniform key: action format.


Quickstart flow

Step order: Provider before API Style

Users pick a provider template first. The description of each template lists which API styles it supports (openai · anthropic). Only after choosing a provider does the API Style step appear — at that point the choice is fully in context: the user knows which provider they picked and can see which styles it offers.

This avoids asking users to know upfront whether a provider speaks the OpenAI or Anthropic wire protocol — a prerequisite most users lack.

Never auto-skip

Even when a provider template supports only one API style, the API Style step is still shown. It presents the constraint as context ("only style supported by X") and lets the user confirm or go back. Auto-skipping hides information and breaks the back-navigation mental model: users wonder why a step they passed through is unreachable via Esc.

Skip Welcome for returning users

The Welcome step has Skip: qsHasProviders. Users who already have a provider configured land directly on the Credential step — they are adding a second provider, not being onboarded, and reading the onboarding copy again would be confusing.


Adding a step

  1. Add a field to your state struct.
  2. Write an Execute function: func(ctx StepContext, state S) (S, StepResult, error).
  3. Optionally write a Skip predicate: func(state S) bool.
  4. Append Step[S]{Name: "...", Execute: ..., Skip: ...} to the slice passed to RunWizard.

The breadcrumb, step counter, and back navigation update automatically.

Documentation

Overview

Package tui provides interactive terminal prompts for the tingly-box CLI.

It is intentionally small and self contained: a handful of single-shot prompts (Confirm, Input, Select, MultiSelect, Spinner) plus a Wizard runner that strings them together with a shared header, breadcrumb and help line.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCancelled = errors.New("cancelled")
	ErrBack      = errors.New("back")
)

Common navigation outcomes.

View Source
var ErrRequired = errors.New("this field is required")

ErrRequired is returned by Input when a required field is left blank.

Functions

func RunQuickstart

func RunQuickstart(mgr QuickstartManager) error

RunQuickstart runs the interactive Tingly Box quickstart wizard.

func RunWizard

func RunWizard[S any](title string, initial S, steps []Step[S]) (S, error)

RunWizard executes the wizard and returns the final state.

func WithSpinner

func WithSpinner[T any](message string, fn func() (T, error)) (T, error)

WithSpinner renders an animated spinner while fn runs and returns its result.

Types

type Action

type Action int

Action describes how a prompt was dismissed.

const (
	ActionConfirm Action = iota // user confirmed (Enter / y)
	ActionBack                  // user requested to go back (Esc / ←)
	ActionCancel                // user aborted (Ctrl+C / q)
)

type ConfirmOptions

type ConfirmOptions struct {
	Header      string // optional header (rendered above the prompt)
	DefaultYes  bool   // pressing Enter chooses Yes
	CanGoBack   bool   // allow Esc/← to go back
	Description string // optional one-line description
}

ConfirmOptions tunes a Confirm prompt.

type InputOptions

type InputOptions struct {
	Header      string
	Placeholder string
	Required    bool
	Mask        bool
	Initial     string
	Validate    func(string) error
	CharLimit   int
	CanGoBack   bool
}

InputOptions tunes an Input prompt.

type MultiSelectItem

type MultiSelectItem[T any] struct {
	Title       string
	Description string
	Value       T
	Selected    bool
}

MultiSelectItem is a single row in a MultiSelect prompt.

type MultiSelectOptions

type MultiSelectOptions struct {
	Header    string
	Initial   map[any]bool
	PageSize  int
	CanGoBack bool
}

MultiSelectOptions tunes a MultiSelect prompt.

type QuickstartManager

type QuickstartManager interface {
	ListProviders() []*typ.Provider
	GetProvider(name string) (*typ.Provider, error)
	AddProvider(name, apiBase, token string, apiStyle protocol.APIStyle) (string, error)
	SaveConfig() error

	GetServerPort() int
	SetupServerWithPort(port int) error
	StartServer() error

	GetGlobalConfig() *serverconfig.Config
	FetchAndSaveProviderModels(providerUUID string) error
}

QuickstartManager is the surface the wizard needs from the host (the CLI's AppManager). Defined here as an interface so this package stays a leaf.

type Result

type Result[T any] struct {
	Value  T
	Action Action
}

Result wraps a prompt value with the dismissal Action.

func Confirm

func Confirm(prompt string, opts ...ConfirmOptions) (Result[bool], error)

Confirm shows a y/n prompt and returns the selection.

func Input

func Input(prompt string, opts ...InputOptions) (Result[string], error)

Input shows a text-input prompt and returns the entered value.

func MultiSelect

func MultiSelect[T any](prompt string, items []MultiSelectItem[T], opts ...MultiSelectOptions) (Result[[]T], error)

MultiSelect shows a many-of selection list.

func Select

func Select[T any](prompt string, items []SelectItem[T], opts ...SelectOptions) (Result[T], error)

Select shows a one-of selection list.

func (Result[T]) IsBack

func (r Result[T]) IsBack() bool

func (Result[T]) IsCancel

func (r Result[T]) IsCancel() bool

func (Result[T]) IsConfirm

func (r Result[T]) IsConfirm() bool

type SelectItem

type SelectItem[T any] struct {
	Title       string
	Description string
	Value       T
}

SelectItem is a single row in a Select prompt.

type SelectOptions

type SelectOptions struct {
	Header    string
	Initial   any  // initial selection value (matched via fmt %v)
	PageSize  int  // visible items, default 8
	CanGoBack bool // allow Esc/← to go back
}

SelectOptions tunes a Select prompt.

type Step

type Step[S any] struct {
	Name    string
	Skip    func(state S) bool
	Execute func(ctx StepContext, state S) (S, StepResult, error)
}

Step describes a single phase in a wizard.

type StepContext

type StepContext struct {
	Header string // ready-to-print breadcrumb/title
	Index  int    // 0-indexed position among the active (non-skipped) steps
	Total  int    // total active steps
}

StepContext carries presentation info passed to each Step's Execute.

type StepResult

type StepResult int

StepResult signals what the wizard should do after a step.

const (
	StepContinue StepResult = iota // advance to the next step
	StepBack                       // go back to the previous step
	StepDone                       // wizard finished successfully
	StepSkip                       // skip this step
	StepCancel                     // user cancelled
)

Jump to

Keyboard shortcuts

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