vt100 lets a Shoddy program boss a terminal around: move the
cursor to an exact spot, paint text in bold or underline, clear the screen,
scroll a region, and so on. It does this the way terminals have always been
told what to do — by printing short, invisible escape sequences
mixed in among your ordinary text. An escape sequence is just a string that
starts with one special "escape" character. The terminal spots it, quietly
carries out the instruction, and prints none of it. Every word in this
machine is a little factory that builds one such string and hands it back to
you. Nothing here touches the screen itself — you glue the pieces together
with & and hand the finished string to Print
when you're ready. There's also one word going the other way:
EvalKey takes a raw keystroke sequence the terminal sent
— an arrow key, a function key — and tells you which key it was.
In 1978, Digital Equipment Corporation shipped a video terminal called the
VT100: a keyboard and a green-on-black screen that plugged
into a big shared computer somewhere down the hall. It couldn't compute
anything itself. Its whole job was to show whatever characters the computer
sent down the wire, and to send back whatever the person typed. The clever
part was that the same wire had to carry two kinds of thing —
ordinary text to be displayed, and commands like "move the cursor to the top"
or "start underlining." The trick was to reserve one otherwise-unused
character, the escape character (value 27), as a flag.
Earlier terminals had used it, and by then it was being written down as a
standard. Plain text prints as itself. But the moment the terminal sees an
escape, it stops and reads the next few bytes as an instruction instead of
printing them. Most of those instructions began with escape followed by an
opening bracket — a pairing called the Control Sequence
Introducer, or CSI. That ESC [ is the seed nearly every
word in this machine grows from.
The VT100 was a runaway success, and its particular set of escape sequences was written into a standard — ANSI X3.64 in the United States, and its international twin ECMA-48 — so that other makers could build terminals that spoke the same language. That is the quiet miracle worth pausing on: the VT100 itself is a museum piece, long gone, and yet its language never died. Every terminal window you have ever opened is pretending to be a VT100, or something descended from one — the one on your Mac, the one in Linux, the black window in Windows, the shell inside your code editor. When a modern program prints coloured text or draws a progress bar in place, it is sending very nearly the same bytes a 1978 DEC terminal would have understood. Learn these codes and you have learned a language that has outlived the hardware it was invented for by nearly fifty years.
The VT100 also carried its own past on its back. Its predecessor, the
VT52, spoke a shorter, simpler dialect, and the VT100 could
switch into a compatibility mode to keep old software happy. That is why
this machine has a whole family of Vt52… words alongside the
modern ones. They're a fossil layer, meaningful only once the terminal has
actually been switched into VT52 mode — but they're part of the real story,
so they're here.
A plain Print can only do one thing: drop a line of text at
the bottom and shove everything else up. That's fine for a list of numbers.
But it's no way to build anything that feels alive — a game board that
redraws in place, a status line pinned to the top, a menu you can highlight,
a progress bar that fills without scrolling the world away. All of that needs
two powers a bare terminal already has but won't give you through
Print alone: putting the cursor exactly where you want it, and
changing how the next characters look. vt100 hands you both, as
readable words. Instead of memorising that "go to row 5, column 10" is the
cryptic string ESC [ 5 ; 10 H, you write
CursorPos(5, 10) and let the machine spell it out. Reach for it
whenever a text program wants to draw rather than just scroll — a dashboard,
a board game, a full-screen editor, anything that should look put-together
rather than printed one line at a time.
The rhythm of using vt100 is always the same: build a
string, then print it. Each word returns a small chunk of escape
codes; you stitch the chunks together with &, drop your
actual text in among them, and hand the whole thing to Print
once.
Include "vt100.shoddy"
Def Main()
' Wipe the screen, jump to the top-left, greet in bold.
Print(ClearScreen() & CursorHome() & Bold() & "READY" & AttrsOff())
' Put a label at row 5, column 10, underlined.
Print(CursorPos(5, 10) & Underline() & "SCORE: 0" & AttrsOff())
' Decode a raw keystroke captured from the terminal.
Let k = EvalKey(Esc() & "OA") ' the Up arrow, application mode
Select Case k
Case KeyUp()
Print("moving up")
Case Else
Print("some other key")
A few things worth keeping in mind:
Print ends the line.
That's why the example builds one big string and prints it once,
rather than printing each escape code on its own. A stray newline in the
middle of a run of escape codes is harmless on most terminals, but it isn't
guaranteed to be — so concatenate first, print last.Bold,
Underline, Blink and friends stay on until you say
otherwise — the terminal has no idea where your word ends. Finish a
highlighted run with AttrsOff (which clears all
attributes at once) or the rest of your screen will keep wearing it.CursorPos(1, 1)
is the top-left corner, the same spot CursorHome jumps to. Row
comes first, then column.EvalKey is a decoder; InKey is the
keyboard, and InKey is not this machine's.
EvalKey takes a raw sequence and tells you which key it
represents. The sequence usually comes from the InKey builtin —
a non-blocking, unechoed read that returns one pending keystroke, or
"" when nothing is pending. (Non-blocking: it never stops and
waits. Unechoed: the key is not printed to the screen.) InKey is
documented on terminal's page along with
Print, Input and InputLine, because
reading a keystroke is protocol-agnostic and has nothing to do with DEC. What
is VT100 is what comes back: arrows and PF1–4 arrive as exactly
the application-mode sequences EvalKey recognises. So
EvalKey(InKey()) polls the live keyboard from a game loop (see
mills/pac-vt100). Nothing needs including for InKey
itself, and EvalKey is equally happy with a sequence replayed
from a test or built up by hand. Whole-line, echoed input remains
Input, also terminal's.SetCursorKeyApp / SetAltKeypad).
EvalKey knows both forms and folds them to the same
Vt100Key, so you don't have to track which mode is live.You don't need any of this to use the machine — it's here for the curious, and for anyone reading the raw bytes in a hex viewer.
It all comes down to one builtin. The whole machine is
built on Chr, which turns a number into the single character with
that code. The escape character is Chr(27), wrapped up as
Esc; nearly everything else starts from Csi, which
is just Esc() & "[" — the Control Sequence Introducer. From
there each word is a one-liner. Bold is
Csi() & "1m", and ClearScreen is
Csi() & "2J". The words that take arguments splice the
numbers in with Str, so CursorPos(row, col) is
Csi() & Str(row) & ";" & Str(col) & "H". There is
no state, no cleverness — just string-building over Chr.
A handful of codes were reconstructed, not copied. This
machine was transcribed from an old escape-code reference table, and a few of
that table's rows disagree with the well-established VT100/VT52 protocol. Where
they conflict, the machine follows the standard and says so in a comment.
RequestStatus and RequestCursorPos use the proper
CSI form (ESC [ 5 n, ESC [ 6 n) even though the
table abbreviated them. And Vt52CursorPos was rebuilt from the
real VT52 spec, adding the Y introducer and the +32 offset each
coordinate needs — which is why it addresses row 0, column 0 as
ESC Y followed by two spaces. If you're driving a real terminal
or emulator, sanity-check these against it.
Some words share their exact bytes with others — on purpose.
Both the VT100 and the older VT52 reuse short escape sequences, so the same
raw bytes can mean different things depending on which mode the terminal is
in. Vt52CursorLeft and IndexDown are both
ESC D; Vt52CursorHome and TabSet are
both ESC H. That collision is a genuine property of the protocol,
not a slip in this file — which mode the terminal is actually in decides what
the bytes do.
Names dodge a couple of landmines. The reverse-video
attribute is called ReverseVideo, not Reverse,
because Reverse is a core builtin (it flips lists and arrays).
Shadowing it here would break every other machine and program that includes
this file. Likewise the key constructors are named KeyUp,
KeyDown and so on rather than the plainer names keys uses, so the two machines can be included side by
side.
The return path: EvalKey. Going from terminal
to host, EvalKey is one big Select Case over the raw
string. It recognises both the normal and application-mode forms of every
arrow and keypad key and returns a Vt100Key. One honest
limitation: in numeric keypad mode a keypad digit sends the very same byte as
the matching top-row digit. So a bare "5" is classified as
KeyPad("5") by convention — the byte alone can't prove it came
from the keypad rather than the number row. Anything EvalKey
doesn't recognise comes back as KeyUnknown(raw), handing you the
original string so nothing is ever silently lost.
Every word, plus the Vt100Key type
| Word | Description |
|---|---|
| Esc() | The escape character itself, Chr(27) — the
flag that tells a terminal "an instruction follows." The root everything else
is grown from. |
| Csi() | The Control Sequence Introducer, Esc() &
"[". Nearly every ANSI-style code below starts here. |
| Word | Description |
|---|---|
| CursorUp(n) | Move the cursor up n rows. |
| CursorDown(n) | Move the cursor down n rows. |
| CursorRight(n) | Move the cursor right n columns. |
| CursorLeft(n) | Move the cursor left n columns. |
| CursorHome() | Jump to the top-left corner (row 1, column 1). |
| CursorPos(row, col) | Jump to an exact spot. Rows and columns are 1-based; row first, then column. |
| HvHome() | The same as CursorHome — the source
table's alias, using the final byte f instead of H.
Kept as its own word because the table lists it separately. |
| HvPos(row, col) | The f-byte alias of
CursorPos; same effect. |
| IndexDown() | Move down one line, scrolling the screen if
already at the bottom (ESC D). |
| ReverseIndex() | Move up one line, scrolling the screen if
already at the top (ESC M). |
| NextLine() | Move to the start of the next line
(ESC E). |
| SaveCursor() | Remember the current cursor position (and
attributes) so RestoreCursor can return to it. |
| RestoreCursor() | Jump back to the position last stored by
SaveCursor. |
| Word | Description |
|---|---|
| ClearEol() | Clear from the cursor to the end of the line. |
| ClearBol() | Clear from the start of the line up to the cursor. |
| ClearLine() | Clear the whole line the cursor is on. |
| ClearEos() | Clear from the cursor to the end of the screen. |
| ClearBos() | Clear from the top of the screen down to the cursor. |
| ClearScreen() | Clear the entire screen. |
| Word | Description |
|---|---|
| AttrsOff() | Turn all attributes back off — back to plain text. End every highlighted run with this. |
| Bold() | Start bold (bright) text. |
| LowIntensity() | Start dim (low-intensity) text. |
| Underline() | Start underlined text. |
| Blink() | Start blinking text. |
| ReverseVideo() | Swap foreground and background (reverse video).
Named ReverseVideo to avoid clashing with the Reverse
builtin. |
| Invisible() | Start hidden (invisible) text. |
| Word | Description |
|---|---|
| SetScrollRegion(top, bottom) | Confine scrolling to the rows
from top to bottom; the rest of the screen stays
put — handy for a fixed header or footer. |
| Word | Description |
|---|---|
| TabSet() | Set a tab stop at the current column
(ESC H). |
| TabClear() | Clear the tab stop at the current column. |
| TabClearAll() | Clear every tab stop. |
| Word | Description |
|---|---|
| DoubleHeightTop() | Make this line the top half of a double-height line. |
| DoubleHeightBottom() | Make this line the bottom half of a double-height line. |
| SingleWidthHeight() | Return the line to normal single width and height. |
| DoubleWidthHeight() | Make this line double width and single height. |
| Word | Description |
|---|---|
| SetNewlineMode() | Newline mode: Enter sends both carriage return and line feed. |
| SetLinefeedMode() | Line feed mode: Enter sends line feed only. |
| SetCursorKeyApp() | Put the arrow keys in application mode (they
send the ESC O forms). |
| SetCursorKeyNormal() | Put the arrow keys back in normal (cursor) mode. |
| Set132Cols() | Switch to 132-column display. |
| Set80Cols() | Switch to 80-column display. |
| SetSmoothScroll() | Smooth (gradual) scrolling. |
| SetJumpScroll() | Jump (instant) scrolling. |
| SetReverseScreen() | Reverse the whole screen (dark on light). |
| SetNormalScreen() | Return the screen to normal (light on dark). |
| SetOriginRelative() | Make cursor positions relative to the scrolling region. |
| SetOriginAbsolute() | Make cursor positions relative to the whole screen. |
| SetAutoWrap() | Turn on auto-wrap: text past the last column continues on the next line. |
| ResetAutoWrap() | Turn off auto-wrap. |
| SetAutoRepeat() | Turn on key auto-repeat (holding a key repeats it). |
| ResetAutoRepeat() | Turn off key auto-repeat. |
| SetInterlace() | Turn on interlaced display. |
| ResetInterlace() | Turn off interlaced display. |
| SetVt52Mode() | Drop the terminal into VT52 compatibility mode —
after this it no longer understands CSI codes, only the short
Vt52… ones. |
| SetAnsiMode() | Climb back out of VT52 mode into ANSI mode
(ESC <). This is a VT52-side command, since a terminal in VT52
mode won't parse CSI at all. |
| Word | Description |
|---|---|
| SetAltKeypad() | Application keypad mode: keypad keys send
distinct ESC O codes rather than plain digits. |
| SetNumKeypad() | Numeric keypad mode: keypad keys send plain digits, the same bytes as the top-row number keys. |
| Word | Description |
|---|---|
| SetUkG0() | Select the UK character set as G0. |
| SetUkG1() | Select the UK character set as G1. |
| SetUsG0() | Select the US (ASCII) character set as G0. |
| SetUsG1() | Select the US (ASCII) character set as G1. |
| SetSpecialG0() | Select the special graphics / line-drawing set as G0. |
| SetSpecialG1() | Select the special graphics / line-drawing set as G1. |
| SetAltRomG0() | Select the alternate ROM character set as G0. |
| SetAltRomG1() | Select the alternate ROM character set as G1. |
| SetAltRomSpecialG0() | Select the alternate ROM special-graphics set as G0. |
| SetAltRomSpecialG1() | Select the alternate ROM special-graphics set as G1. |
| SetShift2() | Single-shift 2 (ESC N): use G2 for the
next character only. |
| SetShift3() | Single-shift 3 (ESC O): use G3 for the
next character only. |
| Word | Description |
|---|---|
| RequestStatus() | Ask the terminal to report its status
(ESC [ 5 n). |
| RequestCursorPos() | Ask the terminal to report where the cursor
is (ESC [ 6 n). |
| RequestTerminalType() | Ask the terminal to identify what it is
(ESC [ 0 c). |
| ResetTerminal() | Reset the terminal to its power-on state
(ESC c). |
| Word | Description |
|---|---|
| ScreenAlignmentTest() | Fill the screen with a test pattern of
Es for checking alignment. |
| TestPowerUp() | Invoke the power-up self test. |
| TestLoopback() | Invoke the data-loopback test. |
| TestPowerUpRepeat() | Repeat the power-up self test continuously. |
| TestLoopbackRepeat() | Repeat the loopback test continuously. |
| LedsOff() | Turn all four keyboard LEDs off. |
| Led1On() | Turn keyboard LED 1 on. |
| Led2On() | Turn keyboard LED 2 on. |
| Led3On() | Turn keyboard LED 3 on. |
| Led4On() | Turn keyboard LED 4 on. |
These only mean anything once the terminal has been switched into VT52
mode with SetVt52Mode; there it speaks only these short
ESC-prefixed codes and no CSI at all. Several share their bytes
with ANSI-mode words above — a real quirk of the protocol.
| Word | Description |
|---|---|
| Vt52SetGraphics() | Enter VT52 graphics mode (ESC F). |
| Vt52ResetGraphics() | Leave VT52 graphics mode (ESC G). |
| Vt52CursorUp() | Move the cursor up one line. |
| Vt52CursorDown() | Move the cursor down one line. |
| Vt52CursorRight() | Move the cursor right one column. |
| Vt52CursorLeft() | Move the cursor left one column
(ESC D — same bytes as IndexDown). |
| Vt52CursorHome() | Jump to the home position
(ESC H — same bytes as TabSet). |
| Vt52CursorPos(row, col) | Direct cursor addressing: sends
ESC Y then each 0-based coordinate offset by +32. Reconstructed
from the real VT52 spec, so row 0, column 0 addresses as
ESC Y space space — verify against your terminal. |
| Vt52ReverseIndex() | Move up one line, scrolling if needed
(ESC I). |
| Vt52ClearEol() | Clear to the end of the line. |
| Vt52ClearEos() | Clear to the end of the screen. |
| Vt52Ident() | Ask the terminal to identify itself
(ESC Z). |
| Vt52IdentResponse() | The reply a VT52 sends to an identify
request (ESC / Z). |
| Word | Description |
|---|---|
| Vt100Key | The sum type EvalKey returns:
KeyUp, KeyDown, KeyLeft,
KeyRight, KeyPF1–KeyPF4 (the function
keys), KeyPad(Ch) (a keypad key, carrying its character),
KeyEnter, and KeyUnknown(Raw) (anything
unrecognised, carrying the original string). |
| EvalKey(raw) | Classify one raw keystroke sequence into a
Vt100Key. Recognises both the normal and application-mode forms
of each arrow and keypad key, so you needn't track the terminal's mode.
Anything it doesn't know comes back as KeyUnknown(raw). Pair
with the InKey builtin — terminal's,
not this machine's — to poll the live keyboard. |
This machine documents no builtins of its own. It used to be
the only page mentioning InKey; that word and the rest of the raw
console family are now documented on terminal's
page, and every word below is a Def declared here.
| User | How | |
|---|---|---|
| pac-vt100 | Both
directions of the protocol — EvalKey decodes arrow escapes in
the pure core; cursor addressing and clearing paint the shell. | |
| weather-glass | The banding: day rows plain, night rows dim, marker rows bold, the header in reverse video. |
None — every escape sequence is built on the Chr builtin
alone, standalone by design.