Skip to content

Embedding

mike-ward edited this page Sep 2, 2026 · 4 revisions

Embedding go-term

go-term is a Go library. You embed it into a go-gui application. This page covers the two embedding layers:

  • term — the core widget. It gives you one terminal in one window.
  • term/workspace — the pane manager. It gives you tabs and split panes.

The examples/minimal directory holds a complete single-terminal program. The examples/falcon directory holds a complete workspace program.

Requirements

  • Go 1.26 or newer (the go directive in go.mod)
  • a go-gui window
  • macOS, Linux, or Windows

Add the module to your project:

go get github.com/go-gui-org/go-term@latest

Build against v0.9.0, the API-freeze release, or newer v0.10.x. What the docs document is what v1.0.0 will keep. The v0.10 breaking change (Cfg.CursorBlink from *bool to bool, plus CursorLocked) is the only one since the freeze.

The minimal embed

This program creates a window, starts a shell, and renders it:

package main

import (
	"log"

	"github.com/go-gui-org/go-gui/gui"
	"github.com/go-gui-org/go-gui/gui/backend"
	"github.com/go-gui-org/go-term/term"
)

func main() {
	gui.SetTheme(gui.ThemeDark.WithBorders(true))

	var tm *term.Term
	w := gui.NewWindow(gui.WindowCfg{
		Title:  "go-term minimal",
		Width:  800,
		Height: 500,
		OnInit: func(w *gui.Window) {
			var err error
			tm, err = term.New(w, term.Cfg{
				TextStyle: gui.TextStyle{Family: "monospace", Size: 14},
			})
			if err != nil {
				log.Fatalf("term.New: %v", err)
			}
			w.UpdateView(tm.View)
		},
	})
	defer func() {
		if tm != nil {
			_ = tm.Close()
		}
	}()

	backend.Run(w)
}

The steps:

  1. Create a gui.NewWindow with an OnInit callback.
  2. Call term.New inside OnInit, with the window and a term.Cfg.
  3. Pass tm.View to w.UpdateView.
  4. Call tm.Close when the window closes.

New starts the shell and its reader goroutine before it returns.

term.Cfg

term.Cfg carries the widget's options. Every field is optional. A zero value selects the built-in default.

Field Meaning
TextStyle Font family and size. Zero means gui.CurrentTheme().M5
Themes Named themes for runtime switching. The first entry is the initial theme
Command, Args The binary to run instead of $SHELL
Env Extra environment variables for the child, applied last
Dir Working directory for the child. Empty inherits the process CWD
Identity Value for TERM_PROGRAM. Defaults to go-term
ScrollbackRows Scrollback cap. Zero means 5000, negative disables it
BellMode How BEL is signalled: auto, audible, visual, both, none
BellFlashDuration Visual-bell overlay duration. Zero uses the built-in 100 ms. Negative disables it
ScrollbarWidth Scrollbar thumb width in px. Negative hides the scrollbar
MinimumContrast WCAG ratio floor, 1.0–21.0, applied at render time
MiddleClickPaste Middle-click pastes. Off by default
NotifyAfter Notify when a long command finishes while you look away
CursorStyle Initial cursor shape: block, underline, bar (exported term.CursorStyle)
CursorBlink Whether the cursor blinks. Seeds the cursor, not an override
CursorLocked When true, ignore the child's DECSCUSR shape/blink changes
AllowOSC52Write Permit child programs to write the clipboard. Off by default
DisableGraphics Skip sixel, Kitty, and iTerm2 image decoding
NoWindowHandler Do not install a window event handler. Set for pane managers
DownloadDir Where OSC 1337 file transfers land. Empty disables transfers
OnDownload Receives each OSC 1337 download. Nil with DownloadDir set uses the built-in writer
RecordPath Start a session recording at this path
RecordInput Record keystrokes into recordings. Off by default
KeyBindings Override the default chords for term-level actions

Lifecycle

New spawns the child process and the reader goroutine. It also starts the blink, scroll, momentum, write, and resize loops.

Call Close to kill the child, close the PTY, and stop the loops. Always call it when the window closes, or you leak file descriptors and goroutines. Close is idempotent.

Use Cfg.OnExit to detect child death. Use Term.Alive to poll it.

