class

TermBuf::Terminal

Inherits TermBuf::Drawing < Reference < Object

Stability: stable — changes only in a major release.

The terminal, as an application talks to it.

One fibre owns the buffer. Every drawing method builds a command and sends it there, so ordering is total and nothing needs locking. A command that has something to report — a paint, a shutdown — carries a reply channel and the caller waits on it; the rest are fire and forget.

Input is Input::Stream's: one fibre reads the device, in an execution context of its own when the terminal is a real one because a blocking read would otherwise stall every fibre sharing that thread, and a second turns what it read into the events on #events.

Whatever happens, the terminal is given back: the owning fibre restores it on the way out, signal handlers restore it before dying, and at_exit catches anything that got past both.

Constants

COMMAND_CAPACITY = 256

Deep enough that a full redraw does not stall the fibre sending it, shallow enough to be backpressure rather than an unbounded queue.

CURSOR_SHAPE_MODE = "cursor_shape"

The name the cursor shape is registered under, so that asking for a different one replaces the mode rather than stacking a second one.

CURSOR_SHAPE_RESET = "\e[0 q"

DECSCUSR's own reset, which gives the shape back to whatever the terminal was configured for.

TITLE_STACK = Tty::Mode.new("title", "\e[22;0t", "\e[23;0t")

The window title saved on the terminal's own stack. Pushed the first time a title is set and popped by Tty#leave, so a program that changed the title leaves the one it found behind — and a takeover after a suspend saves it again.

CSI 22 ; 0 t pushes the icon name and the window title, CSI 23 ; 0 t pops them. A terminal with Capability::Titles and no stack ignores both and the title simply stays changed, which is where a program that never pushed ends up anyway.

Constructors

new(tty : Tty, *, capabilities : Capabilities = Capabilities::NONE, size : ScreenSize | Nil = nil, pending_input : Bytes = Bytes.empty, warnings : Array(String) = [] of String, width_spec : String | Nil = nil, probe_widths : Bool = false, quirks : Quirk = Quirk::None, detect_composed_drift : Bool = true, clear_overhang : Bool = true, signals : Bool | Nil = nil)

The device is the one thing a terminal cannot be built without, so it stays positional; everything else is named. There are ten of them and most default, which is exactly the shape that reads wrong positionally.

Source
open(input : IO = STDIN, *, output : IO = STDOUT, env : Hash(String, String) = ENV.to_h, probe : Bool = true, detect_composed_drift : Bool = true) : Terminal

Detects what the terminal can do, takes it over, and starts running.

The block form is the one to reach for: it gives the terminal back even when the body raises, which no amount of care in the body can guarantee on its own.

Source

Class methods

open(input : IO = STDIN, *, output : IO = STDOUT, env : Hash(String, String) = ENV.to_h, probe : Bool = true, detect_composed_drift : Bool = true, & : Terminal -> ) : Nil

Opens a terminal, yields it, and closes it however the block ends.

Source

Instance methods

after(span : Time::Span) : Input::Nonce

Asks for an Events::Timer in span from now, and returns the nonce that will name it.

The tick travels the input channel, so it is ordered against everything the terminal said before it was armed rather than racing it. It is a floor and not a promise: the event arrives no sooner than span, and no sooner than the application drains what is queued ahead of it.

Source
batch

Builds a frame's worth of drawing and sends it as one channel operation.

Source
cancel(nonce : Input::Nonce) : Nil

Withdraws the timer nonce names. Nothing is delivered for it, even when it is called after the timer has already gone off.

Source
capabilities

What the terminal was found to be able to do. The encoder emits nothing that is not in here.

Source
clear_overhang=(value : Bool) : Bool

Whether to write the cell after a glyph that may have painted outside its own columns. See Painter#clear_overhang?, which this is.

On unless the application says otherwise: every terminal measured draws over a neighbouring cell without repainting it, so ink left there stays until something writes that cell again.

Source
clear_overhang?

Whether to write the cell after a glyph that may have painted outside its own columns. See Painter#clear_overhang?, which this is.

On unless the application says otherwise: every terminal measured draws over a neighbouring cell without repainting it, so ink left there stays until something writes that cell again.

Source
clipboard

The system clipboard, written through the terminal with OSC 52.

A copy goes out in order with the frames around it, the way a colour change does. See Clipboard for what a terminal will take and for why nothing comes back to say whether it took it.

Source
close

Restores the terminal and stops. Safe to call more than once, and safe to call from an exception handler.

Source
closed?

