tui

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

Package tui is fft's interactive terminal UI.

It does not send requests itself. Every action it offers becomes an fft command line, and a Runner executes that line through the same command tree the shell reaches — so the read-only gate, the exit codes, curated validation and the output contract hold in the UI exactly as they do in a script, and every screen can show the command it stands for. The package defines the contract and cmd/fft implements it, which keeps this package free of an import of main.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(ctx context.Context, opts Options) error

Run shows the UI until the user quits or ctx is cancelled. Runs still in flight when it returns are the Runner owner's to cancel.

func ShellQuote added in v0.9.0

func ShellQuote(s string) string

ShellQuote is s spelled for a command line that fft prints for a user to paste: [shellQuote]'s spelling, without the verdict on whether it is portable.

Types

type Arg

type Arg struct {
	Name     string
	Required bool
}

Arg is a positional argument.

type Catalog

type Catalog interface {
	// Groups is every operation, grouped by the tag the API documents it under,
	// in tag order. Each operation appears once, under its first tag.
	Groups() []OperationGroup

	// Table renders stdout — what cmd printed under -o json — as the table the same
	// command prints in a shell. It is only asked for a command whose Table is set;
	// "" means there are no rows.
	Table(cmd Command, stdout []byte) (string, error)
}

Catalog describes every operation the UI can send, and the fft command that sends each one. It is read-only, and safe to read from any goroutine.

type Chunk

type Chunk struct {
	// Stderr says the bytes were written to standard error rather than standard
	// output. Each stream is coalesced on its own, and their chunks interleave in
	// the order the runner flushed them, which is not necessarily the order the
	// command wrote them in.
	Stderr bool

	// Bytes is the output, which is not split on any boundary: a chunk may end
	// mid-line, and mid-rune.
	Bytes []byte

	// Dropped says output was discarded before this chunk because the UI was not
	// keeping up. A run is never held up to deliver its output.
	Dropped bool
}

Chunk is part of a streaming run's output, as it was written.

It is a copy the runner is done with, so a consumer may keep it. What arrives here is also still counted towards the capped Result the run ends with, so a screen that shows chunks live and a screen that shows the result at the end agree about what was written.

type Command

type Command struct {
	// Path is the command line that reaches it, without the program name and
	// before any of the user's arguments: {"facility", "list"}.
	Path []string

	// Curated says the command is hand-written rather than generated from the
	// API's schema.
	Curated bool

	// Args are its positional arguments, in order.
	Args []Arg

	// Flags are the flags the user may fill in. Those the UI decides — the body
	// flags, the global ones — are not among them.
	Flags []Flag

	// Body says the command reads a request body from --file. BodyRequired says it
	// refuses to run without one.
	Body         bool
	BodyRequired bool

	// Example says the command prints a sample body of its own with --example, one
	// that may differ from the operation's SampleBody.
	Example bool

	// Confirms says the command asks before it acts, in its own words and after
	// looking up what it is about to change. The UI sends such a command without
	// asking first, and puts the command's question to the user instead.
	Confirms bool

	// Table says [Catalog.Table] can render what the command prints.
	Table bool
}

Command is an fft command, as the request form needs to know it.

type Flag

type Flag struct {
	Name     string
	Kind     FlagKind
	Usage    string
	Required bool

	// Default is the value the command uses when the flag is not given, as the
	// flag spells it; "" when it has none worth showing.
	Default string

	// Enum lists the values the flag accepts, empty when it accepts any.
	Enum []string

	// WithExample says the flag may be given together with --example, and so may
	// shape the sample body the command prints.
	WithExample bool
}

Flag is one flag the user may give.

type FlagKind

type FlagKind int

FlagKind is the kind of value a flag takes.

const (
	// FlagString takes any text.
	FlagString FlagKind = iota
	// FlagBool is on or off, and takes no value.
	FlagBool
	// FlagInt takes a whole number.
	FlagInt
	// FlagFloat takes a number.
	FlagFloat
	// FlagDuration takes a Go duration: 30s, 2m.
	FlagDuration
	// FlagList takes the flag once per value. The form takes the values
	// comma-separated.
	FlagList
	// FlagPairs takes the flag once per name=value pair. A value may hold commas
	// of its own, so the form starts a new pair only at a comma followed by a name
	// and an equals sign.
	FlagPairs
)

The kinds of value a flag takes. The form checks a value against its kind before anything runs; the command checks it again.

type History

type History interface {
	// Read returns every recorded request, oldest first. off says why requests are
	// not being recorded now, "" when they are; what was recorded before is
	// returned either way. It is called from a goroutine of its own.
	Read() (entries []history.Entry, off string, err error)
}

