class

TermBuf::Tty

Inherits Reference < Object

Stability: internal

The terminal device itself: the modes it is in, the screen it is showing, and how big it is.

Everything here is reversible, and reversing it is the point. A program that leaves a terminal in raw mode on the alternate screen has made the user's shell unusable, so #leave undoes exactly what #enter did and can be called any number of times.

It works over any pair of IOs. When those are not a terminal — a pipe, a spec — the mode changes are skipped and the escape sequences still go out, which is what makes the whole driver testable without a device.

Constants

BRACKETED_PASTE = Mode.new("bracketed-paste", "\e[?2004h", "\e[?2004l")

Pasted text arrives marked as pasted, rather than as a very fast typist triggering every key binding on the way past.

FOCUS_EVENTS = Mode.new("focus-events", "\e[?1004h", "\e[?1004l")

The terminal reports the window gaining and losing focus.

KITTY_KEYBOARD = Mode.new("kitty-keyboard", "\e[>1u", "\e[<u")

The kitty keyboard protocol, which tells apart keystrokes an ordinary terminal reports identically. This pushes a flag set onto the terminal's own stack and the reset pops it.

MOUSE_SGR = Mode.new("mouse-sgr", "\e[?1002h\e[?1006h", "\e[?1006l\e[?1002l")

Mouse reporting in the SGR encoding, which is the one that can name a column past 223. Both are asked for together, and given back in the reverse order.

The tracking mode is 1002, button-event tracking: press, release, and motion while a button is held. Mode 1000 reports only the press and the release, so a widget that takes the pointer on press never learns where it went and nothing can be dragged. 1002 supersedes 1000 on every terminal that has the SGR encoding, which is the only kind this mode is for, so there is no reason to ask for the lesser one.

Enabling this is the application's call — terminal.enable TermBuf::Tty::MOUSE_SGR — and nothing in this shard does it uninvited: a terminal reporting the mouse is one that no longer lets the person select and copy text with it, which is a trade only the application can weigh. Once it is on, a report arrives as Events::Mouse with its coordinates already converted to buffer cells. Tty#leave sends the reset.

See MOUSE_SGR_ANY for the mode that reports motion with no button held as well, and MOUSE_SGR_CLICKS for the lesser one, which is kept to be measured rather than to be used.

MOUSE_SGR_ANY = Mode.new("mouse-sgr", "\e[?1003h\e[?1006h", "\e[?1006l\e[?1003l")

Mouse reporting as MOUSE_SGR, but with mode 1003 — any-event tracking, which reports motion with no button held too. That is what hover needs: a highlight that follows the pointer, a tooltip, a cursor shape that changes over a hot spot.

It is not the default because of what it costs. Every cell the pointer travels over is a report, so crossing a wide terminal is a hundred events to read, decode, and act on, and over ssh it is a hundred packets. An application that only drags wants MOUSE_SGR.

All three share the registry name mouse-sgr, because the terminal has one mouse tracking mode and not three: asking for 1003 after 1002 replaces the tracking rather than adding to it. Tty#enable writes a replacement of the same name whose bytes differ, so the change reaches the terminal, and #leave sends the one reset that belongs to whichever was asked for last.

MOUSE_SGR_CLICKS = Mode.new("mouse-sgr", "\e[?1000h\e[?1006h", "\e[?1006l\e[?1000l")

Mouse reporting in the SGR encoding with mode 1000 — normal tracking, which reports the press and the release and nothing in between.

This is the X10-compatible tracking every terminal has had for forty years, and no application in this shard wants it: a widget that takes the pointer on press never learns where it went, so nothing can be dragged. It is here to be measured. What a terminal does under 1000 and under 1002 when nothing is held down is a property of that terminal, and one that has been seen to differ from what the mode is defined as — a terminal that sends motion reports while no button is down makes a widget reading a motion report as "a button is held" wrong. scripts/caps_check.cr records the answer per terminal, and the mouse page of examples/validate.cr cycles the three modes so it can be watched.

Shares the registry name mouse-sgr with the two below it; see MOUSE_SGR_ANY for what that means.

VTIME = 5

Constructors