The exported methods are safe to call from any goroutine: View, Close, Cwd, Theme, SetTheme, Rows, Cols, Write, PID, Alive, SetFocused, HandleWindowEvent, SetMinimumContrast, SetKeyBindings, SetNotifyAfter, SetCursorStyle, SetCursorBlink, SetCursorLocked, StartRecording, StopRecording, SendInput.

Callbacks

Cfg carries callbacks for the events a host cares about:

Callback Fires Thread
OnTitle The child sets the window title (OSC 0/1/2) main
OnNotify A child asks for a desktop notification (OSC 9/777) background
OnActivity Bell or OSC 133 command end (ActivityKind: Bell, CommandDone, CommandFailed) main
OnExit The child process exits reader
OnClickFocus The user clicks the canvas main
OnInput User input is about to go to the child main

With OnTitle nil, the widget sets the window title itself. That is right for a single-terminal window. A pane manager sets OnTitle to keep titles per pane.

OnInput receives every user byte sequence. It runs alongside the local write and cannot suppress it. A pane manager mirrors input to sibling panes with it. Mouse reporting and focus reports are excluded on purpose.

OnActivity fires once per explicit event — a BEL or an OSC 133 D mark — not per PTY read. Plain screen output is not reported: a timer-driven app would otherwise mark every background tab permanently. Command kinds need shell integration (OSC 133).

Themes

Themes are named color schemes. NamedTheme pairs a display name with a Theme.

term.BundledThemes returns the bundled corpus, sorted by name. There are 602 themes. term.DefaultTheme is the fallback used when Cfg.Themes is empty.

Register Default first and the corpus after it. The first entry seeds the grid and decides COLORFGBG at spawn. That value cannot be corrected once the child runs, so the order matters.

themes := []term.NamedTheme{
	{Name: "Default", Theme: term.DefaultTheme},
}
themes = append(themes, term.BundledThemes()...)

Theme.IsDark reports the light or dark character of a theme. The child asks the same question through DSR ?996.

A light theme does not fix an application's own colors. A truecolor SGR is not themeable. Cfg.MinimumContrast is the render-time floor that forces text above a contrast ratio against its cell background. The grid keeps the color the child sent, so copy, search, and recordings are unaffected.

The child environment

The child environment is the parent's, with the host terminal identity scrubbed and TERM, COLORTERM, and COLORFGBG set for the pane. Entries in Cfg.Env apply last, so they win over all of it, including TERM_PROGRAM.

TERM_PROGRAM is how TUI file managers choose their image protocol. yazi and superfile use the Kitty Graphics Protocol under a name they recognize and fall back to sixel otherwise. Set Cfg.Identity (or an Env entry) to name a known emulator and get its image quality. Falcon advertises Falcon.

Multi-terminal windows

A window with several panes is a pane manager. Set Cfg.NoWindowHandler to true so the widget does not install its own window event handler.

Then the manager owns the window-level event dispatch. It routes events to the focused pane:

  • keyboard input to the focused pane's View container
  • Term.HandleWindowEvent for the rest

Use Term.SetFocused to move focus between panes. OnClickFocus tells you when the user clicks a pane. OnActivity tells you when a background pane did something.

Term.Rows, Term.Cols, Term.Write, Term.PID, Term.Alive, and Term.Cwd support introspection without touching internal state. Term.SendInput replays captured input onto a pane, per its bracketed-paste state.

The workspace layer

term/workspace is the ready-made pane manager. It builds a multi-tab, multi-pane window on top of term, and it reads the user config file.

ws, err := workspace.New(w, workspace.Cfg{
	TextStyle:  defaultTextStyle(),
	Themes:     themeList(),
	Identity:   "Falcon",
	ConfigPath: "",
	SavePath:   defaultSavePath, // where Cmd+S writes the layout
})
if err != nil {
	log.Fatal(err)
}
w.UpdateView(ws.View)

workspace.Cfg adds the window-level fields:

Field Meaning
Identity TERM_PROGRAM for every pane. Defaults to go-term
ConfigPath Path to the user config file. Empty uses the default location
SavePath Where Cmd+S writes the layout. Empty uses the default workspace path
RecordDir Where Cmd+Shift+R recordings land
RecordInput Record keystrokes into recordings
DownloadDir Where OSC 1337 transfers land. Empty disables them
ExitWhenLastShellExits Close the window when the last shell exits
OnLastShellExit Runs instead of the close when the last shell exits
OnColorScheme Reports light/dark changes, so the host can theme its chrome