History is the request history fft records, read where fft keeps it.

It is read directly rather than through `fft history list`: the UI needs every entry, as recorded, to count the operations it lists, and why nothing new is being recorded, which the command only says in prose on stderr.

type Invocation

type Invocation struct {
	// Args is the command line without the program name: {"facility", "list"}.
	// It is run exactly as given; what the UI decides for every run — the output
	// format, the selected project — travels beside it, so Args carries only what
	// the user chose.
	//
	// Args is not secret. It is shown as the equivalent fft command and kept, with
	// known credential-shaped values redacted, in the request history; redaction is
	// a net, not a place to put a secret. Anything sensitive belongs on Stdin.
	Args []string

	// Stdin is what the command reads from standard input. A request body and a
	// password travel here, never in Args, so neither is ever shown as part of a
	// command line or kept in a process listing.
	Stdin []byte

	// Project, when set, is the project the invocation acts on, whichever one the
	// UI has selected since. It is how a request is sent again to the project it
	// was first sent to. It travels beside Args, like the selection it overrides.
	Project string

	// Exclusive runs the invocation alone, with no other run in flight, and after
	// every exclusive invocation started before it. The runner already runs alone
	// every command that reads a shared file and writes it back, since two of those
	// side by side lose one of their updates, and reports those as exclusive in its
	// events; this is for a sequence the UI knows must not interleave with anything
	// else.
	Exclusive bool

	// Background marks a run the UI started on its own — reading the user's roles,
	// signing in ahead of the first request — rather than one the user asked for.
	// It is left out of the request history, which is a record of what the user
	// sent, and whose counts the Operations list shows.
	Background bool

	// Stream asks for the run's output as it is produced, on [RunEvent.Chunk],
	// rather than only in the [Result] at the end. A run that does not end on its
	// own — the emulator, which serves until it is stopped — shows nothing at all
	// without it.
	//
	// It is opt-in because every chunk is an event, and the runner's event buffer
	// backs up onto the runs rather than onto the UI: streaming every run would
	// make a `--all` over a large tenant pay for output nobody is watching.
	Stream bool
}

Invocation is one fft command line to execute.

type Operation

type Operation struct {
	ID          string
	Summary     string
	Description string
	Method      string
	Path        string
	Tag         string

	// Permissions are the permissions the API documents for it, any one of which
	// is enough. Empty when it documents none.
	Permissions []string

	// Mutates says the operation changes data on the tenant: fft refuses it on a
	// read-only project, and the UI asks before sending it.
	Mutates bool

	Deprecated bool

	// SampleBody is the request body synthesized from the API's schema, "" when the
	// operation takes none.
	SampleBody string

	// Command is the command the form for it runs.
	Command Command

	// Also are the other commands that send it: the other hand-written commands
	// that each send one use of it, and `fft api` unless Command is already that.
	// A request recorded through one of them reopens in its form, not Command's,
	// because only that form has the flags it was sent with.
	Also []Command
}

Operation is one operation of the API, as the UI lists and describes it.

type OperationGroup

type OperationGroup struct {
	Tag        string
	Operations []Operation
}

OperationGroup is the operations under one tag.

type Options

type Options struct {
	// Runner executes the commands the UI builds.
	Runner Runner

	// Catalog is every operation the UI can send.
	Catalog Catalog

	// History is the request history the History screen shows and the Operations
	// list counts uses from. nil shows none.
	History History

	// In is where keystrokes come from; it must be a terminal.
	In io.Reader

	// Out is where the UI is drawn. fft passes stderr, so that stdout stays what
	// it is everywhere else: data, and here nothing at all.
	Out io.Writer

	// Project is the project the session starts on, "" to leave the choice to
	// fft's own resolution. It is only shown; the Runner already acts on it.
	Project string

	// Headless is set when fft runs from the environment (FFT_BASE_URL): the config
	// file is not the session's to change, from the first keystroke on.
	Headless bool

	// ReadOnly is set when every write in the session is refused, whatever the
	// project's configuration says: fft tui --read-only, or FFT_READ_ONLY.
	ReadOnly bool

	// Color draws the UI in colour. Without it nothing depends on colour to be
	// understood.
	Color bool

	// Now is the clock the UI measures elapsed time and token lifetimes with. nil
	// means time.Now.
	Now func() time.Time
	// contains filtered or unexported fields
}

Options is what Run needs from its caller.

type Question

type Question struct {
	// ID tells this question from every other one the runner has asked. An answer
	// names it, so that it can only ever answer the question it was given for.
	ID uint64

	// Text is the command's question. It may quote what the API returned, and is
	// sanitized before it is drawn.
	Text string

	// Confirm, when set, is the word the user must type back to say yes. A command
	// that cannot be undone asks for one: a single key is too easy to press by
	// accident. "" means a y answers.
	Confirm string
}