new(input : IO, output : IO, managed : Bool | Nil = nil)
Source
standard

The process's own terminal.

Source

Instance methods

alternate?

Whether the alternate screen has been switched to. Tracked apart from #entered? because the probe switches to it before the takeover, and tracked at all so that #leave pops what was pushed rather than what a capability set settled afterwards says should have been.

Source
disable(mode : Mode) : Nil

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

Source
enable(mode : Mode) : Nil

Turns mode on: now if the terminal has been taken over, and at #enter if it has not.

Registration is by name, so enabling the same mode twice registers it once and writes it once. That is not tidiness. KITTY_KEYBOARD pushes onto a stack the terminal keeps, and a second push against the single pop #leave sends leaves the keyboard changed after the program has gone.

A mode of the same name carrying different bytes is a replacement rather than a repeat, and does go out: cursor_shape is one mode whichever shape it is asking for, and asking for a bar after a block has to reach the terminal.

Source
enter(capabilities : Capabilities = Capabilities::NONE) : Nil

Takes the terminal over: raw mode, the alternate screen, no cursor.

capabilities decides which of the optional modes are worth asking for; asking a terminal to enable something it does not have leaves the request printed on screen. Bracketed paste is registered here, and every mode registered by #enable is written now, which is what makes taking the terminal back after a suspend put the modes back too.

Source
enter_alternate(capabilities : Capabilities = Capabilities::NONE) : Bool

Switches to the alternate screen, without the rest of the takeover.

This is where the probe goes. A terminal that does not recognise a query prints its payload instead of swallowing it, so asking on the screen the person was looking at leaves rubbish there — and it is still there once the program has given the screen back. On the alternate screen nobody sees any of it and leaving takes it away along with the screen.

Returns whether the screen was switched. Without Capability::AltScreen there is nowhere to put the echo and the caller has #scrub_line instead. Idempotent; #enter calls it, and #leave undoes it whether or not #enter ever ran.

Source
entered?

Set once #enter has run, cleared by #leave.

Source
flush

Pushes whatever is buffered out to the device.

Source
input

Where keystrokes and replies come from.

Source
leave

Gives the terminal back exactly as it was found. Safe to call twice, and safe to call when #enter never ran, which is what makes it usable from a signal handler and from at_exit.

Source
managed?

Whether this is a terminal whose modes are worth changing.

Source
modes

Every mode registered on this terminal, in the order it was enabled. #leave resets them in the reverse of that order.

Source
output

Where escape sequences go.

Source
raw!

Puts the terminal in raw mode, keeping what it was in so #restore_modes can put it back. Crystal's own raw! would do most of this, but it restores to a cooked terminal rather than to whatever was there before, which is not the same thing when a program was started from something other than an ordinary shell.

This has to happen before the terminal is asked anything. A cooked terminal echoes the replies onto the screen and holds them in the line discipline until a newline that never comes, so the queries appear to go unanswered and then all arrive at once the moment raw mode is set.

Idempotent: calling it again keeps the modes first found, not the raw ones, so #restore_modes still has somewhere to go back to.

Source
raw?

Set once the modes have been changed, which happens before #enter when the terminal is about to be probed.

Source
restore_modes

Puts the line discipline back the way it was found. Idempotent, and safe to call when raw mode was never entered.

Source
scrub_line

Blanks the line the cursor is on, using nothing but a carriage return and spaces.

A terminal that does not recognise a query prints its payload instead of swallowing it, so asking one leaves rubbish on the screen the person was looking at — and it is still there once the program gives the screen back. Terminal.app does this with XTGETTCAP, DECRPM and the kitty graphics query, between them putting about forty five characters on the line the program started on.

The fallback for a terminal with no alternate screen to ask on. Where there is one, #enter_alternate puts the echo somewhere nobody is looking and leaving takes it away; a line of spaces is as much as a terminal that has just demonstrated it cannot parse an escape sequence can be asked to understand, and an echo long enough to have wrapped would leave its earlier lines behind.

Source
size

How big the terminal is now. Asked afresh every time, since the answer changes whenever the window does.

Source
write(text : String) : Nil

Writes straight to the device, bypassing the buffer.

Source

Nested types