termui

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: GPL-2.0 Imports: 18 Imported by: 0

README

termui

The terminal as a self-contained vtui component — the embedding surface used by thrupty's vtui hosts and by the planned f4 integration (see F4_INTEGRATION.md).

  • Session owns the terminal goroutine: reads child output into the ByteRing, parses, writes input to the child, waits for exit. The UI thread never blocks on pty I/O — it enqueues vtinput events and reads versioned grid snapshots (Snapshot), re-laying out once per frame only when the parser marked the content dirty.
  • TerminalUI (vtui.UIElement) and TerminalFrame (vtui.Frame) draw a snapshot into a vtui.ScreenBuf, so the terminal becomes a regular frame in any vtui FrameManager (console ANSI, gogpu, X11, Wayland).
  • CommandLine + CmdLineFrame + CmdLineSwitcher implement thrupty's own line discipline for the fast pipes mode: a far-style command-line editor with history (FileHistoryProvider, persisted to os.UserConfigDir()/thrupty/history.json), completion, and busy-switching between the editor and the running child.
sess := termui.NewSession(term, backend) // backend: pty.PtyBackend
ui := termui.NewTerminalUI(sess)

frame := termui.NewTerminalFrame(ui, cols, rows)
vtui.FrameManager.Push(frame)

// The frame callback drives layout pacing and input:
//   sess.EventChan <- ev        // user input, translated for the child
//   snap := sess.Snapshot()     // versioned grid for painting

Documentation

Overview

Package termui extracts the terminal into a self-contained component (TerminalUI) with its own goroutine, step 3 of the vtui integration roadmap (see AGENTS.md, Phase 5).

The terminal thread owns: input events from EventChan, resize requests, writes to the child process and waiting for the child to exit. The UI thread never blocks on pty writes or input translation: it enqueues vtinput events and, once per frame, asks for a layout (LayoutIfDirty) and reads the published Snapshot.

Layout stays frame-paced by design: frameFunc drives LayoutIfDirty when the parser marked the content dirty (the read-side flood pacing — 4 MiB cap per read plus the dirty flag — depends on it). A self-paced layout on the terminal thread would break that pacing, so the terminal thread only lays out on resize.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BGRToRGB

func BGRToRGB(c uint32) uint32

BGRToRGB swaps the terminal's 0x00BBGGRR cell colors into vtui's 0xRRGGBB (the inverse of vtuibridge.RGBToBGR).

func CellToCharInfo

func CellToCharInfo(c *termcell.TextCell, wide uint8) vtui.CharInfo

CellToCharInfo converts one terminal text cell (plus its Wide flags byte, 0 when the cell is not part of a wide glyph) into a vtui CharInfo.

func DefaultHistoryPath

func DefaultHistoryPath() string

DefaultHistoryPath returns os.UserConfigDir()/thrupty/history.json.

Types

type CmdLineAction

type CmdLineAction int

CmdLineAction tells the caller how to change the frame stack.

const (
	CmdLineNoAction CmdLineAction = iota
	CmdLineHide                   // child went busy: detach the frame
	CmdLineShow                   // child idle again: re-attach the same frame
)

type CmdLineFrame

type CmdLineFrame struct {
	vtui.BaseFrame
	CL *CommandLine
}

CmdLineFrame is the minimal vtui frame hosting the pipes-mode CommandLine: a single line docked to the bottom of the screen. It is deliberately NOT modal and never "done" — busy switching only detaches it from the frame stack, the CommandLine instance (text, cursor, history) is never recreated.

func NewCmdLineFrame

func NewCmdLineFrame(cl *CommandLine, cols, rows int) *CmdLineFrame

NewCmdLineFrame docks a CommandLine to the bottom line of a cols×rows console.

func (*CmdLineFrame) Close

func (f *CmdLineFrame) Close()

Close is a no-op: the command line lives as long as its session, so the frame must never become "done" (the FrameManager would garbage-collect it from the stack).

func (*CmdLineFrame) GetType

func (f *CmdLineFrame) GetType() vtui.FrameType

GetType returns TypeUser: the frame is neither a dialog nor a menu.

func (*CmdLineFrame) ProcessKey

func (f *CmdLineFrame) ProcessKey(e *vtinput.InputEvent) bool

ProcessKey forwards to the command line (which also handles focus events).