Whether the terminal has been given back.

Source
colors

The terminal's own colours: the defaults, the cursor, and the palette.

Changes go out in order with the frames around them. See ColorStack for why every one of them needs Capability::KittyColorStack.

Source
cursor(region : Region) : Cursor

A cursor over region, which scrolls and wraps within it.

Source
cursor(rect : Rect, scrollback : Int32 = 0) : Cursor

A cursor over rect, keeping scrollback rows of what scrolls off it.

Source
cursor

The cursor streamed output goes to, covering the whole screen.

cursor.io is an IO, so puts, print, printf, and anything else that writes to one can be pointed at the screen.

Made on first use rather than in the constructor, which would hand a half-built terminal to something that keeps hold of it.

Source
cursor_shape

What the terminal is drawing its own cursor as, as far as it was asked. CursorShape::Default until something else is asked for, and again once the terminal has been given its own setting back.

Source
cursor_shape=(shape : CursorShape) : CursorShape

Asks the terminal to draw its own cursor as shape.

terminal.cursor_shape = :bar
terminal.cursor_blink = false
terminal.cursor_shape = TermBuf::CursorShape::Default

DECSCUSR, CSI Ps SP q, and it goes through the mode registry rather than a bare write: #close sends CSI 0 SP q and the person gets the cursor their terminal was configured for back, and a takeover after a suspend asks for the shape again. Asking for a second shape replaces the first rather than stacking on it.

Whether the shape blinks is #cursor_blink=, and a setter is where it has to live: Crystal allows an assignment method one argument, so the pair cannot be one call. Each of them re-asks with the other's current value, so the order they are set in does not matter.

Does nothing without Capability::CursorShape. This is the terminal's own cursor, not one of the buffer's — see #hardware_cursor= for which of those it follows.

Source
disable(mode : Tty::Mode) : Nil

Turns mode off and forgets it, so a later takeover does not bring it back. A mode that was never enabled is nothing to turn off.

Source
enable(mode : Tty::Mode) : Nil

Turns mode on, and keeps it on across a suspend and resume.

See Tty::Mode for what a mode is and Tty#enable for what enabling one twice does. Before #start the mode is only recorded, and goes out with the rest of the takeover; afterwards it goes through the owning fibre, since writing to the device from anywhere else would land in the middle of a frame.

Source
escape_timeout

See Decoder::ESCAPE_TIMEOUT. Worth raising over a slow link, where a sequence can take longer than that to arrive in full.

Source
escape_timeout=(span : Time::Span) : Time::Span

See Decoder::ESCAPE_TIMEOUT. Worth raising over a slow link, where a sequence can take longer than that to arrive in full.

Source
events

Everything the terminal has to say, in the order it happened.

Source
expect_response(prefix : String, terminator : String) : Input::Pattern

Says that a reply beginning with prefix and ending with terminator is expected, so it arrives as an Events::Response rather than as input.

An application that wants the reply as something more than its bytes can register with Input::Patterns directly and return an event of its own.

Source
forget_resize(handler : ResizeHandler) : Bool

Stops running handler on a resize, and says whether it was registered.

Source
forget_response(pattern : Input::Pattern) : Nil

Stops expecting pattern, so sequences matching it are input again.

Source
hardware_cursor

Which cursor the terminal's own cursor follows, or nil while it is hidden.

Hidden is the default, which is what a full screen application wants: a cursor blinking wherever the last run of text ended is a distraction. An application with somewhere for someone to type points this at the cursor they are typing into, and every paint puts the terminal's cursor back there afterwards.

Source
hardware_cursor=(cursor : Cursor | Nil) : Cursor | Nil

Which cursor the terminal's own cursor follows, or nil while it is hidden.

Hidden is the default, which is what a full screen application wants: a cursor blinking wherever the last run of text ended is a distraction. An application with somewhere for someone to type points this at the cursor they are typing into, and every paint puts the terminal's cursor back there afterwards.

Source
hide_cursor

Hides the terminal's own cursor.

Source
hit(x : Int32, y : Int32, &block : Buffer::Hit | Nil -> ) : Nil

Yields what sits at the buffer cell (x, y), or nil when that is off the screen. See Buffer#hit.

The read goes through #sync, so it sees the buffer between frames rather than part way through one, and the block runs on the fibre that owns it — keep it short, since the frame after it is waiting.

terminal.hit column, row do |hit|
  next unless hit
  terminal.write 0, 0, "clicked #{hit.text}"
end

