runner

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package runner runs an interactive subprocess from inside a Bubble Tea program: it suspends the TUI (releasing the terminal so the subprocess can take over stdin/stdout/stderr), executes the command, then re-enters the alt-screen once the subprocess exits.

Use it for editors ($EDITOR), pagers (less, man), full-screen TUIs (htop, k9s), or one-shot interactive commands (ssh, kubectl exec). For the duration of the run the TUI is fully suspended — the subprocess owns the terminal.

Usage:

// dispatch from a screen's Update on some key:
cmd := exec.Command(os.Getenv("EDITOR"), "/tmp/scratch")
return s, runner.Run(cmd)

// receive the result on a later Update tick:
case runner.Result:
    s.last = msg // msg.Cmd.ProcessState is populated; msg.Err is the run error

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Capture added in v0.19.0

func Capture(cmd *exec.Cmd) tea.Cmd

Capture runs cmd without handing the terminal over, streaming its stdout and stderr back as messages while the TUI stays live.

This is the counterpart to Run, not a mode of it. Run suspends the program and gives the subprocess the real TTY, which is right for an editor, a pager, or htop — and which means the output is gone the moment the TUI repaints. Capture is for the other kind of subprocess: the one whose output you actually want to read. Teeing a full-screen program would be meaningless, so the two cannot be the same call.

The message sequence is CaptureStarted, then a CapturedLine per line, then exactly one Captured. Each message carries the handle needed to ask for the next one — see Next — so the consumer drives the read at its own pace:

case runner.CaptureStarted, runner.CapturedLine:
    return s, runner.Next(msg)

The app shell does this for you when app.Options.OutputKey is set, and forwards every message on to the active screen besides. Nothing here depends on tuilib: the shell translates these messages into log records, because the log format, the source attribution and the read-marker are shell knowledge, not runner knowledge.

Backpressure is real and deliberate: the stream buffers a bounded number of lines and a consumer that stops calling Next will eventually stall the subprocess rather than grow memory without limit.

func CaptureWith added in v0.19.0

func CaptureWith(opts CaptureOptions) tea.Cmd

CaptureWith runs a subprocess with the given options. See Capture.

func Kill added in v0.19.0

func Kill(m CaptureStarted) error

Kill stops the subprocess behind a CaptureStarted, and everything it spawned.

The "and everything it spawned" is the part that matters. Captures are usually a shell wrapping a build, and killing the shell alone leaves the compiler running — still writing into a pipe nobody reads, with no handle left to stop it by. On Unix the whole process group is signalled; see setProcessGroup, and capture_windows.go for what Windows can't do here.

Safe to call on a run that never started or has already exited — both report nil, since in either case there is nothing left to stop.

func Next added in v0.19.0

func Next(msg tea.Msg) tea.Cmd

Next returns the command that reads the next message from the capture that msg belongs to, or nil for anything else — including Captured, after which there is nothing more to read.

func Run

func Run(cmd *exec.Cmd) tea.Cmd

Run returns a tea.Cmd that suspends the program, runs cmd connected to the controlling terminal, and posts a Result when the subprocess exits. The screen is cleared before the subprocess starts (use RunWith with NoClear=true to opt out).

Plumbing the runner takes care of:

  • Stdin/Stdout/Stderr default to os.Stdin/Stdout/Stderr (real TTY file descriptors) when not already set, so the subprocess gets direct terminal access and TIOCGWINSZ works.
  • LINES and COLUMNS env vars are populated from the current terminal size, as a fallback for ncurses-style programs that miss the post-resume SIGWINCH on some terminal emulators (htop, top, less are the usual suspects).

func RunWith

func RunWith(opts Options) tea.Cmd

RunWith runs an interactive subprocess with the given options. See Options for the available knobs (notice, screen-clear).

func RunWithNotice

func RunWithNotice(cmd *exec.Cmd, notice string) tea.Cmd

RunWithNotice is shorthand for RunWith(Options{Cmd: cmd, Notice: notice}). The screen is cleared before the notice is printed.

Types

type CaptureOptions added in v0.19.0

type CaptureOptions struct {
	// Cmd is the subprocess to run. Required.
	Cmd *exec.Cmd
	// Label names the run in the log and in the kill picker. Defaults to
	// the command's base name, which is also what the log uses as the
	// line's Source — for captured output the honest answer to "what
	// produced this line" is the command, not the screen that launched it.
	Label string
}

CaptureOptions configures CaptureWith.

type CaptureStarted added in v0.19.0

type CaptureStarted struct {
	RunID int64
	Label string
	Cmd   *exec.Cmd
	// contains filtered or unexported fields
}

CaptureStarted is delivered when a capture begins. It carries the *exec.Cmd so a consumer can retain a kill handle: nothing else in the sequence offers one, and by the time Captured arrives there is nothing left to signal.

type Captured added in v0.19.0

type Captured struct {
	RunID int64
	Label string
	Cmd   *exec.Cmd
	Err   error
	// contains filtered or unexported fields
}

Captured is delivered once, after the last CapturedLine, when the subprocess has exited. Err is the *exec.ExitError for a non-zero exit, or the start error when the process never ran.

type CapturedLine added in v0.19.0

type CapturedLine struct {
	RunID  int64
	Label  string
	Text   string
	Stderr bool
	// contains filtered or unexported fields
}

CapturedLine is one line of subprocess output.

type Options

type Options struct {
	// Cmd is the subprocess to run. Required.
	Cmd *exec.Cmd
	// Notice, when non-empty, is printed once to stderr after the TUI
	// suspends and before the subprocess starts. Use it for slow handoffs
	// (kubectl exec, ssh, anything with a perceptible connect latency) so
	// the user sees feedback instead of a blank gap. The subprocess is
	// free to clear the screen on startup; that's fine, the goal is
	// feedback during the handoff, not a persistent banner.
	Notice string
	// NoClear suppresses the screen clear that normally precedes the
	// subprocess. By default the terminal is cleared so the alt-screen
	// exit doesn't leave TUI artifacts visible during commands that
	// don't repaint (sh -c, echo, short scripts). Set NoClear=true to
	// preserve whatever was on the normal screen prior to the TUI.
	NoClear bool
}

Options configures RunWith. The zero value clears the screen and prints no notice — the right defaults for typical interactive subprocesses.

type Result

type Result struct {
	Cmd *exec.Cmd
	Err error
}

Result is delivered to your screen's Update when the subprocess exits. Cmd is the same *exec.Cmd you submitted (its ProcessState is populated by the OS); Err is non-nil when the process failed to start or exited with a non-zero status (the typical *exec.ExitError).

Jump to

Keyboard shortcuts

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