func (*CmdLineFrame) ProcessMouse

func (f *CmdLineFrame) ProcessMouse(e *vtinput.InputEvent) bool

ProcessMouse forwards to the command line.

func (*CmdLineFrame) ResizeConsole

func (f *CmdLineFrame) ResizeConsole(w, h int)

ResizeConsole re-docks the frame to the bottom line of the new size.

func (*CmdLineFrame) SetPosition

func (f *CmdLineFrame) SetPosition(x1, y1, x2, y2 int)

SetPosition moves the frame and the command line with it.

func (*CmdLineFrame) Show

func (f *CmdLineFrame) Show(scr *vtui.ScreenBuf)

Show renders the command line (prompt + Edit control).

type CmdLineMode

type CmdLineMode int

CmdLineMode is the input state of a pipes-mode session.

const (
	// CmdLineEdit: the command-line frame is on the overlay stack and owns
	// all input.
	CmdLineEdit CmdLineMode = iota
	// CmdLinePassthrough: the frame is detached, input flows to the
	// terminal (and from there to the busy child) as in PTY mode.
	CmdLinePassthrough
)

type CmdLineSwitcher

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

CmdLineSwitcher is the pure busy-switching state machine, kept free of FrameManager calls so tests can drive it directly. The caller applies the returned actions to the real stack (all on the UI thread).

func NewCmdLineSwitcher

func NewCmdLineSwitcher() *CmdLineSwitcher

NewCmdLineSwitcher starts in edit mode: the pipes shell is not busy at startup and the frame is pushed right away.

func (*CmdLineSwitcher) Mode

func (s *CmdLineSwitcher) Mode() CmdLineMode

Mode reports the current input state.

func (*CmdLineSwitcher) Update

func (s *CmdLineSwitcher) Update(busy bool) CmdLineAction

Update folds one busy-poll sample into the state machine.

type CommandLine

type CommandLine struct {
	vtui.ScreenObject
	Edit       *vtui.Edit
	RichPrompt []vtui.CharInfo

	// OnEnter is called with the line text after Enter; the CommandLine
	// itself never writes to the session.  Runs on the FrameManager
	// goroutine.
	OnEnter func(text string)
	// contains filtered or unexported fields
}

CommandLine is the pipes-mode shell input line: a vtui.Edit with a rich prompt, modeled on f4's command_line.go. One instance lives for the whole session — busy switching only detaches its frame from the stack, so text, cursor and history survive.

func NewCommandLine

func NewCommandLine() *CommandLine

NewCommandLine creates the editor with shell-style history settings and loads the persisted history (when a vtui.GlobalHistoryProvider is set).

func (*CommandLine) Clear

func (cl *CommandLine) Clear()

Clear empties the command line text.

func (*CommandLine) InsertString

func (cl *CommandLine) InsertString(text string)

InsertString adds text to the command line.

func (*CommandLine) IsEmpty

func (cl *CommandLine) IsEmpty() bool

IsEmpty returns true if there is no text in the command line.

func (*CommandLine) ProcessKey

func (cl *CommandLine) ProcessKey(e *vtinput.InputEvent) bool

ProcessKey handles Enter (history + OnEnter), delegates everything else to the Edit control, exits history-browsing mode on edits and opens the autocomplete menu on editing keys (f4 command_line.go:88-121, without the AppConfig gate).

func (*CommandLine) ProcessMouse

func (cl *CommandLine) ProcessMouse(e *vtinput.InputEvent) bool

ProcessMouse delegates to the Edit control.

func (*CommandLine) RefreshPrompt

func (cl *CommandLine) RefreshPrompt()