Coordinates are 0-based buffer cells. An SGR mouse report numbers its columns and rows from one; subtracting that is the mouse decoder's job, not this method's.

Source
images

The images on screen.

They are drawn over the cells after each frame rather than into them, so text written where one sits gets both. See ImageStore.

Source
input

The input side: the fibres that read the device, the decoder they feed, and the sequence patterns the application registered.

Source
issue(command : Command) : Nil

Sends command to the fibre that owns the buffer, which is what every drawing method here does. A closed terminal drops it rather than raising: there is nothing left to draw on.

Source
last_paint_bytes

How many bytes the last paint sent. The point of the buffer is that a frame costs a diff rather than a screenful, and this is how an application checks that it is getting one.

Source
on_resize

Registers handler, run when the screen changes size: after the grids have been resized and before Events::Resize is sent.

This is where an application puts its layout. A Region the application made covers a pane it chose, and the driver has no idea what that pane was meant to be a fraction or an edge of, so it does not move it — see Region#bounds=. Rather than repeating the arithmetic at every Events::Resize, work it out once here:

terminal.on_resize do |size|
  status.region.bounds = Rect.new 0, size.rows - 1, size.columns, 1
  log.region.bounds = Rect.new 0, 0, size.columns, size.rows - 1
end

Handlers run in the order they were registered, on the fibre that owns the buffer. That fibre is the one servicing commands, so a handler must not call back into #batch, #paint, or #sync; moving regions and recomputing rectangles is what it is for. Anything it raises arrives as an Events::Failure and the remaining handlers still run.

Returns the handler, which #forget_resize takes back.

Source
on_resize(handler : ResizeHandler) : ResizeHandler

Registers handler, run when the screen changes size: after the grids have been resized and before Events::Resize is sent.

This is where an application puts its layout. A Region the application made covers a pane it chose, and the driver has no idea what that pane was meant to be a fraction or an edge of, so it does not move it — see Region#bounds=. Rather than repeating the arithmetic at every Events::Resize, work it out once here:

terminal.on_resize do |size|
  status.region.bounds = Rect.new 0, size.rows - 1, size.columns, 1
  log.region.bounds = Rect.new 0, 0, size.columns, size.rows - 1
end

Handlers run in the order they were registered, on the fibre that owns the buffer. That fibre is the one servicing commands, so a handler must not call back into #batch, #paint, or #sync; moving regions and recomputing rectangles is what it is for. Anything it raises arrives as an Events::Failure and the remaining handlers still run.

Returns the handler, which #forget_resize takes back.

Source
paint

Draws, and waits for the bytes to reach the terminal.

Source
paint!

Rewrites every cell, whatever the buffer thinks the terminal is showing. For after a resize, a suspend, or anything else that leaves the screen in a state the buffer cannot know about.

Source
paint_async

Draws without waiting, which is what the frame scheduler uses.

Source
paste_notice

See Decoder::PASTE_NOTICE.

Source
paste_notice=(span : Time::Span) : Time::Span

See Decoder::PASTE_NOTICE.

Source
paste_progress

See Decoder::PASTE_PROGRESS.

Source
paste_progress=(span : Time::Span) : Time::Span

See Decoder::PASTE_PROGRESS.

Source
paste_stall

See Decoder::PASTE_STALL. Worth raising for an application expecting very large pastes over a very slow link.

Source
paste_stall=(span : Time::Span) : Time::Span

See Decoder::PASTE_STALL. Worth raising for an application expecting very large pastes over a very slow link.

Source
policy

What the buffer measures clusters with, so a View cuts writes where the cells will fall.

Source
quirks

What this terminal is known to get wrong. See Quirk.

Source
resize_interval

How often a window resize is acted on, at most.

Dragging a window corner produces a SIGWINCH for every intermediate geometry the window passes through, and acting on each one means a full repaint per pixel of travel. The first resize of a burst goes through at once, since a window that has been still is one whose new size is worth showing immediately; everything after it inside the interval collapses into a single further resize taken on the geometry the window finished at.

Zero turns the limiting off, and every resize is acted on as it arrives.

Source
resize_interval=(resize_interval : Time::Span)

How often a window resize is acted on, at most.

Dragging a window corner produces a SIGWINCH for every intermediate geometry the window passes through, and acting on each one means a full repaint per pixel of travel. The first resize of a burst goes through at once, since a window that has been still is one whose new size is worth showing immediately; everything after it inside the interval collapses into a single further resize taken on the geometry the window finished at.

Zero turns the limiting off, and every resize is acted on as it arrives.

