Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

VT-AC

 ___ ___ _______ _______ _______ | Y | |______| _ | _ |
|. | |.| | |______|. 1 |. 1___|
|. | `-|. |-' |. _ |. |___ |: 1 | |: | |: | |: 1 |
\:.. ./ |::.| |::.|:. |::.. . |
`---' `---' `--- ---`-------'

A VT terminal that could have existed, but didn't.

Not a reproduction of a DEC VT100 — a plausible sibling of one. What makes VT-AC a fantasy machine rather than an emulator is the combination no real terminal had: DEC's command set and screen model, sitting on a 256-colour RGB332 framebuffer, with a CP437 glyph ROM, a pixel-addressable graphics mode, and a bell with a two-octave note table.

It speaks two protocols, and which one is a switch on the front of the machine. Native is VT-AC's own: single-byte commands, direct colour, graphics mode — the protocol a vendor would have shipped if it were competing with DEC rather than copying it. VT-100 is the compatibility mode that same vendor would have had to offer to sell any, and it runs vi, htop and ncurses over a serial line.

Plug in anything with a serial port — an Arduino, a Raspberry Pi, a 6502 computer — and start sending bytes.

VT-AC Demo

80-column VT-100 mode, running htop over a serial link:

VT-AC running htop in 80-column VT-100 mode

Features

  • Two terminal personalities — VT-AC native, or VT-100/ANSI compatibility.
  • 40 and 80 column modes — 40 × 30 at 320 × 240, or 80 × 60 at 640 × 480. Same 8 × 8 font, same square pixels, same 4:3 screen.
  • Full VT-100 emulation — scroll regions, SGR attributes and colour, the alternate screen, tab stops, DEC line drawing, terminal reports. Measured against vttest; the results are in docs/VT100-CONFORMANCE.md.
  • Classic 8 × 8 IBM PC BIOS / Code Page 437 font for character rendering.
  • 256 colours (RGB332), foreground and background settable per 8 × 8 block — or per 8 × 1 pixel row in graphics mode.
  • Text and graphics modes.
  • A bell with configurable duration and a two-octave note table.
  • Desktop app for macOS, Windows and Linux, or run it in your browser with no install at all: acwright.github.io/VT-AC
  • Serial connection with configurable baud rate, parity, data bits and stop bits.
  • Load binary data files straight into the terminal, from the control bar or the command line.
  • A vtac command line that launches the app with flags applied — installed by the app itself.
  • Fullscreen mode and adjustable window scale.

Install

Download the latest release for your platform from the releases page:

PlatformFile
macOS (Apple silicon)VT-AC-2.0.0-mac-arm64.dmg — signed and notarized
Windows (x64)VT-AC-2.0.0-win-x64.exe
Linux (x64)VT-AC-2.0.0-linux-x86_64.AppImage or VT-AC-2.0.0-linux-amd64.deb

Or use it in your browser at acwright.github.io/VT-AC — same terminal, same renderer, serial over the Web Serial API. See Web version.

Upgrading from v1.x? VT-AC is no longer an npm package. See Migrating from v1.x.

Using the app

The window is the terminal, and the strip along the bottom is the control bar.

ControlWhat it does
Load data fileReads a binary file and feeds it through the parser — the -l flag's equivalent
ResetIdentical to sending 0x04
Clear screenIdentical to sending 0x0C
Connect / disconnectOpens the serial port. Tinted by status: dim, amber while connecting, green when open, red on error
9600 8N1Live framing readout. Click it to open Settings at the serial section
VT-AC / VT-100Personality readout. Click to toggle
40 / 80Column readout. Click to toggle — this clears the screen, as a mode switch does on real hardware
Bell muteDimmed until the audio device is live
PasteSends the clipboard as bytes
FullscreenF11 also works
SettingsThe panel below

The settings panel slides in from the right:

  • TERMINAL — personality and column mode, and what each one is.
  • SERIAL — port, baud rate, data bits, parity, stop bits. Defaults to 9600 8-N-1, VT-AC v1's default.
  • DISPLAY — window scale 1× to 6×, and fullscreen. Desktop only.
  • BELL — mute, volume, and a Test button.
  • FILES — the loaded data file, and Reload.
  • COMMAND LINE — installs the vtac shim. Desktop only.

Changes here are saved. Anything set by a command-line flag applies to that launch only — see Command line.

Terminal Personalities

VT-AC parses one of two protocols at a time.