RefreshPrompt rebuilds the rich prompt for the current width, re-reading the working directory every time (tracking the child's cwd is out of v1 scope).

func (*CommandLine) SetFocus

func (cl *CommandLine) SetFocus(f bool)

SetFocus propagates the focus state to the Edit control.

func (*CommandLine) SetPosition

func (cl *CommandLine) SetPosition(x1, y1, x2, y2 int)

SetPosition moves the line; the Edit control starts right after the prompt.

func (*CommandLine) Show

func (cl *CommandLine) Show(scr *vtui.ScreenBuf)

Show draws the prompt and the Edit control.

type FileHistoryProvider

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

FileHistoryProvider implements vtui.HistoryProvider with JSON file persistence (modeled on f4's history_provider.go): one map of history-ID to string list, rewritten on every save.

func NewFileHistoryProvider

func NewFileHistoryProvider(path string) *FileHistoryProvider

NewFileHistoryProvider loads (or starts) the history store at path.

func (*FileHistoryProvider) LoadHistory

func (hp *FileHistoryProvider) LoadHistory(id string) []string

LoadHistory returns a copy of the stored list for id (nil when unknown).

func (*FileHistoryProvider) SaveHistory

func (hp *FileHistoryProvider) SaveHistory(id string, history []string)

SaveHistory stores the list for id and persists the whole store.

type Session

type Session struct {
	// Term is the terminal state (text cells are the single source of
	// truth).  Exported: the reader goroutine and the UI thread already
	// synchronize on Term.Lock.
	Term *terminal.Terminal

	// EventChan is the input queue consumed by Run on the terminal
	// thread.  Senders must use TerminalUI.Enqueue (non-blocking, drop
	// policy, close-safe); never send while holding Term.Lock.
	EventChan chan *vtinput.InputEvent
	// contains filtered or unexported fields
}

Session owns one terminal and its child process I/O (a PtyBackend in shell mode, anonymous pipes in --exec mode, nothing in --demo mode).

func NewExecSession

func NewExecSession(term *terminal.Terminal, stdout io.Reader, stdin io.Writer, child *exec.Cmd) *Session

NewExecSession creates a session around an already-started exec.Cmd on anonymous pipes (the --exec mode: no PTY, no SetSize) and spawns the reader goroutine.

func NewSession

func NewSession(term *terminal.Terminal, backend pty.PtyBackend) *Session

NewSession creates a session around an already-started PTY backend and spawns the reader goroutine pumping child output into the terminal. A nil backend is the demo mode: no child, no reader, Write is a no-op.

func (*Session) Close

func (s *Session) Close()

Close stops the terminal thread and the child process. Idempotent.

func (*Session) Done

func (s *Session) Done() <-chan struct{}

Done is closed when Run returns (after Close).

func (*Session) Exited

func (s *Session) Exited() <-chan struct{}

Exited is closed when the child process ends (the shell exited or crashed). Hosts use it to shut down with the session — thrupty is a single-session application for now.

func (*Session) IsBusy

func (s *Session) IsBusy() bool

IsBusy reports whether the shell is running a foreground command. PTY mode delegates to the backend; exec (pipes) mode checks whether the child has children of its own. Demo mode is never busy.

func (*Session) LayoutIfDirty

func (s *Session) LayoutIfDirty() bool

LayoutIfDirty runs a layout pass when the parser produced new output and publishes the result. Called from the UI thread (frameFunc) — layout is deliberately frame-paced. Reports whether a layout happened.

func (*Session) Resize

func (s *Session) Resize(cols, rows int)

Resize requests a new console size. Non-blocking: a pending stale request is evicted, only the newest size matters.

func (*Session) Run

func (s *Session) Run(inputHandler func(*vtinput.InputEvent))

Run is the terminal thread loop: it consumes input events (handed to inputHandler, which translates and writes them back via Write) and resize requests until Close. Blocking; run it on its own goroutine.

func (*Session) Snapshot

func (s *Session) Snapshot() Snapshot

Snapshot returns the latest published grid by value.

func (*Session) Write

func (s *Session) Write(b []byte)

Write delivers bytes to the child process. It is the ONLY way anything writes to the child (input translation, terminal answers, OSC 52, far2l) and is serialized: the PTY backend is also written from the reader goroutine (OnWriteAnswer), which used to race with UI-thread writes.

type Snapshot

type Snapshot struct {
	Version      uint64
	Cells        []termcell.TextCell
	Wide         []uint8
	DimX, DimY   int
	CursorX      int32
	CursorY      int32
	CursorHidden bool
	CursorShape  int // DECSCUSR (0/1/2 block, 3/4 underline, 5/6 bar)
}

Snapshot is a versioned, immutable view of the terminal text grid, published after every layout pass. Slices are shared with previous snapshots but never mutated after publication: each publish allocates fresh ones. Readers take it by value under a short RLock.

type TerminalFrame

type TerminalFrame struct {
	vtui.BaseFrame
	Term *TerminalUI
}

TerminalFrame wraps a TerminalUI so the FrameManager can host it. The frame is never "done" and never modal: it lives as long as its session.

func NewTerminalFrame

func NewTerminalFrame(ui *TerminalUI, cols, rows int) *TerminalFrame

NewTerminalFrame covers the whole cols×rows console with the terminal.

func (*TerminalFrame) Close

func (f *TerminalFrame) Close()

Close is a no-op: the frame lives as long as its session and must never become "done" (the FrameManager would garbage-collect it from the stack).

func (*TerminalFrame) GetType

func (f *TerminalFrame) GetType() vtui.FrameType

GetType returns TypeUser: the frame is neither a dialog nor a menu.

func (*TerminalFrame) ProcessKey

func (f *TerminalFrame) ProcessKey(e *vtinput.InputEvent) bool

ProcessKey intercepts the host-level shortcuts (F2 demo window, Ctrl+Shift+V paste) and enqueues everything else to the terminal thread.

func (*TerminalFrame) ProcessMouse

func (f *TerminalFrame) ProcessMouse(e *vtinput.InputEvent) bool

ProcessMouse forwards to the terminal element (which shifts the coordinates to terminal-relative cells before enqueueing).

func (*TerminalFrame) ResizeConsole

func (f *TerminalFrame) ResizeConsole(w, h int)

ResizeConsole re-covers the whole console after a host resize.

func (*TerminalFrame) SetPosition

func (f *TerminalFrame) SetPosition(x1, y1, x2, y2 int)

SetPosition moves the frame and the terminal element with it (a size change also notifies the session via TerminalUI.SetPosition).

func (*TerminalFrame) Show

func (f *TerminalFrame) Show(scr *vtui.ScreenBuf)

Show lays out pending output (event-paced: cheap when not dirty) and paints the latest snapshot into the ScreenBuf, clipped to both the frame's rect and the snapshot dimensions — they can disagree for a frame or two right after a resize. The whole rect is always painted (snapshot first, terminal default background for the rest): the bottom frame is opaque, so nothing below has to show through.

type TerminalUI

type TerminalUI struct {
	vtui.ScreenObject

	Session *Session

	// OnDrop, if set, is called when EventChan is full and a non-mouse-move
	// event is dropped.  Mouse moves drop silently (the next move
	// coalesces the position anyway).
	OnDrop func(e *vtinput.InputEvent)
}

TerminalUI is the terminal as a vtui UIElement: a movable rectangle (X1..Y2, absolute vtui cell coordinates) hosting one Session. Input is only enqueued into the session's EventChan — translation and the (potentially blocking) child write happen on the terminal thread, never on the caller's.

func NewTerminalUI

func NewTerminalUI(sess *Session) *TerminalUI

NewTerminalUI wraps a session in a UI element covering the whole terminal grid at the origin.

func (*TerminalUI) Enqueue

func (t *TerminalUI) Enqueue(e *vtinput.InputEvent)

Enqueue delivers an event to the terminal thread. Non-blocking: a full queue drops the event. Never call while holding the terminal lock.

func (*TerminalUI) ProcessKey

func (t *TerminalUI) ProcessKey(e *vtinput.InputEvent) bool

ProcessKey implements vtui.UIElement: the event is queued for the terminal thread and always consumed.

func (*TerminalUI) ProcessMouse

func (t *TerminalUI) ProcessMouse(e *vtinput.InputEvent) bool

ProcessMouse implements vtui.UIElement. vtui coordinates are absolute; the terminal is a rectangle, so the event is shifted to terminal-relative cells before enqueueing.

func (*TerminalUI) ResizeConsole

func (t *TerminalUI) ResizeConsole()

ResizeConsole implements vtui.UIElement: the session is asked (by message, not in place) to adopt the element's current size.

func (*TerminalUI) SetPosition

func (t *TerminalUI) SetPosition(x1, y1, x2, y2 int)

SetPosition moves/resizes the element. A size change (not a bare move) also notifies the session.

func (*TerminalUI) Show

func (t *TerminalUI) Show(scr *vtui.ScreenBuf)

Show is a placeholder: rendering the session's TextCell grid into the vtui ScreenBuf (TextCell -> CharInfo) is roadmap step 5. For now the terminal paints itself through the GPU renderer, so this only maintains the visibility flag.

Jump to

Keyboard shortcuts

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