Replace the inherited Font property with a FontSize property that drives a multi-tier font selection strategy for pixel-perfect OEM rendering: Terminal raster (exact size) > Perfect DOS VGA 437 (multiples of 8px) > Terminal raster (nearest) > stock OEM fallback. Add DxBuf uniform character spacing to ExtTextOut for correct TrueType monospace rendering. Bundle the Perfect DOS VGA 437 font (cmap converted from format 6 to format 0 for Win 3.1 compatibility). Size the test form dynamically from terminal dimensions. Add component documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
333 lines
11 KiB
Markdown
333 lines
11 KiB
Markdown
# 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.
|