NativeVT-100
What it isVT-AC's own protocol — byte-for-byte v1.3.0, plus the ESC extensionsANSI / VT-100 compatibility mode
CommandsSingle bytes, 0x000x1FEscape sequences, ESC [ …
ColourDirect RGB332, 256 colours, via 0x18 / 0x19SGR, quantized onto the same 256
Graphics modeYesNo — VT-100 has no such thing
Good forMicrocontrollers, 8-bit machines, anything sending raw bytesvi, htop, ncurses, anything that expects a terminal
Default

Switch between them with the control bar's VT-AC / VT-100 readout, the settings panel, vtac --mode vt100, or from the wire:

FromToSequence
NativeVT-100ESC 0x03 (1B 03)
VT-100NativeESC [ ? 7000 h
EitherThe launch defaultESC c (RIS)

Each personality is left by a sequence the other one cannot express, so neither is a one-way door. Mode 7000 is VT-AC's own invention, chosen from a range nobody else has claimed: xterm's ctlseqs documents nothing above 2006, DEC's private modes are all below 100, and the other well-known squatters sit at 1000–1016, 2004 and mintty's 7700s.

The cursor, the two colours and the bell are shared between personalities, so switching never teleports the cursor or changes what the screen looks like.

80-Column Mode

40-column80-column
Grid40 × 3080 × 60
Pixels320 × 240640 × 480
Font8 × 88 × 8
Aspect4:34:3

An exact 2× of the 40-column grid — same font, same square pixels, same 8-rows-per-cell graphics mode. Only the number of cells changes, so every command keeps its meaning: SET COLUMN and SET ROW take their operand modulo the dimension, so they simply address further.

Switching clears the screen and homes the cursor, which is what DECCOLM does on real hardware and avoids inventing a reflow policy no VT ever had.

The window does not resize. 80-column mode looks finer in the same window rather than making it jump, and -s scale keeps meaning "how big is the picture".

Switch with the control bar's 40 / 80 readout, the settings panel, vtac --columns 80, or ESC 0x01 / ESC 0x02 from the wire.

VT-100 Mode

In vt100 personality the byte stream goes through a DEC ANSI parser instead of the native one. Supported, in brief:

  • Cursor — CUU/CUD/CUF/CUB, CUP/HVP, CNL/CPL, CHA, VPA, IND/RI/NEL, DECSC/DECRC.
  • Erase and edit — ED, EL, IL, DL, ICH, DCH, ECH. (IL/DL/ICH/DCH are VT102 rather than VT100, and are included deliberately — ncurses needs them.)
  • Attributes — SGR 0/1/4/5/7 and their 22/24/25/27 counterparts, 30–37 and 40–47, bright 90–97 and 100–107, 38;5;n/48;5;n xterm-256, and 38;2;r;g;b truecolour.
  • Scrolling — DECSTBM scroll regions, honoured by LF, IND, RI, IL, DL and auto-wrap alike.
  • Modes — DECCKM, DECCOLM, DECSCNM, DECOM, DECAWM with correct deferred last-column wrap, DECTCEM, the alternate screen (47/1047/1049), IRM, LNM.
  • Character sets — SCS for US ASCII, UK and DEC Special Graphics, SO/SI shifting G0/G1. Line drawing resolves onto glyphs the CP437 ROM already has, which is why that ROM was the right choice for this fiction.
  • Tab stops — HTS, TBC, default every 8 columns.
  • Reports — DA (ESC [ ? 1 ; 2 c, a VT100 with the Advanced Video Option), DECID, DSR/CPR, DECREQTPARM, DECALN.
  • Reset — RIS.

Two colour models, and this is the trap.SGR 38;5;n looks like "256 colours, and VT-AC has 256 colours", but they are not the same 256: the xterm palette is quantized onto the RGB332 cube, so neighbouring xterm greys can land on one VT-AC byte. To address VT-AC's palette exactly, use native mode's 0x18 and 0x19.

What it does not do, deliberately: 132 columns, double-height and double-width lines, VT52 mode, DECSTR, the secondary and tertiary device attributes, and UTF-8 (the glyph ROM is CP437). Each of those is explained, with the vttest results that back the rest of this section, in docs/VT100-CONFORMANCE.md.

Character Set

VT-AC Character Set

Palette

VT-AC Palette

Instruction Set

The native personality is driven by single-byte instructions. A printable command card is in docs/VT-AC.pdf.

Instruction# of BytesCommandDescription
0x001NULLNo operation
0x011HOMEMove cursor to home position (0,0)
0x022Cursor Character2nd byte is character to use, or $00 to turn off (Default=$00)
0x031Cursor ModeToggle cursor mode between solid/blinking (Default=solid)
0x041ResetReset terminal (text mode, clear screen, cursor home-off-solid, bg=$00, fg=$FF)
0x052Bell Duration2nd byte is bell duration in jiffies (i.e., 1/60th of a second) (Default=$3C)
0x062Bell Frequency2nd byte is bell frequency (Default=$3D)
0x071BELLPlay bell sound
0x081BSBackspace
0x091TABMove cursor to next tab stop
0x0A1LFLine feed (move cursor down, same column)
0x0B1Screen ModeToggle screen mode between text/graphics modes (Default=text)
0x0C1Clear ScreenClear the screen
0x0D1CRCarriage return (move cursor to start of line)
0x0E2Set Column2nd byte is column, modulo the column count (Default=$00)
0x0F2Set Row2nd byte is row, modulo the row count (Default=$00)
0x101Delete to SoLDelete to start of line
0x111Delete to EoLDelete to end of line
0x121Delete to SoSDelete to start of screen
0x131Delete to EoSDelete to end of screen
0x141Scroll LeftScroll screen to the left replacing vacated columns with spaces
0x151Scroll RightScroll screen to the right replacing vacated columns with spaces
0x161Scroll UpScroll screen up replacing vacated rows with blank lines
0x171Scroll DownScroll screen down replacing vacated rows with blank lines
0x182Foreground Color2nd byte is foreground color 0 ($00) to 255 ($FF) (Default=$FF)
0x192Background Color2nd byte is background color 0 ($00) to 255 ($FF) (Default=$00)
0x1A2Data Next2nd byte data not command - Allows 0 ($00) to 31 ($1F), and 127 ($7F) as data
0x1B2ESCEscape — 2nd byte selects an extension, see below
0x1C1Cursor LeftMove the cursor to the left
0x1D1Cursor RightMove the cursor to the right
0x1E1Cursor UpMove the cursor up
0x1F1Cursor DownMove the cursor down
0x201ASCIIStandard ASCII Characters
*1ASCIIStandard ASCII Characters
0x7E1ASCIIStandard ASCII Characters
0x7F1DELETEDelete at cursor position
0x801ASCIIExtended ASCII Characters
*1ASCIIExtended ASCII Characters
0xFF1ASCIIExtended ASCII Characters

Escape extensions

0x1B was reserved in v1.3.0 and documented as "reserved for future escape code implementation". This is that future, and it is the one intentional deviation from v1 in the whole 2.0.0 release.

SequenceEffect
ESC 0x0140-column mode (320 × 240)
ESC 0x0280-column mode (640 × 480)
ESC 0x03Enter the VT-100 personality
ESC 0x04Query — reply with personality, columns and rows
ESC 0x1BA literal 0x1B as data

The query replies with five bytes: 1B 04, then the personality, then the column and row counts as their literal values — 1B 04 00 28 1E for 40 × 30, 1B 04 00 50 3C for 80 × 60. The personality byte always reads 00, because the query is a native-mode extension and a terminal in VT-100 mode never reaches it.

Any other second byte is parsed as an ordinary byte, which is exactly what v1 did with the byte after its no-op ESC. A v1 stream containing a stray 0x1B behaves identically, unless the byte that followed it happened to be 0x010x04 or 0x1B.

Text Mode

In text mode, each byte sent to the terminal is interpreted and rendered as characters using the standard 8x8 pixel font. Each character cell can have its own foreground and background color by first setting the foreground and background colors before sending the next byte to be displayed. The terminal supports basic text operations such as cursor movement, line feed, carriage return, and clearing the screen.

The cursor can be controlled using the provided instructions, allowing it to be positioned using row and column commands, turned on or off, and set to solid or blinking mode.

In text mode, in order to display characters with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as data rather than a command.

Graphics Mode

The terminal can be switched into graphics mode to display pixel-based graphics. Data is then interpreted as the next byte row (0-7) in the 8x8 pixel block at the cursor position (column, row). After sending 8 bytes, the cursor automatically moves to the next column. After reaching the end of the row, it wraps to the beginning of the next row. After reaching the end of the screen, it wraps back to the top-left corner. If the cursor is moved manually using the set row and set column instructions, the next byte will be rendered at the top row of that position. In this way, the cursor can be positioned and then a continuous stream of bytes can be sent to the terminal to render pixel data.

Each byte that is sent can be individually colored using the foreground and background colors, enabling the creation of simple graphics and images.

In graphics mode, in order to send pixel data with ASCII values from 0 ($00) to 31 ($1F) or 127 ($7F), the "Data Next" instruction must first be sent to indicate that the next byte should be treated as pixel data rather than a command.

Graphics mode belongs to the native personality. VT-100 mode has no equivalent, and a stream that left the terminal in graphics mode before switching personality still gets text.

Bell

The terminal includes a bell feature that can be triggered using the BELL instruction. The duration and frequency of the bell sound can be configured using the Bell Duration and Bell Frequency instructions, allowing for customizable audio feedback.

How It Works

The bell system uses real-time audio synthesis to generate pure sine wave tones at specific frequencies. When a BELL command is received:

  1. The current bellFrequency and bellDuration settings are captured and added to a playback queue
  2. If no bell is currently playing, playback begins immediately
  3. Bell sounds are played sequentially - each tone completes before the next begins
  4. The audio device remains open while processing queued requests for optimal performance

This queue-based approach ensures that rapid sequences of bell commands (like playing musical scales) are handled smoothly without overlap or dropped notes.

Configuration

Bell Duration (0x05): Sets the length of the bell tone in jiffies (1/60th of a second)

  • Default: $3C (60 jiffies = 1 second)
  • Range: $01 to $FF (0.017 seconds to 4.25 seconds)
  • Example: $0F (15 jiffies = 0.25 seconds)

Bell Frequency (0x06): Sets the musical note to play using hex values from the frequency table below

  • Default: $3D (C6 = 1046.50 Hz)
  • Range: $01 to $54 (C1 to B7)
  • Example: $2E (A4 = 440 Hz - standard concert pitch)

Bell (0x07): Triggers playback with the current duration and frequency settings

Usage Example

To play middle C (C4) for half a second:

0x05 0x1E // Set duration to 30 jiffies (0.5 seconds)
0x06 0x25 // Set frequency to C4 (261.63 Hz)
0x07 // Play the bell

To play a simple ascending scale:

0x05 0x0F // Set duration to quarter second
0x06 0x25 // C4
0x07 // Play
0x06 0x27 // D4
0x07 // Play
0x06 0x29 // E4
0x07 // Play

Bell Frequencies

NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)NoteHexFrequency (Hz)
C1$0132.70C2$0D65.41C3$19130.81C4$25261.63
C#1$0234.65C#2$0E69.30C#3$1A138.59C#4$26277.18
D1$0336.71D2$0F73.42D3$1B146.83D4$27293.66
D#1$0438.89D#2$1077.78D#3$1C155.56D#4$28311.13
E1$0541.20E2$1182.41E3$1D164.81E4$29329.63
F1$0643.65F2$1287.31F3$1E174.61F4$2A349.23
F#1$0746.25F#2$1392.50F#3$1F185.00F#4$2B369.99
G1$0849.00G2$1498.00G3$20196.00G4$2C392.00
G#1$0951.91G#2$15103.83G#3$21207.65G#4$2D415.30
A1$0A55.00A2$16110.00A3$22220.00A4$2E440.00
A#1$0B58.27A#2$17116.54A#3$23233.08A#4$2F466.16
B1$0C61.74B2$18123.47B3$24246.94B4$30493.88
------------------------
C5$31523.25C6$3D1046.50C7$492093.00
C#5$32554.37C#6$3E1108.73C#7$4A2217.46
D5$33587.33D6$3F1174.66D7$4B2349.32
D#5$34622.25D#6$401244.51D#7$4C2489.02
E5$35659.25E6$411318.51E7$4D2637.02
F5$36698.46F6$421396.91F7$4E2793.83
F#5$37739.99F#6$431479.98F#7$4F2959.96
G5$38783.99G6$441567.98G7$503135.96
G#5$39830.61G#6$451661.22G#7$513322.44
A5$3A880.00A6$461760.00A7$523520.00
A#5$3B932.33A#6$471864.66A#7$533729.31
B5$3C987.77B6$481975.53B7$543951.07

Command line

VT-AC installs its own command line. Open Settings → COMMAND LINE → Install, and vtac lands on your PATH. On Windows the installer does it.

Every flag opens the app with that flag applied. Nothing prints terminal output — the window is the output.

vtac -p /dev/ttyUSB0 # connect on launch
vtac -p /dev/ttyUSB0 -b 115200 -a none -d 8 -t 1
vtac --mode vt100 --columns 80 # 80-column VT-100 mode
vtac -l ./examples/characters.bin # load a data file
vtac -f -s 4 # fullscreen, 4× scale
FlagDescriptionDefault
-p, --port <port>Path to the serial port, connected before the window shows
-b, --baudrate <rate>Baud rate9600
-a, --parity <parity>odd | even | nonenone
-d, --databits <bits>5 | 6 | 7 | 88
-t, --stopbits <bits>1 | 1.5 | 21
-m, --mode <mode>native | vt100saved setting
-c, --columns <cols>40 | 80saved setting
-f, --fullscreenOpen fullscreenoff
-s, --scale <scale>Window scale, 1–6saved setting
-l, --load <path>Data file to parse after launch
--app <path>Where the VT-AC application is, if not where vtac looks
-v, --versionPrint the version
-h, --helpShow help

Framing, personality and column flags apply to that launch only. Someone running vtac -p /dev/ttyUSB0 --mode vt100 is talking to one device, not changing what the app does tomorrow — and anything they then change in the settings panel persists normally.

-s and -f follow the same rule from the other direction: leaving -s out means "the size you left it", and vtac -f does not make the app open fullscreen next time.

Web version

acwright.github.io/VT-AC — the same terminal and the same renderer, in a browser tab. What differs:

  • Serial is the Web Serial API, so it needs Chrome or Edge over HTTPS, and a click to pick the port. The settings panel says so if your browser lacks it.
  • Files come from a file picker rather than a native dialog.
  • Settings live in localStorage, so two tabs are two independent terminals sharing one origin.
  • No window scale, no fullscreen button, and no command-line section — all three are the desktop app's.

Development

npm install
npm run dev # Electron app with hot reload
npm run build:web # web bundle → dist/web
npm run preview:web # serve that bundle
npm run cli # build and run the CLI against a dev app
npm test # 531 unit tests
npm run typecheck

Packaging, one platform each:

npm run dist:mac # signed + notarized dmg
npm run dist:win # nsis installer, via Wine
npm run dist:linux # AppImage + deb, via Docker

And the checks that are not unit tests:

npm run verify:palette # RGB332 expansion against images/palette.png
npm run verify:cellmodel # the cell model against v1's own code, byte for byte
npm run icons # regenerate the app icon and web icons
npm run docs:card # regenerate docs/VT-AC.pdf from the HTML
node scripts/vttest-run.mjs 1 2 3 6 # VT-100 conformance, over a serial loopback

Project structure

VT-AC/
├── build/ icons, entitlements, and the art generator
├── bin/vtac dev entry point for the CLI
├── scripts/ packaging, verification and capture helpers
├── docs/ command card, VT-100 conformance
├── src/
│ ├── core/ the terminal itself — no DOM, no Electron
│ │ ├── VTAC.ts orchestrates Screen + the two parsers
│ │ ├── Screen.ts cell model, geometry, scrolling, rasterizer
│ │ ├── Cell.ts cell planes and the attribute bitfield
│ │ ├── Font.ts CP437 glyph ROM, DEC Special Graphics map
│ │ ├── palette.ts RGB332 → RGBA, xterm-256 → RGB332
│ │ ├── keymap.ts key event → bytes, personality-aware
│ │ └── ansi/ the DEC ANSI parser and its dispatch
│ ├── shared/ types, boot contract, the preload API
│ ├── main/ Electron main: window, serial, settings, CLI shim
│ ├── preload/
│ ├── cli/ the `vtac` command
│ ├── renderer/ Vue 3 + Pinia + Tailwind
│ └── tests/
├── electron.vite.config.ts desktop build
├── vite.web.config.ts web build
└── electron-builder.yml packaging

The core is deliberately free of both the DOM and Electron: the same VTAC class runs in the desktop app, the web build and the test suite.

Migrating from v1.x

VT-AC is no longer an npm package.vtac-terminal stays published so nothing that already depends on it breaks, but it is deprecated and will not be updated. v2.0.0 is a desktop app, a web app, and a CLI the app installs.

  1. Remove the old one, or it will win:
    npm uninstall -g vtac-terminal
    
    This matters more than it looks. npm's bin directory usually comes before/usr/local/bin on PATH, so with both installed a bare vtac answers 1.3.0 — a correct v2 install that looks broken.
  2. Install v2.0.0.
  3. Open Settings → COMMAND LINE → Install to get vtac back on your PATH.

Every v1 flag still works, and now launches the app instead of an SDL window. Two are new: --mode and --columns. Two changed meaning slightly: -s and -f no longer have defaults that would overwrite a remembered window.

Terminal behaviour is unchanged. With default settings — native personality, 40 columns — v2.0.0 processes a byte stream identically to v1.3.0. The v1 test suite still runs unmodified against the new core, and npm run verify:cellmodel compares the two implementations byte for byte over the whole framebuffer after every byte of 14 streams.

There is exactly one intentional deviation, and it is the extension point v1 reserved for it: 0x1B (ESC) is no longer a no-op. See Escape extensions.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

TODO

  • Add support for additional character sets.

License

MIT: https://github.com/acwright/VT-AC/blob/main/LICENSE

Credits

About

A fantasy VT terminal — desktop app, web app, and CLI. VT-100/ANSI or its own protocol, over a serial port.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages