Documentation
¶
Overview ¶
Package term owns the terminal itself: taking it over, giving it back, reading what it sends, and writing frames to it without blocking whoever drew them.
It is the only package in the TUI that touches the operating system. Everything above it works in cells, events and frames, and can be tested without a terminal at all — which is the point of putting the parts that need one here and nowhere else.
Index ¶
- Constants
- Variables
- func DetectDepth(lookup func(string) (string, bool)) grid.Depth
- func DetectLocale(lookup func(string) (string, bool)) string
- func Relaunch(argv, env []string) (code int, err error)
- func Suspend() error
- type Config
- type Features
- type Modes
- type Progress
- type ProgressState
- type Terminal
- func (t *Terminal) Attributes() (input.DeviceAttributes, bool)
- func (t *Terminal) Bell()
- func (t *Terminal) CellSize() (image.Point, bool)
- func (t *Terminal) Close() error
- func (t *Terminal) Color() grid.Depth
- func (t *Terminal) Copy(text string) bool
- func (t *Terminal) Events() <-chan input.Event
- func (t *Terminal) Graphics() graphics.Protocol
- func (t *Terminal) Ground() grid.Ground
- func (t *Terminal) Hand(run func() error) (err error)
- func (t *Terminal) InputErr() error
- func (t *Terminal) Keyboard() (input.KeyboardFeatures, bool)
- func (t *Terminal) Locale() string
- func (t *Terminal) Name() (string, bool)
- func (t *Terminal) Notify(text string)
- func (t *Terminal) Paste() bool
- func (t *Terminal) ReportDirectory(path string) error
- func (t *Terminal) SetProgress(progress Progress)
- func (t *Terminal) SetTitle(s string)
- func (t *Terminal) Size() (w, h int, err error)
- func (t *Terminal) Transmit(png []byte) (graphics.Image, error)
- func (t *Terminal) Version() (input.DeviceVersion, bool)
- func (t *Terminal) Wheel() input.Wheel
- func (t *Terminal) Writer() *Writer
- type Writer
Constants ¶
const DrainGrace = 250 * time.Millisecond
DrainGrace is how long to wait for queued frames to reach the terminal before abandoning them. A terminal that has stopped accepting bytes must not be able to hold up an exit, and no amount of waiting makes one start accepting them again.
It is what Writer.Close waits, and the right answer for anyone else with a reason to wait for the terminal to catch up.
const KeyboardCompatible = input.KeyboardDisambiguate | input.KeyboardReportAlternates
KeyboardCompatible is the portable keyboard enhancement set. It makes modified keys unambiguous and reports alternate-layout keycodes without asking for release events or turning ordinary text into escape sequences. Applications needing those less widely reliable behaviours add their features explicitly.
Variables ¶
var ErrClosed = errors.New("term: writer closed")
ErrClosed marks a frame that was handed over after the writer became unusable, or that was still queued when its shutdown grace period ran out. Such a frame is abandoned rather than written. When a terminal write caused the transition, Writer.Err preserves that original failure.
var ErrDrainTimeout = errors.New("term: writer did not drain")
ErrDrainTimeout means accepted frames did not settle before their caller's deadline. The caller still owns the display: handing it elsewhere would let a late frame arrive in the next owner's output.
var ErrImageIDsExhausted = errors.New("term: image identities exhausted")
ErrImageIDsExhausted means a terminal session has used every image identity the graphics protocol can represent. Reusing one would make a new image overwrite an older image that can still be present, so the session refuses another transmission.
var ErrNotTerminal = errors.New("term: not a terminal")
ErrNotTerminal is reported by Open when the process is not attached to a terminal — piped, redirected, or running under something that gave it no tty.
var ErrRelaunch = errors.New("term: relaunch")
ErrRelaunch is reported when a relaunch could not be started at all, as opposed to starting and then failing on its own terms.
Functions ¶
func DetectDepth ¶
DetectDepth works out how much colour a terminal can show from its environment.
This is the one place the library detects rather than asks. Everything else a terminal might not support is requested and ignored if unimplemented, which costs nothing when the guess is wrong. Colour is not like that: a truecolor sequence sent to a terminal that cannot read it does not degrade, it prints wrong, and there is no request that fails safely.
What it reads, in the order it reads it:
- NO_COLOR, set to anything at all, means no colour. That is the whole of the convention, including the part where an empty value still counts.
- COLORTERM naming truecolor or 24-bit means truecolor. Terminals that mean it say so here.
- TERM of "dumb", or no TERM at all, means no colour.
- TERM mentioning 256 means the 256-colour palette.
- Anything else is truecolor.
That last line is a decision worth stating. Plenty of terminals handle 24-bit colour and describe themselves as plain "xterm", so treating an unrecognised TERM as sixteen colours would make the common case worse to fix the rare one. A caller that knows better can use its own answer instead.
lookup reports value and presence separately, so an empty NO_COLOR remains distinguishable from an absent one. It is explicit because the environment belongs to the terminal being driven, which may be a PTY or SSH client rather than this process.
func DetectLocale ¶ added in v0.11.0
DetectLocale returns the locale that decides character encoding in an explicit terminal environment. LC_ALL overrides every category, LC_CTYPE overrides the language default, and LANG is the fallback. An empty string means the environment made no claim.
func Relaunch ¶ added in v0.0.2
Relaunch starts argv again in place of this process, keeping the terminal.
It is what a session does to change something only startup can decide — which screen it draws on, above all. A program cannot move an interface from the alternate screen into the terminal's own scrollback while it is running: the two are different rendering models, and the second needs the terminal's own scroll region from the beginning. Starting again is the answer, and starting again without the user losing their window, its size, or the output already in it means keeping the terminal.
The terminal is kept because the file descriptors are. On Unix it is the same process as well: exec replaces the image rather than starting something beside it, so there is no second process, no shell waiting on one, and nothing to lose a signal on the way through.
What the caller has to do first ¶
Give the terminal back. Terminal.Close, and every deferred call that would have run on the way out of a normal exit, has to have run. This does not do it: this package cannot know which terminal a caller took over, and a process that relaunched while still in raw mode hands the next one a mode it did not set, did not expect, and will not restore.
What it returns ¶
On success it does not return on Unix, and on Windows it returns the exit code of the process it ran, because a Windows process cannot be replaced. So a caller writes the same three lines everywhere and the exit is unreachable on Unix:
code, err := term.Relaunch(argv, nil)
if err != nil {
return err
}
os.Exit(code)
Rebuilding argv is the caller's. Which flags to keep, which to drop, and how a session says which mode it is resuming in are decisions about a program's command line, and a library that made them would be a framework for one program.
A nil env inherits this process's environment. Passing one is how a program tells the next run something it can only be told at startup — which is the usual reason to relaunch at all.
A name given more than once keeps its last value, whichever platform this is. That is not a nicety. The obvious way to set a variable for the next run is to append to os.Environ, and appending leaves the old value in front of the new one: exec hands the pair to the kernel untouched and the first one wins, while the Windows path deduplicates and the second one wins. The same call would behave differently on two platforms, and the Unix one would send a program back into the state it was trying to leave — which, for a relaunch, means doing it again forever.
func Suspend ¶ added in v0.0.2
func Suspend() error
Suspend stops this process the way Ctrl+Z does, and returns when it is continued.
It does nothing to the terminal, which is what makes it a separate thing: a program that stopped while holding the alternate screen leaves the user looking at half an interface they cannot type into, so the terminal has to be given back first. [Terminal.Suspend] is the two together and is what a session wants.
The stop signal is sent to this process and not handled, so the default action takes it: the shell that started this program takes the terminal back, prints its prompt, and this call has not returned. Continuing is what returns from it, which is why the continue signal is waited for rather than assumed — the kill returns as soon as the signal is queued, and going on from there would resume an interface while the process was still stopped.
Types ¶
type Config ¶ added in v0.11.0
type Config struct {
// Features are the optional behaviours to request.
Features Features
// AltScreen draws on a screen of its own, leaving the user's scrollback as it
// was. Without it, frames are drawn in place among whatever else is on screen.
AltScreen bool
}
Config is the complete construction state of a terminal session.
Features are requests the driven terminal may or may not support. AltScreen is ownership: it decides whether closing the session restores the screen that was present before it opened. Keeping the two facts distinct lets adapters share feature requests without duplicating that ownership decision.
func (Config) Modes ¶ added in v0.11.0
Modes returns the terminal-mode encoding selected by o for an environment. Probe is deliberately absent from the result: probing is an input round trip, not an output mode.
lookup is the environment of the terminal being driven, not necessarily this process. A local terminal passes its process environment lookup; an SSH adapter passes the client's accepted PTY environment. Nil means no environment facts are available.
type Features ¶ added in v0.13.0
type Features struct {
// Mouse asks for mouse reporting, including movement and not only clicks.
Mouse bool
// Focus asks to be told when the terminal window gains or loses focus, which is
// what lets a UI stop animating while nobody is looking.
Focus bool
// Keyboard is the Kitty keyboard protocol enhancements to request. Use
// [KeyboardCompatible] for the portable set and add features such as
// [input.KeyboardReportEvents] only when the application consumes them.
// Terminals that do not implement the protocol ignore the request.
Keyboard input.KeyboardFeatures
// Probe asks the terminal about itself while [Open] is still running: the
// colour it draws on, and the extensions it claims. See [Terminal.Ground]
// and [Terminal.Attributes].
//
// It is the only setting that costs anything — one round trip to the terminal,
// normally a millisecond and bounded either way — and the only one whose answer
// a session cannot get any other way. A theme that has to be told whether the
// terminal is light is a theme that is wrong for half the people who run it.
Probe bool
}
Features are the optional terminal behaviours a session requests.
They are separate from Config because optional capability requests can travel between terminal transports without carrying the session's screen-ownership decision with them. The zero value asks for none of them, which is a legitimate choice for a session that only wants raw keys.
type Modes ¶ added in v0.3.0
type Modes struct {
// contains filtered or unexported fields
}
Modes is the immutable set of terminal modes a session turns on and later puts back. Its fields stay private so the only way to construct one is from Config, keeping feature requests, screen ownership, and the wire representation from drifting.
Enter and Leave are encodings rather than writes. A local terminal writes them around raw-mode ownership; a transport such as SSH queues them around its own session lifecycle.
type Progress ¶ added in v0.5.0
type Progress struct {
State ProgressState
Percent int
}
Progress is task progress shown by the terminal outside the cell grid, such as in its window or taskbar. Percent is clamped to 0–100 and ignored for None and Indeterminate. The zero value clears an earlier value.
It is deliberately distinct from a progress component drawn inside an interface: native progress remains useful while the terminal window is obscured and belongs to the terminal session's lifecycle rather than to layout or theme.
type ProgressState ¶ added in v0.5.0
type ProgressState uint8
ProgressState is how a host should present the state of one foreground task.
const ( // ProgressNone clears native progress. It is the zero value. ProgressNone ProgressState = iota // ProgressNormal is work proceeding normally. ProgressNormal // ProgressError is work that failed. ProgressError // ProgressIndeterminate is active work whose completion cannot be measured. ProgressIndeterminate // ProgressWarning is work that completed or paused with a warning. ProgressWarning )
type Terminal ¶
type Terminal struct {
// contains filtered or unexported fields
}
Terminal is a terminal taken over for a session.
It is the whole boundary: raw mode, the modes it turned on, the goroutines reading input, and the writer frames go through. Terminal.Close gives all of it back, in the order that leaves the terminal as it was found, and is safe to call more than once — including from a deferred call on a path that already failed. A Terminal must not be copied after construction; the files, modes, goroutines and restoration obligation are one session.
func Open ¶
Open takes over the terminal on standard input and output.
It reports ErrNotTerminal when there is no terminal to take over, which is the case a caller has to handle rather than force: a program whose output is being piped wants to write text, not frames.
func OpenOn ¶ added in v0.0.2
OpenOn takes over a terminal that is not this process's own.
Standard input and output are the ordinary answer and Open is the ordinary call. This exists because they are not the only answer: a program serving a session over a pty holds one at each end, and everything below this line worked on whatever files it was handed long before anything could hand it any.
lookup is the environment of the files' terminal. It is explicit because the terminal may belong to a pty or another session whose TERM and TMUX are not this process's. Nil means no environment facts are available.
OpenOn is also what makes this package testable at all. A terminal that could only ever be the process's own is one whose lifecycle — raw mode, the modes it turns on, the order it puts them back in — could be checked only by running a second program and reading what came out of it.
func (*Terminal) Attributes ¶ added in v0.0.2
func (t *Terminal) Attributes() (input.DeviceAttributes, bool)
Attributes is what the terminal said it was, and whether it said.
The class is of little use. What the extensions carry is worth having: sixel graphics is claimed here and nowhere else, so this is how a program learns it can draw pixels on a terminal that does not speak Kitty's protocol.
func (*Terminal) Bell ¶ added in v0.0.2
func (t *Terminal) Bell()
Bell asks the terminal for its attention.
What that is, is the user's to decide and not this program's: a sound, a flash of the window, a mark on the tab, or nothing at all. That is the reason to send this rather than to invent an attention-getting animation — the user has already told their terminal what they want to happen.
func (*Terminal) CellSize ¶ added in v0.0.3
CellSize is how many pixels one cell is, and whether the terminal said.
It is what a picture has to be fitted with — see graphics.Fit — and it is the one number about a terminal that cannot be worked out from the others. Plenty of terminals do not report it, which is what the false is for: a picture scaled by an invented cell size is a picture the wrong shape, and not showing one is the better answer.
func (*Terminal) Close ¶
Close gives the terminal back.
The order is the reverse of taking it over, and every step runs even if an earlier one failed: a terminal left in raw mode is unusable, so a failure to write the restore sequences must not be a reason to skip leaving raw mode.
func (*Terminal) Color ¶ added in v0.11.0
Color is how much colour this terminal's environment says it can show.
func (*Terminal) Copy ¶ added in v0.0.2
Copy asks the terminal to put text on the system clipboard, reporting false for text too large to carry — see clipboard.MaxPayload.
The terminal does it rather than the process, because the terminal is the only part of this on the user's side of the connection. Over ssh, in a container, or through a multiplexer running elsewhere, shelling out to pbcopy fills a clipboard nobody can paste from.
The sequence is queued beside the frames, so it lands between two of them and never inside one. A terminal is free to refuse and says nothing when it does, so true means asked for rather than done.
func (*Terminal) Events ¶
Events is the terminal's input, closed when the input ends or the session does.
func (*Terminal) Graphics ¶ added in v0.0.2
Graphics is the richest way this terminal will take an image.
It is the environment and the terminal's own claims together, which is what it takes: the environment names the terminal, and only the terminal names sixel. A session that did not ask — see Features.Probe — gets the answer graphics.Detect derives from its environment alone.
func (*Terminal) Ground ¶ added in v0.0.2
Ground is what the terminal's own two colours are, and whether it said.
It answers two questions that used to be one. Which theme suits is decided by the background alone — grid.RGB.Dark turns it into a yes or no. What a translucent layer mixes with needs both, because a cell left at the terminal's own colours has no numbers of its own until this says what they are; that is what grid.Ground is for and why a frame is given one.
A colour the terminal was not asked for, or would not give, comes back as the default — see grid.Color.Default. A session that gets one has to choose for itself, because there is no safe guess: dark is the commoner choice and light is the one that becomes unreadable when guessed wrong.
func (*Terminal) Hand ¶ added in v0.0.2
Hand gives the terminal to something else and takes it back when it returns.
It is what opening an editor, a pager, or anything else that wants the terminal for itself is made of. The session is put back exactly as it was found — the modes it turned on, off in the opposite order, then cooked mode — the child runs with a terminal that has no idea a program was using it, and then the whole of that is done again in reverse.
The reader comes off the terminal first and goes back on last. That is the part nothing else can do for a caller: a session that only restored the modes would still be reading, and every second keystroke would go to this process instead of to the child.
It runs on the caller's goroutine and does not return until run does, which is the point — an interface that drew a frame while a child owned the terminal would draw it over the child. The caller is responsible for there being nothing else writing meanwhile, usually by calling this from its single owner goroutine.
The window may be a different size afterwards, and nothing will have reported it: the signal went to whichever process group was in the foreground. A fresh size is asked for and delivered on Terminal.Events, the same way a resize is.
Where the reader cannot be taken off the terminal this reports errors.ErrUnsupported and does nothing. Handing over while still reading is not a lesser version of this; it is a child that drops every other keystroke. Whether it can is a question about the session and not about the platform: a console can be waited on, and a pipe pretending to be one cannot.
func (*Terminal) InputErr ¶ added in v0.1.0
InputErr reports why Terminal.Events closed.
It must be called only after Events has closed. A clean end of input and a session stopped by Terminal.Close report nil; another read failure is preserved as its cause. The channel close synchronizes the pump's write with this read.
func (*Terminal) Keyboard ¶ added in v0.0.2
func (t *Terminal) Keyboard() (input.KeyboardFeatures, bool)
Keyboard is which of the Kitty keyboard protocol's enhancements the terminal actually turned on, and whether it said.
Asking for them is not the same as getting them, and that difference is invisible in the events themselves. A terminal can accept the request for unambiguous key codes and give nothing for key releases: the protocol is live, Shift+Enter works, the teardown still owes a pop — and every key is held forever as far as this program can tell. A component that waits for a key to be let go would wait for ever, and nothing would say why.
A session that did not ask for the protocol, or a terminal that does not implement it, reports false.
func (*Terminal) Locale ¶ added in v0.8.0
Locale is the terminal environment's character locale. It is the explicit OpenOn environment's answer, not the process environment's, and an empty string means none was supplied.
func (*Terminal) Name ¶ added in v0.0.2
Name is what the terminal called itself when asked, and whether it answered.
It is worth more than anything the environment says, which is why everything here that identifies a terminal prefers it. Environment variables do not survive ssh, do not exist in a container, and are rewritten by a multiplexer; an answer to this came from the terminal that is actually drawing.
The form is whatever the terminal chose — "kitty(0.32.2)", "WezTerm 20240203" — so it is matched against and not parsed.
func (*Terminal) Notify ¶ added in v0.0.2
Notify asks for a desktop notification.
It is for the thing that finished while the user was looking at something else, which is the case a terminal interface cannot answer on its own: the window is not on screen, so nothing drawn in it is seen.
Terminals that do not implement it ignore it, and there is no way to find out which did — so a program that has something to say should say it in the interface as well, and treat this as the extra it is.
func (*Terminal) Paste ¶ added in v0.0.2
Paste asks the terminal what is on the system clipboard.
The answer arrives on Terminal.Events as an ordinary input.Paste, because that is what it is: a component that already inserts what the user pasted needs nothing further to insert what they copied somewhere else.
It reports whether this request was queued. False means a previous unidentified request is still eligible for an answer. Most terminals refuse to answer, because a program that can read the clipboard can read what the user copied out of a password manager; that refusal has no reply, so true does not promise an answer.
func (*Terminal) ReportDirectory ¶ added in v0.0.2
ReportDirectory tells the terminal which directory the program is working in.
It is the other half of leaving relative paths alone. A terminal finds paths in its own output and offers to open them, and it can only resolve "src/main.go" against a directory it knows — which, once a program has changed its own, is not the one the shell started in. Without this the terminal's own path handling quietly stops working for exactly the output a program produces, and the reason a program declines to make a relative path a hyperlink is that the terminal would do it better.
The path is made absolute, because a relative one tells the terminal nothing it did not already have. An empty path reports the process's own working directory.
It is written beside the frames, so it lands between two of them and never inside one. A terminal that does not implement it ignores it.
func (*Terminal) SetProgress ¶ added in v0.5.0
SetProgress changes task progress outside the cell grid. Unsupported terminals ignore it. Repeating an unchanged value writes nothing; active values are refreshed often enough for terminals that expire the indicator.
func (*Terminal) SetTitle ¶ added in v0.0.2
SetTitle names the terminal's window, and remembers to put back whatever it was called before.
The title is where a program says what it is doing to somebody who is not looking at it: a tab in a window behind this one, a taskbar entry, a window list. That is the whole of its value, and it is why the text should be the task and not the program's name — which the user can already see.
It is queued beside the frames, so it lands between two of them and never inside one. A terminal that does not implement it ignores it, and one that does not implement the title stack ignores the putting back — which is why a program that cares should set something sensible on the way out rather than rely on it.
func (*Terminal) Transmit ¶ added in v0.0.3
Transmit sends a picture to the terminal and returns the handle it now knows it by, which is what puts one in a frame — see graphics.Image.Paint.
The number is this session's to allocate, because two pictures under one name are one picture: nothing above this can know what else has been sent. It is sent beside the frames, so it lands between two of them and never inside one, and it happens once — placing it again on every frame that shows it costs nothing more.
A terminal that cannot show pictures is not asked. Whether this one can is Terminal.Graphics, and a caller that sends without asking has written a megabyte of base64 to something that will print it. It reports ErrImageIDsExhausted rather than reusing a handle that may still name an image.
func (*Terminal) Version ¶ added in v0.0.2
func (t *Terminal) Version() (input.DeviceVersion, bool)
Version is the number a terminal gives when it will not give a name, and whether it gave one. See input.DeviceVersion for why the numbers are not interpreted here.
func (*Terminal) Wheel ¶ added in v0.0.2
Wheel is what this terminal's wheel reports are worth.
It prefers what the terminal said it was over what the environment claims, for the reason Terminal.Name gives.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer writes frames to the terminal from a goroutine of its own.
The reason it exists is that a terminal write can block for a long time — a remote session, a suspended emulator, a scrolled-back pager — so producers must not perform those writes synchronously.
Successful completion is reported as a watermark rather than as a stream of results: the only question anyone asks is how far the terminal has got, and a counter answers it without a queue to keep in order or a consumer to keep up. Writer.Changes signals when that watermark or Writer.Err may have changed.
Every method is safe for concurrent use. Frames are assigned a sequence while they are appended to one FIFO, so sequence order and write order cannot diverge when several goroutines queue terminal commands at once. A Writer must not be copied after construction; its queue, watermarks and worker are one publication owner.
func NewWriter ¶
NewWriter starts a writer over dst. The caller ends it with Writer.Close.
A nil destination is a programmer error and panics here. A writer accepts frames from the goroutine that draws them and writes on one of its own, so a nil surfaced on first use would report the fault on that second goroutine, in a stack that no longer names whoever failed to open the terminal.
func (*Writer) Changes ¶ added in v0.6.0
func (w *Writer) Changes() <-chan struct{}
Changes is the stable, single-consumer notification channel for writer state. A receive means Writer.Written or Writer.Err may have changed; several changes coalesce into one wake-up, so the current values must always be read from the writer.
func (*Writer) Close ¶
Close drains what it can, stops the goroutine and reports whether anything had to be abandoned. It is idempotent.
A write already inside the terminal cannot be interrupted. When the grace period ends with one outstanding, Close returns without waiting for the goroutine: it finishes on its own, discarding what is left rather than writing it.
func (*Writer) Drain ¶
Drain waits until every frame queued so far has been written or failed, or until the timeout passes.
It is what to call before handing the terminal to another program, so that program does not find half a frame in front of it. A failed frame counts as drained: a broken terminal must not be able to wedge a shutdown.
It takes nothing from Writer.Changes. Waiting here must not cost its consumer a wake-up it is owed, which is what the broadcast channel is for.
func (*Writer) Err ¶
Err is the first write failure, or nil.
A terminal that has failed a write does not recover, and a UI that cannot reach its terminal has nothing left to do, so this is a reason to exit rather than something to retry.
func (*Writer) Queue ¶
Queue takes ownership of a frame and returns the sequence number reserved for it. The sequence is reserved before the goroutine can see the frame, so Writer.Queued already accounts for it when Queue returns.
Queue does not wait for the terminal. A frame handed over after Writer.Close or a terminal failure is accounted for but not retained or written. The failure that made the writer unusable remains available from Writer.Err.
Queue panics after every uint64 sequence has been used. Continuing would return zero and violate the watermark protocol by making new work look older than work already settled.