Skip to main content

ANSI and the terminal

Everything between Arlecchino and the console is escape sequences. This page is what goes out, what comes in, and where the seam is if you want to put something else there.

What goes out

One frame is one call to IArlecchinoTerminal.Write. Inside it are cursor jumps and SGR sequences, emitted only where the style changes — see Rendering. Around the frames sit a handful of mode switches:

SequenceWhenMember
Alternate screen, cursor hiddenThe application startsEnterFullScreen()
Normal screen, cursor shownIt stops, including on a crashLeaveFullScreen()
?1000 ?1002 ?1006 — SGR mouse reportingUseMouse() is onEnableMouse() / DisableMouse()
?2004 — bracketed pasteOn by defaultEnablePaste() / DisablePaste()
OSC 52Ctrl+Insert in a fieldCopyToClipboard(text)
Control keys taken and given backThe terminal is taken and handed backTakeControlKeys() / GiveBackControlKeys()

OSC 52 is the reason a copy works over SSH: the sequence carries the text to whatever is showing the terminal, so it lands on the clipboard of the machine the user is sitting at. Terminals may refuse it and none acknowledge it, so there is nothing to report back — which is why the text also goes down the standard input of the first clipboard program the machine has: pbcopy, termux-clipboard-set, wl-copy, xclip, xsel, tried in that order, and only on Linux, macOS and the BSDs. A program that is not installed fails to start and costs nothing.

TakeControlKeys is what lets an application see Ctrl+C as a key rather than as a signal, and it matters on Windows, where the console decides for itself whether Ctrl+C and Ctrl+Shift+C are the same thing. Both members default to doing nothing, so a terminal of your own needs neither until it has something to say about them; the keys go back to the console the moment the terminal is lent to another program.

When color support is None, no style sequence is emitted at all — not even the per-line reset — and the alternate screen is left alone. An application in that state prints plain text instead of spraying escape codes at a console that cannot read them.

What comes in

Arrows, function keys and mouse reports arrive as escape sequences, so an escape has to be read together with whatever follows it. TerminalInputReader is what does that:

  1. It collects the sequence.
  2. Mouse reports go to the escape-sequence parser inside the package and come back as MouseEvent.
  3. Cursor and function keys it decodes itself.
  4. Anything it does not recognize is replayed key by key — which is what makes a plain Escape work even though it starts the same way.
MemberMeaning
Read(key)Handles one key press, reading further keys itself when it looks like the start of a sequence
ReadPending()Reads everything waiting and returns without blocking; drains mouse events too

The escape timeout

The rest of a sequence does not always arrive with its escape. Over ssh or a loaded terminal an arrow can land a few milliseconds later, and reading only what is already buffered turns it into Esc, [, A. So the reader waits options.EscapeTimeout (25 ms) for the continuation.

That wait is also what a lone Esc costs before it is delivered, which is the trade every terminal editor makes:

options.EscapeTimeout = TimeSpan.FromMilliseconds(10); // local terminal
options.EscapeTimeout = TimeSpan.FromMilliseconds(60); // slow link

Two ways a mouse arrives

MouseAvailable and ReadMouse() exist for terminals that deliver the mouse outside the key stream. In practice that is the Windows console, which reads ReadConsoleInput records instead of SGR reports; everywhere else mouse events are escape sequences among the keys and MouseAvailable stays false. Mouse has the detail.

IArlecchinoTerminal

The whole seam is one interface:

MemberMeaning
Width / HeightSize of the window
KeyAvailable / ReadKey()The key stream
MouseAvailable / ReadMouse()The out-of-band mouse stream, where there is one
Write(text)Composed output; a frame arrives as one call
EnterFullScreen() / LeaveFullScreen()The alternate screen
EnableMouse() / DisableMouse()Mouse reporting
EnablePaste() / DisablePaste()Bracketed paste markers
CopyToClipboard(text)OSC 52

SystemTerminal is the real one. FakeTerminal is the other implementation that ships, and .UseTerminal<T>() is how a third goes in — a remote session, a recording harness, a pipe.

Windows

SystemTerminal turns on ENABLE_VIRTUAL_TERMINAL_PROCESSING as it starts. If the console refuses — an old conhost — color drops to None, the alternate screen is not entered, and the application degrades to plain text.

Turning on virtual-terminal input is a different flag, and enabling it stops Console.ReadKey from delivering keys at all. That is why the Windows mouse path reads the console event queue rather than asking for SGR reports.