Official plugins
Terminal
A terminal for native UI, which runs the shell or any program with Ghostty's terminal emulator.
The terminal plugin is a terminal for apps of native UI: a view
that runs the user's shell, or any program, in a pseudo-terminal, with the
terminal emulator of Ghostty, libghostty-vt. It is
all Go, with no npm package: the emulator is a native library that the
app loads at run time, with no cgo, and that mygo build and mygo dev
put into the app.
Set up
Make a terminal, which starts its program, and show it in a window:
import (
"github.com/egoist/mygo"
"github.com/egoist/mygo/plugins/terminal"
"github.com/egoist/mygo/ui"
)
func main() {
mygo.App.WhenReady(func() {
term, err := terminal.New(terminal.Options{})
if err != nil {
log.Fatal(err)
}
win := mygo.NewWindow(mygo.WindowOptions{
Title: "Terminal",
Content: ui.View(func(c *ui.Context) {
terminal.View(c, term).Fill().AutoFocus()
}),
})
win.OnClosed(func() { term.Close() })
})
if err := mygo.App.Run(); err != nil {
log.Fatal(err)
}
}The terminal example shows this app. Its window uses the shell's title and closes when the shell exits.
terminal.View is an element like any other: size it with Fill or
Grow, and put it next to other elements, as in a pane of an editor. Its
size sets the terminal's, in cells of its font, and a click gives it the
keyboard focus. A terminal shows in one view at a time. Its program starts
once a view first shows it, so that it draws its first screen at the
view's size, or half a second after New at 80×24 when no view does.
The terminal does what terminal apps do:
- colors (16, 256 and 24-bit), bold, italic, faint, inverse, strikethrough, overlines and underlines (single, double, curly, dotted, dashed, in their own colors);
- Unicode: wide characters, emoji and grapheme clusters take the cells of their width, as in Ghostty, and the system's fonts stand in for what the terminal's font lacks;
- box drawing, blocks and Powerline's separators drawn rather than taken from the font, so that the lines of full-screen programs join;
- a scrollback that reflows when the terminal resizes, and the alternate screen of full-screen programs;
- the keyboard as programs ask: legacy sequences, xterm's modifyOtherKeys or the Kitty keyboard protocol, and input methods, which compose at the cursor. In legacy sequences Ctrl+I, Ctrl+M and Ctrl+[ send Tab, Enter and Esc, as in xterm, where Ghostty sends fixterms' CSI u, which shells don't read;
- mouse reports, focus reports, bracketed paste, synchronized output,
hyperlinks (OSC 8), the title (OSC 0 and 2), the working directory
(OSC 7), copying to and reading the clipboard (OSC 52, OSC 5522) as the
app allows (see
ClipboardRead), and paste events of the Kitty clipboard protocol (OSC 5522, mode 5522), which let programs take pasted images.
Using it
- Selecting with the pointer works as in Ghostty: a drag selects cells, a double click words and a triple click lines, and dragging past the top or the bottom scrolls. Option (Alt elsewhere) drags a rectangle (from off a shell's command, where Option+click moves the cursor). Shift-click extends an existing selection from its original anchor; subsequent Shift-clicks can extend or shrink it, including across lines. While a program takes the mouse, as editors do, Shift selects.
- Moving the cursor: Option+click (Alt+click elsewhere) on the
command being typed at a shell's prompt moves the shell's cursor to the
clicked cell, as in Ghostty and kitty, even on a command that wraps over
rows: the terminal sends the Left or Right arrows that take the cursor
there (in the application's mode,
ESC O, when the shell set DECCKM). A click past the end of the command moves to its end and no further, so that a Right arrow does not accept a suggestion (zsh-autosuggestions, fish). It takes a shell that marks its prompt with OSC 133 (shell integration, as Ghostty's, kitty's or the shell's own). Without it, off the command, on a full-screen program's screen and while the view shows the scrollback, Option+click and Option drags select as before; on the command, the click is the cursor's, so a rectangle starts off it. AMirrormarks the prompt's rows for the other terminal too, where a click works the same. - The pointer is Ghostty's: the I-beam over a shell, the arrow while a
program takes the mouse (the I-beam again while Shift is held, as Shift
selects then), and the shape a program asks for with OSC 22 (
pointer,crosshair,col-resize…) until it takes or leaves the mouse again. - Copy and paste are Command+C and Command+V on macOS, which the Edit menu's roles also send, and Control+Shift+C and Control+Shift+V elsewhere, with Control+Insert and Shift+Insert, and on Windows Control+C and Control+V as in Windows Terminal (see Keys). Pastes are bracketed when the program asks, so that pasted lines do not run as commands. The context menu has Copy, Paste, Select All and Clear Scrollback. Options add copying on select and other secondary clicks (see Options).
- Pasting images: a program that reads the clipboard itself, as it
asks with mode 5522 (as coding agents that accept images do), is told
what a paste holds, every format of the clipboard as MIME types (the
text, an image as PNG, files as
text/uri-list, …), and reads what it wants, even over SSH. Only the terminal that pasted answers its read, with what the clipboard held at the paste; other terminals showing the same program, as other windows on a session, stay silent. Other programs get the text alone. An image alone pastes an empty bracketed paste to a program that asks for bracketed pastes, as VS Code's terminal does, which agents such as Claude Code take as their cue to read the image from the clipboard themselves; to any other, nothing. - Reading the clipboard: a program asks with OSC 52 (
?) or OSC 5522 and is refused by default, with an empty clipboard for OSC 52 and an EPERM status for OSC 5522;ClipboardReadallows it or asks the app first (see Options). The clipboard is read, and the answer written, on the main thread as the view builds a frame, so what the program reads is what the clipboard held then, not when it asked. - Scrolling: the wheel and the touchpad scroll the scrollback, as do Shift+Page Up and Shift+Page Down. Full-screen programs get the wheel as arrow keys, or as mouse reports when they take the mouse. Typing scrolls back to the bottom.
- Zooming: the wheel with Control held (Command on macOS), and
Control (Command) with =, - and 0 make the text larger, smaller, and its
size again, a DIP a step, as in Ghostty;
OnZoomtakes zooms instead. - Links: Command+click (Control+click elsewhere) opens a hyperlink a program printed (OSC 8), or a URL printed as text, even while a program takes the mouse; the pointer is a hand over them while Command is held.
- Shortcuts of the app come first: Command shortcuts on macOS are never
sent to the program, and keys that the window or an element around the
terminal handles with
Shortcut, or that a menu of the app has as its key equivalent on macOS, go there.
Options
term, err := terminal.New(terminal.Options{
Command: []string{"htop"}, // the user's login shell when empty
Dir: "/tmp", // the home directory when empty
Env: []string{"EDITOR=vim"}, // added to the app's environment
Font: terminal.Font{Family: "JetBrains Mono", Size: 14},
Theme: terminal.DarkTheme(), // follows the window when nil
Cursor: terminal.CursorBar, // a block when zero
OnTitle: func(title string) { win.SetTitle(title) },
OnExit: func(code int) { win.Close() },
})-
Commandruns a program instead of the shell. The shell runs as a login shell (-zsh), as terminal apps start it, so that it reads the user's profile:$SHELL, else/bin/zshon macOS and/bin/shelsewhere, and PowerShell on Windows. -
ShellIntegration: trueenables zsh prompt integration. A temporary.zshenvrestores the originalZDOTDIRbefore sourcing the user's startup files and installs OSC 133 prompt markers after the user's prompt theme. Powerlevel10k is asked to emit its own markers for asynchronous redraws. When columns change, the marked prompt is cleared at its old width before reflow, and zsh redraws it afterSIGWINCH; right and multiline prompts therefore do not accumulate in the scrollback. Command output and alternate screens retain their normal resize behavior. The user's files are never modified, temporary files are removed when the terminal exits or closes, and other local shells are unchanged. WithConn, the source must provide OSC 133 markers and redraw its prompt on resize; the plugin does not inject shell startup configuration into remote sessions. Integration is off by default. -
Envadds to the environment, which hasTERM=xterm-256color,COLORTERM=truecolor, andLANG=en_US.UTF-8when no locale is set, as for apps started from the Finder. -
Fontsets the font: the system's monospaced font at 13 DIPs by default (see Fonts). -
Themesets the colors: the foreground, the background, the cursor's and the text's under it, the selection's and the selected text's, and the palette of 16 colors;DarkTheme, when set too, sets those of dark windows (see Themes).terminal.DarkTheme()andterminal.LightTheme()are the window's background and text with Visual Studio Code's palette, and without a theme the terminal takes the one of the window's appearance as it changes. Programs may change the colors (OSC 4, 10, 11, 12). -
CursorandNoBlinkset the cursor, which programs may change. -
Scrollbackis about how many bytes of output to keep above the screen: 10 MB by default, none when negative. -
ScrollMultiplierscales scrolling, as Ghostty'smouse-scroll-multiplier:Discreteis the rows a notch of a mouse wheel scrolls (3 by default; on Windows it scales the "lines to scroll" setting as 3 would be the setting's), andPrecisionscales a touchpad's distance (1 by default).SetScrollMultiplierchanges it in a running terminal. -
MinimumContrastis the least contrast text has with its background, as Ghostty'sminimum-contrast: a WCAG ratio from 1 (none, the default) to 21. Text with less, as a program's light gray on a light theme, darkens or lightens as little as reaches it, keeping its hue.SetMinimumContrastchanges it in a running terminal. -
Transparentleaves the background undrawn, as Ghostty'sbackground-opacity, so that what is under the view shows through: the window's material, or an element's translucent background. Cells with a background color of their own still draw it. -
Paddingis the room around the grid, in DIPs across (X) and down (Y), as Ghostty'swindow-padding-xandwindow-padding-y: 6 across and 4 down by default, none when negative.PaddingColoris what it shows, as Ghostty'swindow-padding-color: the background by default, or withterminal.PaddingExtendthe backgrounds of the cells nearest to it, so that a full-screen program's colors reach the edges of the view rather than stopping at the grid's. As Ghostty'sextend, it extends only rows whose every cell has a background of its own, so that a prompt's highlights, or a program's tabs and boxes, keep to their cells;terminal.PaddingExtendAlwaysextends every row's. Either way, a lone character in inverse video, as a program draws its own cursor, keeps its background in its cell.term, err := terminal.New(terminal.Options{ Padding: terminal.Padding{X: 14, Y: 8}, PaddingColor: terminal.PaddingExtend, }) -
OptionAsAltmakes Option on macOS the Alt key of programs (Meta), instead of the key typing accented letters: Option-E sends Esc and e, without the ´ it types otherwise. -
CopyOnSelect: terminal.CopyOnSelectClipboardcopies the selection to the clipboard as the pointer ends it, and as Select All makes it, as Ghostty'scopy-on-select = clipboard. Ghostty'sprimary, Linux's selection clipboard, is not there, as native UI has none.SetCopyOnSelectchanges it in a running terminal. -
Keybindsbind keys to actions, over the defaults (see Keys). -
RightClickis what a secondary click does, as Ghostty'sright-click-action: the context menu by default,RightClickPaste,RightClickCopy,RightClickCopyOrPaste(copy the selection, or paste when there is none, as Windows Terminal) orRightClickIgnore. Copying lets go of the selection. A program that takes the mouse still gets the click, unless Shift is held. -
NoClipboardWritekeeps programs from writing to the clipboard (OSC 52 and OSC 5522), as Ghostty'sclipboard-write = deny; reads are governed byClipboardRead, and what a paste offers a program is still answered. -
ClipboardReadis what programs get when they read the clipboard (OSC 52's "?", and the Kitty clipboard protocol's OSC 5522):ClipboardDenyby default, as Ghostty'sclipboard-read = deny(Ghostty asks by default, but a terminal that asks needs a view), orClipboardAllowto answer with what the clipboard holds, orClipboardAskto ask the app first. The clipboard is read and the answer written on the main thread, as the view builds its next frame, which is also the frame the terminal's own prompt shows in: a read of a terminal no view shows waits, and one of a closed terminal is never answered. -
OnClipboardRequestasks the app before a program reads the clipboard, whenClipboardReadisClipboardAsk. It runs on the main thread with aClipboardRequest: the MIME types the program asked for (Mimes), whether it asked for the types available (List, the Kitty protocol's targets listing, which is answered without asking), the selection it wants (Primary) and what the clipboard held as text (Text), for a prompt about it.AllowandDenyanswer the request, in the hook or later and from any goroutine, and the first of them wins; a request the hook leaves unanswered waits. Without the hook the terminal draws its own prompt in the view, one read at a time, with a one-line preview of what the program would read and Allow and Deny buttons (Enter allows, Escape denies):OnClipboardRequest: func(req terminal.ClipboardRequest) { if req.Text == "" && !req.List { req.Deny() // nothing to leak return } askTheUser(func(ok bool) { if ok { req.Allow() } else { req.Deny() } }) }, -
OnCopyruns on the main thread after the terminal writes text to the clipboard, with what made the copy:CopyUser(the copy keys, the context menu, a secondary click that copies),CopySelect(CopyOnSelect) orCopyProgram(OSC 52). An app may say so, as Ghostty'sapp-notifications = clipboard-copydoes with a toast:OnCopy: func(text string, by terminal.CopySource) { if by != terminal.CopyProgram { showToast("Copied to clipboard") } }, -
OnClearsees the user's clear on the main thread, before the terminal clears:ClearScreen, forActionClearScreen's keys and the context menu's Clear. It reports whether it took the clear, which then clears nothing here. A window showing a server's terminal takes it, for the server's terminal to clear (see A server's terminal, shown elsewhere). It is not asked on a full-screen program's screen. -
OnTitle,OnExit,OnBellandOnNotifyrun on a goroutine of the terminal when the program sets the title, ends, rings the bell or asks for a desktop notification. They may run before the window that shows the terminal exists, whichNewWindowmakes afterNew: callbacks that use the window wait for it, as the terminal example's do (<-readybeforewin, andclose(ready)onceNewWindowsetwin).Doneis closed once the program exited, andExitCodereturns its code (-1 for a program that could not start); the terminal then shows "[Process exited]". -
OpenLinktakes the links Command+click opens, on the main thread, and with it file paths printed as text too (/abs/path,~/notes.md,./a.go,src/app.go:12:3,main.go:250,app.ts(12,3),app.go#L12C3, a quoted path with spaces, a path in Chinese prose with no space around it), the place in the file aspath:line:colhowever it was printed, and without the quotes: a relative path is the program's, as ofDir. It reports whether it opened the link; a hyperlink or a URL it leaves opens with the system, as every one does withoutOpenLink. An app opens a remote shell's paths in a preview of its own this way, rather than the system opening them locally. -
OnPastesees a paste of the clipboard, by the keys, a Paste menu item or a secondary click, on the main thread, with every format the clipboard holds as atransfer.Data, and takes it by returning true: nothing is pasted. An app driving a remote shell uploads the clipboard's files (d.Files()) or image (d.Read(transfer.PNG)) this way, and pastes their remote paths withPasteinstead:OnPaste: func(d transfer.Data) bool { files, _ := d.Files() if len(files) == 0 { return false // text pastes as usual } go func() { term.Paste(strings.Join(upload(files), " ")) }() return true }, -
OnPasteStatustells how a paste to a program that reads the clipboard itself goes, on a goroutine of the terminal, with aPasteProgress:PasteOffered, thenPasteSendingwithSentofTotalbytes as the program reads (bytes written to it, encoded as the protocol sends them), thenPasteSent; orPasteNotTakenwhen the program has not read it 5 seconds after the offer was written (it may still, until another paste comes), orPasteNoTextwhen a paste with no text, as only an image, goes to a program that reads neither the clipboard nor bracketed pastes, which pastes nothing. An app shows a large image's progress over a slow link this way, and says why nothing pasted. -
OnKeysees a key press on the main thread before the keybinds and before the terminal sends it, and takes it by returning true: neither the key nor the text it types reaches the program. Unlike aShortcutaround the view, which runs with the next frame, it keeps its place among the keys typed after it, as does what it sends withSend. Keys that only type text may come as their text alone, as on Linux, andOnKeydoes not see those. -
OnScrollsees the wheel scroll the view by whole rows (negative up) on the main thread before the terminal scrolls its scrollback, and takes it by returning true: the scrollback stays. It is not asked while a program gets the wheel: as mouse reports when it takes the mouse (unless Shift is held), or as arrow keys on a full-screen program's screen (alternate scroll, which\x1b[?1007lturns off). AConnwhose history lives at the other end, as a remote multiplexer streaming only its screen, sends the wheel there this way. A notch of a mouse wheel is three rows, or on Windows as many as its "lines to scroll" setting asks (3 unless changed), as in Windows Terminal; a touchpad scrolls by the distance it moves. -
OnZoomsees a zoom on the main thread: the wheel with Control held (Command on macOS), which zooms beforeOnScrolland a program that takes the mouse see it, or a key bound to a font size action. ItsZoomhas how manySteps, rolling up positive (a wheel's event is a notch; a trackpad's moves add up to them), orReset. It takes the zoom by returning true, as an app that sizes all its text at once does; without it, or for a zoom it leaves, the terminal changes its font's size itself, a DIP a step, down to 1, and back toFont.Sizeon a reset. macOS's pinch, which native UI does not report, does not zoom.
Keys
The terminal binds keys to actions, as Ghostty's keybind option does,
with Ghostty's defaults for the platform, which terminal.DefaultKeybinds
returns:
| Action | macOS | Linux and Windows | Windows also |
|---|---|---|---|
ActionCopy |
Cmd+C | Ctrl+Shift+C, Ctrl+Insert | Ctrl+C (performable) |
ActionPaste |
Cmd+V | Ctrl+Shift+V, Shift+Insert | Ctrl+V (performable) |
ActionSelectAll |
Cmd+A | Ctrl+Shift+A | |
ActionClearScreen |
Cmd+K | Ctrl+Shift+K | |
ActionIncreaseFontSize |
Cmd+=, Cmd++ | Ctrl+=, Ctrl++ | |
ActionDecreaseFontSize |
Cmd+- | Ctrl+- | |
ActionResetFontSize |
Cmd+0 | Ctrl+0 | |
ActionScrollPageUp, …Down |
Shift+ or Cmd+Page Up/Down | Shift+Page Up/Down | Ctrl+Shift+Page Up/Down |
ActionScrollToTop, …Bottom |
Cmd+Home/End | Shift+Home/End (Linux) | Ctrl+Shift+Home/End |
The keys that clear the screen and scroll are performable too, so that a
full-screen program, as an editor, gets them. Options.Keybinds bind more,
over the defaults: a binding replaces the defaults of its key and
modifiers, and one to ActionNone unbinds them, so that the program gets
the key; NoDefaultKeybinds starts from none, as Ghostty's
keybind = clear:
term, err := terminal.New(terminal.Options{Keybinds: []terminal.Keybind{
// Windows Terminal's Control+C and Control+V on Linux too.
{Mods: ui.Ctrl, Key: ui.KeyC, Action: terminal.ActionCopy, Performable: true},
{Mods: ui.Ctrl, Key: ui.KeyV, Action: terminal.ActionPaste, Performable: true},
// Control+Shift+A reaches the program.
{Mods: ui.Ctrl | ui.Shift, Key: ui.KeyA, Action: terminal.ActionNone},
}})A Performable binding, as Ghostty's performable: prefix, takes the key
only when its action does something: a copy only with a selection, a
paste only when the clipboard holds something, clearing the screen and
scrolling only off a full-screen program's screen; otherwise the key goes
on to the program, as if unbound: Control+C interrupts when nothing is
selected. A performable copy also lets go of the selection, so that the
key's next press interrupts. ActionClearScreen clears the scrollback and
the screen above the cursor's line, which moves to the top with the
shell's prompt, as Kitty's clear_terminal to_cursor, and the context
menu's Clear does it too; Terminal.ClearScreen does the same from code,
and Terminal.ClearScrollback clears the scrollback alone.
ActionIgnore takes the key and does nothing.
A key goes first to the app's shortcuts, then to OnKey, then to the
keybinds, then to the program. The context menu shows the keys bound to
its items.
Fonts
terminal.Font holds what Ghostty's font options set:
term, err := terminal.New(terminal.Options{Font: terminal.Font{
Family: "JetBrains Mono, Menlo, monospace", // tried in order
Size: 14, // DIPs, 13 when zero
Weight: 500, // 400 when zero
Features: []string{"-calt", "+ss01"}, // as Ghostty's font-feature
CellWidth: 1.03, // wider cells, unchanged glyph size
LineHeight: 1.2, // as adjust-cell-height = 20%
Thicken: true, // as font-thicken, on macOS
}})-
Familyis a list of families, asui.Element.Fonttakes it: the system's monospaced font when empty. Characters the fonts lack come from the system's fallback fonts. A font the app carries, rather than one the user installed, is added withui.RegisterFont, its styles under one family so that bold and italic text take them://go:embed fonts var fonts embed.FS for _, name := range []string{"Regular", "Bold", "Italic", "BoldItalic"} { data, _ := fonts.ReadFile("fonts/JetBrainsMono-" + name + ".ttf") ui.RegisterFont(data, "JetBrains Mono") } -
Weightsets how heavy text is, from 100 to 900, of the weights the family has; bold text is 300 heavier, at least 700. -
Featuresturn OpenType features on and off:"ss01"or"+ss01"on,"calt=0"or"-calt"off,"cv05=2"an alternate. Coding fonts make most of their ligatures withcalt, the others withliga; a ligature spans the cells of its characters. On macOS, kerning is off unless the list nameskern, so that each character keeps its place in its cells;"+kern"keeps the font's. -
CellWidthmultiplies the font's horizontal advance to size columns, without scaling glyphs or changing row height. Zero or less means1;1.03requests 3% wider cells, and below1glyphs overlap. Cell widths round to whole display pixels, so small increments can share a width or jump a pixel. Wide characters, the cursor, selection, mouse reports and box drawing all follow the adjusted grid; glyphs too wide for a cell, as emoji are, shrink to the font's own cell width. -
LineHeightmakes rows taller or shorter than the font's line height, with the text centered in them; box drawing still joins. -
Thickendraws text with a thicker stroke, with Core Text's font smoothing at its strongest whatever the colors and the system's setting, as Ghostty does. Without it, text is as AppKit draws it: smoothed more the lighter it is, while the user leaves smoothing on. It changes nothing on Linux and Windows, as in Ghostty.
SetFont changes the font of a running terminal; the grid then takes the
new size of its cells.
Themes
The plugin carries the hundreds of themes Ghostty ships (from
iTerm2-Color-Schemes),
which terminal.GhosttyTheme returns by name, as Ghostty's theme option
takes it:
mocha, err := terminal.GhosttyTheme("Catppuccin Mocha")
latte, err := terminal.GhosttyTheme("Catppuccin Latte")
term, err := terminal.New(terminal.Options{
Theme: latte, // in light windows
DarkTheme: mocha, // in dark windows, as theme = light:…,dark:… in Ghostty
})As in Ghostty, a theme file of the user's in Ghostty's themes directory
($XDG_CONFIG_HOME/ghostty/themes, ~/.config/ghostty/themes by default)
comes first, and an absolute path names a theme file.
terminal.GhosttyThemes() lists the names, the user's themes with
Ghostty's, for a picker, and terminal.ParseGhosttyTheme reads a theme in
Ghostty's format from bytes:
palette = 0=#45475a
palette = 1=#f38ba8
background = #1e1e2e
foreground = #cdd6f4
cursor-color = #f5e0dc
cursor-text = #1e1e2e
selection-background = #f5e0dc
selection-foreground = #1e1e2eSetTheme(theme, dark) changes the themes of a running terminal.
Search
Search searches the screen and the scrollback for text, as Ghostty's find
does, with libghostty-vt's search: the matches are highlighted as they are
found, a frame's share of the scrollback at a time, and follow the output
that comes; the newest is selected, and shown. Matching is exact but for the
case of ASCII letters. SearchNext selects the next match toward older
output, up into the scrollback, and SearchPrevious toward newer output,
both wrapping around and, unless the match shows, scrolling it to the
middle of the view, clear of a find bar over an edge; EndSearch
(or Search("")) ends the search. The terminal draws no find bar: the app
draws one, and binds Ghostty's keys to these methods, Cmd+F, Cmd+G,
Cmd+Shift+G and Escape on macOS. OnSearch tells it, on the main thread,
how the search goes, for its "1 of 3":
term, err := terminal.New(terminal.Options{
OnSearch: func(s terminal.SearchStatus) {
// s.Selected of s.Total matches of s.Text; s.Searching while more
// of the scrollback is to go through.
bar.status = fmt.Sprintf("%d of %d", s.Selected, s.Total)
},
})The theme's Search and SearchText color the matches and their text, and
SearchSelected and SearchSelectedText the selected one, as Ghostty's
search-background, search-foreground, search-selected-background and
search-selected-foreground, which a theme file sets too: Ghostty's light
amber and light orange under black text when zero.
Methods
The terminal's methods are safe from any goroutine:
Sendsends bytes to the program as if typed, andPastepastes text.PasteDatapastes atransfer.Data, as the paste keys paste the clipboard: a program that reads the clipboard itself is offered every format, as MIME types, and reads those it wants, when it asks (atransfer.Lazyrepresentation is made only then, on a goroutine of the terminal); any other gets the text.PasteisPasteDatawith text, which such a program reads astext/plain.Feedshows bytes as if the program wrote them, text and escape sequences.Title,Dir(the directory the shell reported),Size(in cells) andText(the screen and its scrollback) read the terminal.HoveredLinkreturns the link Command (Control elsewhere) is held over in the view, asOpenLinktakes it, whether it is a file path, and the rectangle of its cells relative to the view: read it afterViewto show a preview of the link over the view.SetFontandSetThemechange the font and the colors;SetFontalso sets the size a zoom resets to, and drops the zoom.Resizesets the size of a terminal no view shows, andSnapshotreturns what a terminal shows as escape sequences: its scrollback and screen with their styles, the cursor and the modes programs set. Fed to a new terminal of the same size, it shows the same, as when a server keeps a session's screen for the windows that attach to it.Closehangs up the program, which gets SIGHUP, and frees the terminal.
Without a program
Conn connects the terminal to anything that reads and writes, such as an
SSH session or a serial port, instead of a program: what the terminal reads
from it shows, and what is typed is written to it. A Conn with a method
Resize(cols, rows int) error is told the size of the terminal:
term, err := terminal.New(terminal.Options{Conn: session})A terminal whose Conn never sends anything shows what the app writes
with Feed, as a log with colors would.
A server's terminal, shown elsewhere
A server that keeps sessions running, as a multiplexer does, keeps each
session's terminal, which the program's output goes to, and windows show it
from another process. A Mirror of the server's terminal frames what it
shows: Frame returns VT sequences that bring a terminal showing the frames
before to what the server's shows now. The window's terminal reads them from
its Conn as if a program wrote them, and shows the server's screen and
scrollback cell for cell, with its cursor and the modes that change what it
sends (the cursor keys', the keyboard protocols', the mouse's, bracketed
paste), the colors programs set (OSC 4, 10, 11, 12) and the rows of a
shell's prompt (OSC 133), while it still selects, copies, opens links,
composes and moves the shell's cursor on Option+click as any terminal does. It never lays out a program's output itself, so it cannot
come to show something else: a window fed the program's output from a
snapshot of the server's screen can, as programs that redraw by moving the
cursor relative to where it is show, as agents' interfaces do.
// The server, one Mirror for each window, its first frame starting it over:
m, err := term.NewMirror()
frame, due := m.Frame() // after output; due: a synchronized update holds itFrames draw rows where they are, and rows that scroll off pass through the
window's screen in order, so that its scrollback is the server's; soft wraps
stay soft. Each cell is drawn in its own style, so an empty cell stays empty
after a colored one. A frame reads the terminal as a view does, taking its
record of what changed: mirror a terminal no view shows, as a server's, with
as many Mirrors as it has windows. A frame does not come while a program holds a synchronized update
(mode 2026), up to a second: Frame then reports that one is due. The
window's cursor takes its own Cursor and NoBlink while the program
leaves the cursor's shape alone, and the program's when it sets one.
The window's terminal sets ResizeByConn: its view only asks Conn for the
size it fits, and the screen keeps its size until the window calls Resize,
where the server's stream says the server resized, before the frames of the
new size.
term, err := terminal.New(terminal.Options{Conn: stream, ResizeByConn: true})
// Where the server's stream says it resized to cols×rows:
term.Resize(cols, rows)The server's terminal answers the program's questions, so it needs what
only the window knows. A Conn with ResizePixels(cols, rows, cellWidth, cellHeight int) error is told the size of the window's cells in pixels,
and one with UseTheme(theme *terminal.Theme) error the window's colors;
the server gives them to its terminal with ResizePixels and UseTheme.
Programs asking the size in pixels, or taking the mouse in pixels (mode
1016), then count in the window's cells, and those asking the colors (OSC
4, 10, 11, 12) or whether they are dark get the window's. A program that
asked (mode 2031) is told when they go from light to dark or back, as a
terminal a view shows tells it when the window's appearance changes.
// The server, where a window's stream says so:
term.ResizePixels(cols, rows, cellWidth, cellHeight)
term.UseTheme(theme)The user's clears are the server's to make: cleared in the window alone,
the server's next frames, drawn from a screen still full, would bring rows
back in the wrong places. The window's OnClear takes them and asks the
server, whose terminal clears; the next frame shows it.
// The window:
OnClear: func(c terminal.Clear) bool {
stream.Clear(c) // however the app asks its server
return true
},
// The server, asked:
term.ClearScreen()Programs' writes to the clipboard (OSC 52) go on with the frames, for the
window's terminal to write with its own policy. With ClipboardRead: terminal.ClipboardForward on the server's terminal, programs' reads go on
too, and the window's terminal answers them, asking the user as it would:
the server's answers nothing. The server answers programs' other queries
itself, as the terminal they write to.
History returns the server's scrollback and screen down to the cursor as
rows, without the cursor and the modes, for a terminal that takes a session
over after its program ended, as after the server restarted: fed to it, the
cursor is left under what it shows, for the next program to start below it
rather than over it. A full-screen program's screen has none.
The library
libghostty-vt is a native library of Ghostty, built for each platform from
the version of Ghostty the plugin binds, and published with MyGo. The
plugin's package names those builds and their SHA-256 in
mygo-plugin.json, and the CLI puts the one of each platform into the
apps it builds, among their resources, as
libghostty-vt.dylib, libghostty-vt.so or ghostty-vt.dll: a macOS app
signs it with the app, and a universal app gets both architectures in one
file.
A program not built by the CLI, as under go run and go test, downloads
the library once into the user's cache (<cache>/mygo/natives/), where
the CLI keeps those it downloads, and checks its SHA-256. Packaged apps
never download it. terminal.LibraryPath returns the library a program
loads, and $MYGO_GHOSTTY_VT names another library.
terminal.Load loads the library, which New does too: call it first to
report a missing library before showing a window.
The library runs on macOS 13 or later, as Ghostty does, so apps with a
terminal set minimumSystemVersion to 13.0 or later; on Linux with glibc
2.28 or later (Debian 10, Ubuntu 20.04), on x64 and arm64; and on Windows
10 1809 or later, whose ConPTY runs the program, on x64 and arm64.
Testing
ui.NewTester runs a terminal view without a window: type into it with
Type and Key, and read what the program printed with Text:
func TestShell(t *testing.T) {
term, err := terminal.New(terminal.Options{Command: []string{"/bin/sh"}})
if err != nil {
t.Fatal(err)
}
defer term.Close()
tt := ui.NewTester(func(c *ui.Context) { terminal.View(c, term).Fill().AutoFocus() }, 640, 400)
tt.Type("echo hello")
tt.Key(0, ui.KeyEnter)
for !strings.Contains(term.Text(), "\nhello") {
time.Sleep(10 * time.Millisecond)
tt.Frame()
}
}