Source
restore

Gives the terminal back without going through the owning fibre. What the signal handlers and at_exit call, since by then there may be no fibre left to ask.

Source
scheduling?

Whether the frame scheduler is running.

Source
signals

What the operating system says, and what this terminal does about it.

SIGTERM, SIGINT and SIGHUP give the terminal back and re-raise themselves; SIGWINCH becomes an Events::Resize; SIGTSTP and SIGCONT hand the screen over and take it back. An application that wants a first interrupt to ask rather than kill says so here:

terminal.signals.mode Signal::INT, TermBuf::Input::Signals::Mode::WarnThenExit

and then draws something on the Events::Signal that arrives, calling Input::Signals#reset_count if the person decides to stay.

Source
size

How big the terminal was when it was last looked at. Updated by the owning fibre on a resize, so this is a snapshot rather than a promise.

Source
stages

The chain every event walks before the application sees it.

Two stages are built during #start:

  • :resize consumes the Events::Signal for SIGWINCH and answers it with Events::Resize, since the buffer has to be resized and marked for redraw before anyone is told, and a window change should be one event rather than two.
  • :signals passes everything through. It is a placeholder, there so that an application with a policy about signals has somewhere named to put it: replace that one entry and leave the rest of the chain alone.

See Input::Stage for what a stage may do with an event and Input::Stages for changing the chain while events are flowing: #push adds one, #replace reorders or removes.

terminal.stages.replace terminal.stages.to_a.reverse
Source
start

Takes the terminal over and starts the fibres that run it.

Source
start_frame_scheduler(fps : Int32 = 60) : Nil

Starts painting automatically at up to fps frames a second, coalescing whatever was drawn in between. A paint with nothing to do costs nothing, so this is safe to leave running.

Off by default: an application that draws in response to input knows better than a timer when a frame is worth sending.

Source
started?

Whether the owning fibre is running.

Source
stop_frame_scheduler

Stops the scheduler. Explicit paints keep working.

Source
sync

Runs action against the buffer on the owning fibre, and waits.

The escape hatch for anything the drawing API does not cover, and the only safe way to read the buffer: doing it from another fibre would race with whoever is drawing.

Source
title

The window title this terminal was last asked for, or nil when it has not been asked for one — or was asked to give the original back.

Source
title=(text : String | Nil) : String | Nil

Sets the window title, or gives back the one the terminal started with.

terminal.title = "termbuf — #{path}"
terminal.title = nil            # whatever it said before

The title goes out in order with the frames around it, the way a colour or a clipboard write does. The terminal is asked to save the title it had the first time one is set and #close asks for it back, so a program that changed the title does not leave its own behind in a tab somebody goes on using.

Does nothing at all without Capability::Titles: a terminal that does not take the sequence prints it.

Source
total_paint_bytes

How many bytes every paint has sent between them.

Source
tty

The device underneath, for anything the driver does not wrap.

Source
warn_composed_drift=(warn_composed_drift : Bool)

Whether to give the screen back and say so on stderr the first time a cluster this terminal will misplace is drawn.

An application that handles Events::Warning itself will want this off: the event arrives either way, and being pulled out of the alternate screen mid-frame is worse than a message it chose where to put. On by default, since a terminal quietly misrendering is the thing being guarded against.

Source
warn_composed_drift?

Whether to give the screen back and say so on stderr the first time a cluster this terminal will misplace is drawn.

An application that handles Events::Warning itself will want this off: the event arrives either way, and being pulled out of the alternate screen mid-frame is worse than a message it chose where to put. On by default, since a terminal quietly misrendering is the thing being guarded against.

Source
width_readings

What the width probe asked and what came back, for diagnostics. Empty when it did not run.

Source
widths

How this terminal measures a grapheme cluster, which is what the buffer writes with. Measured at startup unless TERMBUF_WIDTHS=off or the terminal declined to answer.

Source
window_resized(size : ScreenSize = @tty.size) : Nil

Tells the terminal its window changed size.

This is what the SIGWINCH handler calls. An application that does not get the signal — one driving a pty whose size it sets itself, say — calls it in the signal's place, and passes the size rather than leaving it to be read off the terminal.

Rate limited by #resize_interval. The leading edge is issued straight away; a call inside the interval instead leaves a single fibre to sleep out the remainder and issue one resize at the end of it, reading the size then so that the repaint lands on the geometry the window settled at rather than one it passed through. Further calls inside the interval do no more than replace what that fibre will use.

Source

Nested types