diff --git a/forms/README.md b/forms/README.md new file mode 100644 index 0000000..f725943 --- /dev/null +++ b/forms/README.md @@ -0,0 +1,547 @@ +# Remote Forms System + +A remote GUI system for Windows 3.1. Forms are designed visually in +Delphi 1.0's IDE, converted to a text protocol by a Linux tool, and +served to a Delphi 1.0 client over serial. The client creates native +Windows 3.1 controls and sends user events back to the server. + +## Architecture + +``` + Linux Serial Windows 3.1 + +-----------------+ +----------------+ +------------------+ + | Delphi IDE | | | | | + | design → .DFM | | Transport | | TFormClient | + | | | | (newline- | | creates native | + | dfm2form | | delimited) | | controls from | + | | | | | | commands, sends | + | .form file | | Server -----> |---->| events back | + | | | | commands | | | + | FormServerT | | | | | + | loads .form, | | <----- Client |<----| | + | sends commands | | events | | | + +-----------------+ +----------------+ +------------------+ +``` + +## Components + +| File | Language | Platform | Purpose | +|---------------|-----------|----------|---------------------------------------| +| `dfm2form.c` | C | Linux | Converts binary DFM to `.form` text | +| `formsrv.h` | C | Linux | Server library header | +| `formsrv.c` | C | Linux | Server library implementation | +| `formcli.pas` | Pascal | Win 3.1 | Client form engine (Delphi 1.0) | +| `protocol.md` | — | — | Protocol specification | + +## Building + +``` +make # builds dfm2form and formsrv.o +make clean # removes build artifacts +``` + +Requires GCC on Linux. The Delphi unit (`formcli.pas`) is compiled as +part of a Delphi 1.0 project on Windows. + +## DFM Converter + +`dfm2form` reads Delphi 1.0 binary DFM files (TPF0 format) and outputs +`.form` files containing protocol commands. + +``` +dfm2form [-i ] [output.form] +``` + +**Options:** + +- `-i ` — Set the form ID (default: 1). Each form needs a + unique ID when serving multiple forms. + +**Examples:** + +``` +dfm2form login.dfm # output to stdout +dfm2form login.dfm login.form # output to file +dfm2form -i 2 settings.dfm settings.form +``` + +**Output** is a sequence of protocol commands: + +``` +FORM.CREATE 1 400 300 "Login" +CTRL.CREATE 1 1 Label 20 20 100 17 Caption="Username:" +CTRL.CREATE 1 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0 +CTRL.CREATE 1 3 Label 20 52 100 17 Caption="Password:" +CTRL.CREATE 1 4 Edit 120 50 200 21 Text="" MaxLength=32 TabOrder=1 +CTRL.CREATE 1 5 Button 245 90 75 25 Caption="OK" TabOrder=2 +CTRL.CREATE 1 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3 +EVENT.BIND 1 5 Enter +FORM.SHOW 1 +``` + +The converter maps Delphi class names to protocol control types, +extracts geometry and properties, and emits `EVENT.BIND` for any +event handler assignments in the DFM that are not auto-wired (see +Events below). Unknown control classes are skipped with a warning. + +## Server Library (C) + +The server loads `.form` files and sends their commands to a remote +client through a pluggable transport interface. It also receives +events from the client and dispatches them to a callback. + +### Transport Interface + +Implement `FormTransportT` to connect the server to your communication +channel: + +```c +typedef struct { + int (*readMessage)(char *buf, int32_t maxLen, void *ctx); + void (*writeMessage)(const char *buf, void *ctx); + void *ctx; +} FormTransportT; +``` + +- `readMessage` — Read one complete message into `buf`. Return the + number of bytes read, or 0 if no message is available. Must not + block. +- `writeMessage` — Send a null-terminated message string. The + transport adds framing (e.g., CR+LF for serial). +- `ctx` — Opaque pointer passed through to both callbacks. + +### API + +```c +// Create/destroy +FormServerT *formServerCreate(FormTransportT *transport); +void formServerDestroy(FormServerT *server); + +// Load a .form file. Returns the form ID, or -1 on error. +int32_t formServerLoadFile(FormServerT *server, const char *path); + +// Send all commands for a loaded form to the client. +void formServerSendForm(FormServerT *server, int32_t formId); + +// Form visibility +void formServerShowForm(FormServerT *server, int32_t formId); +void formServerHideForm(FormServerT *server, int32_t formId); + +// Destroy a form (sends FORM.DESTROY and removes from store) +void formServerDestroyForm(FormServerT *server, int32_t formId); + +// Update a control property +void formServerSetProp(FormServerT *server, int32_t formId, + int32_t ctrlId, const char *prop, + const char *value); + +// Bind/unbind opt-in events +void formServerBindEvent(FormServerT *server, int32_t formId, + int32_t ctrlId, const char *eventName); +void formServerUnbindEvent(FormServerT *server, int32_t formId, + int32_t ctrlId, const char *eventName); + +// Set event callback +void formServerSetEventCallback(FormServerT *server, + EventCallbackT cb, void *userData); + +// Poll for one incoming event. Returns true if an event was processed. +bool formServerPollEvent(FormServerT *server); +``` + +### Event Callback + +```c +typedef void (*EventCallbackT)(int32_t formId, int32_t ctrlId, + const char *eventName, const char *data, + void *userData); +``` + +- `formId` / `ctrlId` — Identify which control fired the event. +- `eventName` — `"Click"`, `"Change"`, `"Select"`, etc. +- `data` — Event-specific data (may be empty). See the Events + section below for formats. + +### Server Example + +```c +#include "formsrv.h" + +void onEvent(int32_t formId, int32_t ctrlId, + const char *eventName, const char *data, void *userData) +{ + printf("Event: form=%d ctrl=%d event=%s data=%s\n", + formId, ctrlId, eventName, data); + + FormServerT *server = (FormServerT *)userData; + + // React to a button click by updating a label + if (ctrlId == 5 && strcmp(eventName, "Click") == 0) { + formServerSetProp(server, formId, 1, "Caption", "\"Clicked!\""); + } +} + +int main(void) +{ + FormTransportT transport = { myReadFn, myWriteFn, myCtx }; + FormServerT *server = formServerCreate(&transport); + + formServerSetEventCallback(server, onEvent, server); + + int32_t formId = formServerLoadFile(server, "login.form"); + formServerSendForm(server, formId); + + // Main loop + while (running) { + formServerPollEvent(server); + } + + formServerDestroy(server); + return 0; +} +``` + +## Client Engine (Delphi 1.0) + +The client receives protocol commands, creates native Windows 3.1 +controls, and sends user events back through the transport. + +### Transport Interface + +Subclass `TFormTransport` to provide your communication channel: + +```pascal +TFormTransport = class(TObject) +public + function ReadMessage(Buf: PChar; BufSize: Integer): Integer; virtual; abstract; + procedure WriteMessage(Buf: PChar; Len: Integer); virtual; abstract; +end; +``` + +- `ReadMessage` — Read one complete message into `Buf`. Return the + number of bytes read, or 0 if no message is available. +- `WriteMessage` — Send `Len` bytes from `Buf` as a complete message. + +PChar-based (not short strings) to handle messages up to 4096 bytes. + +### Serial Transport Example + +```pascal +TSerialTransport = class(TFormTransport) +private + FComm: TKPComm; + FRecvBuf: array[0..4095] of Char; + FRecvLen: Integer; +public + function ReadMessage(Buf: PChar; BufSize: Integer): Integer; override; + procedure WriteMessage(Buf: PChar; Len: Integer); override; +end; + +function TSerialTransport.ReadMessage(Buf: PChar; BufSize: Integer): Integer; +var + S: string; + I: Integer; + Len: Integer; +begin + Result := 0; + { Accumulate serial data } + S := FComm.Input; + if Length(S) > 0 then + begin + Move(S[1], FRecvBuf[FRecvLen], Length(S)); + Inc(FRecvLen, Length(S)); + end; + { Scan for newline } + for I := 0 to FRecvLen - 1 do + begin + if FRecvBuf[I] = #10 then + begin + Len := I; + if (Len > 0) and (FRecvBuf[Len - 1] = #13) then + Dec(Len); + if Len > BufSize - 1 then + Len := BufSize - 1; + Move(FRecvBuf, Buf^, Len); + Buf[Len] := #0; + { Shift remainder } + Move(FRecvBuf[I + 1], FRecvBuf, FRecvLen - I - 1); + Dec(FRecvLen, I + 1); + Result := Len; + Exit; + end; + end; +end; + +procedure TSerialTransport.WriteMessage(Buf: PChar; Len: Integer); +var + Msg: string; +begin + Msg := StrPas(Buf) + #13#10; + FComm.Output := Msg; +end; +``` + +### Using TFormClient + +```pascal +var + Transport: TSerialTransport; + Client: TFormClient; + +{ Setup } +Transport := TSerialTransport.Create; +Transport.FComm := KPComm1; { your TKPComm instance } +Client := TFormClient.Create(Transport); + +{ Main loop (PeekMessage style) } +while not Terminated do +begin + while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do + begin + TranslateMessage(Msg); + DispatchMessage(Msg); + end; + Client.ProcessMessages; { pumps incoming commands } + Yield; +end; + +{ Cleanup } +Client.Free; +Transport.Free; +``` + +`ProcessMessages` calls `ReadMessage` in a loop until no more +messages are available, dispatching each command as it arrives. + +## Supported Controls + +| Type | Delphi Class | Description | +|------------|-------------|--------------------------------| +| `Label` | TLabel | Static text label | +| `Edit` | TEdit | Single-line text input | +| `Button` | TButton | Push button | +| `CheckBox` | TCheckBox | Check box with label | +| `ListBox` | TListBox | Scrollable list of items | +| `ComboBox` | TComboBox | Drop-down list with text input | +| `Memo` | TMemo | Multi-line text input | + +### Creating Controls + +Controls are created with `CTRL.CREATE`, specifying position, size, +and optional inline properties: + +``` +CTRL.CREATE [Key="val" ...] +``` + +Example: + +``` +CTRL.CREATE 1 3 Edit 120 50 200 21 Text="hello" MaxLength=32 TabOrder=1 +``` + +Control IDs are positive integers assigned by the server. They must +be unique within a form. + +### Updating Controls + +Properties can be changed at any time after creation with `CTRL.SET`: + +``` +CTRL.SET Key="val" [Key="val" ...] +``` + +Example: + +``` +CTRL.SET 1 3 Text="world" Enabled=0 +``` + +## Properties + +### Caption + +- **Applies to:** Label, Button, CheckBox +- **Format:** Quoted string +- **Example:** `Caption="Submit"` + +The display text for labels, buttons, and check boxes. + +### Text + +- **Applies to:** Edit, ComboBox, Memo +- **Format:** Quoted string +- **Example:** `Text="Hello world"` + +The editable text content. For Memo controls, use `\n` for line +breaks within the quoted string: + +``` +CTRL.SET 1 5 Text="Line one\nLine two\nLine three" +``` + +### Items + +- **Applies to:** ListBox, ComboBox +- **Format:** Quoted string, items separated by `\n` +- **Example:** `Items="Red\nGreen\nBlue"` + +Replaces the entire item list. The control is cleared before the +new items are added. + +### Checked + +- **Applies to:** CheckBox +- **Format:** `0` (unchecked) or `1` (checked) +- **Example:** `Checked=1` + +### Enabled + +- **Applies to:** All control types +- **Format:** `0` (disabled) or `1` (enabled) +- **Example:** `Enabled=0` + +### Visible + +- **Applies to:** All control types +- **Format:** `0` (hidden) or `1` (visible) +- **Example:** `Visible=0` + +### MaxLength + +- **Applies to:** Edit +- **Format:** Integer (0 = no limit) +- **Example:** `MaxLength=50` + +Maximum number of characters the user can type. + +### ReadOnly + +- **Applies to:** Edit, Memo +- **Format:** `0` (editable) or `1` (read-only) +- **Example:** `ReadOnly=1` + +### ScrollBars + +- **Applies to:** Memo +- **Format:** Integer 0-3 +- **Values:** + - `0` — ssNone (no scroll bars) + - `1` — ssHorizontal + - `2` — ssVertical + - `3` — ssBoth +- **Example:** `ScrollBars=2` + +### ItemIndex + +- **Applies to:** ListBox, ComboBox +- **Format:** Integer (-1 = no selection) +- **Example:** `ItemIndex=2` + +### TabOrder + +- **Applies to:** All windowed controls (all types except Label) +- **Format:** Integer +- **Example:** `TabOrder=3` + +Controls the keyboard tab navigation order within the form. + +## Events + +### Auto-wired Events + +These events are automatically connected when a control is created. +No `EVENT.BIND` command is needed. + +| Control Type | Event | Data Sent | +|-------------|---------|-------------------------------| +| Button | Click | (none) | +| CheckBox | Click | (none) | +| Edit | Change | `"new text"` | +| Memo | Change | `"new text"` | +| ListBox | Select | ` "selected text"` | +| ComboBox | Select | ` "selected text"` | +| ComboBox | Change | `"new text"` | + +### Opt-in Events + +These events require an explicit `EVENT.BIND` command before the +client will send them. Use `EVENT.UNBIND` to disconnect. + +| Event | Data Sent | Notes | +|-----------|-----------------------|---------------------------------| +| DblClick | (none) | Double-click on any control | +| KeyDown | `` | Windows virtual key code | +| KeyUp | `` | Windows virtual key code | +| Enter | (none) | Control received focus | +| Exit | (none) | Control lost focus | +| MouseDown | `