class TermBuf::Terminal

Overview

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 read on a fibre of its own, in its own execution context when the terminal is a real device, because a blocking read would otherwise stall every other fibre sharing that thread.

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.

Included Modules

Defined in:

termbuf/terminal/terminal.cr

Constant Summary

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.

EVENT_CAPACITY = 256

Events waiting for the application. Once this fills, the reader stops reading, which is the right way round: the terminal's own buffer then applies backpressure to the keyboard rather than memory growing here.

Constructors

Class Method Summary

Instance Method Summary

Instance methods inherited from module TermBuf::Drawing

clear(style : Style = Style::DEFAULT) : Nil clear, fill(rect : Rect, char : Char = ' ', style : Style = Style::DEFAULT) : Nil fill, issue(command : Command) : Nil issue, passthrough(bytes : Bytes) : Nil
passthrough(text : String) : Nil
passthrough
, scroll(rect : Rect, lines : Int32, style : Style = Style::DEFAULT) : Nil scroll, scroll_region(region : Region, lines : Int32, style : Style = Style::DEFAULT) : Nil scroll_region, write(x : Int32, y : Int32, text : String, style : Style = Style::DEFAULT) : Nil write, write_char(x : Int32, y : Int32, char : Char, style : Style = Style::DEFAULT) : Nil write_char

Constructor Detail

def self.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) #

[View source]
def self.open(input : IO = STDIN, output : IO = STDOUT, env : Hash(String, String) = ENV.to_h, probe : 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.


[View source]

Class Method Detail

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

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


[View source]

Instance Method Detail

def batch(& : Batcher -> ) : Nil #

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


[View source]
def capabilities : Capabilities #

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


[View source]
def close : Nil #

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


[View source]
def closed? : Bool #

Whether the terminal has been given back.


[View source]
def cursor(region : Region) : Cursor #

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


[View source]
def cursor(rect : Rect, scrollback : Int32 = 0) : Cursor #

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


[View source]
def cursor : 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.


[View source]
def decoder : Decoder #

Turns the bytes the terminal sends into events. Held here rather than made on the reader fibre so that its deadlines can be adjusted before anything starts reading.


[View source]
def escape_timeout : Time::Span #

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


[View source]
def 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.


[View source]
def events : Channel(Event) #

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


[View source]
def expect_response(prefix : String, terminator : String) : ResponsePattern #

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


[View source]
def forget_response(pattern : ResponsePattern) : Nil #

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


[View source]
def hardware_cursor : 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.


[View source]
def 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.


[View source]
def hide_cursor : Nil #

Hides the terminal's own cursor.


[View source]
def issue(command : Command) : Nil #
Description copied from module TermBuf::Drawing

Sends command on, or collects it. What a Terminal and a Batcher disagree about, and all they disagree about.


[View source]
def last_paint_bytes : Int32 #

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.


[View source]
def paint : Nil #

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


[View source]
def paint! : Nil #

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.


[View source]
def paint_async : Nil #

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


[View source]
def paste_notice : Time::Span #

[View source]
def paste_notice=(span : Time::Span) : Time::Span #

[View source]
def paste_progress : Time::Span #

[View source]
def paste_progress=(span : Time::Span) : Time::Span #

[View source]
def paste_stall : Time::Span #

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


[View source]
def 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.


[View source]
def responses : ResponseRegistry #

The replies the application is waiting for. Anything arriving from the terminal that matches one becomes an Events::Response; everything else goes to the decoder, because an escape sequence nobody asked for is a key someone pressed.


[View source]
def restore : Nil #

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.


[View source]
def scheduling? : Bool #

Whether the frame scheduler is running.


[View source]
def size : ScreenSize #

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.


[View source]
def start : Nil #

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


[View source]
def 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.


[View source]
def started? : Bool #

Whether the owning fibre is running.


[View source]
def stop_frame_scheduler : Nil #

Stops the scheduler. Explicit paints keep working.


[View source]
def sync(&action : Buffer -> Nil) : Nil #

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.


[View source]
def total_paint_bytes : Int64 #

How many bytes every paint has sent between them.


[View source]
def tty : Tty #

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


[View source]
def width_readings : Array(TermBuf::WidthProbe::Reading) #

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


[View source]
def widths : Unicode::WidthPolicy #

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.


[View source]