Add TKPAnsi ANSI BBS terminal emulation component for Delphi 1.0

TKPAnsi is a TCustomControl descendant providing a visual ANSI terminal
with 16-color palette, scrollback buffer, blinking cursor, and ANSI music.
Supports CSI sequences (cursor movement, erase, SGR colors/attributes,
insert/delete lines/chars, scroll), DEC private modes (wrap, cursor
visibility), and keyboard translation (arrows, function keys, etc.).

Test app (TESTMAIN.PAS) updated to wire TKPAnsi to TKPComm as a
full terminal: received data feeds the terminal display, keystrokes
are sent out the serial port.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-25 23:12:23 -06:00
parent c7dc74ecdc
commit c3ae983a73
4 changed files with 1836 additions and 128 deletions

1582
delphi/KPANSI.PAS Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,8 @@ program KPTest;
uses uses
Forms, Forms,
TestMain in 'TESTMAIN.PAS', TestMain in 'TESTMAIN.PAS',
KPComm in 'KPCOMM.PAS'; KPComm in 'KPCOMM.PAS',
KPAnsi in 'KPANSI.PAS';
begin begin
Application.CreateForm(TMainForm, MainForm); Application.CreateForm(TMainForm, MainForm);

210
delphi/README.md Normal file
View file

@ -0,0 +1,210 @@
# KP Serial Components for Delphi 1.0
Native Delphi 1.0 components for serial communications and ANSI BBS terminal
emulation under Windows 3.1. Both components install to the **KP** component
palette tab.
## Components
### TKPComm — Serial Communications (`KPCOMM.PAS`)
Non-visual `TComponent` descendant providing RS-232 serial I/O via the
Windows 3.1 comm API (`OpenComm`, `BuildCommDCB`, `SetCommState`,
`EnableCommNotification`, etc.).
**Published properties:**
| Property | Type | Default | Description |
|---|---|---|---|
| CommPort | Integer | 1 | COM port number (116) |
| Settings | string | `9600,N,8,1` | Baud, parity, data bits, stop bits |
| PortOpen | Boolean | False | Open/close the port |
| InBufferSize | Integer | 4096 | Receive buffer size (bytes) |
| OutBufferSize | Integer | 4096 | Transmit buffer size (bytes) |
| RThreshold | Integer | 0 | Receive notification threshold (0 = disabled) |
| SThreshold | Integer | 0 | Send notification threshold (0 = disabled) |
| Handshaking | THandshaking | hsNone | Flow control (hsNone, hsXonXoff, hsRtsCts, hsBoth) |
| InputLen | Integer | 0 | Max bytes per Input read (0 = all available) |
| InputMode | TInputMode | imText | imText or imBinary |
| DTREnable | Boolean | True | Assert DTR on open |
| RTSEnable | Boolean | True | Assert RTS on open |
| NullDiscard | Boolean | False | Discard received null bytes |
| EOFEnable | Boolean | False | Treat Ctrl-Z as EOF |
| ParityReplace | string | `?` | Replacement char for parity errors |
| OnComm | TNotifyEvent | nil | Fired on comm events (receive, send, modem line changes, errors) |
**Public runtime properties:**
| Property | Type | Description |
|---|---|---|
| Input | string | Read received data from the buffer |
| Output | string | Write data to the transmit buffer |
| InBufferCount | Integer | Bytes waiting in receive buffer |
| OutBufferCount | Integer | Bytes waiting in transmit buffer |
| CTSHolding | Boolean | CTS line state (shadow, toggled on transitions) |
| DSRHolding | Boolean | DSR line state |
| CDHolding | Boolean | CD/RLSD line state |
| Break | Boolean | Set/clear break condition |
| CommEvent | Integer | Last event code (comEvReceive, comEvCTS, comEvtBreak, etc.) |
**Usage:**
```pascal
Comm := TKPComm.Create(Self);
Comm.CommPort := 1;
Comm.Settings := '9600,N,8,1';
Comm.RThreshold := 1;
Comm.OnComm := CommEvent;
Comm.PortOpen := True;
{ Send data }
Comm.Output := 'ATZ' + #13;
{ In OnComm handler }
if Comm.CommEvent = comEvReceive then
Data := Comm.Input;
```
### TKPAnsi — ANSI BBS Terminal Emulator (`KPANSI.PAS`)
Visual `TCustomControl` descendant providing a full ANSI terminal display with
scrollback, blinking cursor, 16-color palette, and ANSI music.
**Published properties:**
| Property | Type | Default | Description |
|---|---|---|---|
| Cols | Integer | 80 | Terminal width in columns (1256) |
| Rows | Integer | 25 | Terminal height in rows (1255) |
| ScrollbackSize | Integer | 500 | Maximum scrollback lines |
| CursorVisible | Boolean | True | Show/hide blinking block cursor |
| Font | TFont | Terminal 9pt | Monospace font for rendering |
| Color | TColor | clBlack | Default background color |
| TabStop | Boolean | True | Accept keyboard focus |
| OnKeyData | TKeyDataEvent | nil | Fired when user presses a key |
**Public methods and properties:**
| Member | Kind | Description |
|---|---|---|
| Write(const S: string) | method | Feed data to the terminal for parsing and display |
| Clear | method | Move screen to scrollback, blank the display, home cursor |
| Reset | method | Reset all attributes, clear screen, home cursor |
| CursorRow | Integer | Current cursor row (0-based, read-only) |
| CursorCol | Integer | Current cursor column (0-based, read-only) |
**Supported ANSI escape sequences:**
CSI (ESC\[) sequences:
| Sequence | Name | Action |
|---|---|---|
| ESC\[*n*A | CUU | Cursor up *n* rows |
| ESC\[*n*B | CUD | Cursor down *n* rows |
| ESC\[*n*C | CUF | Cursor forward *n* columns |
| ESC\[*n*D | CUB | Cursor back *n* columns |
| ESC\[*r*;*c*H | CUP | Cursor position (1-based row;col) |
| ESC\[*r*;*c*f | HVP | Same as CUP |
| ESC\[*n*J | ED | Erase display (0=below, 1=above, 2=all) |
| ESC\[*n*K | EL | Erase line (0=right, 1=left, 2=all) |
| ESC\[*n*S | SU | Scroll up *n* lines |
| ESC\[*n*T | SD | Scroll down *n* lines |
| ESC\[*n*L | IL | Insert *n* blank lines at cursor |
| ESC\[*n*M | DL | Delete *n* lines at cursor |
| ESC\[*n*@ | ICH | Insert *n* blank characters at cursor |
| ESC\[*n*P | DCH | Delete *n* characters at cursor |
| ESC\[*params*m | SGR | Set graphic rendition (see below) |
| ESC\[s | SCP | Save cursor position |
| ESC\[u | RCP | Restore cursor position |
SGR codes:
| Code | Effect |
|---|---|
| 0 | Reset all attributes |
| 1 | Bold (bright foreground) |
| 5 | Blink (rendered as bright background) |
| 7 | Reverse video |
| 22 | Cancel bold |
| 25 | Cancel blink |
| 27 | Cancel reverse |
| 3037 | Foreground color (ANSI 07) |
| 4047 | Background color (ANSI 07) |
DEC private modes:
| Sequence | Effect |
|---|---|
| ESC\[?7h | Enable line wrap (default) |
| ESC\[?7l | Disable line wrap |
| ESC\[?25h | Show cursor |
| ESC\[?25l | Hide cursor |
Control characters:
| Char | Effect |
|---|---|
| CR (#13) | Carriage return |
| LF (#10) | Line feed (scrolls at bottom) |
| BS (#8) | Backspace (no erase) |
| TAB (#9) | Tab to next 8-column stop |
| BEL (#7) | System beep |
**ANSI Music:**
Detected by `ESC[M` followed by a music string terminated by Ctrl-N (#14).
Syntax: `T<tempo> L<length> O<octave> <notes>` where notes are AG with
optional sharp (`#`/`+`) or flat (`-`), duration (1/2/4/8/16), and dot (`.`)
for dotted notes. `P` inserts a rest. `>` and `<` shift octave. Played
asynchronously via the Windows 3.1 Sound API.
**Keyboard mapping (via OnKeyData):**
| Key | Output |
|---|---|
| Printable chars | The character itself |
| Enter | CR (#13) |
| Backspace | BS (#8) |
| Tab | TAB (#9) |
| Escape | ESC (#27) |
| 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 / Delete | ESC\[@ / ESC DEL |
| F1F4 | ESC OP / OQ / OR / OS |
| F5F10 | ESC Ot / Ou / Ov / Ow / Ox / Oy |
## Test Application
`KPTEST.DPR` / `TESTMAIN.PAS` — a minimal terminal application that wires the
two components together. The form is created entirely in code (no DFM).
**Layout:** A toolbar row at the top with port number, settings, Open/Close
buttons, and a status label. The TKPAnsi terminal fills the rest of the form.
**Wiring:**
- `TKPComm.OnComm` handler reads `Input` and passes it to `TKPAnsi.Write`
- `TKPAnsi.OnKeyData` handler sends keystrokes to `TKPComm.Output`
## Building
Requires Delphi 1.0.
1. Open `KPTEST.DPR` in the Delphi IDE
2. Compile and run (F9)
To install the components for design-time use:
1. Component > Install Component
2. Add `KPCOMM.PAS` and `KPANSI.PAS`
3. Both appear on the **KP** palette tab
## Files
| File | Description |
|---|---|
| `KPCOMM.PAS` | TKPComm serial communications component |
| `KPANSI.PAS` | TKPAnsi ANSI terminal emulator component |
| `KPTEST.DPR` | Test application project file |
| `TESTMAIN.PAS` | Test application main form unit |

View file

@ -1,18 +1,24 @@
unit TestMain; unit TestMain;
{ Test application for the TKPComm serial communications component. } { Test application for TKPComm and TKPAnsi components. }
{ Form and all controls are created in code (no DFM required). } { Form and all controls are created in code (no DFM required). }
{ }
{ Layout: toolbar row at top (port, settings, open/close, status), }
{ TKPAnsi terminal filling the rest of the form. Received serial data }
{ is fed to the terminal via TKPAnsi.Write; keystrokes from the terminal }
{ are sent out via TKPComm.Output. }
interface interface
uses uses
SysUtils, Classes, WinTypes, WinProcs, Messages, SysUtils, Classes, WinTypes, WinProcs, Messages,
Forms, Controls, StdCtrls, KPComm; Forms, Controls, StdCtrls, KPComm, KPAnsi;
type type
TMainForm = class(TForm) TMainForm = class(TForm)
private private
FComm: TKPComm; FComm: TKPComm;
FAnsi: TKPAnsi;
FLabelPort: TLabel; FLabelPort: TLabel;
FEditPort: TEdit; FEditPort: TEdit;
FLabelSettings: TLabel; FLabelSettings: TLabel;
@ -20,14 +26,9 @@ type
FBtnOpen: TButton; FBtnOpen: TButton;
FBtnClose: TButton; FBtnClose: TButton;
FLabelStatus: TLabel; FLabelStatus: TLabel;
FLabelRecv: TLabel; procedure AnsiKeyData(Sender: TObject; const Data: string);
FMemoRecv: TMemo;
FEditSend: TEdit;
FBtnSend: TButton;
FLabelInfo: TLabel;
procedure BtnCloseClick(Sender: TObject); procedure BtnCloseClick(Sender: TObject);
procedure BtnOpenClick(Sender: TObject); procedure BtnOpenClick(Sender: TObject);
procedure BtnSendClick(Sender: TObject);
procedure CommEvent(Sender: TObject); procedure CommEvent(Sender: TObject);
procedure UpdateStatus; procedure UpdateStatus;
public public
@ -40,10 +41,23 @@ var
implementation implementation
procedure TMainForm.AnsiKeyData(Sender: TObject; const Data: string);
begin
if FComm.PortOpen and (Length(Data) > 0) then
begin
try
FComm.Output := Data;
except
on E: Exception do
{ Ignore send errors from keyboard input }
end;
end;
end;
procedure TMainForm.BtnCloseClick(Sender: TObject); procedure TMainForm.BtnCloseClick(Sender: TObject);
begin begin
FComm.PortOpen := False; FComm.PortOpen := False;
FMemoRecv.Lines.Add('--- Port closed ---');
UpdateStatus; UpdateStatus;
end; end;
@ -55,29 +69,12 @@ begin
FComm.Settings := FEditSettings.Text; FComm.Settings := FEditSettings.Text;
FComm.RThreshold := 1; FComm.RThreshold := 1;
FComm.PortOpen := True; FComm.PortOpen := True;
FMemoRecv.Lines.Add('--- Port opened on COM' +
FEditPort.Text + ' at ' + FEditSettings.Text + ' ---');
except except
on E: Exception do on E: Exception do
FMemoRecv.Lines.Add('Open failed: ' + E.Message); FAnsi.Write('Open failed: ' + E.Message + #13#10);
end;
UpdateStatus;
end;
procedure TMainForm.BtnSendClick(Sender: TObject);
begin
if Length(FEditSend.Text) = 0 then
Exit;
try
FComm.Output := FEditSend.Text + #13;
FMemoRecv.Lines.Add('TX: ' + FEditSend.Text);
FEditSend.Text := '';
except
on E: Exception do
FMemoRecv.Lines.Add('Send failed: ' + E.Message);
end; end;
UpdateStatus; UpdateStatus;
FAnsi.SetFocus;
end; end;
@ -90,32 +87,9 @@ begin
begin begin
S := FComm.Input; S := FComm.Input;
if Length(S) > 0 then if Length(S) > 0 then
FMemoRecv.Lines.Add('RX: ' + S); FAnsi.Write(S);
end; end;
comEvCTS:
FMemoRecv.Lines.Add('CTS changed');
comEvDSR:
FMemoRecv.Lines.Add('DSR changed');
comEvCD:
FMemoRecv.Lines.Add('CD changed');
comEvRing:
FMemoRecv.Lines.Add('Ring');
comEvEOF:
FMemoRecv.Lines.Add('EOF received');
comEvtBreak:
FMemoRecv.Lines.Add('Break received');
comEvtFrame:
FMemoRecv.Lines.Add('Framing error');
comEvtOverrun:
FMemoRecv.Lines.Add('Overrun error');
comEvtRxOver:
FMemoRecv.Lines.Add('RX buffer overflow');
comEvtParity:
FMemoRecv.Lines.Add('Parity error');
comEvtTxFull:
FMemoRecv.Lines.Add('TX buffer full');
end; end;
UpdateStatus;
end; end;
@ -123,9 +97,9 @@ constructor TMainForm.Create(AOwner: TComponent);
begin begin
inherited CreateNew(AOwner); inherited CreateNew(AOwner);
Caption := 'KPComm Test'; Caption := 'KPComm ANSI Terminal';
Width := 500; Width := 660;
Height := 380; Height := 460;
BorderStyle := bsSingle; BorderStyle := bsSingle;
{ Serial component } { Serial component }
@ -159,11 +133,10 @@ begin
FEditSettings.Width := 140; FEditSettings.Width := 140;
FEditSettings.Text := '9600,N,8,1'; FEditSettings.Text := '9600,N,8,1';
{ Row 2: Open/Close buttons and status }
FBtnOpen := TButton.Create(Self); FBtnOpen := TButton.Create(Self);
FBtnOpen.Parent := Self; FBtnOpen.Parent := Self;
FBtnOpen.Left := 8; FBtnOpen.Left := 300;
FBtnOpen.Top := 38; FBtnOpen.Top := 8;
FBtnOpen.Width := 65; FBtnOpen.Width := 65;
FBtnOpen.Height := 25; FBtnOpen.Height := 25;
FBtnOpen.Caption := 'Open'; FBtnOpen.Caption := 'Open';
@ -171,8 +144,8 @@ begin
FBtnClose := TButton.Create(Self); FBtnClose := TButton.Create(Self);
FBtnClose.Parent := Self; FBtnClose.Parent := Self;
FBtnClose.Left := 80; FBtnClose.Left := 372;
FBtnClose.Top := 38; FBtnClose.Top := 8;
FBtnClose.Width := 65; FBtnClose.Width := 65;
FBtnClose.Height := 25; FBtnClose.Height := 25;
FBtnClose.Caption := 'Close'; FBtnClose.Caption := 'Close';
@ -181,56 +154,20 @@ begin
FLabelStatus := TLabel.Create(Self); FLabelStatus := TLabel.Create(Self);
FLabelStatus.Parent := Self; FLabelStatus.Parent := Self;
FLabelStatus.Left := 160; FLabelStatus.Left := 450;
FLabelStatus.Top := 44; FLabelStatus.Top := 12;
FLabelStatus.Caption := 'Closed'; FLabelStatus.Caption := 'Closed';
{ Receive area } { ANSI terminal }
FLabelRecv := TLabel.Create(Self); FAnsi := TKPAnsi.Create(Self);
FLabelRecv.Parent := Self; FAnsi.Parent := Self;
FLabelRecv.Left := 8; FAnsi.Left := 0;
FLabelRecv.Top := 70; FAnsi.Top := 38;
FLabelRecv.Caption := 'Received:'; FAnsi.OnKeyData := AnsiKeyData;
FMemoRecv := TMemo.Create(Self);
FMemoRecv.Parent := Self;
FMemoRecv.Left := 8;
FMemoRecv.Top := 86;
FMemoRecv.Width := 476;
FMemoRecv.Height := 186;
FMemoRecv.ScrollBars := ssVertical;
FMemoRecv.ReadOnly := True;
{ Send row }
FEditSend := TEdit.Create(Self);
FEditSend.Parent := Self;
FEditSend.Left := 8;
FEditSend.Top := 280;
FEditSend.Width := 400;
FBtnSend := TButton.Create(Self);
FBtnSend.Parent := Self;
FBtnSend.Left := 416;
FBtnSend.Top := 280;
FBtnSend.Width := 65;
FBtnSend.Height := 25;
FBtnSend.Caption := 'Send';
FBtnSend.Enabled := False;
FBtnSend.OnClick := BtnSendClick;
{ Status info line }
FLabelInfo := TLabel.Create(Self);
FLabelInfo.Parent := Self;
FLabelInfo.Left := 8;
FLabelInfo.Top := 316;
FLabelInfo.Width := 476;
FLabelInfo.Caption := 'RX: 0 TX: 0 Event: 0';
end; end;
procedure TMainForm.UpdateStatus; procedure TMainForm.UpdateStatus;
var
S: string;
begin begin
if FComm.PortOpen then if FComm.PortOpen then
FLabelStatus.Caption := 'Open' FLabelStatus.Caption := 'Open'
@ -239,28 +176,6 @@ begin
FBtnOpen.Enabled := not FComm.PortOpen; FBtnOpen.Enabled := not FComm.PortOpen;
FBtnClose.Enabled := FComm.PortOpen; FBtnClose.Enabled := FComm.PortOpen;
FBtnSend.Enabled := FComm.PortOpen;
FEditSend.Enabled := FComm.PortOpen;
S := 'RX: ' + IntToStr(FComm.InBufferCount) +
' TX: ' + IntToStr(FComm.OutBufferCount);
if FComm.PortOpen then
begin
if FComm.CTSHolding then
S := S + ' CTS: On'
else
S := S + ' CTS: Off';
if FComm.DSRHolding then
S := S + ' DSR: On'
else
S := S + ' DSR: Off';
if FComm.CDHolding then
S := S + ' CD: On'
else
S := S + ' CD: Off';
end;
S := S + ' Event: ' + IntToStr(FComm.CommEvent);
FLabelInfo.Caption := S;
end; end;