Question is what a running command asks the user, in its own words: the confirmation `fft facility delete` asks in a shell, naming the facility it has looked up.

type Result

type Result struct {
	// ExitCode is the exit code the same command line would have given the shell.
	ExitCode int

	// Status is the HTTP status of the last response the tenant sent, and 0 when
	// no response arrived — a refused write, a usage error, a cancelled run. A
	// command that sends requests side by side reports the one that finished last,
	// which is not necessarily the one it sent last.
	Status int

	// Stdout is the command's data: the API's own document under -o json.
	Stdout []byte

	// Stderr is everything else the command said: errors, warnings, notices.
	Stderr []byte

	// StdoutTruncated and StderrTruncated say the command wrote more than the
	// runner keeps, and the stream holds only the first part of what it wrote. A
	// truncated document is not the API's answer, and must not pass for one.
	StdoutTruncated bool
	StderrTruncated bool

	// Project is the project the command acted on, "" when it never got as far as
	// choosing one.
	Project string

	// Duration is how long the command executed, excluding the time it queued.
	Duration time.Duration

	// Recorded says the run was added to the request history, and so that what
	// the UI read of the history is out of date.
	Recorded bool
}

Result is what a finished invocation produced.

type RunEvent

type RunEvent struct {
	ID    RunID
	State RunState

	// Invocation is the invocation as the runner accepted it. Its Stdin is always
	// nil: what a run reads may be a secret, and an event is not where it goes.
	Invocation Invocation

	// At is when the invocation entered State.
	At time.Time

	// Result is set when State is [RunDone], and zero otherwise.
	Result Result

	// Question, on a [RunRunning] event, is a question the command asks before it
	// goes on. The run waits until [Runner.Answer] answers it, or until it is
	// cancelled, which answers no.
	Question *Question

	// Chunk, on a [RunRunning] event, is output the run has produced since the
	// last one. It arrives only for an [Invocation] that asked to Stream, and it
	// says nothing about the run's state: a chunk is not a start, and a consumer
	// that times a run must leave its clock alone.
	Chunk *Chunk
}

RunEvent reports a change in an invocation's state.

type RunID

type RunID uint64

RunID identifies one invocation for the lifetime of a Runner.

type RunState

type RunState int

RunState is where an invocation is in its life.

const (
	// RunQueued is an accepted invocation waiting for a free slot.
	RunQueued RunState = iota
	// RunRunning is an invocation that holds a slot: it is executing, or waiting
	// for a command that needs the config file to itself to finish.
	RunRunning
	// RunDone is an invocation that has finished, however it finished.
	RunDone
)

type Runner

type Runner interface {
	// Start accepts inv and returns at once. Its progress arrives on Events. The
	// project it acts on is the one selected when Start is called, however long
	// the run then waits for its turn.
	Start(inv Invocation) (RunID, error)

	// Cancel stops an invocation. One that has not started executing ends with exit
	// code 130 and has sent nothing. One that is executing ends as the command does
	// when interrupted: 130 if the interruption stopped it, and its own result if
	// it finished its work first — a write that landed is reported as landed.
	// Cancelling a finished or unknown run does nothing.
	Cancel(id RunID)

	// Events delivers every state change of every run. The runner closes it once
	// it has been shut down and its last run has finished.
	Events() <-chan RunEvent

	// Answer answers question q of run id. An answer to a question the run is no
	// longer asking — it was cancelled, or it has ended — does nothing.
	Answer(id RunID, q uint64, yes bool)

	// SetProject selects the project the invocations started after it act on. ""
	// leaves the choice to fft's own resolution: the active project, or the
	// environment.
	SetProject(name string)

	// SetEmulator points the invocations started after it at a local emulator
	// serving baseURL, "" to point them back at the selected project.
	//
	// It is not a project: the emulator is not in fft's config file and must never
	// be written to it, and it cannot stand in for Google's sign-in, so a runner
	// reaches it by giving a run the headless environment a shell would export. The
	// commands that manage the config file are not redirected — the projects the UI
	// switches back to are in that file, and a run that could not read it would
	// leave the UI with nothing to return to.
	//
	// Like SetProject, it is taken when a run is started: a run already in flight
	// keeps the target it was started with.
	SetEmulator(baseURL string)
}

Runner executes invocations in the background and reports on them.

Its lifecycle belongs to whoever built it, so there is no Close here: the UI only starts, cancels and watches.

Jump to

Keyboard shortcuts

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