ask

package
v0.14.1 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package ask draws a question of the shared type (internal/core/question) in a plain Terminal (spc-2610030911534855, itd-2610030810370060): the layout from a question to lines at a width and a colour rung, and the sanitising every part passes before it is measured or drawn.

The layout is pure: it takes the window's width and the colour rung and returns the lines, so every layout is a golden test with an injected width. It is a front door, so it holds no limit of its own: how much each part may hold is the question package's check, and a question taller than the window is drawn whole for the Terminal to scroll.

Index

Constants

View Source
const ExitInterrupted = 130

ExitInterrupted is the exit code a front door ends with on ErrInterrupted: 128 plus SIGINT, as a shell reports an interrupted command.

View Source
const NumberedHint = "Type a number, or part of a name to narrow the list, then Enter."

NumberedHint is the numbered reader's one instruction, written beneath every list it draws.

View Source
const TypedHint = "Type a number, or type your answer, then Enter."

TypedHint is the numbered reader's instruction beneath a question with a typed part: a line that is not a number is the answer itself.

View Source
const TypedPrefix = "or type:"

TypedPrefix opens the line the typed part is drawn on.

Variables

View Source
var ErrInterrupted = errors.New("interrupted")

ErrInterrupted is Ctrl-C at a question: the terminal is restored and a newline written, nothing is recorded for the question, and the front door keeps the answers already given and exits 130.

Functions

func Draw

func Draw(a question.Ask, f Frame) []string

Draw draws a in the frame f, top to bottom: the chip line (the chip, then the tab strip when a has more than one part), the material (each paragraph wrapped on its own, a paragraph of lines a line for each, each list item with a hanging indent), the ask, the options numbered from 1 with Later as the last number, and the Now: and Change later: lines the question carries. One blank line separates each.

Every part is sanitised first (Safe), so widths are of what is drawn. Prose is wrapped at min(width, 80) less its indent. An option's label sits on its numbered line with its meaning beneath it, unless the window is wider than 80 and holds the label column and a meaning column of at least 60, when the meaning sits beside the label, wrapped at a measure of at most 80.

func Layout

func Layout(a question.Ask, width int, mode term.ColorMode) []string

Layout draws a for a window width columns wide at the colour rung mode: the first part, its first option current, the marks of a UTF-8 locale.

func Prepare

func Prepare(a question.Ask) (question.Ask, error)

Prepare holds a to the structural check (question.Check) and returns it sanitised (Safe): the one door a question passes before it is drawn. Until the companion bundle's limits are run on this path, a question drawn in a plain Terminal is held to the structural check alone (spc-2610030911534855, Scope "Out"). A refused question is returned as a *Refusal naming every finding.

func Safe

func Safe(a question.Ask) question.Ask

Safe returns a copy of a with every part sanitised for the screen (spc-2610030911534855, "Sanitising before drawing"): question.Safe, the one copy the drawing and the AI-written interviews' check both use, so a question is measured and drawn as the same text.

Types

type Answer

type Answer struct {
	ID    string
	Value string
	Label string
	// Later is true when the way to decide later was chosen.
	Later bool
	// Typed is true when the answer is text typed for the question's typed
	// part: Value is then the text as typed, trimmed.
	Typed bool
}

Answer is one part's answer: the question's id and the value chosen, as the question carried them, with the label drawn for it.

type Frame

type Frame struct {
	Width int
	Mode  term.ColorMode
	// ASCII draws the marks without a UTF-8 locale (term.UTF8Locale false):
	// '>' for the glyph and '|' between tabs.
	ASCII bool
	// Tab is the part drawn, counted from zero, of an Ask with more than one.
	Tab int
	// Current is the current option, counted from zero; the number of options
	// is Later. A value outside that range marks no option.
	Current int
}

Frame is how a question is drawn: the window's width, the colour rung, the locale, and which part and which option are current.

type Key

type Key struct {
	Kind KeyKind
	Rune rune
}

Key is one key press.

func DecodeKeys

func DecodeKeys(b []byte) (keys []Key, rest []byte)

DecodeKeys decodes what one read from a raw terminal holds. A sequence is taken whole or not at all: an unknown CSI or SS3 sequence is dropped, a lone ESC at the end of the read is Escape (a terminal sends a sequence in one write), and a character split across reads is returned in rest for the next. Other control bytes are dropped.

type KeyKind

type KeyKind int

KeyKind is what one key press is to the answer loop.

const (
	KeyRune KeyKind = iota // a printable character: Key.Rune
	KeyUp
	KeyDown
	KeyPageUp
	KeyPageDown
	KeyHome
	KeyEnd
	KeyEnter
	KeyBackspace
	KeyEscape
	KeyLeft
	KeyRight
	KeyTab
	KeyInterrupt // Ctrl-C, the byte 0x03 under raw mode
	KeySuspend   // Ctrl-Z, the byte 0x1a under raw mode
)

The keys the arrow-key list acts on.

type Mode

type Mode int

Mode is how the answer loop takes a choice.

const (
	// Arrows is the arrow-key list with typing to narrow, in raw mode.
	Arrows Mode = iota
	// Numbered reads whole lines and never touches the terminal's modes.
	Numbered
)

func SelectMode

func SelectMode(getenv func(string) string, list string) (m Mode, why string)

