Emulators and Pseudo-Terminals
One-shot commands and embedded terminals are built from two lower layers, which this page covers for code that needs them directly: a pseudo-terminal, which runs a program as a terminal window does and hands back the bytes it writes, and a terminal emulator, which turns those bytes into what the terminal shows. Neither knows about the other, so output recorded ahead of time can be fed to an emulator with no program run, and a program's output can go somewhere other than an emulator.
Pseudo-terminals need Linux or macOS.
Pseudo-terminals
run_pty runs a program on a new pseudo-terminal until it exits,
in its own session with the terminal as its controlling terminal,
calling on_output with what the program writes, standard output and standard error together.
It returns how the program exited (Exited),
why it couldn't start (FailedToStart),
or that it outran its timeout (TimedOut),
and hands out a PtyConnection that writes to the program
without blocking and resizes its terminal.
Cancelling it hangs the program up, as closing a terminal window does.
use_pty runs a program while its component is mounted,
as a terminal window or a multiplexer's pane does:
it starts once its size is positive, resizes the terminal as its size changes,
and starts the program again when its argv or environment change.
It hangs the program up in the background, so a program slow to exit doesn't hold up the app's rendering.
Hanging up in the background
The render loop waits for a cancelled effect to finish before it renders again,
and hanging up a program that ignores SIGHUP takes up to two hangup_graces.
So use_pty, and use_command for each run, call run_pty in a task of the app's task group
and await that task through asyncio.shield, which keeps their effect's cancellation from waiting for it:
sequenceDiagram
participant L as Render loop
participant E as Effect
participant R as run_pty, in the app's task group
L->>E: Start the effect
E->>R: Start run_pty and await it through shield
alt The program exits, or outruns its timeout
R->>R: Hang up whatever it left running
R-->>E: Exited or TimedOut
E->>E: Set the status, or report the run's outcome
else Cancelled, by cancel, another run or argv, or unmounting
L->>E: Cancel, then wait for it to finish
E->>R: Cancel
E-->>L: Finished at once
Note over L: Renders again
R->>R: Hang up the program, for up to two hangup_graces
R-->>R: Ends cancelled, with no outcome to return
end
Note over L,R: An app that stops waits for every hangup still going
A program that ends by itself returns its outcome through the effect as usual,
but only once run_pty has hung up what it left running.
Only that effect waits for it, not the render loop,
but a program that exits leaving a background job that ignores SIGHUP reads as running until that job is gone.
A cancelled run has no outcome to return:
cancel has already made it Cancelled, and another run or argv reports its own.
Emulators
An Emulator is fed the bytes a program writes,
answers the program's queries through on_reply,
and snapshot turns what its terminal shows
into an immutable Screen.
A snapshot of a terminal that didn't change is the same Screen as before,
and rows that didn't change are shared between snapshots, so a canvas drawing one redraws only when it changed.
scrolled_off returns the rows that scrolled off the top of the screen
since it was last called, for an emulator given a scrollback.
ScreenView is the Canvas painter
that draws a Screen, with its cursor when asked,
and Transcript the one that draws
everything a program printed.
Colors
A program's colors reach the app's terminal as the program chose them, so they follow the user's theme as they would
had the program run there:
a palette color (ESC [ 31 m, or ESC [ 38 ; 5 ; n m) stays an IndexedColor,
and an RGB color a Color.
A palette entry the program changed (with OSC 4) is the RGB color it changed it to,
since the user's terminal doesn't know it.
A cell in the program's default colors leaves them unset, so it follows the text_style of the canvas showing it.
To have those follow the user's theme too, give the canvas the terminal's default colors,
as text_terminal_default | text_bg_terminal_default | content_terminal_default
(see Terminal colors).
A cell whose colors the program swapped (inverse video) or hid (invisible text) has to name its default colors,
so it takes the emulator's foreground and background instead, which are white and black unless the program
changes them (with OSC 10 and 11).
set_default_colors changes them on a running emulator, as the user changing their terminal's theme does,
though a color the program set stays what it set.
API
counterweight.pseudoterminal.run_pty
async
run_pty(
argv: Sequence[str],
columns: int,
rows: int,
on_output: Callable[[bytes], None],
on_start: Callable[[PtyConnection], None] = ...,
env: Mapping[str, str] = ...,
clear_env: bool = ...,
stdin: StandardInput = ...,
hangup_grace: timedelta = ...,
timeout: None = None,
) -> Exited | FailedToStart
run_pty(
argv: Sequence[str],
columns: int,
rows: int,
on_output: Callable[[bytes], None],
on_start: Callable[[PtyConnection], None] = ...,
env: Mapping[str, str] = ...,
clear_env: bool = ...,
stdin: StandardInput = ...,
hangup_grace: timedelta = ...,
timeout: timedelta | None = ...,
) -> Exited | FailedToStart | TimedOut
run_pty(
argv: Sequence[str],
columns: int,
rows: int,
on_output: Callable[[bytes], None],
on_start: Callable[
[PtyConnection], None
] = lambda connection: None,
env: Mapping[str, str] = frozendict(),
clear_env: bool = False,
stdin: StandardInput = "pty",
hangup_grace: timedelta = HANGUP_GRACE,
timeout: timedelta | None = None,
) -> Exited | FailedToStart | TimedOut
Runs argv on a new pseudo-terminal columns by rows, in its own session with the terminal as its controlling
terminal, until it exits.
| PARAMETER | DESCRIPTION |
|---|---|
on_output
|
Called on the event loop with each read of what the program writes, standard output and standard error together, as a terminal shows them. |
on_start
|
Called once the program is running, with the connection to write to it and resize its terminal.
TYPE:
|
env
|
Variables to set in the program's environment.
The program gets the app's environment (unless
TYPE:
|
clear_env
|
Leave out the app's environment, so the program gets only the terminal's variables and
TYPE:
|
stdin
|
Where the program reads its standard input from.
TYPE:
|
hangup_grace
|
How long the program has to exit after each signal that ends it (see below).
TYPE:
|
timeout
|
How long the program may run before it's hung up and
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Exited | FailedToStart | TimedOut
|
How the program exited, why it couldn't start, or that it ran out of time. |
Once the program exits, what it wrote just before is read for up to DRAIN_TIMEOUT.
Then, or when cancelled or out of time, the program is hung up, as closing a terminal window does:
its process group, which includes anything it started in the background, gets SIGHUP,
and whatever is still running hangup_grace later gets SIGTERM, then SIGKILL after another,
so no program outlives the call that ran it.
Cancelled while hanging up, it sends SIGKILL at once.
What the program writes while it's hung up isn't passed to on_output.
counterweight.pseudoterminal.PtyConnection
dataclass
PtyConnection(
master: int,
pid: int,
pending: bytearray = bytearray(),
is_writing: bool = False,
is_closed: bool = False,
)
The master side of a running program's pseudo-terminal, which run_pty hands out while the program runs.
Writes that the program isn't reading yet are buffered and flushed when the terminal is writable,
so a handler that writes never blocks the event loop.
Once run_pty returns, writing and resizing do nothing.
resize
Sets the terminal's size, which sends the program SIGWINCH, as resizing a terminal window does.
counterweight.pseudoterminal.StandardInput
StandardInput = Literal['pty', 'devnull']
Where the program reads its standard input from: its terminal, for a program that takes keys,
or /dev/null, so a program that reads input gets end of file rather than waiting for keys that never come.
counterweight.pseudoterminal.use_pty
use_pty(
argv: Sequence[str],
columns: int,
rows: int,
on_output: Callable[[bytes], None],
env: Mapping[str, str] = frozendict(),
clear_env: bool = False,
hangup_grace: timedelta = HANGUP_GRACE,
) -> Pty
Runs argv on a pseudo-terminal while the calling component is mounted, as a terminal window does,
with its terminal columns by rows, reading its standard input from the terminal.
See run_pty for env, clear_env and hangup_grace,
and for how the program is hung up when the component unmounts.
The program starts once columns and rows are both positive, so a component that sizes the terminal
from its own box with use_rects, which is zero on its first render, starts the program at its real size.
Later changes to them resize the terminal, which sends the program SIGWINCH, except to a size that isn't positive.
A change to argv, env or clear_env hangs the program up and starts it again.
Hanging up goes on in the background, so a program slow to exit doesn't hold up the app's rendering,
and an app that stops waits for it.
| PARAMETER | DESCRIPTION |
|---|---|
on_output
|
Called, from the event loop rather than during a render, with each read of what the program writes.
The latest render's |
counterweight.pseudoterminal.Pty
dataclass
counterweight.pseudoterminal.Starting
dataclass
The program hasn't started yet: its terminal is waiting for a size, or the program is being spawned.
counterweight.emulator.Emulator
Emulator(
columns: int,
rows: int,
on_reply: Callable[[bytes], None],
foreground: Color = WHITE,
background: Color = BLACK,
scrollback: int = 0,
)
A terminal emulator columns wide and rows tall, fed what a program writes.
on_reply receives the bytes the emulator answers the program's queries with
(cursor position reports, device attributes), which belong on the program's input, as a terminal would send them.
foreground and background are the colors the program's default colors stand for wherever a cell has to name them
(see Screen.background), until the program changes them.
Its palette starts as DEFAULT_PALETTE.
scrollback is how many of the rows that scroll off the top of the screen
scrolled_off can return, at most MAX_SCROLLBACK.
Use it as a context manager, or call close, to free the emulator's memory as soon as it's done with.
terminal
instance-attribute
terminal = Terminal(
cols=columns,
rows=rows,
scrollback=scrollback
and scrollback + rows_per_page(columns),
fg=foreground,
bg=background,
)
free
instance-attribute
free = weakref.finalize(
self,
free_render_state,
self.state[0],
self.row_iterator[0],
self.cells[0],
)
classify_keys
instance-attribute
classify_keys = ffi.new(
"GhosttyCellData[5]",
[
lib.GHOSTTY_CELL_DATA_WIDE,
lib.GHOSTTY_CELL_DATA_HAS_STYLING,
lib.GHOSTTY_CELL_DATA_CONTENT_TAG,
lib.GHOSTTY_CELL_DATA_CODEPOINT,
lib.GHOSTTY_CELL_DATA_STYLE_ID,
],
)
classify_values
instance-attribute
classify_values = ffi.new(
"void*[5]",
[
ffi.cast("void*", self.wide),
ffi.cast("void*", self.has_styling),
ffi.cast("void*", self.content_tag),
ffi.cast("void*", self.codepoint),
ffi.cast("void*", self.style_id),
],
)
__exit__
__exit__(
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None
feed
feed(data: bytes) -> None
Feed the emulator bytes the program wrote, raising whatever on_reply raised while it ran.
resize
Resize the terminal, reflowing what it shows, as resizing a terminal window does.
set_default_colors
Changes the colors the program's default colors stand for wherever a cell has to name them
(see Screen.background),
as the user changing their terminal's theme does, keeping everything else the terminal shows.
A color the program set (with OSC 10 or 11) stays what it set.
scrolled_off
The rows that scrolled off the top of the screen since the last call, oldest first,
or the newest scrollback of them if more did.
It returns each row once, as it was when it scrolled off, so a program's whole output
is what each call returns in turn, followed by the screen.
Erasing the scrollback (ESC [ 3 J) doesn't take back rows it already returned,
and a program drawing on the alternate screen scrolls nothing off.
update
Brings the render state up to date with the terminal, setting aside the rows that scrolled off the screen since the last update, and returns the colors the program's default colors stand for.
The render state's rows stay dirty until a snapshot cleans them, however many updates come first.
request_overscan
request_overscan(above: int) -> None
Asks the next update to capture above rows above the screen, or as many as there are.
row_ids
The ids of the first count rows the render state captured, top to bottom.
convert_row
The cells of the row rows is at.
A styled cell's value holds its style's id within the row's page, so within one row, a cell value that's a lone codepoint maps to the same cells wherever it recurs, and a style id to the same style, which saves reading the style's colors and attributes again.
erased_style
The style of a blank cell whose value holds the background the program erased it with, and no style.
read_style
The style of the cell cells is at, leaving colors that are the program's defaults unset,
but where it has to name them, as foreground and background.
style_color
A style's color, or None for the program's default.
palette_color
Palette entry index, which follows the user's theme as an IndexedColor,
unless the program changed it, since the user's terminal doesn't know the color it changed it to.
cursor_position
Where the cursor is, at the first column of a wide cell it's on, or None if it's hidden.
counterweight.emulator.Screen
dataclass
Screen(
columns: int,
rows: int,
lines: Rows,
cursor: Position | None,
modes: Modes,
title: str,
foreground: Color,
background: Color,
)
What a program's terminal shows at one moment: its rows of cells, its cursor, its modes and its title.
Compared by identity: a snapshot of a terminal that changed is a new Screen, and a snapshot of one that didn't
is the same Screen as before, so a canvas drawing it draws again only when the program's output changed it.
A row the program didn't change is the same tuple as in the previous snapshot.
lines
instance-attribute
One row of cells for each of the terminal's rows, each taking exactly columns cells.
A cell in the program's default colors leaves them unset, so they follow the style of the canvas showing it,
and one in a palette color is an IndexedColor, which follows the user's theme,
unless the program changed that palette entry (with OSC 4), which makes it the RGB Color the program chose.
Where the emulator and counterweight's grapheme_width disagree on how wide
a grapheme cluster is, the row keeps the emulator's columns, which the program laid its line out by:
a cluster narrower here is followed by spaces, and one wider here, or with no width, is drawn as spaces.
cursor
instance-attribute
Where the cursor is, or None if the program hid it.
background
instance-attribute
background: Color
The colors that the program's default colors stand for wherever a cell has to name them: where the program swapped a cell's colors (inverse video, and the drawn cursor) or made its text the color of its background (invisible text).
counterweight.emulator.Modes
dataclass
Modes(
application_cursor_keys: bool = False,
bracketed_paste: bool = False,
mouse_tracking: MouseTracking = "none",
sgr_mouse: bool = False,
focus_events: bool = False,
alternate_screen: bool = False,
)
The modes a program has set that change how its input must be encoded.
application_cursor_keys
class-attribute
instance-attribute
application_cursor_keys: bool = False
DECCKM (?1): cursor keys send ESC O A rather than ESC [ A.
bracketed_paste
class-attribute
instance-attribute
bracketed_paste: bool = False
?2004: pastes are wrapped in ESC [ 200 ~ and ESC [ 201 ~.
mouse_tracking
class-attribute
instance-attribute
mouse_tracking: MouseTracking = 'none'
Which mouse events the program asked for (?9, ?1000, ?1002, ?1003), if any.
sgr_mouse
class-attribute
instance-attribute
sgr_mouse: bool = False
?1006: mouse events are reported in SGR format.
focus_events
class-attribute
instance-attribute
focus_events: bool = False
?1004: the program wants ESC [ I and ESC [ O when it gains and loses focus.
alternate_screen
class-attribute
instance-attribute
alternate_screen: bool = False
?1049 (or ?47, ?1047): the program is drawing on the alternate screen, as full-screen programs do.
counterweight.emulator.MouseTracking
MouseTracking = Literal[
"none", "x10", "normal", "button", "any"
]
Which mouse events a program asked its terminal to report, from fewest to most.
counterweight.emulator.MAX_SCROLLBACK
module-attribute
The most rows an emulator's scrollback can hold: as many as libghostty captures above its screen at once.
counterweight.painters.terminal.ScreenView
dataclass
A Canvas painter that draws a terminal's screen,
with its cursor as a cell with its colors swapped when show_cursor is set,
such as while the canvas is focused.
The box sizes the terminal, not the other way round, so its natural size is zero:
size the canvas with its style, and resize the terminal to the canvas's content box.
Cells in the program's default colors follow the canvas's text_style,
and rows and columns past the box's edges are cut off.