The workspace wires the multi-pane callbacks for you: focus, activity, bell, title, exit, and broadcast input.

Save and restore

Workspace.Save(path) writes the tab and pane layout to a JSON file, atomically. workspace.Restore(w, cfg, path) rebuilds the workspace from that file. The workspace.save command (default Cmd+S) triggers the save at runtime. It writes to Cfg.SavePath when set. Otherwise it uses the default workspace path.

Restore falls back to a fresh workspace on a missing, unparseable, or version-mismatched file. A bad workspace file never blocks startup.

workspace.DefaultWorkspacePath and workspace.DefaultConfigPath return the standard locations, resolved against $XDG_CONFIG_HOME, ~/.config, or os.UserConfigDir() (<base>/go-term/config and <base>/go-term/workspace.json).

Falcon overrides both since v0.10: it derives <base>/falcon/config and <base>/falcon/workspace.json from the default and passes them as Cfg.ConfigPath / SavePath, so both files live under the application name. It restores on startup and saves on quit, with two paths so a read-only template layout is possible. See Falcon.

The config file

The workspace layer reads the user config file: an INI-style file that overrides fonts, theme, scrollback, bell, scrollbar, cursor, keybindings, and the child environment. It can be reloaded without restarting. The full reference is on the Configuration page.

Session replay

term.NewReplay returns a Term that plays back a .gtr recording instead of spawning a shell. The widget runs the entire normal stack — parser, grid, rendering, scrollback, search, graphics — against recorded bytes.

ReplayCfg controls playback:

Field Meaning
Path The .gtr file to play. Required
Speed Playback multiplier. 2 plays twice as fast
IdleLimit Caps any single gap between frames. Zero means no cap
Loop Restart at the end of the stream
Controls Interpret space, +/-, ., and 0 as playback commands

Cfg.Command, Args, Env, and Dir are ignored for replay: no child exists. The widget holds on the final frame until Close.

Recordings store the PTY bytes verbatim. A rendering bug reproduces in replay, which makes recordings the preferred way to file a bug report. Record them with StartRecording, or with falcon's Cmd+Shift+R. The gotermrec CLI inspects and converts them. See Usage.

Latency instrumentation

GOTERM_LATENCY measures the delay between a keystroke and its frame. Set it to 1 (or to a sample-batch size). Every 25 keystrokes, go-term logs the percentiles for these spans:

  • key→echo: the round trip through the child
  • echo→wake: the reader goroutine to the main thread
  • wake→paint: the frame itself
  • the total of the three spans
  • the time that onDraw ran
  • the number of PTY reads that landed while the keystroke was outstanding

The variable is off by default. It does nothing when off. The numbers are a lower bound: GPU submit, compositor, and vsync are invisible from inside the process.

GOTERM_LATENCY=1 go run . # in examples/falcon

API stability

The public surface was frozen at v0.9.0, the release where the export audit and Godoc pass landed, and held through v0.10.0. What the docs document is what v1.0.0 keeps. Build against v0.9.0 or newer.

v0.10.0 shipped one breaking change: Cfg.CursorBlink from *bool to bool (now seeds rather than overrides; the override half moved to the new Cfg.CursorLocked which covers shape and blink together) and the internal cursorShape became the exported term.CursorStyle. nil becomes false; *true becomes CursorBlink: true, CursorLocked: true.

The kept surface is deliberately small — the widget, themes, actions, recording and replay, the live setters, and the activity/input taps. Everything that pkg.go.dev documents is stable. Names not documented there are internal:

  • Term is an opaque handle. All fields are unexported. Embedders interact through methods only.
  • term/workspace keeps Workspace, New, Restore, Close, View, Cfg, Save, DefaultWorkspacePath, DefaultConfigPath, LiveTermCount, and ActivePane. The per-tab and keyboard-command methods (AddTab, SplitPane, NextPane, and the rest) are unexported. A pane manager is one window with one workspace. Its commands run through the keybindings.
  • Import only the term package. The source-file organisation is not a contract.

Pin v0.9.0 or newer in go.mod. Use Cfg zero values for everything you do not set. Read the changelog before a minor upgrade.

Clone this wiki locally