SelectMode picks the mode (spc-2610030911534855, "The numbered fallback"; open question 2, decided to honour both variables): numbered under TERM=dumb, which cannot move its cursor; numbered for ABCD_ACCESSIBLE non-empty, else ACCESSIBLE non-empty, so a screen-reader user who cannot set a file first gets it for one session, and abcd's own variable is the one named when both are set; numbered for interview.list set to numbered; arrows otherwise. why says which, or is empty for the default.

func (Mode) String

func (m Mode) String() string

type Outcome

type Outcome int

Outcome is what a key leaves the list waiting on.

const (
	// Pending: the list waits for more keys.
	Pending Outcome = iota
	// Done: every part of the Ask is answered.
	Done
	// Interrupted: Ctrl-C. Nothing is recorded for the Ask.
	Interrupted
	// Suspended: Ctrl-Z. The loop stops the process and redraws on return.
	Suspended
)

func (Outcome) String

func (o Outcome) String() string

type Picker

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

Picker is the arrow-key list's state (spc-2610030911534855, "The answer loop: Arrow keys first, typing to narrow"): a pure function of key events, with no terminal, so every behaviour is a test of keys in and state out.

Up and Down move the current entry; Page Up and Page Down move a window; Home and End go to the ends, Later being the last. A printable character narrows: the typed text is matched by question.Matches against each answer's label and value, and Backspace widens, Escape clears. Text that is all digits is a number instead: it narrows nothing, and Enter chooses the entry with that number, which is the entry's place in the full list, so a number read off a narrowed list chooses what it names. Enter otherwise chooses the current entry, except that on a question with a typed part, Enter on text no answer matches answers with the text. Left, Right and Tab move between the parts of a tabbed Ask, and the Ask is done when every part is answered. Later is never narrowed away: when nothing matches, it is the current entry.

func NewPicker

func NewPicker(a question.Ask) *Picker

NewPicker is the list for a, its first part shown, each part's default entry current (its first, where the question names no default), nothing typed.

func (*Picker) Above

func (p *Picker) Above() int

Above and Below are how many kept answers lie above and below the window.

func (*Picker) Answers

func (p *Picker) Answers() []Answer

Answers is every answered part's answer, in the Ask's order.

func (*Picker) Apply

func (p *Picker) Apply(k Key) Outcome

Apply takes one key and returns what the list then waits on.

func (*Picker) Below

func (p *Picker) Below() int

func (*Picker) Collapsed

func (p *Picker) Collapsed(f Frame) []string

Collapsed is what stays on screen once the Ask is answered: one plain line per part, "› <chip>: <label>".

func (*Picker) Current

func (p *Picker) Current() int

Current is the current entry, as an index into the part's options; the number of options is Later.

func (*Picker) Lines

func (p *Picker) Lines(f Frame) (head, live []string)

Lines draws the list in the frame f: head is the part above the answers (the chip line, the material and the ask, then a blank line), drawn once per part; live is the rest, redrawn as keys arrive: the filter line, the window of kept answers with how many lie above and below, Later, and the Now: and Change later: lines. f's Tab and Current are the picker's.

func (*Picker) SetWindow

func (p *Picker) SetWindow(n int)

SetWindow sets how many entries the list shows at once, at least five.

func (*Picker) Shown

func (p *Picker) Shown() []int

Shown is the answers the filter keeps, as indices into the part's options (a long list's choices); Later is never among them and always offered.

func (*Picker) Tab

func (p *Picker) Tab() int

Tab is the part shown, counted from zero.

func (*Picker) Typed

func (p *Picker) Typed() string

Typed is the text typed into the part shown.

type Refusal

type Refusal struct {
	Findings []question.Finding
}

Refusal is a question the structural check refuses: it is never drawn.

func (*Refusal) Error

func (r *Refusal) Error() string

type Terminal

type Terminal struct {
	// In is the terminal the person answers at.
	In *os.File
	// Out is where the question is drawn.
	Out io.Writer
	// Getenv reads the environment (TERM, ABCD_ACCESSIBLE, ACCESSIBLE, and
	// COLUMNS and LINES for a size the terminal does not answer).
	Getenv func(string) string
	// Mode is the colour rung (term.ResolveColorMode).
	Mode term.ColorMode
	// ASCII draws the marks without a UTF-8 locale.
	ASCII bool
	// Roots are where Put reads the interview.list setting from
	// (layered.InterviewList): the machine's ~/.abcd.noindex/config.json under
	// Roots.Home, a repository's under Roots.Repo refused.
	Roots layered.Roots
}

Terminal is where the answer loop puts a question: the keyboard it reads, the stream it draws on, and how it draws. The question is drawn on Out, which a front door makes stderr, so a --json result on stdout stays clean.

A caller under signal.NotifyContext (or any handler of its own for SIGINT, SIGTERM or SIGHUP) must not draw: while an arrow-key list is up, such a signal from outside restores the terminal and is raised again with its default action (term.RawSession), so the process ends as on an unhandled signal, and neither the caller's handler nor any deferred cleanup runs.

func (Terminal) Put

func (t Terminal) Put(a question.Ask) ([]Answer, error)

Put asks a at the terminal and returns its answers, one per part. It holds a to the structural check first (as Prepare does), reads interview.list through layered.InterviewList (a refusal is said in one line on Out; a repository's fault is passed over for the machine's setting, a fault in the machine's own file gives the numbered reader), then takes the answer in the mode SelectMode picks; an arrow-key list the terminal cannot take (TERM=dumb, or raw mode refused) falls to the numbered reader with one line on Out saying so, never a failed interview. Ctrl-C returns ErrInterrupted.

Jump to

Keyboard shortcuts

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