# TKPAnsi - ANSI BBS Terminal Emulation Component TKPAnsi is a visual Delphi 1.0 component providing ANSI/VT100 terminal emulation with scrollback, cursor blinking, and ANSI music support. It is a TCustomControl descendant that renders incoming data using standard ANSI escape sequences for cursor positioning, color attributes, and screen manipulation. Installs to the **KP** component palette tab. ## Properties ### Published (Design-Time and Runtime) | Property | Type | Default | Description | |---|---|---|---| | FontSize | Integer | 12 | Font point size (4-72). Controls the OEM terminal font size. The control auto-sizes Width and Height from FontSize, Cols, and Rows. | | Cols | Integer | 80 | Terminal columns (1-256). | | Rows | Integer | 25 | Terminal rows (1-255). | | ScrollbackSize | Integer | 500 | Maximum scrollback history lines. | | CursorVisible | Boolean | True | Show or hide the cursor (also controlled by DEC mode ?25h/l). | | Color | TColor | clBlack | Control background color. | | OnKeyData | TKeyDataEvent | nil | Keyboard data callback. | | TabStop | Boolean | True | Accept keyboard focus. | ### Public (Read-Only) | Property | Type | Description | |---|---|---| | CursorCol | Integer | Current 0-based cursor column. | | CursorRow | Integer | Current 0-based cursor row. | ## Methods ### Public | Method | Description | |---|---| | Create(AOwner) | Constructor. Allocates screen lines, initializes defaults. | | Destroy | Destructor. Frees paint font, screen, and scrollback. | | Clear | Moves the current screen to scrollback and allocates fresh blank lines. Resets the cursor to 0,0. | | Reset | Resets all attributes and parse state, then calls Clear. | | Write(S) | Parses the string and renders immediately. Convenience wrapper around WriteDeferredBuf + FlipToScreen. | | WriteDeferredBuf(Buf, Len) | Parses a PChar buffer into the cell buffer, then renders dirty rows. Used for bulk serial data. | | FlipToScreen | Renders all dirty rows to the screen DC. Handles coalesced scrolls, cursor ghost cleanup, and scrollbar updates. | | TickBlink | Toggles cursor and text blink state every 500 ms using GetTickCount. Call from the application's main loop. | ## Events ### OnKeyData ```pascal TKeyDataEvent = procedure(Sender: TObject; const Data: string) of object; ``` Fires when keyboard input produces data to send to the serial port. | Key | Data sent | |---|---| | Printable characters | The character itself | | CR, LF, BS, TAB, ESC | The control character | | Arrow Up/Down/Right/Left | ESC[A / ESC[B / ESC[C / ESC[D | | Home / End | ESC[H / ESC[K | | Page Up / Page Down | ESC[V / ESC[U | | Insert | ESC[@ | | Delete | #127 | | F1-F10 | ESC OP through ESC OY | | ENQ (#5) | ESC[?1;0c (Device Attributes response) | | DSR query (ESC[6n) | ESC[row;colR (cursor position report) | ## Types ### TParseState ``` psNormal Normal character input psEscape Received ESC, awaiting next byte psCSI In CSI sequence (ESC[...), parsing parameters psCSIQuestion DEC private mode (ESC[?...) psMusic Accumulating ANSI music string (ESC[M...^N) ``` ### TTermCell ```pascal TTermCell = record Ch: Char; { Display character } FG: Byte; { Foreground palette index 0-15 } BG: Byte; { Background palette index 0-15 } Bold: Boolean; { Bold attribute (FG +8 at render) } Blink: Boolean; { Blink attribute (toggles on TickBlink) } end; ``` ### TTermLineRec ```pascal TTermLineRec = record Cells: array[0..255] of TTermCell; end; PTermLine = ^TTermLineRec; ``` ### ANSI Color Palette Standard 16-color ANSI palette in BGR TColor order: ``` 0 Black 8 Dark Gray 1 Red 9 Bright Red 2 Green 10 Bright Green 3 Brown/Yellow 11 Bright Yellow 4 Blue 12 Bright Blue 5 Magenta 13 Bright Magenta 6 Cyan 14 Bright Cyan 7 Light Gray 15 Bright White ``` ## ANSI Escape Sequences ### CSI Sequences (ESC[ params final-byte) | Final | Name | Description | |---|---|---| | A | CUU | Cursor up P1 rows (default 1) | | B | CUD | Cursor down P1 rows (default 1) | | C | CUF | Cursor forward P1 columns (default 1) | | D | CUB | Cursor back P1 columns (default 1) | | H, f | CUP/HVP | Cursor position to row P1, column P2 (1-based) | | J | ED | Erase display: 0=below, 1=above, 2=all | | K | EL | Erase line: 0=to end, 1=from start, 2=entire | | L | IL | Insert P1 blank lines at cursor | | M | DL | Delete P1 lines at cursor | | P | DCH | Delete P1 characters at cursor | | S | SU | Scroll up P1 lines | | T | SD | Scroll down P1 lines | | @ | ICH | Insert P1 blank characters at cursor | | m | SGR | Set graphic rendition (see below) | | s | SCP | Save cursor position | | u | RCP | Restore cursor position | | c | DA | Device attributes (P1=0: respond ESC[?1;0c) | | n | DSR | Device status report (P1=5: OK, P1=6: cursor pos) | ### SGR Codes (ESC[ code m) | Code | Effect | |---|---| | 0 | Reset all attributes | | 1 | Bold (maps FG index +8 for bright colors) | | 5 | Blink (cell visibility toggles every 500 ms) | | 7 | Reverse video (swap FG and BG at render) | | 22 | Normal intensity (cancel bold) | | 25 | Blink off | | 27 | Reverse off | | 30-37 | Foreground color (black, red, green, yellow, blue, magenta, cyan, white) | | 40-47 | Background color (same order) | ### DEC Private Modes (ESC[? code h/l) | Mode | h (set) | l (reset) | |---|---|---| | ?7 | Enable auto-wrap at right margin | Disable auto-wrap | | ?25 | Show cursor | Hide cursor | ### Control Characters | Char | Action | |---|---| | BEL (#7) | MessageBeep | | BS (#8) | Cursor left one column | | TAB (#9) | Advance to next tab stop (multiple of 8) | | LF (#10) | Cursor down; scroll at bottom | | CR (#13) | Cursor to column 0 | | ENQ (#5) | Respond with device attributes | ## ANSI Music ANSI music is triggered by `ESC[M` and terminated by `^N` (Ctrl-N, #14). The string between these delimiters contains musical notation: | Command | Description | |---|---| | T*n* | Set tempo in BPM (32-255) | | L*n* | Set default note length (1=whole, 2=half, 4=quarter, 8=eighth) | | O*n* | Set octave (0-7) | | < / > | Shift octave down / up | | A-G | Play note (optional # or + for sharp, - for flat) | | P*n* | Pause for duration *n* | Notes accept an optional duration digit after the letter (e.g., C4 = quarter note C). A trailing dot adds 50% to the duration. Uses the Windows 3.1 sound API: OpenSound, SetVoiceAccent, SetVoiceNote, StartSound, CloseSound. ## Font Selection CreatePaintFont selects the best available font for the requested FontSize. The selection order ensures pixel-perfect rendering when possible: 1. **Terminal raster at the exact size.** If the Terminal font has a raster at the requested cell height, use it. Terminal is pixel-perfect but only available at specific heights (typically 12 and 24 pixels on VGA). 2. **Perfect DOS VGA 437 at multiples of 8.** If the requested cell height is a multiple of 8 (8, 16, 24, 32, ...) and the font is installed, use it. This TrueType CP437 font renders pixel-perfectly at these sizes. Install `DOSVGA.TTF` via Control Panel > Fonts. 3. **Terminal raster at the nearest available size.** Search outward from the requested height (alternating up and down) to find the closest Terminal raster variant. 4. **Stock OEM_FIXED_FONT.** Last resort. Always available. ### Supported Point Sizes (96 DPI) | FontSize | Cell Height | Font Used | |---|---|---| | 6 | 8 | Perfect DOS VGA 437 | | 9 | 12 | Terminal raster | | 12 | 16 | Perfect DOS VGA 437 | | 18 | 24 | Terminal raster | | 24 | 32 | Perfect DOS VGA 437 | | 30 | 40 | Perfect DOS VGA 437 | | 36 | 48 | Perfect DOS VGA 437 | Other sizes fall back to the nearest Terminal raster height. ### Installing the Perfect DOS VGA 437 Font 1. Open Control Panel and double-click Fonts. 2. Click Add. 3. Browse to the directory containing `DOSVGA.TTF` and select it. 4. Click OK to install. The font is free for personal and commercial use. See `fonts/DOSVGA.TXT` for license details. ## Rendering Architecture ### Split-Phase Design Rendering is split into parsing and drawing to avoid interleaving CPU work with GDI kernel transitions: 1. **Parse phase** (ParseDataBuf): Pure CPU loop that fills the FScreen cell buffer with characters and attributes. No GDI calls. Three inline fast paths eliminate function-call overhead for printable runs, CSI parameter accumulation, and common control characters (ESC, CR, LF). 2. **Render phase** (FlipToScreen or WriteDeferredBuf): Flushes coalesced scrolls via a single ScrollDC call, then redraws only dirty rows via RenderRow. ### Dirty Row Tracking Each row has a boolean dirty flag (FDirtyRow). FAllDirty forces a full redraw. DirtyBlinkRows marks only the cursor row and rows containing blink cells, reducing blink overhead from ~63 ms to ~3 ms on a 486. ### RenderRow Scans cells for color runs (consecutive cells with the same effective FG and BG). Each run is drawn with a single ExtTextOut call using ETO_OPAQUE (fills background and renders text in one operation). A DxBuf array forces uniform character spacing for TrueType fonts. ### Scroll Coalescing Multiple scroll-up operations during a parse batch are accumulated in FPendingScrolls. The render phase issues one ScrollDC to shift on-screen pixels, then only redraws the newly exposed bottom rows. ### Blink Cursor and text blink are driven by TickBlink, which uses GetTickCount to toggle FBlinkOn every 500 ms. No WM_TIMER is used. The application calls TickBlink from its main loop. ## Scrollback Buffer - **FScreen**: TList of PTermLine holding the active terminal display. - **FScrollback**: TList of PTermLine holding history (up to ScrollbackSize). - **FScrollPos**: 0 = live view, >0 = viewing history (lines scrolled back). - Vertical scrollbar (WS_VSCROLL) allows the user to scroll through history. - TrimScrollback batch-frees excess lines in a single pass. ## Usage Example ```pascal FAnsi := TKPAnsi.Create(Self); FAnsi.FontSize := 12; FAnsi.Parent := Self; FAnsi.Left := 0; FAnsi.Top := 38; FAnsi.OnKeyData := AnsiKeyData; { In the main loop: } Len := FComm.ReadInputBuf(@Buf, BufSize); if Len > 0 then FAnsi.WriteDeferredBuf(@Buf, Len); FAnsi.TickBlink; FAnsi.FlipToScreen; { Size the form to fit: } ClientWidth := FAnsi.Width; ClientHeight := FAnsi.Top + FAnsi.Height; ``` ## Platform Notes - Targets Windows 3.1 / Delphi 1.0. - Uses CS_OWNDC for a persistent device context (font and background mode survive across GetDC/ReleaseDC cycles). - Uses `{ }` comments (Delphi 1.0 does not support `//` comments). - The Perfect DOS VGA 437 TrueType font requires Windows 3.1's TrueType rasterizer. The font's cmap was converted from format 6 to format 0 and its OS/2 table downgraded from version 3 to version 1 for Win 3.1 compatibility. - Requested with DEFAULT_CHARSET (not OEM_CHARSET) to prevent Win 3.1's font mapper from rejecting TrueType fonts in favor of raster substitutes.