More cleanup. Major docs update.

This commit is contained in:
Scott Duensing 2026-04-18 17:00:18 -05:00
parent 05e69f60a7
commit a8c38267bc
137 changed files with 10341 additions and 6233 deletions

316
README.md
View file

@ -1,226 +1,110 @@
# DVX -- DOS Visual eXecutive
A windowed GUI compositor and widget toolkit targeting DJGPP/DPMI on
486+ hardware. VESA VBE 2.0+ LFB only. Motif-inspired visual style
with 2px bevels, fixed bitmap fonts, and dirty-rect compositing.
DVX is a windowed GUI environment and application platform for DOS.
It provides a Motif-inspired desktop, a cooperative multitasking kernel,
a widget toolkit, a compiled BASIC language with a visual form designer,
and a small set of bundled applications.
The system runs on 86Box (primary target) and real DOS hardware.
The system is a compositor built on VESA VBE 2.0+ linear framebuffer
video modes. A dirty-rectangle compositor keeps redraw cost proportional
to what actually changes on screen, so the desktop is responsive even at
1024x768 on modest hardware.
## Architecture
## What's Included
DVX is built as a set of DXE3 dynamic modules loaded by a bootstrap
executable. The loader resolves dependencies and loads modules in
topological order, then hands control to the shell.
```
dvx.exe (loader)
|
+-- libs/libtasks.lib cooperative task switcher
+-- libs/libdvx.lib core GUI (draw, comp, wm, app, widget infra)
+-- libs/texthelp.lib shared text editing helpers
+-- libs/listhelp.lib shared list/dropdown helpers
+-- libs/dvxshell.lib shell (app lifecycle, desktop)
+-- libs/taskmgr.lib task manager (Ctrl+Esc, separate DXE)
|
+-- libs/basrt.lib BASIC runtime (VM + values)
+-- libs/serial.lib serial communications (rs232 + packet + seclink)
|
+-- widgets/*.wgt 26 widget type plugins
|
+-- apps/*/*.app DXE applications
```
## Directory Structure
| Directory | Output | Description |
|--------------|-------------------------|--------------------------------------------|
| `loader/` | `bin/dvx.exe` | Bootstrap loader, platform layer, stb_ds |
| `core/` | `bin/libs/libdvx.lib` | Core GUI library (5 layers + widget infra) |
| `tasks/` | `bin/libs/libtasks.lib` | Cooperative task switching |
| `texthelp/` | `bin/libs/texthelp.lib` | Shared text editing helpers |
| `listhelp/` | `bin/libs/listhelp.lib` | Shared list/dropdown helpers |
| `shell/` | `bin/libs/dvxshell.lib` | DVX Shell (app lifecycle, desktop) |
| `taskmgr/` | `bin/libs/taskmgr.lib` | Task Manager (separate DXE, Ctrl+Esc) |
| `widgets/` | `bin/widgets/*.wgt` | 26 individual widget DXE modules |
| `apps/` | `bin/apps/*/*.app` | Application DXE modules |
| `tools/` | `bin/dvxres`, `bin/mkicon`, `bin/mktbicon` | Resource and icon tools (host native) |
| `config/` | `bin/config/`, `bin/libs/`, `bin/widgets/` | INI config, themes, wallpapers, dep files |
| `rs232/` | `lib/librs232.a` | ISR-driven UART serial driver |
| `packet/` | `lib/libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| `security/` | `lib/libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
| `seclink/` | `lib/libseclink.a` | Secure serial link (channels, encryption) |
| `proxy/` | `bin/secproxy` | Linux proxy: 86Box <-> telnet BBS |
| `termdemo/` | `bin/termdemo.exe` | Standalone encrypted terminal demo |
## Build
Requires the DJGPP cross-compiler at `~/djgpp/djgpp`.
```
make # build everything
make clean # remove all build artifacts
./mkcd.sh # build + create ISO for 86Box
```
The top-level Makefile builds in dependency order:
```
core -> tasks -> loader -> texthelp -> listhelp -> widgets -> shell -> taskmgr -> apps
tools (host native, parallel)
```
Build output goes to `bin/` (executables, DXE modules, config) and
`obj/` (intermediate object files).
## Runtime Directory Layout (bin/)
```
bin/
dvx.exe bootstrap loader (entry point)
libs/
libtasks.lib task switching library
libdvx.lib core GUI library
texthelp.lib text editing helpers
listhelp.lib list/dropdown helpers
dvxshell.lib DVX shell
taskmgr.lib task manager (Ctrl+Esc)
basrt.lib BASIC runtime (VM + values)
serial.lib serial communications stack
*.dep dependency files for load ordering
widgets/
box.wgt VBox/HBox/Frame containers
button.wgt push button
... (26 widget modules total)
*.dep dependency files
apps/
progman/progman.app Program Manager (desktop)
notepad/notepad.app text editor
clock/clock.app clock display
dvxdemo/dvxdemo.app widget showcase
cpanel/cpanel.app control panel
imgview/imgview.app image viewer
dvxbasic/dvxbasic.app DVX BASIC IDE + compiler
config/
dvx.ini system configuration
themes/ color theme files (.thm)
wpaper/ wallpaper images
```
## Core Architecture (5 Layers)
1. **dvxVideo** -- VESA VBE init, LFB mapping, backbuffer, pixel format, color packing
2. **dvxDraw** -- Rectangle fills, bevels, text rendering, cursor/icon drawing
3. **dvxComp** -- Dirty rectangle tracking, merge, compositor, LFB flush
4. **dvxWm** -- Window stack, chrome, drag/resize, menus, scrollbars, hit testing
5. **dvxApp** -- Event loop, input polling, public API, color schemes, wallpaper
## Widget System
Widgets are isolated DXE modules. Core knows nothing about individual
widget types -- no compile-time enum, no union, no per-widget structs in
dvxWidget.h.
* **Dynamic type IDs**: `wgtRegisterClass()` assigns IDs at load time
* **void *data**: Each widget allocates its own private data struct
* **ABI-stable dispatch**: `WidgetClassT.handlers[]` is an array of function
pointers indexed by `WGT_METHOD_*` constants (0-20, room for 32). Core
dispatches via `w->wclass->handlers[WGT_METHOD_PAINT]` etc., so adding
new methods does not break existing widget DXE binaries
* **Generic drag**: `WGT_METHOD_ON_DRAG_UPDATE` and `WGT_METHOD_ON_DRAG_END`
provide widget-level drag support without per-widget hacks in core
* **Per-widget API registry**: `wgtRegisterApi("name", &api)` replaces the monolithic API
* **Per-widget headers**: `widgets/widgetButton.h` etc. provide typed macros
* **Shared helpers**: texthelp.lib (text editing) and listhelp.lib (dropdown/list)
* **All limits dynamic**: widget child arrays, app slots, and desktop callbacks
are stb_ds dynamic arrays with no fixed maximums
## DXE Module System
All modules are DXE3 dynamic libraries loaded by dvx.exe at startup.
The loader recursively scans `libs/` for `.lib` files and `widgets/` for `.wgt`
files, reads `.dep` files for dependencies, and loads in topological
order. Widget modules that export `wgtRegister()` have it called after
loading.
Each `.dep` file lists base names of modules that must load first, one
per line. Comments start with `#`.
## App Types
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows,
registers callbacks, and returns. The app lives through event callbacks
in the shell's main loop. No dedicated task or stack needed.
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for
the app. `appMain` runs its own loop calling `tsYield()` to share CPU.
Used for apps that need continuous processing (clocks, terminal
emulators, games).
## Crash Recovery
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL
before loading any apps, so even crashes during app initialization are
caught. If an app crashes, the handler `longjmp`s back to the shell's
main loop, the crashed app is force-killed, and the shell continues
running. Diagnostic information (registers, faulting EIP) is logged to
`dvx.log`.
## Bundled Applications
| App | File | Type | Description |
|-----------------|----------------|-----------|------------------------------------------------------------|
| Program Manager | `progman.app` | Callback | App launcher grid, system info dialog |
| Notepad | `notepad.app` | Callback | Text editor with File/Edit menus, open/save, clipboard |
| Clock | `clock.app` | Main-loop | Digital clock display; multi-instance capable |
| DVX Demo | `dvxdemo.app` | Callback | Widget system showcase demonstrating 31 widget types |
| Control Panel | `cpanel.app` | Callback | Themes, wallpaper, video mode, mouse configuration |
| Image Viewer | `imgview.app` | Callback | Displays BMP, PNG, JPEG, GIF images with aspect-ratio zoom |
| DVX BASIC | `dvxbasic.app` | Callback | Visual Basic 3 clone: form designer, compiler, VM |
## Serial / Networking Stack
A layered encrypted serial communications stack for connecting DVX to
remote systems (BBS, etc.) through 86Box's emulated UART:
| Layer | Library | Description |
|----------|------------------|---------------------------------------------------|
| rs232 | `librs232.a` | ISR-driven UART driver, ring buffers, flow control |
| packet | `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| security | `libsecurity.a` | 1024-bit DH, XTEA-CTR cipher, DRBG RNG |
| seclink | `libseclink.a` | Channel multiplexing, per-packet encryption |
| proxy | `secproxy` | Linux bridge: 86Box serial <-> telnet BBS |
## Third-Party Dependencies
All third-party code is vendored as single-header libraries:
| Library | Location | Purpose |
|-------------------|--------------------|---------------------------------|
| stb_image.h | `core/thirdparty/` | Image loading (BMP, PNG, JPEG, GIF) |
| stb_image_write.h | `core/thirdparty/` | PNG export for screenshots |
| stb_ds.h | `core/thirdparty/` | Dynamic arrays and hash maps |
stb_ds implementation is compiled into dvx.exe (the loader) with
`STBDS_REALLOC`/`STBDS_FREE` overridden to use `dvxRealloc`/`dvxFree`,
so all `arrput`/`arrfree` calls in DXE code are tracked per-app. The
functions are exported via `dlregsym` to all DXE modules.
* **DVX Shell** -- desktop, app lifecycle manager, Task Manager, crash
recovery. The shell is the program the user interacts with and the
host for all running applications.
* **Widget toolkit** -- 31 widget types as plug-in modules: buttons,
labels, text inputs, list boxes, tree views, tab controls, sliders,
spinners, progress bars, canvases, scroll panes, splitters, an ANSI
terminal, and more.
* **DVX BASIC** -- a Visual Basic 3 compatible language with a visual
form designer, per-procedure code editor, project management, and a
bytecode compiler and VM. BASIC programs compile to standalone DVX
applications.
* **Help system** -- a full hypertext help viewer with table of contents,
keyword index, full-text search, and inline images. Help files are
compiled from a plain-text source format.
* **Serial / networking stack** -- an ISR-driven UART driver, HDLC
packet framing with CRC-16 and Go-Back-N reliable delivery, 1024-bit
Diffie-Hellman key exchange, and XTEA-CTR encryption, plus a Linux
bridge that connects a virtual serial port to a telnet BBS.
* **Control Panel** -- themes, wallpapers, video mode, mouse
configuration, all with live preview.
* **Bundled applications** -- Program Manager (desktop launcher),
Notepad, Clock, Image Viewer, DVX Demo, Control Panel, DVX BASIC
IDE, Help Viewer, Resource Editor, Icon Editor, Help Editor, BASIC
Demo.
## Target Hardware
* CPU: 486 baseline, Pentium-optimized paths where significant
* Video: VESA VBE 2.0+ with LFB (no bank switching)
* Platform: 86Box emulator (trusted reference), real DOS hardware
* Resolutions: 640x480, 800x600, 1024x768 at 8/15/16/32 bpp
* DOS with a 386-class CPU or newer (486 recommended, Pentium for best
performance)
* VESA BIOS Extensions 2.0 or newer with linear framebuffer support
* 4 MB of extended memory minimum; 8 MB or more recommended
* Any resolution the video card reports at 8, 15, 16, 24, or 32 bits
per pixel. 640x480, 800x600, and 1024x768 are the common cases.
* Two-button mouse; scroll wheel supported if a compatible driver is
loaded
## Building and Deploying
```
make # build everything
make clean # remove all build artifacts
./mkcd.sh # build and produce a CD-ROM ISO image
```
`make` builds in dependency order: core, tasks, loader, helpers,
host tools, widgets, shell, task manager, apps. Build output goes to
`bin/` and intermediate objects to `obj/`.
`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
image for use with an emulator or for burning to physical media.
## Directory Layout
```
dvxgui/
assets/ logo and splash artwork
bin/ build output (runtime tree; mirrors what ships)
config/ shipped INI, themes, wallpapers
docs/ HTML exports of the built-in help content
LICENSE.txt MIT license
Makefile top-level build
mkcd.sh build-and-package script
src/
apps/ bundled applications
include/ shared include files (BASIC library declarations)
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
loader/ bootstrap executable
tools/ host-native development tools and the BASIC compiler
widgets/ widget plug-in modules
```
See `src/tools/README.md` for the host-side development tools
(resource tool, icon generators, help compiler, proxy), and
`src/apps/kpunch/README.md` for the SDK contract applications must
follow.
## Documentation
Full reference documentation ships with the system as browsable help:
* `docs/dvx_system_reference.html` -- system API, widget catalog
* `docs/dvx_basic_reference.html` -- DVX BASIC language reference
* `docs/dvx_help_viewer.html` -- help viewer guide
The same content is available from inside DVX via the Help Viewer and
Program Manager.
## License
MIT. See `LICENSE.txt`.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

10
mkcd.sh
View file

@ -22,7 +22,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for 86Box
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for the target emulator
#
# Usage: ./mkcd.sh
#
@ -30,8 +30,8 @@
# an ISO 9660 image from the bin/ directory. The ISO uses short
# 8.3 filenames (-iso-level 1) for DOS compatibility.
#
# The ISO is placed in 86Box's data directory so it can be mounted
# as a CD-ROM drive.
# The ISO is placed in the emulator's data directory so it can be
# mounted as a CD-ROM drive.
set -e
@ -58,7 +58,7 @@ while IFS= read -r f; do
done < <(find "$SCRIPT_DIR/bin/widgets/" -name "*.wgt" -type f 2>/dev/null)
echo "$WGT_COUNT widget modules found in bin/widgets/."
# Clean DOSBox-X local filesystem artifacts
# Clean emulator local-filesystem artifacts
find "$SCRIPT_DIR/bin/" -name ".DBLOCALFILE*" -delete 2>/dev/null
# Create the ISO image
@ -78,5 +78,5 @@ mkisofs \
echo "ISO created: $ISO_PATH"
echo "Size: $(du -h "$ISO_PATH" | cut -f1)"
echo ""
echo "In 86Box, mount $ISO_PATH as a CD-ROM drive."
echo "Mount $ISO_PATH as a CD-ROM drive in the target emulator."
echo "Then from DOS: D:\\DVX.EXE (or whatever drive letter)"

View file

@ -1,203 +1,472 @@
# DVX Shell Applications
DXE3 application modules for the DVX Shell. Each app is a `.app` file
(DXE3 shared object) in a subdirectory under `apps/`. The Program
Manager scans this directory at startup and displays all discovered
apps as launchable icons.
Every DVX application is a DXE3 module packaged as a `.app` file,
installed under `bin/apps/kpunch/<name>/<name>.app`. The Program
Manager scans this directory at startup and displays each discovered
app as a launchable icon on the desktop.
This directory holds the source for the bundled applications plus
the BASIC-based sample apps. Two flavours of app live side by side:
* **C apps** -- written directly against the DVX SDK headers,
compiled to a DXE3 by `dxe3gen`. See `progman/`, `clock/`,
`dvxdemo/`, `cpanel/`, `dvxhelp/`.
* **BASIC apps** -- `.dbp` projects compiled to `.app` files by the
host-side `bascomp`. The compiler links the BASIC bytecode into a
copy of `basstub.app`, which acts as the runtime host. See
`iconed/`, `notepad/`, `imgview/`, `basicdemo/`, `resedit/`,
`dvxhelp/helpedit/`.
The rest of this document covers writing a C app against the SDK.
For BASIC apps, see `dvxbasic/README.md`.
## DXE App Contract
Every app exports two symbols:
Every `.app` binary exports two required symbols and one optional
symbol:
* `appDescriptor` (`AppDescriptorT`) -- name, hasMainLoop, multiInstance, stackSize, priority
* `appMain` (`int appMain(DxeAppContextT *)`) -- entry point
```c
AppDescriptorT appDescriptor; // required
int32_t appMain(DxeAppContextT *ctx); // required
void appShutdown(void); // optional
```
Optional: `appShutdown` (`void appShutdown(void)`) -- called during
graceful shutdown.
### `appDescriptor`
### Callback-Only Apps (hasMainLoop = false)
A statically-initialised struct read by the shell at load time to
decide how to launch the app:
`appMain()` creates windows, registers callbacks, and returns 0. The
shell drives everything through event callbacks. No dedicated task or
stack is allocated. Lifecycle ends when the last window closes.
```c
typedef struct {
char name[SHELL_APP_NAME_MAX]; // display name (<= 64 chars)
bool hasMainLoop; // see below
bool multiInstance; // may have multiple copies open
int32_t stackSize; // SHELL_STACK_DEFAULT or byte count
int32_t priority; // TS_PRIORITY_* for main-loop apps
} AppDescriptorT;
```
### Main-Loop Apps (hasMainLoop = true)
Example (from `clock/clock.c`):
A cooperative task is created for the app. `appMain()` runs its own
loop calling `tsYield()` to share CPU. Lifecycle ends when `appMain()`
returns.
```c
AppDescriptorT appDescriptor = {
.name = "Clock",
.hasMainLoop = true,
.multiInstance = true,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_LOW
};
```
### `appMain`
Entry point. The shell calls it with a `DxeAppContextT` that exposes
the shell's GUI context, the app's ID, its install directory, a
writable config directory, launch arguments, and a help-topic query
callback for F1.
Returning from `appMain` signals that the app has finished; the
shell unloads the DXE, frees per-app memory, and reclaims the app
slot.
### `appShutdown`
Called by the shell when the app is force-killed (by the Task
Manager) or when the shell is shutting down with this app still
running. Main-loop apps use it to set a flag that their main loop
observes, so the loop exits cleanly and `appMain` can return.
## Applications
## App Types
### Callback-only apps (`hasMainLoop = false`)
`appMain` creates windows, registers callbacks, and returns 0. The
shell drives all further work by invoking those callbacks in
response to user input and timers. No dedicated task is allocated;
no per-app stack is consumed. The app's lifetime ends when the last
window closes (or when every callback has released all references).
Use this pattern for event-driven UI: Notepad, Program Manager, DVX
Demo, Control Panel, Image Viewer.
### Main-loop apps (`hasMainLoop = true`)
The shell creates a cooperative task for the app and calls
`appMain` from inside it. `appMain` runs a loop that calls
`tsYield()` periodically to share the CPU. Exit by breaking out of
the loop and returning.
Use this pattern when the app needs continuous background work that
does not map cleanly onto event callbacks: Clock (polls the system
clock every second), terminal emulators, games.
## Resource Conventions
Apps attach resources to their `.app` binary using `dvxres build`
(see `src/tools/README.md`). The standard resource names are:
| Name | Type | Meaning |
|---------------|--------|---------|
| `icon32` | icon | 32x32 BMP shown in Program Manager and the Task Manager. |
| `name` | text | Human-readable display name (overrides `appDescriptor.name` for UI where available). |
| `author` | text | Author / maintainer name. |
| `publisher` | text | Publisher or studio. |
| `copyright` | text | Copyright notice. |
| `version` | text | Version string, free-form. |
| `description` | text | One-sentence description shown in About dialogs. |
| `helpfile` | text | Optional relative path to a `.hlp` file for F1 context help. |
Additional application-specific resources may be attached with any
unique name. The runtime reads them via
`dvxResOpen(appPath)` / `dvxResRead(handle, name, &size)` /
`dvxResClose(handle)`.
Typical `.res` manifest:
```
# clock.res
icon32 icon icon32.bmp
name text "Clock"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios"
description text "Digital clock with date display"
```
## Bundled Applications
### Program Manager (progman)
| | |
|---|---|
| File | `apps/progman/progman.app` |
| File | `apps/kpunch/progman/progman.app` |
| Type | Callback-only |
| Multi-instance | No |
The desktop app. Scans `apps/` recursively for `.app` files and
displays them in a grid. Double-click or Enter launches an app.
Includes a Help menu with system information dialog and About box.
The desktop. Recursively scans `apps/` for `.app` files and displays
them as a launchable grid with icons, names, and publishers. Double
click or Enter to launch. Includes a Help menu with the system help
viewer and the About / System Info dialog. Registers with
`shellRegisterDesktopUpdate()` so the grid refreshes when apps are
loaded, terminated, or crash.
Registers with `shellRegisterDesktopUpdate()` to refresh when apps
are loaded, crash, or terminate.
Widget headers used: `box/box.h`, `button/button.h`, `label/label.h`,
`statusBar/statBar.h`, `textInput/textInpt.h`, `imageButton/imgBtn.h`,
`scrollPane/scrlPane.h`, `wrapBox/wrapBox.h`.
Widget headers used: `widgetBox.h`, `widgetButton.h`, `widgetLabel.h`,
`widgetStatusBar.h`, `widgetTextInput.h`.
### Notepad (notepad)
### Notepad (notepad, BASIC)
| | |
|---|---|
| File | `apps/notepad/notepad.app` |
| Type | Callback-only |
| Multi-instance | Yes |
| File | `apps/kpunch/notepad/notepad.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Text editor with File menu (New, Open, Save, Save As) and Edit menu
(Cut, Copy, Paste, Select All). Uses the TextArea widget for all
editing. 32KB text buffer limit. Tracks dirty state for save prompts.
Keyboard accelerators for all menu commands.
Widget headers used: `widgetTextInput.h`.
Plain-text editor with File menu (New, Open, Save, Save As) and Edit
menu (Cut, Copy, Paste, Select All). Built from a BASIC project
(`notepad.dbp` + `notepad.frm`); demonstrates the form designer,
TextInput widget, and common dialog integration.
### Clock (clock)
| | |
|---|---|
| File | `apps/clock/clock.app` |
| File | `apps/kpunch/clock/clock.app` |
| Type | Main-loop |
| Multi-instance | Yes |
Digital clock display showing time and date. Demonstrates the main-loop
app pattern -- polls the system clock every second and invalidates the
window to trigger a repaint. Uses raw `onPaint` callbacks (no widgets)
to draw centered text.
Widget headers used: none (raw paint callbacks).
Digital clock showing time and date. Polls the system clock every
second and invalidates the window to trigger a repaint. Uses the raw
`onPaint` callback (no widgets) to draw centered text. Reference
implementation for main-loop apps.
### DVX Demo (dvxdemo)
| | |
|---|---|
| File | `apps/dvxdemo/dvxdemo.app` |
| File | `apps/kpunch/dvxdemo/dvxdemo.app` |
| Type | Callback-only |
| Multi-instance | No |
Comprehensive widget system showcase. Opens multiple windows
demonstrating 31 of the 32 widget types (all except Timer):
Widget toolkit showcase. Opens multiple windows demonstrating
virtually every widget type:
* Main window: raw paint callbacks (gradients, patterns, text)
* Widget demo: form widgets (TextInput, Checkbox, Radio, ListBox)
* Controls window: TabControl with tabs for advanced widgets
(Dropdown, ProgressBar, Slider, Spinner, TreeView, ListView,
ScrollPane, Toolbar, Canvas, Splitter, Image)
* Terminal window: AnsiTerm widget
* Main window -- raw paint callbacks (gradients, patterns, text)
* Widget demo -- form widgets (TextInput, Checkbox, Radio, ListBox)
* Controls window -- tabbed advanced widgets (Dropdown,
ProgressBar, Slider, Spinner, TreeView, ListView, ScrollPane,
Toolbar, Canvas, Splitter, Image)
* Terminal window -- AnsiTerm widget
Widget headers used: 25 of the 26 widget headers (all except widgetTimer.h).
Resources: `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`, `save.bmp`.
Resources include `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`,
`save.bmp`. Reference implementation for widget-based UIs.
### Control Panel (cpanel)
| | |
|---|---|
| File | `apps/cpanel/cpanel.app` |
| File | `apps/kpunch/cpanel/cpanel.app` |
| Type | Callback-only |
| Multi-instance | No |
System configuration with four tabs:
* **Mouse** -- scroll direction, double-click speed, acceleration
* **Colors** -- all 20 system colors with live preview, theme load/save
* **Desktop** -- wallpaper image selection and display mode
* **Video** -- resolution and color depth switching
* **Mouse** -- scroll direction, wheel speed, double-click speed,
acceleration, cursor speed
* **Colors** -- every system colour with live preview; theme load
and save
* **Desktop** -- wallpaper image and display mode
* **Video** -- resolution and colour depth switching
Changes preview live. OK saves to `DVX.INI`, Cancel reverts to the
state captured when the control panel was opened.
Changes preview live. OK writes to `CONFIG/DVX.INI`; Cancel reverts
to the snapshot taken when the panel opened.
Widget headers used: `widgetBox.h`, `widgetButton.h`, `widgetCanvas.h`,
`widgetDropdown.h`, `widgetLabel.h`, `widgetListBox.h`, `widgetSlider.h`,
`widgetSpacer.h`, `widgetTabControl.h`.
### Image Viewer (imgview)
### Image Viewer (imgview, BASIC)
| | |
|---|---|
| File | `apps/imgview/imgview.app` |
| Type | Callback-only |
| Multi-instance | Yes |
Displays BMP, PNG, JPEG, and GIF images. The image is scaled to fit
the window while preserving aspect ratio. Resize the window to zoom.
Open files via the File menu or by launching with Run in the Task
Manager.
Widget headers used: none (raw paint callbacks with `dvxLoadImage()`).
| File | `apps/kpunch/imgview/imgview.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Displays BMP, PNG, JPEG, and GIF images scaled to fit the window
with aspect ratio preserved. Resize to zoom. Open via the File menu
or via launch arguments. Built from a BASIC project using the
Canvas widget.
### DVX BASIC (dvxbasic)
| | |
|---|---|
| File | `apps/dvxbasic/dvxbasic.app` |
| File | `apps/kpunch/dvxbasic/dvxbasic.app` |
| Type | Callback-only |
| Multi-instance | No |
Visual Basic 3 clone with form designer, per-procedure code editor,
project management, and stack-based bytecode compiler/VM. Supports
event-driven programming with visual forms, controls, and a full
BASIC language implementation.
Visual Basic 3 compatible IDE: form designer, per-procedure code
editor, project manager, compiler, and runtime VM. See
`dvxbasic/README.md` for a detailed architecture description.
See `apps/dvxbasic/README.md` for detailed documentation.
`dvxbasic/` also builds:
Also builds `basrt.lib` -- the BASIC runtime library (VM + values)
as a separate DXE so compiled BASIC apps can use it independently.
* `bin/libs/kpunch/basrt/basrt.lib` -- the BASIC runtime (VM +
values) as a separately loadable library, so compiled BASIC apps
do not carry their own copy of the runtime.
* `bin/apps/kpunch/dvxbasic/basstub.app` -- the stub used as the
template for every compiled BASIC app.
### DVX Help Viewer (dvxhelp)
| | |
|---|---|
| File | `apps/kpunch/dvxhelp/dvxhelp.app` |
| Type | Callback-only |
| Multi-instance | Yes |
Renders compiled `.hlp` files produced by `dvxhlpc`. Supports table
of contents, keyword index, full-text search (trigram), hyperlinks,
and inline images.
### Icon Editor (iconed, BASIC)
| | |
|---|---|
| File | `apps/kpunch/iconed/iconed.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Paint-program-style editor for creating 32x32 BMP icons. Pen /
eraser / fill tools, 16-colour palette, BMP save / load.
### Help Editor (helpedit, BASIC)
| | |
|---|---|
| File | `apps/kpunch/dvxhelp/helpedit.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Editor for `.dhs` help source files with syntax-aware controls. Runs
the compiled system help viewer by default for live preview.
### Resource Editor (resedit, BASIC)
| | |
|---|---|
| File | `apps/kpunch/resedit/resedit.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`,
browse its resources, add, replace, extract, or remove them.
### BASIC Demo (basicdemo, BASIC)
| | |
|---|---|
| File | `apps/kpunch/basicdemo/basicdemo.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Gallery of short BASIC programs demonstrating the language and the
form designer. Intended as sample source for reading.
## Build
```
make # builds all 7 app DXE modules
make clean # removes objects and app files
make # build every app (C apps + BASIC apps)
make <appname> # build a single app
make clean # remove all app build artifacts
```
Each app compiles to a single `.o` (or multiple for DVX BASIC),
then is packaged via `dxe3gen` into a `.app` DXE exporting
`appDescriptor` and `appMain`.
The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
Output goes to `bin/apps/<name>/<name>.app`.
* **C apps** compile each `<app>/<app>.c` to an object file, run
`dxe3gen -U` on it to produce `<app>.app`, then attach resources
with `dvxres build` if a `.res` manifest exists.
* **BASIC apps** run `bascomp <app>.dbp -o <app>.app -release`,
which handles project-file parsing, compilation, resource
attachment, and linking into `basstub.app`.
Build output goes to `bin/apps/kpunch/<app>/<app>.app`.
## Writing a New C App
Minimal skeleton:
```c
#include "dvxApp.h"
#include "dvxWgt.h"
#include "shellApp.h"
#include <stdint.h>
#include <stdbool.h>
// Prototypes
static int32_t appMainImpl(DxeAppContextT *ctx);
static void onClose(WindowT *win);
// Required descriptor
AppDescriptorT appDescriptor = {
.name = "My App",
.hasMainLoop = false,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = 0
};
static void onClose(WindowT *win) {
(void)win;
// Callback-only apps: nothing to do; the shell reaps us when
// the last window closes.
}
int32_t appMain(DxeAppContextT *ctx) {
return appMainImpl(ctx);
}
static int32_t appMainImpl(DxeAppContextT *ctx) {
AppContextT *ac = ctx->shellCtx;
WindowT *win = dvxCreateWindow(ac, appDescriptor.name, 100, 100, 320, 240, true);
if (!win) {
return -1;
}
win->onClose = onClose;
return 0;
}
```
Add a Makefile target that mirrors the existing apps (see
`apps/kpunch/Makefile` for the pattern), drop a `.res` manifest next
to the source, and provide a 32x32 `icon32.bmp`. `make <app>` should
then produce `bin/apps/kpunch/<app>/<app>.app`.
## Testing Locally
After `make`, the app is deployed in-tree under `bin/apps/kpunch/`.
Launch the full system (`bin/dvx.exe`) and the Program Manager picks
up the new app automatically on the next boot.
To iterate quickly: rebuild the single app, restart DVX, and launch
it. The log at `bin/DVX.LOG` records load errors, missing
dependencies, and per-app crash diagnostics.
## Files
```
apps/
Makefile top-level build for all apps
apps/kpunch/
Makefile top-level app build
README.md this file
# C apps
progman/
progman.c Program Manager
notepad/
notepad.c text editor
progman.c Program Manager
dvxhelp.hcf help-compiler config (system reference)
clock/
clock.c digital clock
clock.c digital clock (main-loop reference)
clock.res
icon32.bmp
dvxdemo/
dvxdemo.c widget demo
logo.bmp DVX logo bitmap
new.bmp toolbar icon
open.bmp toolbar icon
sample.bmp sample image
save.bmp toolbar icon
dvxdemo.c widget showcase
dvxdemo.res
icon32.bmp
logo.bmp / new.bmp / open.bmp / sample.bmp / save.bmp
cpanel/
cpanel.c control panel
cpanel.c Control Panel
cpanel.res
icon32.bmp
dvxhelp/
dvxhelp.c help viewer
dvxhelp.res
help.dhs source for the dvxhelp app's own help
hlpformat.h binary format shared with the compiler
sample.dhs small sample source
icon32.bmp
helpedit/ BASIC project: help editor
helpedit.dbp
helpedit.frm
ICON32.BMP
# BASIC apps
iconed/
iconed.dbp
iconed.frm
ICON32.BMP
notepad/
notepad.dbp
notepad.frm
ICON32.BMP
imgview/
imgview.c image viewer
dvxbasic/
compiler/ BASIC compiler (lexer, parser, codegen)
runtime/ VM and value system
formrt/ form runtime (BASIC <-> DVX widgets)
ide/ IDE (editor, designer, project, properties)
samples/ sample .bas, .frm, .dbp files
imgview.dbp
imgview.frm
ICON32.BMP
basicdemo/
basicdemo.dbp
basicdemo.frm
ICON32.BMP
resedit/
resedit.dbp
resedit.frm
ICON32.BMP
dvxbasic/ see dvxbasic/README.md
compiler/ lexer, parser, codegen, strip, obfuscate
runtime/ VM + value system
formrt/ form runtime (BASIC <-> DVX widgets)
ide/ IDE front end
stub/ bascomp + basstub
...
```

View file

@ -234,7 +234,7 @@ static void applyMouseConfig(void) {
static void buildColorsTab(WidgetT *page) {
// Color list on the left, sliders on the right
WidgetT *hbox = wgtHBox(page);
hbox->weight = 100;
hbox->weight = WGT_WEIGHT_FILL;
hbox->spacing = wgtPixels(CP_SPACING);
// Left side: color name list + theme controls
@ -252,7 +252,7 @@ static void buildColorsTab(WidgetT *page) {
}
sColorList = wgtListBox(leftVbox);
sColorList->weight = 100;
sColorList->weight = WGT_WEIGHT_FILL;
sColorList->onChange = onColorSelect;
wgtListBoxSetItems(sColorList, colorNames, ColorCountE);
wgtListBoxSetSelected(sColorList, 0);
@ -265,7 +265,7 @@ static void buildColorsTab(WidgetT *page) {
scanThemes();
sThemeList = wgtDropdown(themeRow);
sThemeList->weight = 100;
sThemeList->weight = WGT_WEIGHT_FILL;
wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));
WidgetT *loadBtn = wgtButton(themeRow, "Apply");
@ -314,7 +314,7 @@ static void buildColorsTab(WidgetT *page) {
sColorSwatch = wgtCanvas(sliderBox, CP_SWATCH_W, CP_SWATCH_H);
// Absorb remaining vertical space below the sliders
wgtSpacer(rightVbox)->weight = 100;
wgtSpacer(rightVbox)->weight = WGT_WEIGHT_FILL;
updateColorSliders();
}
@ -325,7 +325,7 @@ static void buildDesktopTab(WidgetT *page) {
scanWallpapers();
sWpaperList = wgtListBox(page);
sWpaperList->weight = 100;
sWpaperList->weight = WGT_WEIGHT_FILL;
sWpaperList->onDblClick = onApplyWallpaper;
wgtListBoxSetItems(sWpaperList, sWpaperLabels, arrlen(sWpaperEntries));
@ -346,7 +346,7 @@ static void buildDesktopTab(WidgetT *page) {
clearBtn->onClick = onClearWallpaper;
clearBtn->prefW = wgtPixels(CP_WPAPER_BTN_W);
wgtSpacer(btnRow)->weight = 100;
wgtSpacer(btnRow)->weight = WGT_WEIGHT_FILL;
wgtLabel(btnRow, "Mode:");
static const char *modeItems[] = {"Stretch", "Tile", "Center"};
@ -367,7 +367,7 @@ static void buildMouseTab(WidgetT *page) {
static const char *wheelItems[] = {"Normal", "Reversed"};
sWheelDrop = wgtDropdown(wheelRow);
sWheelDrop->weight = 100;
sWheelDrop->weight = WGT_WEIGHT_FILL;
sWheelDrop->onChange = onWheelChange;
wgtDropdownSetItems(sWheelDrop, wheelItems, 2);
wgtDropdownSetSelected(sWheelDrop, sAc->wheelDirection < 0 ? 1 : 0);
@ -382,7 +382,7 @@ static void buildMouseTab(WidgetT *page) {
wgtLabel(wheelStepRow, "Slow");
sWheelStepSldr = wgtSlider(wheelStepRow, MOUSE_WHEEL_STEP_MIN, MOUSE_WHEEL_STEP_MAX);
sWheelStepSldr->weight = 100;
sWheelStepSldr->weight = WGT_WEIGHT_FILL;
sWheelStepSldr->onChange = onWheelStepSlider;
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
@ -402,7 +402,7 @@ static void buildMouseTab(WidgetT *page) {
wgtLabel(dblRow, "Fast");
sDblClickSldr = wgtSlider(dblRow, 200, 900);
sDblClickSldr->weight = 100;
sDblClickSldr->weight = WGT_WEIGHT_FILL;
sDblClickSldr->onChange = onDblClickSlider;
int32_t dblMs = prefsGetInt(sPrefs, "mouse", "doubleclick", MOUSE_DBLCLICK_DEFAULT_MS);
@ -421,7 +421,7 @@ static void buildMouseTab(WidgetT *page) {
static const char *accelItems[] = {"Off", "Low", "Medium", "High"};
sAccelDrop = wgtDropdown(accelRow);
sAccelDrop->weight = 100;
sAccelDrop->weight = WGT_WEIGHT_FILL;
sAccelDrop->onChange = onAccelChange;
wgtDropdownSetItems(sAccelDrop, accelItems, 4);
@ -447,7 +447,7 @@ static void buildMouseTab(WidgetT *page) {
wgtLabel(speedRow, "Slow");
sSpeedSldr = wgtSlider(speedRow, 2, 32);
sSpeedSldr->weight = 100;
sSpeedSldr->weight = WGT_WEIGHT_FILL;
sSpeedSldr->onChange = onSpeedSlider;
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
@ -501,7 +501,7 @@ static void buildVideoTab(WidgetT *page) {
}
sVideoList = wgtListBox(page);
sVideoList->weight = 100;
sVideoList->weight = WGT_WEIGHT_FILL;
sVideoList->onDblClick = onVideoApply;
wgtListBoxSetItems(sVideoList, sVideoLabels, sVideoCount);
@ -1153,7 +1153,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Tab control fills the window
WidgetT *tabs = wgtTabControl(root);
tabs->weight = 100;
tabs->weight = WGT_WEIGHT_FILL;
WidgetT *mouseTab = wgtTabPage(tabs, "Mouse");
WidgetT *colorsTab = wgtTabPage(tabs, "Colors");

View file

@ -1,97 +1,55 @@
# DVX BASIC
A Visual Basic 3 clone for the DVX GUI system. Provides a visual form
designer, per-procedure code editor, project management, and a
stack-based bytecode compiler/VM for running event-driven BASIC programs.
A Visual Basic 3 clone for the DVX GUI system. Provides a visual form
designer, per-procedure code editor, project management, and a stack-
based bytecode compiler and VM for running event-driven BASIC programs.
## Architecture
## Documentation
DVX BASIC is built as a DXE3 app (dvxbasic.app) loaded by the DVX shell.
It consists of four major subsystems:
For BASIC authors, the complete user reference is split across several
`.dhs` help sources in this directory, all compiled into the DVX BASIC
Reference:
### Compiler (compiler/)
- `langref.dhs` -- language reference: data types, keywords, operators,
control flow, built-in functions, I/O, error codes.
- `ideguide.dhs` -- IDE guide: menus, toolbar, project model, form
designer, code editor, debugger, preferences.
- `ctrlover.dhs` -- control overview: common widget properties and
events from a BASIC author's perspective.
- `form.dhs` -- form lifecycle, properties, events.
- `basrt.dhs` -- BASIC runtime reference: DECLARE LIBRARY mechanism,
shipped include files.
Single-pass compiler that translates BASIC source to stack-based p-code.
After `make`, the compiled form is `bin/apps/kpunch/dvxbasic/dvxbasic.hlp`
(viewable via the Help Viewer app or from within the IDE via F1) and
`docs/dvx_basic_reference.html`.
- **lexer.c** -- Tokenizer with keyword lookup, type suffixes, hex literals,
line continuation (`_`), and `?` as PRINT shortcut.
- **parser.c** -- Recursive descent parser. Handles all VB3 statements,
expressions, operator precedence (VB-correct: `^` binds tighter than
unary `-`), Sub/Function with forward references, bare sub calls,
OPTION EXPLICIT, STATIC, CONST, DEF FN, SELECT CASE, ON ERROR GOTO,
UDTs (nested), arrays of UDTs, DECLARE LIBRARY.
- **codegen.c** -- Bytecode emitter. Dynamic arrays (stb_ds) for code,
constants, and proc table. Module building, forward reference patching,
unresolved reference detection.
- **symtab.c** -- Symbol table with local/global scoping, type tracking.
- **opcodes.h** -- ~90 bytecode instructions. OP_END (explicit END statement)
vs OP_HALT (implicit end of module).
## Subdirectories (for maintainers)
### Runtime (runtime/)
- **vm.c** -- Stack-based virtual machine. Step-limited execution for
cooperative multitasking. Event handler dispatch via basVmCallSub
(runs to completion, no step limit). basVmCallSubWithArgsOut for
events that return values (QueryUnload Cancel parameter).
- **values.c** -- Tagged value system: Integer, Long, Single, Double,
String (ref-counted), Boolean, Array (ref-counted, multi-dim), UDT
(ref-counted, nested), Object (opaque host pointer), Ref (ByRef).
### Form Runtime (formrt/)
- **formrt.c** -- Bridge between BASIC VM and DVX widgets. Loads .frm
files at runtime, creates windows and controls, dispatches events to
BASIC code. Supports property get/set, method calls, and control
creation via the widget interface system.
Events: Click, DblClick, Change, GotFocus, LostFocus, KeyPress,
KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, Scroll, Load,
QueryUnload, Unload, Resize, Activate, Deactivate, Timer.
### IDE (ide/)
- **ideMain.c** -- Main IDE orchestration. File switching via
`activateFile()`, per-procedure editing, Object/Event dropdowns
with hash-based syntax highlighting, dirty tracking with
`sEditorFileIdx` ownership, compile-and-run with IDE window
hiding, VB-style event loop.
- **ideDesigner.c** -- Visual form designer. Live widget creation,
drag/resize handles, widget interface property persistence
(including WGT_IFACE_ENUM with named values), code-in-FRM files.
- **ideProject.c** -- Project system (.dbp files, INI format). Project
window with tree view, project properties dialog with startup form
dropdown and icon browser. All files loaded into memory at project
open.
- **ideProperties.c** -- Property editor. ListView with type-aware
editing (bool toggle, int spinner, enum cycling, string input).
Interface property display via widget descriptors.
- **ideToolbox.c** -- Widget palette loaded from registered widget
interfaces.
- `compiler/` -- lexer, parser, codegen, symbol table, opcodes.
Single-pass compiler that translates BASIC source to stack-based
p-code.
- `runtime/` -- VM and tagged value system (values.c, vm.c, serialize.c).
- `formrt/` -- form runtime, the bridge between the BASIC VM and the
DVX widget system.
- `ide/` -- the IDE itself (main, designer, project, properties,
toolbox, menu editor).
- `stub/` -- `basstub` (runtime loader for compiled BASIC `.app`
files) and `bascomp` (standalone command-line compiler).
## Project Files
project.dbp INI-format project file
module.bas BASIC module (code only)
form.frm Form file (layout + code section after "End")
form.frm Form file (layout + code after the form's End)
.bas modules are compiled before .frm code sections so CONST
declarations are available to form event handlers.
`.bas` modules compile before `.frm` code sections, so CONST
declarations are visible to form event handlers.
## Build
make -C apps/dvxbasic # cross-compile for DJGPP
make -C apps/dvxbasic tests # build native test programs
make -C src/apps/kpunch/dvxbasic # cross-compile for DOS
make -C src/apps/kpunch/dvxbasic tests # build native test programs
Test programs: test_compiler, test_vm, test_lex, test_quick.
162 test sections covering the compiler, VM, and language features.
## File Structure
apps/dvxbasic/
compiler/ Lexer, parser, codegen, symbol table, opcodes
runtime/ VM and tagged value system
formrt/ Form runtime (BASIC <-> DVX widget bridge)
ide/ IDE (main, designer, project, properties, toolbox)
samples/ Sample .bas, .frm, .dbp files
dvxbasic.res App resources (icons, toolbar buttons)
Makefile Builds basrt.lib (runtime) + dvxbasic.app (IDE)
Test programs: `test_compiler`, `test_vm`, `test_lex`, `test_quick`,
`test_compact`.

View file

@ -559,7 +559,7 @@ static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffse
int32_t newOffset = newTarget - newPcAfter;
if (newOffset < -32768 || newOffset > 32767) {
if (newOffset < INT16_MIN || newOffset > INT16_MAX) {
return false;
}

View file

@ -40,137 +40,142 @@
typedef struct {
const char *text;
uint8_t textLen; // precomputed so lookupKeyword() can length-reject cheaply
BasTokenTypeE type;
} KeywordEntryT;
#define KW(s, t) { s, (uint8_t)(sizeof(s) - 1), t }
static const KeywordEntryT sKeywords[] = {
{ "AND", TOK_AND },
{ "APP", TOK_APP },
{ "APPEND", TOK_APPEND },
{ "AS", TOK_AS },
{ "BASE", TOK_BASE },
{ "BINARY", TOK_BINARY },
{ "BOOLEAN", TOK_BOOLEAN },
{ "BYVAL", TOK_BYVAL },
{ "CALL", TOK_CALL },
{ "CASE", TOK_CASE },
{ "CHDIR", TOK_CHDIR },
{ "CHDRIVE", TOK_CHDRIVE },
{ "CLOSE", TOK_CLOSE },
{ "CREATECONTROL", TOK_CREATECONTROL },
{ "CREATEFORM", TOK_CREATEFORM },
{ "CURDIR", TOK_CURDIR },
{ "CURDIR$", TOK_CURDIR },
{ "CONST", TOK_CONST },
{ "DATA", TOK_DATA },
{ "DECLARE", TOK_DECLARE },
{ "DEF", TOK_DEF },
{ "DEFDBL", TOK_DEFDBL },
{ "DEFINT", TOK_DEFINT },
{ "DEFLNG", TOK_DEFLNG },
{ "DEFSNG", TOK_DEFSNG },
{ "DEFSTR", TOK_DEFSTR },
{ "DIM", TOK_DIM },
{ "DIR", TOK_DIR },
{ "DIR$", TOK_DIR },
{ "DO", TOK_DO },
{ "DOEVENTS", TOK_DOEVENTS },
{ "DOUBLE", TOK_DOUBLE },
{ "ELSE", TOK_ELSE },
{ "ELSEIF", TOK_ELSEIF },
{ "END", TOK_END },
{ "EOF", TOK_EOF_KW },
{ "EQV", TOK_EQV },
{ "ERASE", TOK_ERASE },
{ "ERR", TOK_ERR },
{ "ERROR", TOK_ERROR_KW },
{ "EXPLICIT", TOK_EXPLICIT },
{ "EXIT", TOK_EXIT },
{ "FALSE", TOK_FALSE_KW },
{ "FILECOPY", TOK_FILECOPY },
{ "FILELEN", TOK_FILELEN },
{ "FOR", TOK_FOR },
{ "FUNCTION", TOK_FUNCTION },
{ "GET", TOK_GET },
{ "GETATTR", TOK_GETATTR },
{ "GOSUB", TOK_GOSUB },
{ "GOTO", TOK_GOTO },
{ "HIDE", TOK_HIDE },
{ "IF", TOK_IF },
{ "IMP", TOK_IMP },
{ "INIREAD", TOK_INIREAD },
{ "INIREAD$", TOK_INIREAD },
{ "INIWRITE", TOK_INIWRITE },
{ "INPUT", TOK_INPUT },
{ "INTEGER", TOK_INTEGER },
{ "IS", TOK_IS },
{ "KILL", TOK_KILL },
{ "LBOUND", TOK_LBOUND },
{ "LET", TOK_LET },
{ "LINE", TOK_LINE },
{ "LOAD", TOK_LOAD },
{ "LONG", TOK_LONG },
{ "LOOP", TOK_LOOP },
{ "ME", TOK_ME },
{ "MKDIR", TOK_MKDIR },
{ "MOD", TOK_MOD },
{ "INPUTBOX", TOK_INPUTBOX },
{ "INPUTBOX$", TOK_INPUTBOX },
{ "MSGBOX", TOK_MSGBOX },
{ "NAME", TOK_NAME },
{ "NEXT", TOK_NEXT },
{ "NOT", TOK_NOT },
{ "NOTHING", TOK_NOTHING },
{ "ON", TOK_ON },
{ "OPEN", TOK_OPEN },
{ "OPTIONAL", TOK_OPTIONAL },
{ "OPTION", TOK_OPTION },
{ "OR", TOK_OR },
{ "OUTPUT", TOK_OUTPUT },
{ "PRESERVE", TOK_PRESERVE },
{ "PRINT", TOK_PRINT },
{ "PUT", TOK_PUT },
{ "RANDOM", TOK_RANDOM },
{ "RANDOMIZE", TOK_RANDOMIZE },
{ "READ", TOK_READ },
{ "REDIM", TOK_REDIM },
{ "REM", TOK_REM },
{ "REMOVECONTROL", TOK_REMOVECONTROL },
{ "RESTORE", TOK_RESTORE },
{ "RESUME", TOK_RESUME },
{ "RETURN", TOK_RETURN },
{ "RMDIR", TOK_RMDIR },
{ "SEEK", TOK_SEEK },
{ "SELECT", TOK_SELECT },
{ "SET", TOK_SET },
{ "SETATTR", TOK_SETATTR },
{ "SETEVENT", TOK_SETEVENT },
{ "SHARED", TOK_SHARED },
{ "SHELL", TOK_SHELL },
{ "SHOW", TOK_SHOW },
{ "SINGLE", TOK_SINGLE },
{ "SLEEP", TOK_SLEEP },
{ "STATIC", TOK_STATIC },
{ "STEP", TOK_STEP },
{ "STRING", TOK_STRING_KW },
{ "SUB", TOK_SUB },
{ "SWAP", TOK_SWAP },
{ "THEN", TOK_THEN },
{ "TIMER", TOK_TIMER },
{ "TO", TOK_TO },
{ "TRUE", TOK_TRUE_KW },
{ "TYPE", TOK_TYPE },
{ "UBOUND", TOK_UBOUND },
{ "UNLOAD", TOK_UNLOAD },
{ "UNTIL", TOK_UNTIL },
{ "WEND", TOK_WEND },
{ "WHILE", TOK_WHILE },
{ "WITH", TOK_WITH },
{ "WRITE", TOK_WRITE },
{ "XOR", TOK_XOR },
{ NULL, TOK_ERROR }
KW("AND", TOK_AND),
KW("APP", TOK_APP),
KW("APPEND", TOK_APPEND),
KW("AS", TOK_AS),
KW("BASE", TOK_BASE),
KW("BINARY", TOK_BINARY),
KW("BOOLEAN", TOK_BOOLEAN),
KW("BYVAL", TOK_BYVAL),
KW("CALL", TOK_CALL),
KW("CASE", TOK_CASE),
KW("CHDIR", TOK_CHDIR),
KW("CHDRIVE", TOK_CHDRIVE),
KW("CLOSE", TOK_CLOSE),
KW("CREATECONTROL", TOK_CREATECONTROL),
KW("CREATEFORM", TOK_CREATEFORM),
KW("CURDIR", TOK_CURDIR),
KW("CURDIR$", TOK_CURDIR),
KW("CONST", TOK_CONST),
KW("DATA", TOK_DATA),
KW("DECLARE", TOK_DECLARE),
KW("DEF", TOK_DEF),
KW("DEFDBL", TOK_DEFDBL),
KW("DEFINT", TOK_DEFINT),
KW("DEFLNG", TOK_DEFLNG),
KW("DEFSNG", TOK_DEFSNG),
KW("DEFSTR", TOK_DEFSTR),
KW("DIM", TOK_DIM),
KW("DIR", TOK_DIR),
KW("DIR$", TOK_DIR),
KW("DO", TOK_DO),
KW("DOEVENTS", TOK_DOEVENTS),
KW("DOUBLE", TOK_DOUBLE),
KW("ELSE", TOK_ELSE),
KW("ELSEIF", TOK_ELSEIF),
KW("END", TOK_END),
KW("EOF", TOK_EOF_KW),
KW("EQV", TOK_EQV),
KW("ERASE", TOK_ERASE),
KW("ERR", TOK_ERR),
KW("ERROR", TOK_ERROR_KW),
KW("EXPLICIT", TOK_EXPLICIT),
KW("EXIT", TOK_EXIT),
KW("FALSE", TOK_FALSE_KW),
KW("FILECOPY", TOK_FILECOPY),
KW("FILELEN", TOK_FILELEN),
KW("FOR", TOK_FOR),
KW("FUNCTION", TOK_FUNCTION),
KW("GET", TOK_GET),
KW("GETATTR", TOK_GETATTR),
KW("GOSUB", TOK_GOSUB),
KW("GOTO", TOK_GOTO),
KW("HIDE", TOK_HIDE),
KW("IF", TOK_IF),
KW("IMP", TOK_IMP),
KW("INIREAD", TOK_INIREAD),
KW("INIREAD$", TOK_INIREAD),
KW("INIWRITE", TOK_INIWRITE),
KW("INPUT", TOK_INPUT),
KW("INTEGER", TOK_INTEGER),
KW("IS", TOK_IS),
KW("KILL", TOK_KILL),
KW("LBOUND", TOK_LBOUND),
KW("LET", TOK_LET),
KW("LINE", TOK_LINE),
KW("LOAD", TOK_LOAD),
KW("LONG", TOK_LONG),
KW("LOOP", TOK_LOOP),
KW("ME", TOK_ME),
KW("MKDIR", TOK_MKDIR),
KW("MOD", TOK_MOD),
KW("INPUTBOX", TOK_INPUTBOX),
KW("INPUTBOX$", TOK_INPUTBOX),
KW("MSGBOX", TOK_MSGBOX),
KW("NAME", TOK_NAME),
KW("NEXT", TOK_NEXT),
KW("NOT", TOK_NOT),
KW("NOTHING", TOK_NOTHING),
KW("ON", TOK_ON),
KW("OPEN", TOK_OPEN),
KW("OPTIONAL", TOK_OPTIONAL),
KW("OPTION", TOK_OPTION),
KW("OR", TOK_OR),
KW("OUTPUT", TOK_OUTPUT),
KW("PRESERVE", TOK_PRESERVE),
KW("PRINT", TOK_PRINT),
KW("PUT", TOK_PUT),
KW("RANDOM", TOK_RANDOM),
KW("RANDOMIZE", TOK_RANDOMIZE),
KW("READ", TOK_READ),
KW("REDIM", TOK_REDIM),
KW("REM", TOK_REM),
KW("REMOVECONTROL", TOK_REMOVECONTROL),
KW("RESTORE", TOK_RESTORE),
KW("RESUME", TOK_RESUME),
KW("RETURN", TOK_RETURN),
KW("RMDIR", TOK_RMDIR),
KW("SEEK", TOK_SEEK),
KW("SELECT", TOK_SELECT),
KW("SET", TOK_SET),
KW("SETATTR", TOK_SETATTR),
KW("SETEVENT", TOK_SETEVENT),
KW("SHARED", TOK_SHARED),
KW("SHELL", TOK_SHELL),
KW("SHOW", TOK_SHOW),
KW("SINGLE", TOK_SINGLE),
KW("SLEEP", TOK_SLEEP),
KW("STATIC", TOK_STATIC),
KW("STEP", TOK_STEP),
KW("STRING", TOK_STRING_KW),
KW("SUB", TOK_SUB),
KW("SWAP", TOK_SWAP),
KW("THEN", TOK_THEN),
KW("TIMER", TOK_TIMER),
KW("TO", TOK_TO),
KW("TRUE", TOK_TRUE_KW),
KW("TYPE", TOK_TYPE),
KW("UBOUND", TOK_UBOUND),
KW("UNLOAD", TOK_UNLOAD),
KW("UNTIL", TOK_UNTIL),
KW("WEND", TOK_WEND),
KW("WHILE", TOK_WHILE),
KW("WITH", TOK_WITH),
KW("WRITE", TOK_WRITE),
KW("XOR", TOK_XOR),
{ NULL, 0, TOK_ERROR }
};
#undef KW
#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1)
@ -507,26 +512,29 @@ const char *basTokenName(BasTokenTypeE type) {
static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
// Case-insensitive keyword lookup
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
const char *kw = sKeywords[i].text;
int32_t kwLen = (int32_t)strlen(kw);
// Case-insensitive keyword lookup. Short-circuits on length mismatch
// (via cached keyword length) and on the very first character, both of
// which reject the vast majority of entries before doing a full scan.
char firstUp = (text[0] >= 'a' && text[0] <= 'z') ? (char)(text[0] - 32) : text[0];
if (kwLen != len) {
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
const KeywordEntryT *kw = &sKeywords[i];
if (kw->textLen != len || kw->text[0] != firstUp) {
continue;
}
bool match = true;
for (int32_t j = 0; j < len; j++) {
if (upperChar(text[j]) != kw[j]) {
for (int32_t j = 1; j < len; j++) {
if (upperChar(text[j]) != kw->text[j]) {
match = false;
break;
}
}
if (match) {
return sKeywords[i].type;
return kw->type;
}
}
@ -809,7 +817,7 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
long val = atol(lex->token.text);
if (val >= -32768 && val <= 32767) {
if (val >= INT16_MIN && val <= INT16_MAX) {
lex->token.intVal = (int32_t)val;
return TOK_INT_LIT;
}

View file

@ -214,6 +214,7 @@ typedef enum {
// ============================================================
#define BAS_MAX_TOKEN_LEN 256
#define BAS_LEX_ERROR_LEN 256
typedef struct {
BasTokenTypeE type;
@ -243,7 +244,7 @@ typedef struct {
int32_t line; // current line (1-based)
int32_t col; // current column (1-based)
BasTokenT token; // current token
char error[256];
char error[BAS_LEX_ERROR_LEN];
} BasLexerT;
// ============================================================

View file

@ -28,6 +28,34 @@
#ifndef DVXBASIC_OPCODES_H
#define DVXBASIC_OPCODES_H
// ============================================================
// Variable scope tags
// Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed
// by the VM to choose between globals, call-frame locals, and
// form-scoped variables. Numeric values are part of the bytecode
// ABI -- do not reorder.
// ============================================================
typedef enum {
SCOPE_GLOBAL = 0,
SCOPE_LOCAL = 1,
SCOPE_FORM = 2 // per-form variable (persists while form is loaded)
} BasScopeE;
// ============================================================
// File channel modes (BasFileChannelT.mode and OP_FILE_OPEN arg).
// Numeric values are part of the bytecode ABI -- do not reorder.
// ============================================================
typedef enum {
BAS_FILE_MODE_CLOSED = 0,
BAS_FILE_MODE_INPUT = 1,
BAS_FILE_MODE_OUTPUT = 2,
BAS_FILE_MODE_APPEND = 3,
BAS_FILE_MODE_RANDOM = 4,
BAS_FILE_MODE_BINARY = 5
} BasFileModeE;
// ============================================================
// Data type tags (used in Value representation)
// ============================================================

View file

@ -589,7 +589,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
}
if (argc < minArgs || argc > sym->paramCount) {
char buf[256];
char buf[BAS_PARSE_ERR_SCRATCH];
if (minArgs == sym->paramCount) {
snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
} else {
@ -862,7 +862,7 @@ static void error(BasParserT *p, const char *msg) {
static void errorExpected(BasParserT *p, const char *what) {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Expected %s, got %s", what, basTokenName(p->lex.token.type));
error(p, buf);
}
@ -897,7 +897,7 @@ static void expect(BasParserT *p, BasTokenTypeE type) {
return;
}
if (p->lex.token.type != type) {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Expected %s, got %s", basTokenName(type), basTokenName(p->lex.token.type));
error(p, buf);
return;
@ -1094,7 +1094,7 @@ static void parseAssignOrCall(BasParserT *p) {
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
error(p, buf);
return;
@ -1333,7 +1333,7 @@ static void parseAssignOrCall(BasParserT *p) {
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
error(p, buf);
return;
@ -1413,7 +1413,7 @@ static void parseAssignOrCall(BasParserT *p) {
}
}
if (!p->hasError && argc != sym->paramCount) {
char buf[256];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
error(p, buf);
return;
@ -2225,7 +2225,7 @@ static void parseDim(BasParserT *p) {
// Check for duplicate
BasSymbolT *existing = basSymTabFind(&p->sym, name);
if (existing != NULL && existing->isDefined) {
char buf[256];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Variable '%s' already declared", name);
error(p, buf);
return;
@ -2575,7 +2575,7 @@ static void parseFor(BasParserT *p) {
// Emit FOR_INIT -- sets up the for-loop state in the VM
basEmit8(&p->cg, OP_FOR_INIT);
basEmitU16(&p->cg, (uint16_t)loopVar->index);
basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : (loopVar->scope == SCOPE_FORM ? 2 : 0));
basEmit8(&p->cg, (uint8_t)loopVar->scope);
int32_t loopBody = basCodePos(&p->cg);
@ -2602,7 +2602,7 @@ static void parseFor(BasParserT *p) {
// Emit FOR_NEXT with backward jump to loop body
basEmit8(&p->cg, OP_FOR_NEXT);
basEmitU16(&p->cg, (uint16_t)loopVar->index);
basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : (loopVar->scope == SCOPE_FORM ? 2 : 0));
basEmit8(&p->cg, (uint8_t)loopVar->scope);
int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2));
basEmit16(&p->cg, backOffset);
@ -3224,7 +3224,7 @@ static void parseModule(BasParserT *p) {
BasSymbolT *sym = p->sym.symbols[i];
if ((sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION) && !sym->isDefined && !sym->isExtern) {
char buf[256];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Undefined %s: %s",
sym->kind == SYM_SUB ? "Sub" : "Function", sym->name);
error(p, buf);
@ -3480,19 +3480,19 @@ static void parseOpen(BasParserT *p) {
uint8_t mode;
if (check(p, TOK_INPUT)) {
mode = 1; // INPUT
mode = BAS_FILE_MODE_INPUT;
advance(p);
} else if (check(p, TOK_OUTPUT)) {
mode = 2; // OUTPUT
mode = BAS_FILE_MODE_OUTPUT;
advance(p);
} else if (check(p, TOK_APPEND)) {
mode = 3; // APPEND
mode = BAS_FILE_MODE_APPEND;
advance(p);
} else if (check(p, TOK_RANDOM)) {
mode = 4; // RANDOM
mode = BAS_FILE_MODE_RANDOM;
advance(p);
} else if (check(p, TOK_BINARY)) {
mode = 5; // BINARY
mode = BAS_FILE_MODE_BINARY;
advance(p);
} else {
error(p, "Expected INPUT, OUTPUT, APPEND, RANDOM, or BINARY after FOR");
@ -3632,7 +3632,7 @@ static void parsePrimary(BasParserT *p) {
// Integer literal
if (tt == TOK_INT_LIT) {
int32_t val = p->lex.token.intVal;
if (val >= -32768 && val <= 32767) {
if (val >= INT16_MIN && val <= INT16_MAX) {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, (int16_t)val);
} else {
@ -4001,7 +4001,7 @@ static void parsePrimary(BasParserT *p) {
}
if (argc < builtin->minArgs || argc > builtin->maxArgs) {
char buf[256];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (int)builtin->minArgs, (int)builtin->maxArgs, (int)argc);
error(p, buf);
return;
@ -4061,7 +4061,7 @@ static void parsePrimary(BasParserT *p) {
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
error(p, buf);
return;
@ -4142,7 +4142,7 @@ static void parsePrimary(BasParserT *p) {
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
error(p, buf);
return;
@ -5415,7 +5415,7 @@ static void parseStatement(BasParserT *p) {
sym->isDefined = true;
sym->codeAddr = basCodePos(&p->cg);
} else {
char buf[512];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Name '%s' already used", labelName);
error(p, buf);
}
@ -5436,7 +5436,7 @@ static void parseStatement(BasParserT *p) {
break;
default: {
char buf[256];
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unexpected token: %s", basTokenName(tt));
error(p, buf);
break;

View file

@ -43,11 +43,17 @@
// Parser state
// ============================================================
// Parse-error string sizes. PARSE_ERROR_LEN is the final message stored
// on the parser; PARSE_ERR_SCRATCH is used for temporary formatting before
// copying into it (with a "Line N: " prefix).
#define BAS_PARSE_ERROR_LEN 1024
#define BAS_PARSE_ERR_SCRATCH 512
typedef struct {
BasLexerT lex;
BasCodeGenT cg;
BasSymTabT sym;
char error[1024];
char error[BAS_PARSE_ERROR_LEN];
bool hasError;
int32_t errorLine;
int32_t prevLine; // line of the previous token (for error reporting)

View file

@ -41,6 +41,7 @@ void basSymTabInit(BasSymTabT *tab);
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
void basSymTabLeaveLocal(BasSymTabT *tab);
static bool namesEqual(const char *a, const char *b);
static uint32_t nameHashCI(const char *name);
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
// Determine scope: local > form > global.
@ -55,13 +56,15 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
scope = SCOPE_GLOBAL;
}
uint32_t h = nameHashCI(name);
// Check for duplicate in current scope (skip ended form symbols)
for (int32_t i = 0; i < tab->count; i++) {
if (tab->symbols[i]->formScopeEnded) {
continue;
}
if (tab->symbols[i]->scope == scope && namesEqual(tab->symbols[i]->name, name)) {
if (tab->symbols[i]->nameHash == h && tab->symbols[i]->scope == scope && namesEqual(tab->symbols[i]->name, name)) {
return NULL; // duplicate
}
}
@ -74,6 +77,7 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1);
sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
sym->nameHash = h;
sym->kind = kind;
sym->scope = scope;
sym->dataType = dataType;
@ -119,24 +123,28 @@ void basSymTabEnterLocal(BasSymTabT *tab) {
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
uint32_t h = nameHashCI(name);
// Search local scope first
if (tab->inLocalScope) {
for (int32_t i = tab->count - 1; i >= 0; i--) {
if (tab->symbols[i]->scope == SCOPE_LOCAL && namesEqual(tab->symbols[i]->name, name)) {
return tab->symbols[i];
BasSymbolT *s = tab->symbols[i];
if (s->nameHash == h && s->scope == SCOPE_LOCAL && namesEqual(s->name, name)) {
return s;
}
}
}
// Search form scope and global scope
for (int32_t i = tab->count - 1; i >= 0; i--) {
if (tab->symbols[i]->formScopeEnded) {
BasSymbolT *s = tab->symbols[i];
if (s->formScopeEnded) {
continue;
}
if ((tab->symbols[i]->scope == SCOPE_FORM || tab->symbols[i]->scope == SCOPE_GLOBAL) &&
namesEqual(tab->symbols[i]->name, name)) {
return tab->symbols[i];
if (s->nameHash == h && (s->scope == SCOPE_FORM || s->scope == SCOPE_GLOBAL) &&
namesEqual(s->name, name)) {
return s;
}
}
@ -145,13 +153,16 @@ BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
uint32_t h = nameHashCI(name);
for (int32_t i = 0; i < tab->count; i++) {
if (tab->symbols[i]->formScopeEnded) {
BasSymbolT *s = tab->symbols[i];
if (s->formScopeEnded) {
continue;
}
if (tab->symbols[i]->scope == SCOPE_GLOBAL && namesEqual(tab->symbols[i]->name, name)) {
return tab->symbols[i];
if (s->nameHash == h && s->scope == SCOPE_GLOBAL && namesEqual(s->name, name)) {
return s;
}
}
@ -209,6 +220,29 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
}
// ============================================================
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
// Caller computes hash of search-name once; each entry stores its own
// precomputed hash, so per-entry comparison is a fast uint32 compare
// that nearly always rejects non-matches without calling namesEqual.
// ============================================================
static uint32_t nameHashCI(const char *name) {
uint32_t h = 0x811C9DC5u;
while (*name) {
char c = *name;
if (c >= 'a' && c <= 'z') {
c -= 32;
}
h ^= (uint32_t)(uint8_t)c;
h *= 0x01000193u;
name++;
}
return h;
}
// ============================================================
// Case-insensitive name comparison
// ============================================================

View file

@ -49,15 +49,7 @@ typedef enum {
SYM_TYPE_DEF // user-defined TYPE
} BasSymKindE;
// ============================================================
// Symbol scope
// ============================================================
typedef enum {
SCOPE_GLOBAL,
SCOPE_LOCAL,
SCOPE_FORM // per-form variable (persists while form is loaded)
} BasScopeE;
// BasScopeE moved to opcodes.h (shared with runtime).
// ============================================================
// Symbol entry
@ -75,6 +67,7 @@ typedef struct {
typedef struct {
char name[BAS_MAX_SYMBOL_NAME];
uint32_t nameHash; // FNV-1a over uppercase(name); fast-reject during lookup
BasSymKindE kind;
BasScopeE scope;
uint8_t dataType; // BAS_TYPE_* for variables/functions

View file

@ -52,8 +52,8 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
Weight Integer R/W Layout weight. 0 = fixed size, >0 = share extra space proportionally.
Visible Boolean R/W Whether the control is visible.
Enabled Boolean R/W Whether the control accepts user input.
BackColor Long R/W Background color as a 32-bit ARGB value.
ForeColor Long R/W Foreground (text) color as a 32-bit ARGB value.
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
.endtable

View file

@ -51,7 +51,7 @@ The Form is the top-level container representing a DVX window. It is declared in
Left Integer 0 Initial X position. Used when Centered is False.
Top Integer 0 Initial Y position. Used when Centered is False.
Layout String "VBox" Content box layout: "VBox" (vertical) or "HBox" (horizontal).
AutoSize Boolean True When True, the window shrink-wraps to fit its content.
AutoSize Boolean False When True, the window shrink-wraps to fit its content.
Resizable Boolean True Whether the user can resize the window at runtime.
Centered Boolean True When True, the window is centered on screen. When False, Left/Top are used.
.endtable

View file

@ -751,7 +751,7 @@ WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout) {
if (api) {
WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api;
WidgetT *box = createFn(root);
box->weight = 100;
box->weight = WGT_WEIGHT_FILL;
return box;
}
}
@ -2728,7 +2728,7 @@ int32_t CommIsReady(int32_t handle) {
// SecLink receive callback: routes data into per-channel ring buffers
static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
CommSlotT *slot = (CommSlotT *)ctx;
CommChanBufT *cb = &slot->channels[channel & 0x7F];
CommChanBufT *cb = &slot->channels[channel & SECLINK_MAX_CHANNEL];
for (int i = 0; i < len; i++) {
int32_t next = (cb->head + 1) % SECLINK_CHAN_BUF_SIZE;

View file

@ -157,6 +157,7 @@
#define SYNTAX_STRING 2
#define SYNTAX_COMMENT 3
#define SYNTAX_NUMBER 4
#define SYNTAX_OPERATOR 5
#define SYNTAX_TYPE 6
// View mode for activateFile
@ -698,15 +699,17 @@ static const char *sSyntaxColorNames[] = {
"Types", // 6 = SYNTAX_TYPE
};
// Default syntax colors (0x00RRGGBB; 0 = use widget default)
// Default syntax colors (0x00RRGGBB; 0 = use widget default).
// Designated initializers keep the SYNTAX_* index and the color aligned
// so there's no drift if indices are reordered later.
static const uint32_t sDefaultSyntaxColors[SYNTAX_COLOR_COUNT] = {
0x00000000, // default -- not used (widget fg)
0x00000080, // keyword -- dark blue
0x00800000, // string -- dark red
0x00008000, // comment -- dark green
0x00800080, // number -- purple
0x00808000, // operator -- dark yellow
0x00008080, // type -- teal
[SYNTAX_DEFAULT] = 0, // widget's fg color
[SYNTAX_KEYWORD] = 0x00000080, // dark blue
[SYNTAX_STRING] = 0x00800000, // dark red
[SYNTAX_COMMENT] = 0x00008000, // dark green
[SYNTAX_NUMBER] = 0x00800080, // purple
[SYNTAX_OPERATOR] = 0x00808000, // dark yellow
[SYNTAX_TYPE] = 0x00008080, // teal
};
// Preferences dialog state (used by several onPrefs* handlers)
@ -1143,7 +1146,7 @@ static void buildWindow(void) {
dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP);
dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
dvxAddAccel(accel, 0x1B, 0, CMD_STOP);
dvxAddAccel(accel, KEY_ESCAPE, 0, CMD_STOP);
dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE);
dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT);
@ -1219,7 +1222,7 @@ static void buildWindow(void) {
sStatusBar = wgtStatusBar(tbRoot);
WidgetT *statusBar = sStatusBar;
sStatus = wgtLabel(statusBar, "");
sStatus->weight = 100;
sStatus->weight = WGT_WEIGHT_FILL;
// Fit height to content, keeping full screen width
dvxFitWindowH(sAc, sWin);
@ -5211,7 +5214,7 @@ static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift)
(void)shift;
// Delete key
if (keyCode != (0x53 | 0x100) && keyCode != 127) {
if (keyCode != KEY_DELETE && keyCode != KEY_ASCII_DEL) {
return;
}
@ -5773,13 +5776,13 @@ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) {
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
// Ctrl+C: copy selected control
if (key == 3 && (mod & ACCEL_CTRL)) {
if (key == KEY_CTRL_C && (mod & ACCEL_CTRL)) {
dsgnCopySelected();
return;
}
// Ctrl+X: cut selected control
if (key == 24 && (mod & ACCEL_CTRL)) {
if (key == KEY_CTRL_X && (mod & ACCEL_CTRL)) {
dsgnCopySelected();
if (sDesigner.selectedIdx >= 0) {
@ -5796,7 +5799,7 @@ static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
}
// Ctrl+V: paste control
if (key == 22 && (mod & ACCEL_CTRL)) {
if (key == KEY_CTRL_V && (mod & ACCEL_CTRL)) {
dsgnPasteControl();
return;
}
@ -6552,7 +6555,7 @@ static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
}
// Delete key (scancode 0x53 with extended flag)
if (keyCode != (0x53 | 0x100) && keyCode != 127) {
if (keyCode != KEY_DELETE && keyCode != KEY_ASCII_DEL) {
return;
}
@ -6603,7 +6606,7 @@ static void openFindDialog(bool showReplace) {
findRow->spacing = wgtPixels(4);
wgtLabel(findRow, "Find:");
sFindInput = wgtTextInput(findRow, 256);
sFindInput->weight = 100;
sFindInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sFindInput, sFindText);
// Replace checkbox + input
@ -6613,7 +6616,7 @@ static void openFindDialog(bool showReplace) {
wgtCheckboxSetChecked(sReplCheck, showReplace);
sReplCheck->onChange = onReplCheckChange;
sReplInput = wgtTextInput(replRow, 256);
sReplInput->weight = 100;
sReplInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sReplInput, sReplaceText);
// Options row: scope + direction + case
@ -7799,7 +7802,7 @@ static void showBreakpointWindow(void) {
sBreakpointList = wgtListView(root);
if (sBreakpointList) {
sBreakpointList->weight = 100;
sBreakpointList->weight = WGT_WEIGHT_FILL;
sBreakpointList->onKeyDown = onBreakpointListKeyDown;
sBreakpointList->onDblClick = onBreakpointListDblClick;
wgtListViewSetMultiSelect(sBreakpointList, true);
@ -7846,7 +7849,7 @@ static void showCallStackWindow(void) {
sCallStackList = wgtListView(root);
if (sCallStackList) {
sCallStackList->weight = 100;
sCallStackList->weight = WGT_WEIGHT_FILL;
static const ListViewColT cols[] = {
{ "Procedure", wgtChars(16), ListViewAlignLeftE },
@ -7892,19 +7895,19 @@ static void showCodeWindow(void) {
wgtLabel(dropdownRow, "Object:");
sObjDropdown = wgtDropdown(dropdownRow);
sObjDropdown->weight = 100;
sObjDropdown->weight = WGT_WEIGHT_FILL;
sObjDropdown->onChange = onObjDropdownChange;
wgtDropdownSetItems(sObjDropdown, NULL, 0);
wgtLabel(dropdownRow, "Function:");
sEvtDropdown = wgtDropdown(dropdownRow);
sEvtDropdown->weight = 100;
sEvtDropdown->weight = WGT_WEIGHT_FILL;
sEvtDropdown->onChange = onEvtDropdownChange;
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
sEditor->weight = 100;
sEditor->weight = WGT_WEIGHT_FILL;
wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
// Apply saved syntax colors
@ -7959,7 +7962,7 @@ static void showImmediateWindow(void) {
sImmediate = wgtTextArea(immRoot, IDE_MAX_IMM);
if (sImmediate) {
sImmediate->weight = 100;
sImmediate->weight = WGT_WEIGHT_FILL;
sImmediate->readOnly = false;
sImmediate->onChange = onImmediateChange;
} else {
@ -7999,7 +8002,7 @@ static void showLocalsWindow(void) {
sLocalsList = wgtListView(root);
if (sLocalsList) {
sLocalsList->weight = 100;
sLocalsList->weight = WGT_WEIGHT_FILL;
static const ListViewColT cols[] = {
{ "Name", wgtChars(12), ListViewAlignLeftE },
@ -8034,7 +8037,7 @@ static void showOutputWindow(void) {
WidgetT *outRoot = wgtInitWindow(sAc, sOutWin);
sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT);
sOutput->weight = 100;
sOutput->weight = WGT_WEIGHT_FILL;
sOutput->readOnly = true;
if (sOutputLen > 0) {
@ -8061,7 +8064,7 @@ static void showPreferencesDialog(void) {
// ---- Tab control ----
WidgetT *tabs = wgtTabControl(root);
tabs->weight = 100;
tabs->weight = WGT_WEIGHT_FILL;
// ======== General tab ========
WidgetT *generalPage = wgtTabPage(tabs, "General");
@ -8093,14 +8096,14 @@ static void showPreferencesDialog(void) {
// Project Defaults section
WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults");
prjFrame->spacing = wgtPixels(2);
prjFrame->weight = 100;
prjFrame->weight = WGT_WEIGHT_FILL;
WidgetT *r1 = wgtHBox(prjFrame);
r1->spacing = wgtPixels(4);
WidgetT *l1 = wgtLabel(r1, "Author:");
l1->minW = wgtPixels(80);
sPrefsDlg.defAuthor = wgtTextInput(r1, 64);
sPrefsDlg.defAuthor->weight = 100;
sPrefsDlg.defAuthor->weight = WGT_WEIGHT_FILL;
wgtSetText(sPrefsDlg.defAuthor, prefsGetString(sPrefs, "defaults", "author", ""));
WidgetT *r2 = wgtHBox(prjFrame);
@ -8108,7 +8111,7 @@ static void showPreferencesDialog(void) {
WidgetT *l2 = wgtLabel(r2, "Publisher:");
l2->minW = wgtPixels(80);
sPrefsDlg.defPublisher = wgtTextInput(r2, 64);
sPrefsDlg.defPublisher->weight = 100;
sPrefsDlg.defPublisher->weight = WGT_WEIGHT_FILL;
wgtSetText(sPrefsDlg.defPublisher, prefsGetString(sPrefs, "defaults", "publisher", ""));
WidgetT *r3 = wgtHBox(prjFrame);
@ -8116,7 +8119,7 @@ static void showPreferencesDialog(void) {
WidgetT *l3 = wgtLabel(r3, "Version:");
l3->minW = wgtPixels(80);
sPrefsDlg.defVersion = wgtTextInput(r3, 16);
sPrefsDlg.defVersion->weight = 100;
sPrefsDlg.defVersion->weight = WGT_WEIGHT_FILL;
wgtSetText(sPrefsDlg.defVersion, prefsGetString(sPrefs, "defaults", "version", "1.0"));
WidgetT *r4 = wgtHBox(prjFrame);
@ -8124,12 +8127,12 @@ static void showPreferencesDialog(void) {
WidgetT *l4 = wgtLabel(r4, "Copyright:");
l4->minW = wgtPixels(80);
sPrefsDlg.defCopyright = wgtTextInput(r4, 64);
sPrefsDlg.defCopyright->weight = 100;
sPrefsDlg.defCopyright->weight = WGT_WEIGHT_FILL;
wgtSetText(sPrefsDlg.defCopyright, prefsGetString(sPrefs, "defaults", "copyright", ""));
wgtLabel(prjFrame, "Description:");
sPrefsDlg.defDescription = wgtTextArea(prjFrame, 512);
sPrefsDlg.defDescription->weight = 100;
sPrefsDlg.defDescription->weight = WGT_WEIGHT_FILL;
sPrefsDlg.defDescription->minH = wgtPixels(48);
wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", ""));
@ -8146,11 +8149,11 @@ static void showPreferencesDialog(void) {
WidgetT *colorsHBox = wgtHBox(colorsPage);
colorsHBox->spacing = wgtPixels(8);
colorsHBox->weight = 100;
colorsHBox->weight = WGT_WEIGHT_FILL;
// Left: color list
sPrefsDlg.colorList = wgtListBox(colorsHBox);
sPrefsDlg.colorList->weight = 100;
sPrefsDlg.colorList->weight = WGT_WEIGHT_FILL;
sPrefsDlg.colorList->onChange = onColorListChange;
wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, SYNTAX_COLOR_COUNT);
@ -8158,22 +8161,22 @@ static void showPreferencesDialog(void) {
// Right: RGB sliders + value labels + swatch preview
WidgetT *sliderBox = wgtVBox(colorsHBox);
sliderBox->spacing = wgtPixels(2);
sliderBox->weight = 100;
sliderBox->weight = WGT_WEIGHT_FILL;
wgtLabel(sliderBox, "Red:");
sPrefsDlg.sliderR = wgtSlider(sliderBox, 0, 255);
sPrefsDlg.sliderR = wgtSlider(sliderBox, 0, BAS_RGB_COMPONENT_MAX);
sPrefsDlg.sliderR->onChange = onColorSliderChange;
sPrefsDlg.lblR = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sPrefsDlg.lblR, AlignEndE);
wgtLabel(sliderBox, "Green:");
sPrefsDlg.sliderG = wgtSlider(sliderBox, 0, 255);
sPrefsDlg.sliderG = wgtSlider(sliderBox, 0, BAS_RGB_COMPONENT_MAX);
sPrefsDlg.sliderG->onChange = onColorSliderChange;
sPrefsDlg.lblG = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sPrefsDlg.lblG, AlignEndE);
wgtLabel(sliderBox, "Blue:");
sPrefsDlg.sliderB = wgtSlider(sliderBox, 0, 255);
sPrefsDlg.sliderB = wgtSlider(sliderBox, 0, BAS_RGB_COMPONENT_MAX);
sPrefsDlg.sliderB->onChange = onColorSliderChange;
sPrefsDlg.lblB = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sPrefsDlg.lblB, AlignEndE);
@ -8358,7 +8361,7 @@ static void showWatchWindow(void) {
sWatchList = wgtListView(root);
if (sWatchList) {
sWatchList->weight = 100;
sWatchList->weight = WGT_WEIGHT_FILL;
sWatchList->onKeyDown = onWatchListKeyDown;
sWatchList->onDblClick = onWatchListDblClick;

View file

@ -253,7 +253,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
WidgetT *capLbl = wgtLabel(capRow, "Caption:");
capLbl->minW = wgtPixels(60);
sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT);
sMed.captionInput->weight = 100;
sMed.captionInput->weight = WGT_WEIGHT_FILL;
sMed.captionInput->onChange = onCaptionChange;
// Name row
@ -262,7 +262,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
WidgetT *namLbl = wgtLabel(namRow, "Name:");
namLbl->minW = wgtPixels(60);
sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME);
sMed.nameInput->weight = 100;
sMed.nameInput->weight = WGT_WEIGHT_FILL;
sMed.nameInput->onChange = onNameChange;
// Check row
@ -275,7 +275,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// Listbox
sMed.listBox = wgtListBox(root);
sMed.listBox->weight = 100;
sMed.listBox->weight = WGT_WEIGHT_FILL;
sMed.listBox->onChange = onListClick;
// Arrow buttons

View file

@ -159,7 +159,7 @@ static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *va
lbl->minW = wgtPixels(PPD_LABEL_W);
WidgetT *input = wgtTextInput(row, maxLen);
input->weight = 100;
input->weight = WGT_WEIGHT_FILL;
wgtSetText(input, value);
return input;
@ -383,7 +383,7 @@ WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onCli
WidgetT *root = wgtInitWindow(ctx, sPrjWin);
sTree = wgtTreeView(root);
sTree->weight = 100;
sTree->weight = WGT_WEIGHT_FILL;
sTree->onChange = onTreeSelChanged;
prjRebuildTree(prj);
@ -658,7 +658,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
sfLbl->minW = wgtPixels(PPD_LABEL_W);
sPpd.startupForm = wgtDropdown(sfRow);
sPpd.startupForm->weight = 100;
sPpd.startupForm->weight = WGT_WEIGHT_FILL;
// Populate with form names from the project
sPpd.formNames = NULL;
@ -732,7 +732,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
hlpLbl->minW = wgtPixels(PPD_LABEL_W);
sPpd.helpFileInput = wgtTextInput(hlpRow, DVX_MAX_PATH);
sPpd.helpFileInput->weight = 100;
sPpd.helpFileInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sPpd.helpFileInput, prj->helpFile);
WidgetT *hlpBrowse = wgtButton(hlpRow, "Browse...");
@ -742,7 +742,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
// Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:");
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
sPpd.description->weight = 100;
sPpd.description->weight = WGT_WEIGHT_FILL;
sPpd.description->minH = wgtPixels(PPD_DESC_H);
wgtSetText(sPpd.description, prj->description);

View file

@ -1253,7 +1253,7 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
// Splitter: tree on top, property list on bottom
WidgetT *splitter = wgtSplitter(root, false);
splitter->weight = 100;
splitter->weight = WGT_WEIGHT_FILL;
wgtSplitterSetPos(splitter, (PRP_WIN_H - CHROME_TOTAL_TOP - CHROME_TOTAL_BOTTOM) / 2);
// Control tree (top pane)

View file

@ -31,7 +31,7 @@
.h1 DVX BASIC IDE Guide
DVX BASIC is a Visual BASIC development environment for the DVX GUI System. It provides a VB3-style integrated development environment with a code editor, form designer, project system, and a full interactive debugger -- all running natively on DOS under the DVX windowing system.
DVX BASIC is a Visual BASIC development environment for DVX. It provides a VB3-style integrated development environment with a code editor, form designer, project system, and a full interactive debugger -- all integrated into DVX itself.
This guide covers every feature of the IDE: menus, toolbar, editor, form designer, project management, debugger, and auxiliary windows.
@ -51,7 +51,7 @@ The DVX BASIC IDE is modeled after Visual Basic 3.0. It consists of several floa
.item Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging.
.endlist
The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine (VM). Programs execute in cooperative slices (10,000 VM steps per slice), yielding to the DVX event loop between slices so the GUI remains responsive.
The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so the GUI remains responsive. Call DoEvents in long-running loops to keep the event loop flowing.
.link ide.menu.file File Menu
.link ide.menu.edit Edit Menu
@ -109,6 +109,8 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
Save File Ctrl+S Save the active file to disk.
Save All Save all modified files in the project.
---
Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build.
---
Remove File Remove the selected file from the project (prompts to save if modified).
---
Exit Close the IDE (prompts to save unsaved changes).
@ -182,6 +184,8 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
Step Out Ctrl+Shift+F8 Run until the current SUB/FUNCTION returns.
Run to Cursor Ctrl+F8 Run until execution reaches the line where the cursor is positioned.
---
Output Window to Log Checkbox: when enabled, PRINT output is also mirrored to the DVX log. Persisted in preferences.
---
Toggle Breakpoint F9 Toggle a breakpoint on the current editor line.
---
Clear Output Clear the Output window.
@ -274,9 +278,12 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
.h1 Help Menu
.table
Menu Item Description
--------- -----------
About DVX BASIC... Show the About dialog with version and copyright information.
Menu Item Shortcut Description
--------- -------- -----------
DVX BASIC Help F1 Open the DVX BASIC help (language reference, IDE guide, and control reference).
DVX API Reference Open the DVX developer API reference.
---
About DVX BASIC... Show the About dialog with version and copyright information.
.endtable
.link ide.overview Back to Overview
@ -358,16 +365,17 @@ The editor shows one procedure at a time. Each procedure has its own buffer, and
.h2 Syntax Highlighting
The editor applies real-time syntax coloring as you type. The following categories are highlighted in distinct colors:
The editor applies real-time syntax coloring as you type. Seven categories are highlighted in distinct colors; the colors themselves are editable in Tools > Preferences > Colors.
.table
Category Examples
-------- --------
Default Text Identifiers, whitespace.
Keywords IF, THEN, FOR, NEXT, SUB, FUNCTION, DIM, PRINT, SELECT, CASE, DO, LOOP, WHILE, WEND, END, EXIT, CALL, GOSUB, GOTO, RETURN, DECLARE, CONST, TYPE, AND, OR, NOT, XOR, MOD, etc.
Type names INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, BYTE, TRUE, FALSE
String literals "Hello, World!"
Types INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, TRUE, FALSE
Strings "Hello, World!"
Comments ' This is a comment, REM This is a comment
Numbers 42, 3.14
Numbers 42, 3.14, &HFF
Operators =, <, >, +, -, *, /, \, &
.endtable
@ -400,7 +408,7 @@ The Form Designer provides a visual design surface for editing .frm files. Switc
.h2 Design Surface
.list
.item Grid snapping -- controls snap to an 8-pixel grid (DSGN_GRID_SIZE) when placed or resized.
.item Grid snapping -- controls snap to an 8-pixel grid when placed or resized.
.item Selection -- click a control to select it. The selected control is highlighted with grab handles.
.item Grab handles -- 6x6 pixel handles appear on the right edge (E), bottom edge (S), and bottom-right corner (SE) of the selected control. Drag a handle to resize the control.
.item Reordering -- drag a control vertically to reorder it within the form's layout (VBox/HBox).
@ -436,12 +444,12 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi
.item Name -- the project display name (up to 32 characters).
.item Startup Form -- which form to show automatically when the program starts.
.item Metadata -- Author, Company, Version, Copyright, Description, and Icon Path (for compiled binaries).
.item File list -- relative paths (8.3 DOS names) of all .bas and .frm files in the project. Each entry tracks whether it is a form file.
.item File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.
.endlist
.h2 Source Map
.h2 How Projects are Compiled
When the project is compiled, all files are concatenated into a single source stream. A source map tracks which lines belong to which file, enabling accurate error reporting and debugger navigation across multiple files. For .frm files, an injected BEGINFORM directive is prepended to the code section.
When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.
.h2 Project Operations
@ -494,7 +502,7 @@ Each control type exposes different properties (e.g., Caption, Text, Width, Heig
.h1 Toolbox
The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Controls are loaded dynamically from the widget plugin registry -- any widget DXE that provides a basName appears in the toolbox.
The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox.
Click a tool to select it (the active tool name is stored in the designer state), then click on the form to place a new instance. Click the same tool again to deselect it and return to pointer mode.
@ -547,20 +555,17 @@ Click a tool to select it (the active tool name is stored in the designer state)
.index Breakpoints
.index Stepping
.index Debug Mode
.index DBG_IDLE
.index DBG_RUNNING
.index DBG_PAUSED
.h1 Debugger
The DVX BASIC IDE includes a full interactive debugger. The debugger operates as a state machine with three states:
The DVX BASIC IDE includes a full interactive debugger. The debugger has three states:
.table
State Description
----- -----------
DBG_IDLE No program loaded or running.
DBG_RUNNING Program is executing (VM running in slices).
DBG_PAUSED Execution is paused at a breakpoint or step point. The IDE GUI is fully interactive.
State Description
----- -----------
Idle No program loaded or running.
Running The program is executing.
Paused Execution is paused at a breakpoint or step point. The IDE is fully interactive -- you can inspect and change variables, set or clear breakpoints, step, continue, or stop.
.endtable
.h2 Starting a Debug Session
@ -593,7 +598,7 @@ Not every line can have a breakpoint. The IDE validates the line content and sil
.h3 Breakpoint Storage
Each breakpoint records the project file index, the code line number within the file, the procedure index, and the procedure name (as Object.Event). When the project is compiled, breakpoints are converted to concatenated source line numbers that match the VM's OP_LINE opcodes.
Each breakpoint records the file it belongs to, the line number within that file, and the procedure name (Object.Event). Breakpoints are stored in the project and survive compilation.
.h3 Visual Indicators
@ -617,20 +622,20 @@ When lines are added or removed in the editor, breakpoints below the edit point
Run to Cursor Ctrl+F8 Run until execution reaches the line under the cursor.
.endtable
.h2 The Debug Run Loop
.h2 Debug Run Behavior
When a program is running in debug mode, the IDE enters a cooperative loop:
When a program is running under the debugger:
.list
.item The VM executes up to 10,000 steps per slice.
.item If the VM hits a breakpoint (BAS_VM_BREAKPOINT), the state transitions to DBG_PAUSED. The IDE navigates the code editor to the breakpoint line, highlights it in yellow, auto-opens the Locals and Call Stack windows, and updates all debug windows.
.item While paused, the IDE pumps dvxUpdate() continuously, keeping the GUI responsive. The user can inspect variables, modify them in the Immediate window, step, continue, or stop.
.item When the user resumes (F5/Shift+F5/F8/etc.), the state transitions back to DBG_RUNNING and the loop continues.
.item The program runs in short cooperative slices so the IDE remains responsive.
.item When the program hits a breakpoint, execution pauses immediately. The IDE switches to Code View, navigates to the breakpoint line, highlights it in yellow, and opens the Locals and Call Stack windows if they are not already visible.
.item While paused, you can inspect variables in Locals and Watch, evaluate expressions in the Immediate window, assign new values to variables, toggle breakpoints, step, continue, or stop.
.item Resuming (F5, Shift+F5, or any Step command) returns the program to Running state.
.endlist
.h2 Stopping
Press Esc or click the Stop toolbar button at any time to halt execution. The VM is destroyed, debug state resets to DBG_IDLE, and the IDE restores the designer windows that were hidden at run start.
Press Esc or click the Stop toolbar button at any time to halt execution. The program is terminated, the debugger returns to Idle, and the IDE restores any designer or code windows that were hidden at the start of the run.
.link ide.debug.locals Locals Window
.link ide.debug.callstack Call Stack Window
@ -790,7 +795,7 @@ Parse or runtime errors are displayed inline with an Error: prefix.
.h2 Inspecting Variables While Paused
When the debugger is paused at a breakpoint, the Immediate window has access to the running VM's state. Global variable values are copied into the evaluation VM, so expressions like count or name$ & " test" display live values.
When the debugger is paused at a breakpoint, the Immediate window has access to the running program's state. Expressions like count or name$ & " test" display live values.
.h2 Assigning Variables While Paused
@ -815,7 +820,7 @@ Assignment works for:
.item Combined -- items(0).price = 9.99
.endlist
The new value is written directly into the VM's live variable slot (local, global, or form scope). A confirmation message is displayed, and the Locals and Watch windows update automatically to reflect the change.
The new value is written directly into the running program's variable (local, global, or form scope). A confirmation message is displayed, and the Locals and Watch windows update automatically to reflect the change.
If the assignment target cannot be resolved (unknown variable, out-of-bounds index, wrong type), an error message is displayed.
@ -896,33 +901,39 @@ F3 repeats the last search (Find Next) without opening the dialog.
.h1 Preferences
Open via Tools > Preferences. Settings are saved to dvxbasic.ini in the app's config directory.
Open via Tools > Preferences. Settings are saved to dvxbasic.ini in the app's config directory. The dialog has two tabs.
.h2 Editor Section
.h2 General Tab
.h3 Editor Section
.table
Setting Description Default
------- ----------- -------
Skip comments/strings when renaming When renaming a control or form, skip occurrences inside comments and string literals. On
Require variable declaration (OPTION EXPLICIT) When enabled, variables must be declared with DIM before use. Off
Tab width Number of spaces per tab stop (1-8). 3
Insert spaces instead of tabs When enabled, pressing Tab inserts spaces. When disabled, inserts a real tab character. On
Setting Description Default
------- ----------- -------
Skip comments/strings when renaming When renaming a control or form, skip occurrences inside comments and string literals. On
Require variable declaration (OPTION EXPLICIT) When enabled, newly created projects have OPTION EXPLICIT on by default. Off
Tab width Number of spaces per tab stop. 3
Insert spaces instead of tabs When enabled, pressing Tab inserts spaces. When disabled, inserts a real tab character. On
.endtable
.h2 New Project Defaults Section
.h3 New Project Defaults Section
These fields set the default values for new project metadata:
.table
Field Description Default
----- ----------- -------
Author Default author name. (empty)
Company Default company name. (empty)
Version Default version string. 1.0
Copyright Default copyright notice. (empty)
Description Default project description (multi-line). (empty)
Field Description Default
----- ----------- -------
Author Default author name. (empty)
Publisher Default publisher/company name. (empty)
Version Default version string. 1.0
Copyright Default copyright notice. (empty)
Description Default project description (multi-line). (empty)
.endtable
.h2 Colors Tab
Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry and adjust its red, green, and blue components using the three sliders on the right. The Preview swatch shows the current color. Changes are applied to the open editor when you click OK.
.link ide.overview Back to Overview
.topic ide.shortcuts
@ -946,6 +957,7 @@ These fields set the default values for new project metadata:
Ctrl+F Find
Ctrl+H Replace
Ctrl+E Menu Editor
F1 DVX BASIC Help
F3 Find Next
F5 Run
Shift+F5 Debug

File diff suppressed because it is too large Load diff

View file

@ -68,19 +68,11 @@ void basUdtFree(BasUdtT *udt);
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
BasUdtT *basUdtRef(BasUdtT *udt);
void basUdtUnref(BasUdtT *udt);
BasValueT basValBool(bool v);
int32_t basValCompare(BasValueT a, BasValueT b);
int32_t basValCompareCI(BasValueT a, BasValueT b);
BasValueT basValCopy(BasValueT v);
BasValueT basValDouble(double v);
BasStringT *basValFormatString(BasValueT v);
BasValueT basValInteger(int16_t v);
bool basValIsTruthy(BasValueT v);
BasValueT basValLong(int32_t v);
BasValueT basValObject(void *obj);
uint8_t basValPromoteType(uint8_t a, uint8_t b);
void basValRelease(BasValueT *v);
BasValueT basValSingle(float v);
BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text);
BasValueT basValToBool(BasValueT v);
@ -407,16 +399,8 @@ void basUdtUnref(BasUdtT *udt) {
// ============================================================
// Value constructors
// Value constructors (trivial ones moved to values.h as static inline)
// ============================================================
BasValueT basValBool(bool v) {
BasValueT val;
val.type = BAS_TYPE_BOOLEAN;
val.boolVal = v ? -1 : 0;
return val;
}
int32_t basValCompare(BasValueT a, BasValueT b) {
// String comparison
if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
@ -461,27 +445,6 @@ int32_t basValCompareCI(BasValueT a, BasValueT b) {
}
BasValueT basValCopy(BasValueT v) {
if (v.type == BAS_TYPE_STRING && v.strVal) {
basStringRef(v.strVal);
} else if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
basArrayRef(v.arrVal);
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
basUdtRef(v.udtVal);
}
return v;
}
BasValueT basValDouble(double v) {
BasValueT val;
val.type = BAS_TYPE_DOUBLE;
val.dblVal = v;
return val;
}
BasStringT *basValFormatString(BasValueT v) {
char buf[64];
@ -515,14 +478,6 @@ BasStringT *basValFormatString(BasValueT v) {
}
BasValueT basValInteger(int16_t v) {
BasValueT val;
val.type = BAS_TYPE_INTEGER;
val.intVal = v;
return val;
}
bool basValIsTruthy(BasValueT v) {
switch (v.type) {
case BAS_TYPE_INTEGER:
@ -549,22 +504,6 @@ bool basValIsTruthy(BasValueT v) {
}
BasValueT basValLong(int32_t v) {
BasValueT val;
val.type = BAS_TYPE_LONG;
val.longVal = v;
return val;
}
BasValueT basValObject(void *obj) {
BasValueT val;
val.type = BAS_TYPE_OBJECT;
val.objVal = obj;
return val;
}
uint8_t basValPromoteType(uint8_t a, uint8_t b) {
// String stays string (concat, not arithmetic)
if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) {
@ -590,28 +529,6 @@ uint8_t basValPromoteType(uint8_t a, uint8_t b) {
}
void basValRelease(BasValueT *v) {
if (v->type == BAS_TYPE_STRING) {
basStringUnref(v->strVal);
v->strVal = NULL;
} else if (v->type == BAS_TYPE_ARRAY) {
basArrayUnref(v->arrVal);
v->arrVal = NULL;
} else if (v->type == BAS_TYPE_UDT) {
basUdtUnref(v->udtVal);
v->udtVal = NULL;
}
}
BasValueT basValSingle(float v) {
BasValueT val;
val.type = BAS_TYPE_SINGLE;
val.sngVal = v;
return val;
}
BasValueT basValString(BasStringT *s) {
BasValueT val;
val.type = BAS_TYPE_STRING;

View file

@ -31,6 +31,8 @@
#ifndef DVXBASIC_VALUES_H
#define DVXBASIC_VALUES_H
#include "../compiler/opcodes.h" // BAS_TYPE_*
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
@ -157,21 +159,89 @@ struct BasValueTag {
};
};
// Create values
BasValueT basValInteger(int16_t v);
BasValueT basValLong(int32_t v);
BasValueT basValSingle(float v);
BasValueT basValDouble(double v);
// Create values -- trivial constructors inlined so they're fast in vm.c's
// hot path (PUSH_INT16, PUSH_TRUE, etc.).
static inline BasValueT basValInteger(int16_t v) {
BasValueT val;
val.type = BAS_TYPE_INTEGER;
val.intVal = v;
return val;
}
static inline BasValueT basValLong(int32_t v) {
BasValueT val;
val.type = BAS_TYPE_LONG;
val.longVal = v;
return val;
}
static inline BasValueT basValSingle(float v) {
BasValueT val;
val.type = BAS_TYPE_SINGLE;
val.sngVal = v;
return val;
}
static inline BasValueT basValDouble(double v) {
BasValueT val;
val.type = BAS_TYPE_DOUBLE;
val.dblVal = v;
return val;
}
static inline BasValueT basValBool(bool v) {
BasValueT val;
val.type = BAS_TYPE_BOOLEAN;
val.boolVal = v ? -1 : 0;
return val;
}
static inline BasValueT basValObject(void *obj) {
BasValueT val;
val.type = BAS_TYPE_OBJECT;
val.objVal = obj;
return val;
}
BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text);
BasValueT basValBool(bool v);
BasValueT basValObject(void *obj);
// Copy a value (increments string refcount if applicable).
BasValueT basValCopy(BasValueT v);
// Copy a value (increments string/array/udt refcount if applicable).
// Inlined so the hot-path case (integer/float/bool) is a single struct
// return -- no function call, no branch beyond the type test.
static inline BasValueT basValCopy(BasValueT v) {
if (v.type == BAS_TYPE_STRING && v.strVal) {
basStringRef(v.strVal);
} else if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
basArrayRef(v.arrVal);
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
basUdtRef(v.udtVal);
}
// Release a value (decrements string refcount if applicable).
void basValRelease(BasValueT *v);
return v;
}
// Release a value (decrements refcount if applicable). Integer/float/bool
// types are a no-op -- the common case is an immediately-returning branch.
static inline void basValRelease(BasValueT *v) {
if (v->type == BAS_TYPE_STRING) {
basStringUnref(v->strVal);
v->strVal = NULL;
} else if (v->type == BAS_TYPE_ARRAY) {
basArrayUnref(v->arrVal);
v->arrVal = NULL;
} else if (v->type == BAS_TYPE_UDT) {
basUdtUnref(v->udtVal);
v->udtVal = NULL;
}
}
// Convert a value to a specific type. Returns the converted value.
// The original is NOT released -- caller manages lifetime.

View file

@ -963,7 +963,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Get pointer to the loop variable
BasValueT *varSlot;
if (scopeTag == 1) {
if (scopeTag == SCOPE_LOCAL) {
BasCallFrameT *frame = currentFrame(vm);
if (!frame || varIdx >= (uint16_t)frame->localCount) {
@ -972,7 +972,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
varSlot = &frame->locals[varIdx];
} else if (scopeTag == 2) {
} else if (scopeTag == SCOPE_FORM) {
if (!vm->currentFormVars || varIdx >= (uint16_t)vm->currentFormVarCount) {
runtimeError(vm, 9, "Invalid form variable index");
return BAS_VM_ERROR;
@ -1164,8 +1164,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_PRINT_SPC: {
uint8_t n = readUint8(vm);
char spaces[256];
int32_t count = n < 255 ? n : 255;
char spaces[BAS_PRINT_SPC_MAX + 1];
int32_t count = n;
memset(spaces, ' ', count);
spaces[count] = '\0';
@ -1190,11 +1190,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
count = 0;
}
if (count > 255) {
count = 255;
if (count > BAS_PRINT_SPC_MAX) {
count = BAS_PRINT_SPC_MAX;
}
char spaces[256];
char spaces[BAS_PRINT_SPC_MAX + 1];
memset(spaces, ' ', count);
spaces[count] = '\0';
@ -1219,13 +1219,13 @@ BasVmResultE basVmStep(BasVmT *vm) {
col = 1;
}
if (col > 255) {
col = 255;
if (col > BAS_PRINT_SPC_MAX) {
col = BAS_PRINT_SPC_MAX;
}
// TAB outputs spaces to reach the specified column
// For simplicity, just output (col-1) spaces
char spaces[256];
char spaces[BAS_PRINT_SPC_MAX + 1];
int32_t count = col - 1;
memset(spaces, ' ', count);
spaces[count] = '\0';
@ -1543,12 +1543,12 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&vg);
basValRelease(&vb);
if (r < 0) { r = 0; }
if (r > 255) { r = 255; }
if (g < 0) { g = 0; }
if (g > 255) { g = 255; }
if (b < 0) { b = 0; }
if (b > 255) { b = 255; }
if (r < 0) { r = 0; }
if (r > BAS_RGB_COMPONENT_MAX) { r = BAS_RGB_COMPONENT_MAX; }
if (g < 0) { g = 0; }
if (g > BAS_RGB_COMPONENT_MAX) { g = BAS_RGB_COMPONENT_MAX; }
if (b < 0) { b = 0; }
if (b > BAS_RGB_COMPONENT_MAX) { b = BAS_RGB_COMPONENT_MAX; }
if (!push(vm, basValLong((r << 16) | (g << 8) | b))) {
return BAS_VM_STACK_OVERFLOW;
@ -1665,8 +1665,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
count = 0;
}
if (count > 32767) {
count = 32767;
if (count > INT16_MAX) {
count = INT16_MAX;
}
BasStringT *s = basStringAlloc(count + 1);
@ -3162,8 +3162,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Pop args to keep stack balanced
for (int32_t i = 0; i < argc; i++) {
BasValueT tmp;
pop(vm, &tmp);
basValRelease(&tmp);
if (pop(vm, &tmp)) {
basValRelease(&tmp);
}
}
char msg[256];
@ -3245,10 +3246,14 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_BREAKPOINT;
}
// Breakpoint check (linear scan, typically < 20 entries)
for (int32_t i = 0; i < vm->breakpointCount; i++) {
if (vm->breakpoints[i] == (int32_t)lineNum) {
return BAS_VM_BREAKPOINT;
// Breakpoint check (linear scan, typically < 20 entries).
// Gated on non-zero count so release/no-breakpoint programs pay
// no per-line overhead at all.
if (vm->breakpointCount > 0) {
for (int32_t i = 0; i < vm->breakpointCount; i++) {
if (vm->breakpoints[i] == (int32_t)lineNum) {
return BAS_VM_BREAKPOINT;
}
}
}
@ -3556,6 +3561,68 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
return BAS_VM_OK;
}
// Integer-integer fast path -- avoids double conversion and FPU for
// tight integer loops. Only promotes on overflow/division range.
if (op != OP_POW
&& op != OP_DIV_FLT
&& (a.type == BAS_TYPE_INTEGER || a.type == BAS_TYPE_LONG)
&& (b.type == BAS_TYPE_INTEGER || b.type == BAS_TYPE_LONG)) {
int32_t ia = (a.type == BAS_TYPE_INTEGER) ? (int32_t)a.intVal : a.longVal;
int32_t ib = (b.type == BAS_TYPE_INTEGER) ? (int32_t)b.intVal : b.longVal;
int32_t ir = 0;
bool handled = true;
switch (op) {
case OP_ADD_INT:
case OP_ADD_FLT: {
int64_t t = (int64_t)ia + (int64_t)ib;
if (t < INT32_MIN || t > INT32_MAX) { handled = false; break; }
ir = (int32_t)t;
break;
}
case OP_SUB_INT:
case OP_SUB_FLT: {
int64_t t = (int64_t)ia - (int64_t)ib;
if (t < INT32_MIN || t > INT32_MAX) { handled = false; break; }
ir = (int32_t)t;
break;
}
case OP_MUL_INT:
case OP_MUL_FLT: {
int64_t t = (int64_t)ia * (int64_t)ib;
if (t < INT32_MIN || t > INT32_MAX) { handled = false; break; }
ir = (int32_t)t;
break;
}
case OP_IDIV_INT:
if (ib == 0) {
runtimeError(vm, 11, "Division by zero");
return BAS_VM_DIV_BY_ZERO;
}
ir = ia / ib;
break;
case OP_MOD_INT:
if (ib == 0) {
runtimeError(vm, 11, "Division by zero");
return BAS_VM_DIV_BY_ZERO;
}
ir = ia % ib;
break;
default:
handled = false;
break;
}
if (handled) {
if (ir >= INT16_MIN && ir <= INT16_MAX) {
push(vm, basValInteger((int16_t)ir));
} else {
push(vm, basValLong(ir));
}
return BAS_VM_OK;
}
}
double na = basValToNumber(a);
double nb = basValToNumber(b);
basValRelease(&a);
@ -3617,9 +3684,9 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
// Return appropriate type
if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) {
if (result >= -32768.0 && result <= 32767.0) {
if (result >= (double)INT16_MIN && result <= (double)INT16_MAX) {
push(vm, basValInteger((int16_t)result));
} else if (result >= -2147483648.0 && result <= 2147483647.0) {
} else if (result >= (double)INT32_MIN && result <= (double)INT32_MAX) {
push(vm, basValLong((int32_t)result));
} else {
push(vm, basValDouble(result));
@ -3640,7 +3707,17 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
return BAS_VM_STACK_UNDERFLOW;
}
int32_t cmp = vm->compareTextMode ? basValCompareCI(a, b) : basValCompare(a, b);
int32_t cmp;
// Same-type integer fast path: loop conditions like i < n hit this on
// every iteration. Skip basValCompare's mixed-type handling.
if (a.type == BAS_TYPE_INTEGER && b.type == BAS_TYPE_INTEGER) {
cmp = (a.intVal < b.intVal) ? -1 : (a.intVal > b.intVal ? 1 : 0);
} else if (a.type == BAS_TYPE_LONG && b.type == BAS_TYPE_LONG) {
cmp = (a.longVal < b.longVal) ? -1 : (a.longVal > b.longVal ? 1 : 0);
} else {
cmp = vm->compareTextMode ? basValCompareCI(a, b) : basValCompare(a, b);
}
basValRelease(&a);
basValRelease(&b);
@ -3661,13 +3738,6 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
}
// File mode constants (matches compiler/parser.c emission)
#define FILE_MODE_INPUT 1
#define FILE_MODE_OUTPUT 2
#define FILE_MODE_APPEND 3
#define FILE_MODE_RANDOM 4
#define FILE_MODE_BINARY 5
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_FILE_OPEN: {
@ -3695,23 +3765,23 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
if (vm->files[channel].handle) {
fclose((FILE *)vm->files[channel].handle);
vm->files[channel].handle = NULL;
vm->files[channel].mode = 0;
vm->files[channel].mode = BAS_FILE_MODE_CLOSED;
}
const char *modeStr;
switch (mode) {
case FILE_MODE_INPUT:
case BAS_FILE_MODE_INPUT:
modeStr = "rb";
break;
case FILE_MODE_OUTPUT:
case BAS_FILE_MODE_OUTPUT:
modeStr = "wb";
break;
case FILE_MODE_APPEND:
case BAS_FILE_MODE_APPEND:
modeStr = "ab";
break;
case FILE_MODE_RANDOM:
case FILE_MODE_BINARY:
case BAS_FILE_MODE_RANDOM:
case BAS_FILE_MODE_BINARY:
modeStr = "r+b";
break;
default:
@ -3721,7 +3791,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
}
// For RANDOM/BINARY: create file if it doesn't exist, then reopen r+b
if (mode == FILE_MODE_RANDOM || mode == FILE_MODE_BINARY) {
if (mode == BAS_FILE_MODE_RANDOM || mode == BAS_FILE_MODE_BINARY) {
FILE *test = fopen(fnStr.strVal->data, "r");
if (!test) {
// Create the file
@ -3765,7 +3835,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
if (vm->files[channel].handle) {
fclose((FILE *)vm->files[channel].handle);
vm->files[channel].handle = NULL;
vm->files[channel].mode = 0;
vm->files[channel].mode = BAS_FILE_MODE_CLOSED;
}
break;
@ -4268,8 +4338,8 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
n = 0;
}
if (n > 32767) {
n = 32767;
if (n > INT16_MAX) {
n = INT16_MAX;
}
char *buf = (char *)malloc(n + 1);
@ -5123,8 +5193,8 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
n = 0;
}
if (n > 32767) {
n = 32767;
if (n > INT16_MAX) {
n = INT16_MAX;
}
BasStringT *s = basStringAlloc(n + 1);
@ -5263,23 +5333,23 @@ static bool push(BasVmT *vm, BasValueT val) {
}
static int16_t readInt16(BasVmT *vm) {
int16_t val;
memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t));
// x86 tolerates unaligned loads at cost of ~1 cycle; cast-through-pointer is
// faster than memcpy because the compiler can emit a single 16-bit load.
static inline int16_t readInt16(BasVmT *vm) {
int16_t val = *(const int16_t *)&vm->module->code[vm->pc];
vm->pc += sizeof(int16_t);
return val;
}
static uint16_t readUint16(BasVmT *vm) {
uint16_t val;
memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t));
static inline uint16_t readUint16(BasVmT *vm) {
uint16_t val = *(const uint16_t *)&vm->module->code[vm->pc];
vm->pc += sizeof(uint16_t);
return val;
}
static uint8_t readUint8(BasVmT *vm) {
static inline uint8_t readUint8(BasVmT *vm) {
return vm->module->code[vm->pc++];
}

View file

@ -55,6 +55,10 @@
#define BAS_VM_MAX_FILES 16 // open file channels
#define BAS_VM_DEFAULT_STEP_SLICE 10000 // bytecode steps per yield
#define BAS_RGB_COMPONENT_MAX 255 // RGB(r,g,b): max per-channel value
#define BAS_PRINT_SPC_MAX 255 // PRINT SPC()/TAB(): max column count
#define BAS_VM_ERROR_MSG_LEN 256 // runtime error description buffer
// ============================================================
// Result codes
// ============================================================
@ -267,7 +271,7 @@ typedef struct {
typedef struct {
int32_t varIdx; // loop variable slot index
uint8_t scopeTag; // 0 = global, 1 = local, 2 = form
uint8_t scopeTag; // BasScopeE value
BasValueT limit; // upper bound
BasValueT step; // step value
int32_t loopTop; // PC of the loop body start
@ -279,7 +283,7 @@ typedef struct {
typedef struct {
void *handle; // FILE* or platform-specific
int32_t mode; // 0=closed, 1=input, 2=output, 3=append, 4=random, 5=binary
int32_t mode; // BasFileModeE value
} BasFileChannelT;
// ============================================================
@ -411,7 +415,7 @@ typedef struct {
int32_t errorPc; // PC of the instruction that caused the error (for RESUME)
int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT)
bool inErrorHandler; // true when executing error handler code
char errorMsg[256]; // current error description
char errorMsg[BAS_VM_ERROR_MSG_LEN]; // current error description
// I/O callbacks
BasPrintFnT printFn;

View file

@ -384,9 +384,8 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
static const char *lines[] = {
"DVX GUI Compositor",
"",
"A DOS Visual eXecutive windowed GUI",
"compositor for DOS, targeting",
"DJGPP/DPMI.",
"A DOS Visual eXecutive windowed",
"GUI compositor for DOS.",
"",
"Features:",
" - VESA VBE 2.0+ LFB",
@ -508,7 +507,7 @@ static void setupControlsWindow(void) {
wgtLabel(page1, "&Progress:");
WidgetT *pbRow = wgtHBox(page1);
WidgetT *pb = wgtProgressBar(pbRow);
pb->weight = 100;
pb->weight = WGT_WEIGHT_FILL;
wgtProgressBarSetValue(pb, 65);
wgtSetTooltip(pb, "Task progress: 65%");
WidgetT *pbV = wgtProgressBarV(pbRow);
@ -577,13 +576,13 @@ static void setupControlsWindow(void) {
wgtListViewSetSelected(lv, 0);
wgtListViewSetMultiSelect(lv, true);
wgtListViewSetReorderable(lv, true);
lv->weight = 100;
lv->weight = WGT_WEIGHT_FILL;
// --- Tab 4: ScrollPane ---
WidgetT *page4sp = wgtTabPage(tabs, "&Scroll");
WidgetT *sp = wgtScrollPane(page4sp);
sp->weight = 100;
sp->weight = WGT_WEIGHT_FILL;
sp->padding = wgtPixels(4);
sp->spacing = wgtPixels(4);
@ -671,7 +670,7 @@ static void setupControlsWindow(void) {
wgtLabel(page7e, "TextArea:");
WidgetT *ta = wgtTextArea(page7e, 512);
ta->weight = 100;
ta->weight = WGT_WEIGHT_FILL;
wgtSetText(ta, "Multi-line text editor.\n\nFeatures:\n- Word wrap\n- Selection\n- Copy/Paste\n- Undo (Ctrl+Z)");
wgtHSeparator(page7e);
@ -695,7 +694,7 @@ static void setupControlsWindow(void) {
WidgetT *page8s = wgtTabPage(tabs, "S&plit");
WidgetT *hSplit = wgtSplitter(page8s, false);
hSplit->weight = 100;
hSplit->weight = WGT_WEIGHT_FILL;
wgtSplitterSetPos(hSplit, 120);
// Top pane: vertical splitter (tree | list)
@ -735,11 +734,11 @@ static void setupControlsWindow(void) {
// Tests that wgtSetEnabled(w, false) correctly greys out each widget type.
WidgetT *page9d = wgtTabPage(tabs, "&Disabled");
WidgetT *disRow = wgtHBox(page9d);
disRow->weight = 100;
disRow->weight = WGT_WEIGHT_FILL;
// Enabled column
WidgetT *enCol = wgtVBox(disRow);
enCol->weight = 100;
enCol->weight = WGT_WEIGHT_FILL;
wgtLabel(enCol, "Enabled:");
wgtHSeparator(enCol);
wgtLabel(enCol, "A &label");
@ -773,7 +772,7 @@ static void setupControlsWindow(void) {
// Disabled column
WidgetT *disCol = wgtVBox(disRow);
disCol->weight = 100;
disCol->weight = WGT_WEIGHT_FILL;
wgtLabel(disCol, "Disabled:");
wgtHSeparator(disCol);
@ -818,7 +817,7 @@ static void setupControlsWindow(void) {
// Status bar at bottom (outside tabs)
WidgetT *sb = wgtStatusBar(root);
WidgetT *sbLabel = wgtLabel(sb, "Ready");
sbLabel->weight = 100;
sbLabel->weight = WGT_WEIGHT_FILL;
wgtSetName(sbLabel, "advStatus");
wgtLabel(sb, "Line 1, Col 1");
}
@ -977,7 +976,7 @@ static void setupTerminalWindow(void) {
WidgetT *root = wgtInitWindow(sAc, win);
WidgetT *term = wgtAnsiTerm(root, 80, 25);
term->weight = 100;
term->weight = WGT_WEIGHT_FILL;
wgtAnsiTermSetScrollback(term, 500);
// Feed some ANSI content to demonstrate the terminal
@ -1100,7 +1099,7 @@ static void setupWidgetDemo(void) {
WidgetT *lb = wgtListBox(listRow);
wgtListBoxSetItems(lb, listItems, 5);
wgtListBoxSetReorderable(lb, true);
lb->weight = 100;
lb->weight = WGT_WEIGHT_FILL;
// Context menu on the list box
MenuT *lbCtx = wmCreateMenu();
@ -1116,7 +1115,7 @@ static void setupWidgetDemo(void) {
WidgetT *mlb = wgtListBox(listRow);
wgtListBoxSetMultiSelect(mlb, true);
wgtListBoxSetItems(mlb, multiItems, 7);
mlb->weight = 100;
mlb->weight = WGT_WEIGHT_FILL;
wgtHSeparator(root);

View file

@ -1570,7 +1570,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Splitter with TOC tree and content scroll pane
WidgetT *splitter = wgtSplitter(root, true);
splitter->weight = 100;
splitter->weight = WGT_WEIGHT_FILL;
sTocTree = wgtTreeView(splitter);
sTocTree->weight = 0;
@ -1578,7 +1578,7 @@ int32_t appMain(DxeAppContextT *ctx) {
sTocTree->onChange = onTocChange;
sContentScroll = wgtScrollPane(splitter);
sContentScroll->weight = 100;
sContentScroll->weight = WGT_WEIGHT_FILL;
sContentBox = wgtVBox(sContentScroll);
sContentBox->spacing = wgtPixels(HELP_PARA_SPACING);

View file

@ -40,7 +40,7 @@ DVX features include:
.list
.item A window manager with drag, resize, and minimize support
.item Over 25 built-in widget types including buttons, lists, and trees
.item Dynamic application loading via DXE3 modules
.item Dynamic application loading at runtime
.item Theme support with customizable color schemes
.item VESA VBE 2.0+ linear framebuffer rendering
.endlist
@ -106,7 +106,7 @@ and the public API used by applications.
.h1 Widget System
DVX provides a rich set of widgets (controls) for building
application user interfaces. Widgets are loaded as DXE3 plugin
application user interfaces. Widgets are loaded as plugin
modules at startup.
.h2 Layout Widgets

View file

@ -220,7 +220,7 @@ static void buildPmWindow(void) {
// App button grid in a labeled frame. weight=100 tells the layout engine
// this frame should consume all available vertical space (flex weight).
WidgetT *appFrame = wgtFrame(root, "Applications");
appFrame->weight = 100;
appFrame->weight = WGT_WEIGHT_FILL;
if (sAppCount == 0) {
wgtLabel(appFrame, "(No applications found in apps/ directory)");
@ -229,11 +229,11 @@ static void buildPmWindow(void) {
// WrapBox inside flows cells left-to-right, wrapping to
// the next row when the window width is exceeded.
WidgetT *scroll = wgtScrollPane(appFrame);
scroll->weight = 100;
scroll->weight = WGT_WEIGHT_FILL;
wgtScrollPaneSetNoBorder(scroll, true);
WidgetT *wrap = wgtWrapBox(scroll);
wrap->weight = 100;
wrap->weight = WGT_WEIGHT_FILL;
wrap->spacing = wgtPixels(PM_GRID_SPACING);
wrap->align = AlignCenterE;
@ -280,7 +280,7 @@ static void buildPmWindow(void) {
// width so text can be left-aligned naturally.
WidgetT *statusBar = wgtStatusBar(root);
sStatusLabel = wgtLabel(statusBar, "");
sStatusLabel->weight = 100;
sStatusLabel->weight = WGT_WEIGHT_FILL;
updateStatusText();
}
@ -563,7 +563,7 @@ static void showSystemInfo(void) {
WidgetT *root = wgtInitWindow(sAc, win);
WidgetT *ta = wgtTextArea(root, 4096);
ta->weight = 100;
ta->weight = WGT_WEIGHT_FILL;
wgtSetText(ta, info);
// Don't disable -- wgtSetEnabled(false) blocks all input including scrollbar

View file

@ -1,7 +1,7 @@
VERSION DVX 1.00
' resedit.frm -- DVX Resource Editor
'
' Graphical editor for the resource blocks appended to DXE3
' Graphical editor for the resource blocks appended to binary
' files (.app, .wgt, .lib). View, add, remove, and extract
' resources of type icon, text, or binary.
'

View file

@ -22,7 +22,7 @@
' resource.bas -- DVX Resource File Library for DVX BASIC
'
' Provides access to DVX resource blocks appended to DXE3
' Provides access to DVX resource blocks appended to binary
' files (.app, .wgt, .lib). Resources are named data entries
' of type icon, text, or binary.
'

View file

@ -1,180 +1,28 @@
# DVX Shell (dvxshell.lib)
# dvxshell -- DVX Shell Library
The DVX Shell is a DXE3 module loaded by the DVX loader at startup.
It initializes the GUI subsystem, loads DXE3 application modules on
demand, runs the cooperative main loop, and provides crash recovery so
a faulting app does not bring down the entire system.
The DVX shell drives the GUI after the loader hands off. It initializes
the GUI subsystem, loads application modules on demand, runs the
cooperative main loop, and provides crash recovery so one app's fault
does not bring down the whole system.
## Documentation
## Entry Point
Full reference is in `dvxshell.dhs` (this directory), compiled into the
DVX System Reference. Open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
The loader finds and calls `shellMain()` after all libs and widgets
are loaded. `shellMain()`:
Topics covered: `AppDescriptorT` / `DxeAppContextT` contracts, how apps
register (`appMain`, `appDescriptor`, `appShutdown`), callback-only vs
main-loop apps, shell event callbacks, and force-kill behavior.
1. Loads preferences from `CONFIG/DVX.INI`
2. Initializes the GUI via `dvxInit()` with configured video mode
3. Applies saved mouse, color, and wallpaper settings from INI
4. Shows a splash screen ("DVX - DOS Visual eXecutive / Loading...")
5. Initializes the cooperative task system (`tsInit()`)
6. Sets shell task (task 0) to `TS_PRIORITY_HIGH`
7. Gathers system information via the platform layer
8. Initializes the app slot table
9. Points the memory tracker at `currentAppId` for per-app attribution
10. Registers idle callback, Ctrl+Esc handler, and title change handler
11. Installs the crash handler (before loading apps, so init crashes are caught)
12. Loads the desktop app (default: `apps/progman/progman.app`)
13. Dismisses the splash screen
14. Enters the main loop
## Main Loop
Each iteration of the main loop does four things:
1. `dvxUpdate()` -- process input events, dispatch callbacks, composite
dirty rects, flush to LFB
2. `tsYield()` -- give CPU time to app tasks (if any are active)
3. `shellReapApps()` -- clean up any apps that terminated this frame
4. `shellDesktopUpdate()` -- notify desktop app if apps were reaped
An idle callback (`idleYield`) is also registered so that `dvxUpdate()`
yields to app tasks during quiet frames.
## App Lifecycle
### DXE App Contract
Every DXE app exports two symbols:
* `appDescriptor` (`AppDescriptorT`) -- metadata:
- `name` -- display name (max 64 chars)
- `hasMainLoop` -- true for main-loop apps, false for callback-only
- `multiInstance` -- true to allow multiple instances via temp copy
- `stackSize` -- `SHELL_STACK_DEFAULT` (32KB) or explicit byte count
- `priority` -- `TS_PRIORITY_NORMAL` or custom
* `appMain` (`int appMain(DxeAppContextT *)`) -- entry point
Optional export: `appShutdown` (`void appShutdown(void)`) -- called
during graceful shutdown.
### Callback-Only Apps (hasMainLoop = false)
`appMain()` is called directly in the shell's task 0. It creates
windows, registers callbacks, and returns immediately. The app lives
entirely through GUI callbacks. The shell reaps callback-only apps
automatically when their last window closes -- `shellReapApps()` checks
each frame for running callback apps with zero remaining windows.
### Main-Loop Apps (hasMainLoop = true)
A dedicated cooperative task is created. `appMain()` runs in that task
and can do its own polling/processing loop, calling `tsYield()` to
share CPU. Lifecycle ends when `appMain()` returns or the task is
killed.
### App States
```
Free -> Loaded -> Running -> Terminating -> Free
```
| State | Description |
|-------|-------------|
| `AppStateFreeE` | Slot available for reuse |
| `AppStateLoadedE` | DXE loaded, not yet started (transient) |
| `AppStateRunningE` | Entry point called, active |
| `AppStateTerminatingE` | Shutdown in progress, awaiting reap |
### App Slots
App slots are managed as a stb_ds dynamic array (no fixed max). Each
slot tracks: app ID, name, path, DXE handle, state, task ID, entry/
shutdown function pointers, and a pointer to the `DxeAppContextT`
passed to the app.
`DxeAppContextT` is heap-allocated (via `calloc`) so its address is
stable across `sApps` array reallocs -- apps save this pointer in their
static globals and it must not move. The shell frees it during reap.
The `DxeAppContextT` gives each app:
- `shellCtx` -- pointer to the shell's `AppContextT`
- `appId` -- this app's unique ID
- `appDir` -- directory containing the `.app` file (for resources)
- `configDir` -- writable config directory (`CONFIG/<apppath>/`)
### App ID Tracking
`ctx->currentAppId` on AppContextT tracks which app is currently
executing. The shell sets this before calling app code.
`dvxCreateWindow()` stamps `win->appId` directly so the shell can
associate windows with apps for cleanup.
For main-loop apps, `appTaskWrapper` receives the app ID (as an int
cast to `void *`), not a direct pointer to `ShellAppT`. This is because
the `sApps` dynamic array may reallocate between `tsCreate` and the
first time the task runs, which would invalidate a direct pointer.
The shell calls `dvxSetBusy()` before `dlopen` to show the hourglass
cursor during app loading, and clears it after `appMain` returns (for
callback apps) or after task creation (for main-loop apps).
## Crash Recovery
The platform layer installs signal handlers for SIGSEGV, SIGFPE, and
SIGILL via `platformInstallCrashHandler()`. If a crash occurs:
1. Platform handler logs signal name and register dump (DJGPP)
2. Handler `longjmp`s to the `setjmp` point in `shellMain()`
3. `tsRecoverToMain()` fixes the scheduler's bookkeeping
4. Shell logs app-specific info (name, path, task ID)
5. Crashed app is force-killed (`shellForceKillApp()`)
6. Error dialog is shown to the user
7. Desktop is notified to refresh
8. Main loop continues normally
This gives Windows 3.1-style fault tolerance -- one bad app does not
take down the whole system.
## Task Manager Integration
The Task Manager is a separate DXE (`taskmgr.lib` in `taskmgr/`), not
built into the shell. It registers itself at load time via a DXE
constructor that sets the `shellCtrlEscFn` function pointer. The shell
calls this pointer on Ctrl+Esc. If `taskmgr.lib` is not loaded,
`shellCtrlEscFn` is NULL and Ctrl+Esc does nothing.
See `taskmgr/README.md` for full Task Manager documentation.
## Desktop Update Notifications
Apps (especially the desktop app) register callbacks via
`shellRegisterDesktopUpdate()` to be notified when app state changes
(load, reap, crash, title change). Multiple callbacks are supported.
## Files
| File | Description |
|------|-------------|
| `shellMain.c` | Entry point, main loop, crash recovery, splash screen, idle callback |
| `shellApp.h` | App lifecycle types: `AppDescriptorT`, `DxeAppContextT`, `ShellAppT`, `AppStateE`; `shellCtrlEscFn` extern |
| `shellApp.c` | App loading, reaping, task creation, DXE management, per-app memory tracking |
| `shellInfo.h` | System information wrapper |
| `shellInfo.c` | Gathers and caches hardware info via platform layer |
| `Makefile` | Builds `bin/libs/dvxshell.lib` + config/themes/wallpapers |
## Source Files
- `shellApp.h` -- application contract (AppDescriptorT, DxeAppContextT)
- `shellInf.h` -- shell-internal interfaces exposed to apps
- `shellApp.c` -- app registry, load, unload, lifecycle
- `shellInfo.c` -- system info service
- `shellMain.c` -- shell entry point, main loop, crash handler
## Build
```
make # builds dvxshell.lib + dvxshell.dep + config files
make clean # removes objects, library, and config output
```
Depends on: `libtasks.lib`, `libdvx.lib`, `texthelp.lib`, `listhelp.lib`
(via dvxshell.dep).
`make -C src/libs/kpunch/dvxshell` (invoked by the top-level `make`).

View file

@ -33,7 +33,9 @@
The DVX shell library manages the lifecycle of DXE applications: loading, launching, tracking, and reaping. It provides the bridge between the DVX GUI compositor and dynamically loaded DXE3 application modules.
Header: shell/shellApp.h
Header: dvxshell/shellApp.h
Loaded as: bin/libs/dvxshell.lib
.h2 App Model
@ -58,6 +60,30 @@ Every .app DXE module must export these symbols (COFF convention uses leading un
_appShutdown void (*)(void) No
.endtable
The shell resolves appDescriptor at dlopen time; if missing, the load fails with an error dialog. appMain is called immediately for callback-only apps or from the new task for main-loop apps. appShutdown, if present, is called during graceful reap (but not during force kill from crashed apps).
.h2 Callback-Only vs Main-Loop Apps
The app descriptor's hasMainLoop flag selects between two very different lifecycles:
.list
.item hasMainLoop = false (callback-only). The shell calls appMain directly on task 0 at dlopen time. The function creates windows, registers event callbacks, and returns. After that the app has no executing thread of its own -- it exists purely through GUI callbacks dispatched by dvxUpdate. The shell reaps the app automatically when its last window closes. Best for modal tools, dialogs, and event-driven utilities.
.item hasMainLoop = true (main-loop). The shell creates a dedicated cooperative task (via tsCreate) with the descriptor's stackSize (or TS_DEFAULT_STACK_SIZE) and priority. appMain runs in that task and can do its own work loop, calling tsYield or any GUI function that yields. The app terminates when appMain returns (the wrapper sets AppStateTerminatingE) or when forced via shellForceKillApp. Best for terminal emulators, games, and any app with continuous background work.
.endlist
Both app types use the same export interface; only the descriptor's flags differ. Apps cannot switch modes at runtime.
.h2 Icon Conventions
Shell-level UI (Program Manager, Task Manager) displays app icons at 16x16 and 32x32. Icons are not handled by the shell itself; each app embeds its own icons via the DVX resource system (DVX_RES_ICON):
.list
.item 16x16 BMP for toolbar entries and list rows
.item 32x32 BMP for desktop shortcuts and Program Manager tiles
.endlist
The Program Manager reads the app's 32x32 icon resource when building shortcut tiles. Apps without an icon resource fall back to a default shell-provided bitmap.
.h2 State Machine
App slots progress through four states:
@ -116,22 +142,24 @@ Exported by every DXE app as a global named appDescriptor. The shell reads it at
.h2 DxeAppContextT
Passed as the sole argument to appMain(). Gives the app access to the shell's GUI context and its own identity.
Passed as the sole argument to appMain(). Gives the app access to the shell's GUI context and its own identity. Heap-allocated by shellLoadApp so its address is stable across reallocations of the internal app slot table (apps may safely cache the pointer in their static globals).
.table
Field Type Description
----- ---- -----------
shellCtx AppContextT * The shell's GUI context (for creating windows, drawing, etc.).
appId int32_t This app's unique ID (slot index, 1-based).
appPath[DVX_MAX_PATH] char[] Full path to the .app DXE file.
appDir[DVX_MAX_PATH] char[] Directory containing the .app file (for loading resources).
configDir[DVX_MAX_PATH] char[] Writable config directory (e.g. CONFIG/APPS/KPUNCH/DVXBASIC/).
args[1024] char[] Launch arguments (empty string if none).
helpFile[DVX_MAX_PATH] char[] Help file path (for F1 context help).
helpTopic[128] char[] Current help topic ID (updated by the app at runtime).
Field Type Description
----- ---- -----------
shellCtx AppContextT * The shell's GUI context (for creating windows, drawing, etc.).
appId int32_t This app's unique ID (slot index, 1-based).
appPath[DVX_MAX_PATH] char[] Full path to the .app DXE file.
appDir[DVX_MAX_PATH] char[] Directory containing the .app file (for loading resources).
configDir[DVX_MAX_PATH] char[] Writable config directory (e.g. CONFIG/APPS/KPUNCH/DVXBASIC/).
args[1024] char[] Launch arguments (empty string if none).
helpFile[DVX_MAX_PATH] char[] Help file path (for F1 context help).
helpTopic[128] char[] Current help topic ID (updated by the app at runtime).
onHelpQuery void (*)(void *ctx) Optional callback the shell fires on F1 so the app can refresh helpTopic from context.
helpQueryCtx void * Opaque context pointer passed to onHelpQuery.
.endtable
The appDir field is derived from the .app file path at load time so apps can find their own resources via relative paths. This is necessary because the working directory is shared by all apps in DOS.
The appDir field is derived from the .app file path at load time so apps can find their own resources via relative paths. This is necessary because the working directory is shared by all apps in DOS. The config directory is mirrored from the app path: an app at APPS/KPUNCH/DVXBASIC/dvxbasic.app gets CONFIG/APPS/KPUNCH/DVXBASIC/.
.h2 AppStateE
@ -229,9 +257,9 @@ Gracefully shut down a single app. Calls the app's shutdownFn (if present), dest
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
.endcode
Forcibly kill an app without calling its shutdown hook. Used by the Task Manager "End Task" or when an app has crashed and cannot be trusted to run cleanup code.
Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery. Calls the app's shutdownFn (if present) so it can unregister callbacks before its DXE is unmapped, then destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
Cleanup order: windows first (removes from compositor), then task (frees stack), then DXE handle (unmaps code). Closing the DXE before destroying windows would cause callbacks into unmapped code.
Cleanup order: shutdownFn first (so the app can unregister callbacks while still mapped), then windows (removes from compositor), then task (frees stack), then DXE handle (unmaps code). Closing the DXE before destroying windows would cause callbacks into unmapped code.
.h2 shellTerminateAllApps
@ -319,7 +347,18 @@ shellConfigPath(ctx, "settings.ini", path, sizeof(path));
.h1 Desktop Callbacks
The shell provides a notification mechanism for app state changes (load, reap, crash). Desktop managers register a callback to refresh their display when apps come and go.
The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation).
.h2 Shell Event Hooks
.table
AppContextT field Handler Purpose
----------------- ------- -------
idleCallback idleYield Yields to cooperative tasks when dvxUpdate has no input events and no dirty rects.
onCtrlEsc ctrlEscHandler Invokes shellCtrlEscFn if the Task Manager DXE is loaded.
onF1 f1HelpHandler Launches the help viewer, optionally passing the focused app's helpFile and helpTopic.
onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh.
.endtable
.h2 shellRegisterDesktopUpdate
@ -361,7 +400,7 @@ Function pointer set by the taskmgr DXE's constructor. The shell calls this when
.h1 System Information
Header: shell/shellInfo.h
Header: dvxshell/shellInf.h
Thin wrapper around the platform layer's hardware detection. Gathers system information at startup, logs it, and caches the result for display in dialogs.

View file

@ -239,9 +239,9 @@ int shellMain(int argc, char *argv[]) {
// Initialize GUI — switch from VGA splash to VESA mode as late as possible
// so the VGA loading splash stays visible through all the init above.
{
int32_t videoW = prefsGetInt(sPrefs, "video", "width", 640);
int32_t videoH = prefsGetInt(sPrefs, "video", "height", 480);
int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp", 16);
int32_t videoW = prefsGetInt(sPrefs, "video", "width", DVX_DEFAULT_VIDEO_W);
int32_t videoH = prefsGetInt(sPrefs, "video", "height", DVX_DEFAULT_VIDEO_H);
int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp", DVX_DEFAULT_VIDEO_BPP);
dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp);
int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp);
@ -273,10 +273,10 @@ int shellMain(int argc, char *argv[]) {
const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", MOUSE_ACCEL_DEFAULT);
int32_t accelVal = 0;
if (strcmp(accelStr, "off") == 0) { accelVal = 10000; }
else if (strcmp(accelStr, "low") == 0) { accelVal = 100; }
else if (strcmp(accelStr, "medium") == 0) { accelVal = 64; }
else if (strcmp(accelStr, "high") == 0) { accelVal = 32; }
if (strcmp(accelStr, "off") == 0) { accelVal = MOUSE_ACCEL_OFF; }
else if (strcmp(accelStr, "low") == 0) { accelVal = MOUSE_ACCEL_LOW; }
else if (strcmp(accelStr, "medium") == 0) { accelVal = MOUSE_ACCEL_MEDIUM; }
else if (strcmp(accelStr, "high") == 0) { accelVal = MOUSE_ACCEL_HIGH; }
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);

View file

@ -1,449 +1,45 @@
# DVX Core Library (libdvx.lib)
# libdvx -- DVX Core Library
The core GUI infrastructure for DVX, built as a DXE3 module. Provides
VESA video setup, 2D drawing primitives, dirty-rectangle compositing,
a window manager with Motif-style chrome, and the widget infrastructure
(layout engine, event dispatch, class registration). Individual widget
type implementations live in `../widgets/` as separate `.wgt` DXE
modules that register themselves at runtime via `wgtRegisterClass()`.
The core windowing GUI: VESA video backend, drawing primitives, dirty-rect
compositor, window manager, and the public application API. Every DVX
application links against this library.
Core knows nothing about individual widget types. There is no
WidgetTypeE enum, no widget union, and no per-widget structs in
dvxWidget.h. All widget-specific behavior is dispatched through the
WidgetClassT dispatch table.
## Documentation
Full reference is authored in the `.dhs` help sources in this directory
and compiles into the DVX System Reference:
## 5-Layer Architecture
| Layer | Header | Source | Description |
|-------|--------|--------|-------------|
| 1. Video | `dvxVideo.h` | `dvxVideo.c` | VESA VBE init, LFB mapping, backbuffer, pixel format, `packColor()` |
| 2. Draw | `dvxDraw.h` | `dvxDraw.c` | Rect fills, bevels, text, bitmap cursors, focus rects, lines |
| 3. Compositor | `dvxComp.h` | `dvxComp.c` | Dirty rect tracking, merge, clip, LFB flush |
| 4. Window Manager | `dvxWm.h` | `dvxWm.c` | Window stack, chrome, drag/resize, menus, scrollbars, hit test |
| 5. Application | `dvxApp.h` | `dvxApp.c` | Event loop, input polling, color schemes, wallpaper, public API |
Additional modules built into libdvx.lib:
| Header | Source | Description |
|--------|--------|-------------|
| `dvxDialog.h` | `dvxDialog.c` | Modal message box and file open/save dialogs |
| `dvxPrefs.h` | `dvxPrefs.c` | INI-based preferences (read/write with typed accessors) |
| `dvxResource.h` | `dvxResource.c` | Resource system -- icons, text, and binary data appended to DXE files |
| `dvxMem.h` | (header only) | Per-app memory tracking API declarations |
| `dvxWidget.h` | `widgetClass.c`, `widgetCore.c`, `widgetEvent.c`, `widgetLayout.c`, `widgetOps.c`, `widgetScrollbar.c` | Widget infrastructure |
| `dvxWidgetPlugin.h` | (header only) | Plugin API for widget DXE modules |
| -- | `dvxImage.c` | Image loading via stb_image (BMP, PNG, JPEG, GIF) |
| -- | `dvxImageWrite.c` | PNG export via stb_image_write |
- `sysdoc.dhs` -- documentation index (start here)
- `arch.dhs` -- five-layer architecture, display pipeline, module system
- `apiref.dhs` -- every public function, struct, enum, and constant
After `make`, the compiled form is `bin/apps/kpunch/progman/dvxhelp.hlp`
(viewable via the Help Viewer app) and `docs/dvx_system_reference.html`.
## Source Files
| File | Description |
|------|-------------|
| `dvxVideo.c` | VESA mode negotiation, LFB mapping via DPMI, backbuffer alloc, `packColor()` |
| `dvxDraw.c` | `rectFill()`, `rectCopy()`, `drawBevel()`, `drawText()`, `drawTextN()`, `drawTermRow()`, cursor rendering |
| `dvxComp.c` | `dirtyListAdd()`, `dirtyListMerge()`, `flushRect()`, `rectIntersect()` |
| `dvxWm.c` | Window create/destroy, Z-order, chrome drawing, drag/resize, menu bar, scrollbars, minimize/maximize |
| `dvxApp.c` | `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, screenshots |
| `dvxDialog.c` | `dvxMessageBox()`, `dvxFileDialog()` -- modal dialogs with own event loops |
| `dvxPrefs.c` | `prefsLoad()`, `prefsSave()`, typed get/set for string/int/bool |
| `dvxResource.c` | `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` -- resource system |
| `dvxImage.c` | `dvxLoadImage()`, `dvxLoadImageFromMemory()` -- stb_image loader, converts to native pixel format |
| `dvxImageWrite.c` | `dvxSaveImage()` -- PNG writer for screenshots |
| `widgetClass.c` | `wgtRegisterClass()`, `wgtRegisterApi()`, `wgtGetApi()`, class table |
| `widgetCore.c` | Widget allocation, tree ops, focus management, clipboard, hit testing, cursor blink |
| `widgetEvent.c` | `widgetOnMouse()`, `widgetOnKey()`, `widgetOnPaint()`, `widgetOnResize()`, scrollbar management |
| `widgetLayout.c` | Two-pass flexbox layout: bottom-up `calcMinSize`, top-down space allocation with weights |
| `widgetOps.c` | `wgtPaint()`, `wgtLayout()`, `wgtInitWindow()`, text get/set, invalidation |
| `widgetScrollbar.c` | Scrollbar drawing (H/V), thumb calculation, hit testing, drag update |
## Public Headers
| Header | Purpose |
|--------|---------|
| `dvxTypes.h` | All shared types: DisplayT, RectT, BlitOpsT, BevelStyleT, BitmapFontT, ColorSchemeT, WindowT, MenuT, ScrollbarT, CursorT, PopupStateT |
| `dvxVideo.h` | `videoInit()`, `videoShutdown()`, `packColor()`, `setClipRect()`, `resetClipRect()` |
| `dvxDraw.h` | All drawing functions: `rectFill()`, `drawBevel()`, `drawText()`, `drawTextN()`, `drawTermRow()`, etc. |
| `dvxComp.h` | Dirty list operations: `dirtyListAdd()`, `dirtyListMerge()`, `flushRect()`, `rectIntersect()` |
| `dvxWm.h` | Window management: `wmCreateWindow()`, `wmDestroyWindow()`, `wmRaiseWindow()`, menus, scrollbars, chrome |
| `dvxApp.h` | Application API: `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, image I/O |
| `dvxDialog.h` | Modal dialogs: `dvxMessageBox()`, `dvxFileDialog()` |
| `dvxPrefs.h` | INI preferences: `prefsLoad()`, `prefsSave()`, typed accessors |
| `dvxResource.h` | Resource system: `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` |
| `dvxMem.h` | Per-app memory tracking: `dvxMalloc()`, `dvxFree()`, `dvxMemGetAppUsage()`, etc. |
| `dvxWidget.h` | Widget system public API: WidgetT, WidgetClassT, size tags, layout, API registry, `wclsFoo()` dispatch helpers |
| `dvxWidgetPlugin.h` | Plugin API for widget DXE authors: tree ops, focus, scrollbar helpers, shared state |
| `dvxFont.h` | Embedded 8x14 and 8x16 bitmap font data (CP437) |
| `dvxCursor.h` | Mouse cursor AND/XOR mask data (arrow, resize H/V/diag, busy) |
| `dvxPalette.h` | Default 256-color VGA palette for 8-bit mode |
## Platform Layer
| File | Description |
|------|-------------|
| `platform/dvxPlatform.h` | Platform abstraction API (video, input, spans, DXE, crash recovery, memory tracking) |
| `platform/dvxPlatformDos.c` | DJGPP/DPMI implementation (VESA VBE, INT 33h mouse, INT 16h keyboard, asm spans) |
The platform layer is compiled into dvx.exe (the loader), not into
libdvx.lib. Platform functions are exported to all DXE modules via
`platformRegisterDxeExports()`.
## Third-Party Libraries
| File | Description |
|------|-------------|
| `thirdparty/stb_image.h` | Image loading (implementation compiled into dvxImage.c) |
| `thirdparty/stb_image_write.h` | PNG writing (implementation compiled into dvxImageWrite.c) |
| `thirdparty/stb_ds.h` | Dynamic arrays/hash maps (implementation in loader, exported to all DXEs) |
## Dynamic Limits
All major data structures grow dynamically via realloc. There are no
fixed-size limits for:
- **Windows** -- `WindowStackT.windows` is a dynamic array
- **Menus** -- `MenuBarT.menus` and `MenuT.items` are dynamic arrays
- **Accelerator entries** -- `AccelTableT.entries` is a dynamic array
- **Dirty rectangles** -- `DirtyListT.rects` is a dynamic array
- **Submenu depth** -- `PopupStateT.parentStack` is a dynamic array
The only fixed-size buffers remaining are per-element string fields
(`MAX_TITLE_LEN = 128`, `MAX_MENU_LABEL = 32`, `MAX_WIDGET_NAME = 32`)
and the system menu (`SYS_MENU_MAX_ITEMS = 10`).
## Resource System
Resources are appended to DXE3 files (.app, .wgt, .lib) after the
normal DXE content. The DXE loader never reads past the DXE header,
so appended data is invisible to dlopen.
File layout:
[DXE3 content]
[resource data entries] -- sequential, variable length
[resource directory] -- fixed-size entries (48 bytes each)
[footer] -- magic + directory offset + count (16 bytes)
### Resource Types
| Define | Value | Description |
|--------|-------|-------------|
| `DVX_RES_ICON` | 1 | Image data (BMP icon: 16x16, 32x32, etc.) |
| `DVX_RES_TEXT` | 2 | Null-terminated string (author, copyright, etc.) |
| `DVX_RES_BINARY` | 3 | Arbitrary binary data (app-specific) |
### Resource API
| Function | Description |
|----------|-------------|
| `dvxResOpen(path)` | Open a resource handle by reading the footer and directory. Returns NULL if no resources. |
| `dvxResRead(h, name, outSize)` | Find a resource by name and read its data into a malloc'd buffer. Caller frees. |
| `dvxResFind(h, name)` | Find a resource by name and return its directory entry pointer. |
| `dvxResClose(h)` | Close the handle and free associated memory. |
### Key Types
| Type | Description |
|------|-------------|
| `DvxResDirEntryT` | Directory entry: name[32], type, offset, size, reserved (48 bytes) |
| `DvxResFooterT` | Footer: magic (`0x52585644` = "DVXR"), dirOffset, entryCount, reserved (16 bytes) |
| `DvxResHandleT` | Runtime handle: path, entries array, entry count |
## Memory Tracking (dvxMem.h)
Per-app memory tracking wraps malloc/free/calloc/realloc/strdup with a
small header per allocation that records the owning app ID and size.
DXE code does not need to include dvxMem.h -- the DXE export table maps
the standard allocator names to these wrappers transparently.
| Function | Description |
|----------|-------------|
| `dvxMalloc(size)` | Tracked malloc |
| `dvxCalloc(nmemb, size)` | Tracked calloc |
| `dvxRealloc(ptr, size)` | Tracked realloc |
| `dvxFree(ptr)` | Tracked free (falls through to real free on non-tracked pointers) |
| `dvxStrdup(s)` | Tracked strdup |
| `dvxMemSnapshotLoad(appId)` | Record baseline memory for leak detection |
| `dvxMemGetAppUsage(appId)` | Query current memory usage for an app (bytes) |
| `dvxMemResetApp(appId)` | Free all allocations charged to an app |
The global `dvxMemAppIdPtr` pointer is set by the shell to
`&ctx->currentAppId` so the allocator knows which app to charge.
## WidgetT Structure
The WidgetT struct is generic -- no widget-specific fields or union:
```c
typedef struct WidgetT {
int32_t type; // assigned by wgtRegisterClass()
const struct WidgetClassT *wclass; // dispatch table pointer
char name[MAX_WIDGET_NAME];
// Tree linkage
struct WidgetT *parent, *firstChild, *lastChild, *nextSibling;
WindowT *window;
// Geometry (relative to window content area)
int32_t x, y, w, h;
int32_t calcMinW, calcMinH; // computed minimum size
// Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto)
int32_t minW, minH, maxW, maxH;
int32_t prefW, prefH;
int32_t weight; // extra-space distribution (0 = fixed)
// Container properties
WidgetAlignE align;
int32_t spacing, padding; // tagged sizes
// Colors (0 = use color scheme defaults)
uint32_t fgColor, bgColor;
// State
bool visible, enabled, readOnly, focused;
char accelKey;
// User data and callbacks
void *userData;
void *data; // widget-private data (allocated by widget DXE)
const char *tooltip;
MenuT *contextMenu;
void (*onClick)(struct WidgetT *w);
void (*onDblClick)(struct WidgetT *w);
void (*onChange)(struct WidgetT *w);
void (*onFocus)(struct WidgetT *w);
void (*onBlur)(struct WidgetT *w);
} WidgetT;
```
## WidgetClassT Dispatch Table
WidgetClassT is an ABI-stable dispatch table. Method IDs are fixed
constants that never change -- adding new methods appends new IDs
without shifting existing ones. Widget DXEs compiled against an older
DVX version continue to work unmodified.
```c
#define WGT_CLASS_VERSION 1 // bump on breaking ABI change
#define WGT_METHOD_MAX 32 // room for future methods
typedef struct WidgetClassT {
uint32_t version;
uint32_t flags;
void *handlers[WGT_METHOD_MAX];
} WidgetClassT;
```
### Method ID Table
21 methods are currently defined (IDs 0--20). WGT_METHOD_MAX is 32,
leaving room for 11 future methods without a version bump.
| ID | Method ID | Signature | Purpose |
|----|-----------|-----------|---------|
| 0 | `WGT_METHOD_PAINT` | `void (w, d, ops, font, colors)` | Render the widget |
| 1 | `WGT_METHOD_PAINT_OVERLAY` | `void (w, d, ops, font, colors)` | Render overlay (dropdown popup) |
| 2 | `WGT_METHOD_CALC_MIN_SIZE` | `void (w, font)` | Compute minimum size (bottom-up pass) |
| 3 | `WGT_METHOD_LAYOUT` | `void (w, font)` | Position children (top-down pass) |
| 4 | `WGT_METHOD_GET_LAYOUT_METRICS` | `void (w, font, pad, gap, extraTop, borderW)` | Return padding/gap for box layout |
| 5 | `WGT_METHOD_ON_MOUSE` | `void (w, root, vx, vy)` | Handle mouse click |
| 6 | `WGT_METHOD_ON_KEY` | `void (w, key, mod)` | Handle keyboard input |
| 7 | `WGT_METHOD_ON_ACCEL_ACTIVATE` | `void (w, root)` | Handle accelerator key match |
| 8 | `WGT_METHOD_DESTROY` | `void (w)` | Free widget-private data |
| 9 | `WGT_METHOD_ON_CHILD_CHANGED` | `void (parent, child)` | Notification when a child changes |
| 10 | `WGT_METHOD_GET_TEXT` | `const char *(w)` | Return widget text |
| 11 | `WGT_METHOD_SET_TEXT` | `void (w, text)` | Set widget text |
| 12 | `WGT_METHOD_CLEAR_SELECTION` | `bool (w)` | Clear text/item selection |
| 13 | `WGT_METHOD_CLOSE_POPUP` | `void (w)` | Close dropdown popup |
| 14 | `WGT_METHOD_GET_POPUP_RECT` | `void (w, font, contentH, popX, popY, popW, popH)` | Compute popup rectangle |
| 15 | `WGT_METHOD_ON_DRAG_UPDATE` | `void (w, root, x, y)` | Mouse move during drag |
| 16 | `WGT_METHOD_ON_DRAG_END` | `void (w, root, x, y)` | Mouse release after drag |
| 17 | `WGT_METHOD_GET_CURSOR_SHAPE` | `int32_t (w, vx, vy)` | Return cursor ID for this position |
| 18 | `WGT_METHOD_POLL` | `void (w, win)` | Periodic polling (AnsiTerm comms) |
| 19 | `WGT_METHOD_QUICK_REPAINT` | `int32_t (w, outY, outH)` | Fast incremental repaint |
| 20 | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` | `void (parent, child)` | Scroll to make a child visible |
### Typed Dispatch Helpers
Each `wclsFoo()` inline function extracts a handler by stable method
ID, casts it to the correct function pointer type, and calls it with
a NULL check. This gives callers type-safe dispatch with the same
codegen as a direct struct field call.
| Helper | Wraps Method ID |
|--------|-----------------|
| `wclsHas(w, methodId)` | Check if a method is implemented (non-NULL) |
| `wclsPaint(w, d, ops, font, colors)` | `WGT_METHOD_PAINT` |
| `wclsPaintOverlay(w, d, ops, font, colors)` | `WGT_METHOD_PAINT_OVERLAY` |
| `wclsCalcMinSize(w, font)` | `WGT_METHOD_CALC_MIN_SIZE` |
| `wclsLayout(w, font)` | `WGT_METHOD_LAYOUT` |
| `wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW)` | `WGT_METHOD_GET_LAYOUT_METRICS` |
| `wclsOnMouse(w, root, vx, vy)` | `WGT_METHOD_ON_MOUSE` |
| `wclsOnKey(w, key, mod)` | `WGT_METHOD_ON_KEY` |
| `wclsOnAccelActivate(w, root)` | `WGT_METHOD_ON_ACCEL_ACTIVATE` |
| `wclsDestroy(w)` | `WGT_METHOD_DESTROY` |
| `wclsOnChildChanged(parent, child)` | `WGT_METHOD_ON_CHILD_CHANGED` |
| `wclsGetText(w)` | `WGT_METHOD_GET_TEXT` |
| `wclsSetText(w, text)` | `WGT_METHOD_SET_TEXT` |
| `wclsClearSelection(w)` | `WGT_METHOD_CLEAR_SELECTION` |
| `wclsClosePopup(w)` | `WGT_METHOD_CLOSE_POPUP` |
| `wclsGetPopupRect(w, font, contentH, popX, popY, popW, popH)` | `WGT_METHOD_GET_POPUP_RECT` |
| `wclsOnDragUpdate(w, root, x, y)` | `WGT_METHOD_ON_DRAG_UPDATE` |
| `wclsOnDragEnd(w, root, x, y)` | `WGT_METHOD_ON_DRAG_END` |
| `wclsGetCursorShape(w, vx, vy)` | `WGT_METHOD_GET_CURSOR_SHAPE` |
| `wclsPoll(w, win)` | `WGT_METHOD_POLL` |
| `wclsQuickRepaint(w, outY, outH)` | `WGT_METHOD_QUICK_REPAINT` |
| `wclsScrollChildIntoView(parent, child)` | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` |
### WidgetClassT Flags
| Flag | Value | Description |
|------|-------|-------------|
| `WCLASS_FOCUSABLE` | 0x0001 | Can receive keyboard focus |
| `WCLASS_BOX_CONTAINER` | 0x0002 | Uses VBox/HBox layout algorithm |
| `WCLASS_HORIZ_CONTAINER` | 0x0004 | Lays out children horizontally |
| `WCLASS_PAINTS_CHILDREN` | 0x0008 | Widget handles child rendering |
| `WCLASS_NO_HIT_RECURSE` | 0x0010 | Hit testing stops here |
| `WCLASS_FOCUS_FORWARD` | 0x0020 | Accel hit forwards focus to next focusable |
| `WCLASS_HAS_POPUP` | 0x0040 | Has dropdown popup overlay |
| `WCLASS_SCROLLABLE` | 0x0080 | Accepts mouse wheel events |
| `WCLASS_SCROLL_CONTAINER` | 0x0100 | Scroll container (ScrollPane) |
| `WCLASS_NEEDS_POLL` | 0x0200 | Needs periodic polling |
| `WCLASS_SWALLOWS_TAB` | 0x0400 | Tab key goes to widget, not focus nav |
| `WCLASS_RELAYOUT_ON_SCROLL` | 0x0800 | Full relayout on scrollbar drag |
| `WCLASS_PRESS_RELEASE` | 0x1000 | Click = press+release (buttons) |
| `WCLASS_ACCEL_WHEN_HIDDEN` | 0x2000 | Accel matching works when invisible |
## Widget Registration
Each widget DXE exports `wgtRegister()`, called by the loader after
`dlopen`. The WidgetClassT uses the `handlers[]` array indexed by
method IDs:
```c
static int32_t sButtonType;
static const WidgetClassT sButtonClass = {
.version = WGT_CLASS_VERSION,
.flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE,
.handlers = {
[WGT_METHOD_PAINT] = buttonPaint,
[WGT_METHOD_CALC_MIN_SIZE] = buttonCalcMinSize,
[WGT_METHOD_ON_MOUSE] = buttonOnMouse,
[WGT_METHOD_ON_KEY] = buttonOnKey,
[WGT_METHOD_DESTROY] = buttonDestroy,
[WGT_METHOD_GET_TEXT] = buttonGetText,
[WGT_METHOD_SET_TEXT] = buttonSetText,
[WGT_METHOD_ON_ACCEL_ACTIVATE] = buttonAccelActivate,
}
};
static const ButtonApiT sApi = { .create = buttonCreate };
void wgtRegister(void) {
sButtonType = wgtRegisterClass(&sButtonClass);
wgtRegisterApi("button", &sApi);
}
```
## Per-Widget API Registry
The monolithic WidgetApiT is gone. Each widget registers a small API
struct under a name via `wgtRegisterApi()`. Callers retrieve it via
`wgtGetApi()` and cast to the widget-specific type. Per-widget headers
(e.g. `widgetButton.h`) provide typed accessors and convenience macros:
```c
// widgetButton.h
typedef struct {
WidgetT *(*create)(WidgetT *parent, const char *text);
} ButtonApiT;
static inline const ButtonApiT *dvxButtonApi(void) {
static const ButtonApiT *sApi;
if (!sApi) { sApi = (const ButtonApiT *)wgtGetApi("button"); }
return sApi;
}
#define wgtButton(parent, text) dvxButtonApi()->create(parent, text)
```
## Tagged Size Values
Size hints encode both unit type and numeric value in a single int32_t:
| Macro | Encoding | Example |
|-------|----------|---------|
| `wgtPixels(v)` | Bits 31:30 = 00 | `w->minW = wgtPixels(200);` |
| `wgtChars(v)` | Bits 31:30 = 01 | `w->minW = wgtChars(40);` |
| `wgtPercent(v)` | Bits 31:30 = 10 | `w->minW = wgtPercent(50);` |
| `0` | -- | Auto (use computed minimum) |
## Layout Algorithm
Two-pass flexbox-like layout:
1. **Bottom-up** (`calcMinSize`): Each widget computes its minimum size.
Containers sum children along the main axis, max across the cross axis.
2. **Top-down** (`layout`): Available space is allocated to children.
Extra space beyond minimum is distributed proportionally to weights.
`weight=0` means fixed size, `weight=100` is the default flexible weight.
### Cross-Axis Centering
When a child widget has a `maxW` (in a VBox) or `maxH` (in an HBox)
that constrains it smaller than the available cross-axis space, the
layout engine automatically centers the child on the cross axis. This
means setting `maxW` or `maxH` on a child inside a container will both
cap its size and center it within the remaining space.
## Image Loading
Two image loading functions are available:
| Function | Description |
|----------|-------------|
| `dvxLoadImage(ctx, path, outW, outH, outPitch)` | Load from a file path |
| `dvxLoadImageFromMemory(ctx, data, dataLen, outW, outH, outPitch)` | Load from a memory buffer (e.g. resource data) |
Both convert to the display's native pixel format. Caller frees the
returned buffer with `dvxFreeImage()`. Supported formats: BMP, PNG,
JPEG, GIF (via stb_image).
## Exported Symbols
libdvx.lib exports symbols matching these prefixes:
```
dvx*, wgt*, wm*, prefs*, rect*, draw*, pack*, text*, setClip*,
resetClip*, stbi*, stbi_write*, dirtyList*, widget*,
accelParse*, clipboard*, multiClick*,
sCursor*, sDbl*, sDebug*, sClosed*, sFocused*, sKey*,
sOpen*, sDrag*, sClosed*, sKey*
```
Public headers live alongside implementation:
- `dvxTypes.h` -- shared types (DisplayT, RectT, BlitOpsT, ColorSchemeT, etc.)
- `dvxVideo.h` / `.c` -- layer 1: VESA init, LFB mapping, backbuffer
- `dvxDraw.h` / `.c` -- layer 2: rect fill, bevel, text, bitmap blits
- `dvxComp.h` / `.c` -- layer 3: dirty rect list, merge, LFB flush
- `dvxWm.h` / `.c` -- layer 4: window manager, chrome, drag, resize
- `dvxApp.h` / `.c` -- layer 5: event loop, public API, aggregator
- `dvxWgt.h` -- widget base (WidgetT, WidgetClassT, WgtIfaceT)
- `dvxWgtP.h` -- widget-internal helpers
- `dvxDlg.h` / `dvxDialog.c` -- message/input/choice/file dialogs
- `dvxRes.h` / `dvxResource.c` -- .res resource system
- `dvxMem.h` -- memory allocation wrappers
- `dvxPrefs.h` / `.c` -- INI-format preferences
- `dvxFont.h` -- bundled 8x16 bitmap font
- `dvxCur.h` -- mouse cursor bitmaps
- `dvxPal.h` -- 16-color CGA palette
- `widgetCore.c` / `widgetLayout.c` / `widgetEvent.c` / `widgetClass.c`
-- widget runtime helpers shared across all widget plugins
- `dvxImage.c` / `dvxImageWrite.c` -- image loader/writer (BMP, PNG via stb)
- `platform/dvxPlat.h` -- platform-layer interface (implemented per target)
## Build
```
make # builds bin/libs/libdvx.lib + bin/libs/libdvx.dep
make clean # removes objects and library
```
Depends on: `libtasks.lib` (via libdvx.dep).
`make -C src/libs/kpunch/libdvx` (invoked by the top-level `make`).

File diff suppressed because it is too large Load diff

View file

@ -42,7 +42,7 @@ DVX (DOS Visual eXecutive) is a complete windowing GUI compositor targeting DJGP
.item VESA VBE 2.0+ LFB only -- no bank switching. If the hardware cannot provide a linear framebuffer, initialization fails.
.item 486 baseline -- all hot paths are written to be fast on a 486, with Pentium-specific paths where the gain is significant.
.item Single-tasking cooperative model -- applications yield the CPU via tsYield(); there is no preemptive scheduler.
.item 86Box is the trusted reference platform for testing. DOSBox-X is not used; any bugs observed are treated as DVX bugs.
.item A single emulator is the trusted reference platform for testing; any bugs observed there are treated as DVX bugs.
.item No external font or cursor files -- all bitmaps are compiled in as static const data.
.endlist
@ -54,6 +54,10 @@ The runtime environment consists of a bootstrap loader (dvx.exe) that loads core
.link arch.pipeline Display Pipeline
.link arch.windows Window System
.link arch.widgets Widget System
.link arch.dialogs Modal Dialogs
.link arch.resources Resource System
.link arch.prefs Preferences System
.link arch.memtrack Per-App Memory Tracking
.link arch.dxe DXE Module System
.link arch.events Event Model
.link arch.fonts Font System
@ -97,7 +101,7 @@ DVX is organized into five layers, each implemented as a single .h/.c pair. Ever
| +------------------------------------------+ |
| |
| +------------------------------------------+ |
| | Platform Layer (dvxPlatform.h) | | dvxPlatformDos.c
| | Platform Layer (dvxPlat.h) | | dvxPlatformDos.c
| | OS-specific: video, input, asm spans | |
| +------------------------------------------+ |
| |
@ -270,7 +274,7 @@ wmHitTest() iterates the stack front-to-back and returns a hit-part identifier:
.index Menus
.index Submenus
Menus use fixed-size arrays with inline char buffers (no heap strings). Up to 8 menus per bar, items dynamically allocated. Supports cascading submenus via MenuItemT.subMenu pointer. Item types: normal, checkbox, radio. Separators are non-interactive items. The popup state (PopupStateT) tracks a stack of parent frames for cascading submenu nesting.
Menus use fixed-size inline char buffers for labels (MAX_MENU_LABEL = 32, no heap strings). Both menus per bar and items per menu are stored in dynamic arrays that grow on demand. Supports cascading submenus via MenuItemT.subMenu pointer. Item types: normal, checkbox, radio. Separators are non-interactive items. The popup state (PopupStateT) tracks a stack of parent frames for cascading submenu nesting.
.h2 Minimized Windows
@ -287,7 +291,7 @@ Minimized windows display as 64x64 icons at the bottom of the screen with bevele
.h1 Widget System
The widget system (dvxWidget.h) is a retained-mode toolkit layered on top of the window manager. Widgets form a tree rooted at a per-window VBox container.
The widget system (dvxWgt.h) is a retained-mode toolkit layered on top of the window manager. Widgets form a tree rooted at a per-window VBox container.
.h2 WidgetT Base Structure
@ -378,6 +382,143 @@ Each widget DXE registers a small API struct under a name during wgtRegister().
Each widget can register an interface descriptor that describes its BASIC-facing properties, methods, and events. These descriptors are used by the form runtime and IDE for generic dispatch and property panel enumeration. Properties have typed getters/setters (WGT_IFACE_STRING, WGT_IFACE_INT, WGT_IFACE_BOOL, WGT_IFACE_ENUM).
.topic arch.dialogs
.title Modal Dialogs
.toc 1 Modal Dialogs
.index Dialogs
.index Message Box
.index File Dialog
.index Input Box
.h1 Modal Dialogs (dvxDlg.h)
Pre-built modal dialog boxes block the caller and run their own event loop via dvxUpdate() until the user dismisses them. Each dialog sets ctx->modalWindow to prevent input from reaching other windows while open.
.h2 Dialog Types
.table
Function Purpose
-------- -------
dvxMessageBox Generic message box with button/icon flags (MB_OK, MB_YESNO, MB_ICONERROR, etc.)
dvxErrorBox Shortcut for MB_OK + MB_ICONERROR
dvxInfoBox Shortcut for MB_OK + MB_ICONINFO
dvxFileDialog File open/save dialog with directory navigation and filter dropdown
dvxInputBox Single-line text input with OK/Cancel
dvxIntInputBox Integer input with spinner, clamp range, and step
dvxChoiceDialog List selection with OK/Cancel
dvxPromptSave Canonical "Save changes?" Yes/No/Cancel dialog
.endtable
.h2 Flag Encoding
dvxMessageBox flags split into two nibbles: button configuration (low) and icon type (high). This matches the Win16 MessageBox() convention so OR'd flags read naturally:
.code
dvxMessageBox(ctx, "Confirm", "Delete file?", MB_YESNO | MB_ICONQUESTION);
.endcode
Return values are ID_OK / ID_CANCEL / ID_YES / ID_NO / ID_RETRY.
.topic arch.resources
.title Resource System
.toc 1 Resource System
.index Resources
.index DXE Resources
.index DvxResHandleT
.h1 Resource System (dvxRes.h)
Resources are appended to DXE3 files (.app, .wgt, .lib) after the normal DXE content. The DXE loader never reads past the DXE header, so the appended data is invisible to dlopen. Every DXE can carry its own icons, text strings, and binary data without separate resource files.
.h2 On-Disk Layout
.code
[DXE3 content] -- untouched, loaded by dlopen
[resource data entries] -- sequential, variable length
[resource directory] -- fixed-size entries (48 bytes each)
[footer] -- magic + directory offset + count (16 bytes)
.endcode
Readers start from the end: seek to EOF - sizeof(footer), verify the magic (DVX_RES_MAGIC = "DVXR"), then seek to the directory. Entries are sorted at open time so lookups are O(log n) via bsearch.
.h2 Types
.table
Type ID Value Payload
------- ----- -------
DVX_RES_ICON 1 Image data (BMP or PNG, typically 16x16 or 32x32 icons)
DVX_RES_TEXT 2 Null-terminated string (localisable UI text, author, copyright)
DVX_RES_BINARY 3 Arbitrary data (app-specific)
.endtable
.h2 Runtime API
.table
Function Description
-------- -----------
dvxResOpen Open a handle by reading the footer and directory
dvxResRead Look up a resource by name and read its data into a malloc'd buffer
dvxResFind Look up a resource's directory entry (no data copy)
dvxResClose Release the handle
dvxResAppend Append a new resource to a DXE file
dvxResRemove Remove a resource by name
.endtable
The dvxApp.h wrappers (dvxResLoadIcon, dvxResLoadText, dvxResLoadData) combine open + read + close for the common case.
.topic arch.prefs
.title Preferences System
.toc 1 Preferences System
.index Preferences
.index INI files
.index PrefsHandleT
.h1 Preferences System (dvxPrefs.h)
Handle-based API over classic INI files. Multiple files can be open at once. Each prefsLoad / prefsCreate returns a PrefsHandleT opaque pointer passed to subsequent calls and freed with prefsClose. The system INI lives at DVX_INI_PATH = "CONFIG\\DVX.INI".
.h2 Usage Pattern
.code
PrefsHandleT *h = prefsLoad(DVX_INI_PATH);
int32_t w = prefsGetInt(h, "video", "width", 640);
bool sound = prefsGetBool(h, "audio", "enable", true);
const char *bg = prefsGetString(h, "desktop", "wallpaper", "");
prefsSetInt(h, "video", "width", 800);
prefsSave(h);
prefsClose(h);
.endcode
Boolean getters recognise "true"/"yes"/"1" and "false"/"no"/"0" (case-insensitive). If the file does not exist, prefsLoad still returns a valid empty handle with the path captured so prefsSave can write a new file.
.topic arch.memtrack
.title Per-App Memory Tracking
.toc 1 Per-App Memory Tracking
.index Memory Tracking
.index dvxMalloc
.index dvxFree
.h1 Per-App Memory Tracking (dvxMem.h)
All allocations route through dvxMalloc/dvxFree wrappers that prepend a 16-byte header recording the owning app ID and allocation size. The Task Manager displays per-app memory usage, and leaks are detected at app termination.
.h2 Transparent Interception
DXE code does NOT need to include dvxMem.h. The DXE export table maps malloc / free / calloc / realloc / strdup to the dvxXxx wrappers, so standard C code is tracked automatically.
Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:
.table
Function Purpose
-------- -------
dvxMemSnapshotLoad Baseline a newly-loaded app's memory state
dvxMemGetAppUsage Query current bytes allocated for an app
dvxMemResetApp Free every tracked allocation charged to an app
.endtable
The dvxMemAppIdPtr pointer is set by the shell to &ctx->currentAppId so the allocator always knows which app to charge.
.topic arch.dxe
.title DXE Module System
.toc 1 DXE Module System
@ -625,7 +766,7 @@ Bevels are the defining visual element of the Motif aesthetic. Convenience macro
.title Platform Layer
.toc 1 Platform Layer
.index Platform Layer
.index dvxPlatform
.index dvxPlat.h
.index VESA
.index VBE
.index INT 33h
@ -636,7 +777,7 @@ Bevels are the defining visual element of the Motif aesthetic. Convenience macro
.h1 Platform Layer
All OS-specific and CPU-specific code is isolated behind dvxPlatform.h. To port DVX, implement a new dvxPlatformXxx.c against this header.
All OS-specific and CPU-specific code is isolated behind platform/dvxPlat.h. To port DVX, implement a new dvxPlatformXxx.c against this header.
.h2 Implementations
@ -694,7 +835,7 @@ DVX is cross-compiled from Linux using a DJGPP cross-compiler (i586-pc-msdosdjgp
.code
make -- build everything
./mkcd.sh -- build + create ISO for 86Box
./mkcd.sh -- build + create ISO for the target emulator
.endcode
.h2 Build Targets
@ -744,7 +885,7 @@ The -E flag specifies exported symbols (prefixed with underscore per DJGPP conve
.item Verifies critical outputs exist (dvx.exe, libtasks.lib, libdvx.lib, dvxshell.lib).
.item Counts widget modules.
.item Creates an ISO 9660 image from bin/ using mkisofs: -iso-level 1 (strict 8.3 filenames for DOS), -J (Joliet extensions for long names), -V DVX (volume label).
.item Places the ISO at ~/.var/app/net._86box._86Box/data/86Box/dvx.iso for 86Box to mount as CD-ROM.
.item Places the ISO at the target emulator's CD-ROM mount path.
.endlist
.h2 Compiler Flags
@ -761,7 +902,7 @@ The -E flag specifies exported symbols (prefixed with underscore per DJGPP conve
.code
dvxgui/
+-- core/ Core library sources (dvxVideo, dvxDraw, dvxComp, dvxWm, dvxApp, widget infra)
| +-- platform/ Platform abstraction (dvxPlatform.h, dvxPlatformDos.c)
| +-- platform/ Platform abstraction (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c)
| +-- thirdparty/ stb_image, stb_ds, stb_image_write
+-- loader/ Bootstrap loader (dvx.exe)
+-- tasks/ Cooperative task switcher (libtasks.lib)
@ -788,7 +929,7 @@ The -E flag specifies exported symbols (prefixed with underscore per DJGPP conve
+-- security/ DH key exchange, XTEA cipher, DRBG RNG
+-- seclink/ Encrypted channel wrapper
+-- serial/ Combined serial stack DXE
+-- proxy/ Linux proxy (86Box <-> secLink <-> telnet)
+-- proxy/ Linux proxy (emulator <-> secLink <-> telnet)
+-- sql/ SQLite integration
+-- bin/ Build output (dvx.exe, libs/, widgets/, apps/, config/)
+-- obj/ Intermediate object files

View file

@ -1422,7 +1422,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (target && wclsHas(target, WGT_METHOD_ON_KEY)) {
int32_t delta = ctx->mouseWheel * ctx->wheelDirection;
int32_t arrowKey = (delta > 0) ? (0x50 | 0x100) : (0x48 | 0x100);
int32_t arrowKey = (delta > 0) ? KEY_DOWN : KEY_UP;
int32_t steps = abs(delta) * ctx->wheelStep;
for (int32_t s = 0; s < steps; s++) {
@ -1664,7 +1664,7 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
// ============================================================
static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) {
if (w < 640 || h < 480) {
if (w < DVX_DEFAULT_VIDEO_W || h < DVX_DEFAULT_VIDEO_H) {
return;
}
@ -2450,7 +2450,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Ctrl+Esc -- system-wide hotkey (e.g. task manager)
if (scancode == 0x01 && ascii == 0x1B && (shiftFlags & KEY_MOD_CTRL)) {
if (scancode == 0x01 && ascii == KEY_ESCAPE && (shiftFlags & KEY_MOD_CTRL)) {
if (ctx->onCtrlEsc) {
ctx->onCtrlEsc(ctx->ctrlEscCtx);
}
@ -2493,7 +2493,7 @@ static void pollKeyboard(AppContextT *ctx) {
continue;
}
if (ascii == 0x1B) {
if (ascii == KEY_ESCAPE) {
// Cancel -- restore original position/size
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->x = ctx->kbMoveResize.origX;
@ -2647,7 +2647,7 @@ static void pollKeyboard(AppContextT *ctx) {
if (ascii == 0 && platformAltScanToChar(scancode)) {
closeSysMenu(ctx);
// Fall through to dispatchAccelKey below
} else if (ascii == 0x1B) {
} else if (ascii == KEY_ESCAPE) {
closeSysMenu(ctx);
continue;
} else if (ascii == 0 && scancode == 0x48) {
@ -2911,7 +2911,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// ESC closes open dropdown/combobox popup
if (sOpenPopup && ascii == 0x1B) {
if (sOpenPopup && ascii == KEY_ESCAPE) {
// Dirty the popup list area
WindowT *popWin = sOpenPopup->window;
int32_t popX;
@ -2931,7 +2931,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// ESC closes one popup level (or all if at top level)
if (ctx->popup.active && ascii == 0x1B) {
if (ctx->popup.active && ascii == KEY_ESCAPE) {
closePopupLevel(ctx);
continue;
}

View file

@ -49,10 +49,9 @@
// Rects within this many pixels of each other get merged even if they don't
// overlap. A small gap tolerance absorbs jitter from mouse movement and
// closely-spaced UI invalidations (e.g. title bar + content during a drag)
// without bloating merged rects excessively. The value 4 was chosen to match
// the chrome border width -- adjacent chrome/content invalidations merge
// naturally.
#define DIRTY_MERGE_GAP 4
// without bloating merged rects excessively. Derived from CHROME_BORDER_WIDTH
// so adjacent chrome/content invalidations merge naturally.
#define DIRTY_MERGE_GAP CHROME_BORDER_WIDTH
// ============================================================
// Prototypes
@ -123,6 +122,12 @@ void dirtyListClear(DirtyListT *dl) {
void dirtyListInit(DirtyListT *dl) {
dl->count = 0;
// Pre-allocate to the soft cap so per-frame dirtyListAdd never reallocs.
// Worth the ~1KB of heap to take realloc out of the hot path entirely.
if (!dl->rects) {
dl->rects = (RectT *)malloc(DIRTY_SOFT_CAP * sizeof(RectT));
dl->cap = dl->rects ? DIRTY_SOFT_CAP : 0;
}
}

View file

@ -83,9 +83,11 @@
// distance-squared test (d2 between INNER_R2 and OUTER_R2) to draw
// a ring without needing floating-point sqrt.
#define ICON_GLYPH_SIZE 24 // icon glyph drawing area (pixels)
#define ICON_GLYPH_CENTER 12 // center of icon glyph (ICON_GLYPH_SIZE / 2)
#define ICON_OUTER_R2 144 // outer circle radius squared (12*12)
#define ICON_INNER_R2 100 // inner circle radius squared (10*10)
#define ICON_GLYPH_CENTER (ICON_GLYPH_SIZE / 2)
#define ICON_OUTER_R ICON_GLYPH_CENTER
#define ICON_INNER_R (ICON_GLYPH_CENTER - 2)
#define ICON_OUTER_R2 (ICON_OUTER_R * ICON_OUTER_R)
#define ICON_INNER_R2 (ICON_INNER_R * ICON_INNER_R)
#define FD_DIALOG_WIDTH 360 // file dialog window width
#define FD_DIALOG_HEIGHT 340 // file dialog window height
@ -380,7 +382,7 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
}
WidgetT *lb = wgtListBox(root);
lb->weight = 100;
lb->weight = WGT_WEIGHT_FILL;
lb->minH = wgtPixels(CB_LIST_HEIGHT);
lb->onDblClick = cbOnDblClick;
wgtListBoxSetItems(lb, items, itemCount);
@ -482,7 +484,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
// File list
sFd.fileList = wgtListBox(root);
sFd.fileList->weight = 100;
sFd.fileList->weight = WGT_WEIGHT_FILL;
sFd.fileList->onChange = fdOnListClick;
sFd.fileList->onDblClick = fdOnListDblClick;

View file

@ -46,18 +46,22 @@ void dvxResClose(DvxResHandleT *h) {
}
static int dvxResEntryCmp(const void *a, const void *b) {
return strcmp(((const DvxResDirEntryT *)a)->name, ((const DvxResDirEntryT *)b)->name);
}
const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) {
if (!h || !name) {
return NULL;
}
for (uint32_t i = 0; i < h->entryCount; i++) {
if (strcmp(h->entries[i].name, name) == 0) {
return &h->entries[i];
}
}
return NULL;
// Entries are sorted at open() time so bsearch gives O(log n) lookup.
DvxResDirEntryT key;
strncpy(key.name, name, sizeof(key.name) - 1);
key.name[sizeof(key.name) - 1] = '\0';
return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount,
sizeof(DvxResDirEntryT), dvxResEntryCmp);
}
@ -123,6 +127,9 @@ DvxResHandleT *dvxResOpen(const char *path) {
h->entries = entries;
h->entryCount = footer.entryCount;
// Sort once so dvxResFind can bsearch.
qsort(h->entries, h->entryCount, sizeof(DvxResDirEntryT), dvxResEntryCmp);
return h;
}

View file

@ -306,7 +306,10 @@ typedef struct {
#define CHROME_TITLE_HEIGHT 20
#define CHROME_TITLE_PAD 4
#define CHROME_INNER_BORDER 2
#define CHROME_MENU_HEIGHT 20
// Menu bar height: matches the title bar so accent chrome and menu bar
// stack as a uniform vertical rhythm. If the two ever need to differ,
// split these back into independent values.
#define CHROME_MENU_HEIGHT CHROME_TITLE_HEIGHT
#define CHROME_TOTAL_TOP (CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_INNER_BORDER)
#define CHROME_TOTAL_SIDE (CHROME_BORDER_WIDTH + CHROME_INNER_BORDER)
#define CHROME_TOTAL_BOTTOM (CHROME_BORDER_WIDTH + CHROME_INNER_BORDER)
@ -421,26 +424,44 @@ typedef struct {
#define ACCEL_CTRL 0x04
#define ACCEL_ALT 0x08
// Non-ASCII keys are encoded as (scancode | 0x100) to distinguish them
// from regular ASCII values in a single int32_t keycode space.
#define KEY_F1 (0x3B | 0x100)
#define KEY_F2 (0x3C | 0x100)
#define KEY_F3 (0x3D | 0x100)
#define KEY_F4 (0x3E | 0x100)
#define KEY_F5 (0x3F | 0x100)
#define KEY_F6 (0x40 | 0x100)
#define KEY_F7 (0x41 | 0x100)
#define KEY_F8 (0x42 | 0x100)
#define KEY_F9 (0x43 | 0x100)
#define KEY_F10 (0x44 | 0x100)
#define KEY_F11 (0x57 | 0x100)
#define KEY_F12 (0x58 | 0x100)
#define KEY_INSERT (0x52 | 0x100)
#define KEY_DELETE (0x53 | 0x100)
#define KEY_HOME (0x47 | 0x100)
#define KEY_END (0x4F | 0x100)
#define KEY_PGUP (0x49 | 0x100)
#define KEY_PGDN (0x51 | 0x100)
// Non-ASCII keys are encoded as (scancode | KEY_EXT_FLAG) to distinguish
// them from regular ASCII values in a single int32_t keycode space.
#define KEY_EXT_FLAG 0x100
#define KEY_F1 (0x3B | KEY_EXT_FLAG)
#define KEY_F2 (0x3C | KEY_EXT_FLAG)
#define KEY_F3 (0x3D | KEY_EXT_FLAG)
#define KEY_F4 (0x3E | KEY_EXT_FLAG)
#define KEY_F5 (0x3F | KEY_EXT_FLAG)
#define KEY_F6 (0x40 | KEY_EXT_FLAG)
#define KEY_F7 (0x41 | KEY_EXT_FLAG)
#define KEY_F8 (0x42 | KEY_EXT_FLAG)
#define KEY_F9 (0x43 | KEY_EXT_FLAG)
#define KEY_F10 (0x44 | KEY_EXT_FLAG)
#define KEY_F11 (0x57 | KEY_EXT_FLAG)
#define KEY_F12 (0x58 | KEY_EXT_FLAG)
#define KEY_INSERT (0x52 | KEY_EXT_FLAG)
#define KEY_DELETE (0x53 | KEY_EXT_FLAG)
#define KEY_HOME (0x47 | KEY_EXT_FLAG)
#define KEY_END (0x4F | KEY_EXT_FLAG)
#define KEY_PGUP (0x49 | KEY_EXT_FLAG)
#define KEY_PGDN (0x51 | KEY_EXT_FLAG)
#define KEY_UP (0x48 | KEY_EXT_FLAG)
#define KEY_DOWN (0x50 | KEY_EXT_FLAG)
#define KEY_LEFT (0x4B | KEY_EXT_FLAG)
#define KEY_RIGHT (0x4D | KEY_EXT_FLAG)
// ASCII control / printable ranges (non-extended codes).
#define KEY_ESCAPE 0x1B
#define KEY_ASCII_DEL 0x7F // not the same as KEY_DELETE (scancode 0x53)
#define KEY_ASCII_PRINT_FIRST 0x20 // space
#define KEY_ASCII_PRINT_LAST 0x7E // tilde
// Ctrl-letter codes (A=1, B=2, ..., Z=26) used for clipboard shortcuts.
#define KEY_CTRL_C 0x03
#define KEY_CTRL_V 0x16
#define KEY_CTRL_X 0x18
#define KEY_CTRL_Z 0x1A
typedef struct {
int32_t key; // key code: ASCII char or KEY_Fxx for extended
@ -487,6 +508,13 @@ typedef struct {
#define MIN_WINDOW_H 60
#define WM_MAX_FROM_SCREEN (-1) // use screen dimension as max (for maxW/maxH)
// Default / minimum VESA video mode. Used as the lower bound when
// enumerating available modes and as the fallback mode when the
// preferences file doesn't specify otherwise.
#define DVX_DEFAULT_VIDEO_W 640
#define DVX_DEFAULT_VIDEO_H 480
#define DVX_DEFAULT_VIDEO_BPP 16
// Window paint states (for WindowT.paintNeeded)
#define PAINT_NONE 0 // no paint needed
#define PAINT_PARTIAL 1 // only dirty widgets (deferred wgtInvalidatePaint)
@ -644,6 +672,14 @@ typedef struct {
#define MOUSE_ACCEL_DEFAULT "medium"
#define MOUSE_WHEEL_DIR_DEFAULT "normal"
// Mouse acceleration threshold levels. Higher values = less acceleration
// (threshold above which doubling kicks in). MOUSE_ACCEL_OFF effectively
// disables acceleration by setting the threshold above any realistic speed.
#define MOUSE_ACCEL_OFF 10000
#define MOUSE_ACCEL_LOW 100
#define MOUSE_ACCEL_MEDIUM 64
#define MOUSE_ACCEL_HIGH 32
// ============================================================
// Mouse cursor
// ============================================================

View file

@ -81,6 +81,11 @@ struct WidgetClassT;
#define WGT_SIZE_CHARS 0x40000000
#define WGT_SIZE_PERCENT 0x80000000
// Standard "fill remaining space" weight for VBox/HBox children.
// A widget with WGT_WEIGHT_FILL gets one share of extra space; use
// 2*, 3* etc. to give siblings proportionally more space.
#define WGT_WEIGHT_FILL 100
#define wgtPixels(v) ((int32_t)(WGT_SIZE_PIXELS | ((uint32_t)(v) & WGT_SIZE_VAL_MASK)))
#define wgtChars(v) ((int32_t)(WGT_SIZE_CHARS | ((uint32_t)(v) & WGT_SIZE_VAL_MASK)))
#define wgtPercent(v) ((int32_t)(WGT_SIZE_PERCENT | ((uint32_t)(v) & WGT_SIZE_VAL_MASK)))

View file

@ -29,4 +29,69 @@
.image help.png center
DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, core API, libraries, and widget toolkit.
DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, the core libdvx API, the widget toolkit, and the platform abstraction layer.
.h2 What Is libdvx?
libdvx is the heart of DVX -- a DXE3 library that provides:
.list
.item A VESA VBE 2.0+ video backend (LFB only, no bank switching).
.item Optimised 2D drawing primitives (rectangle fill, bevels, text, bitmap blits, cursors).
.item A dirty-rectangle compositor that minimises LFB traffic.
.item A window manager with Motif-style chrome, drag/resize, menus, and scrollbars.
.item A retained-mode widget toolkit with automatic layout.
.item Modal dialog helpers, an INI preferences system, a DXE resource system, and per-app memory tracking.
.endlist
Applications written in C link against libdvx (and the widget DXEs they need) to build native DVX programs. Applications written in DVX BASIC compile to bytecode that runs on top of libdvx via the form runtime.
.h2 Target Audience
This document is aimed at developers writing native C code for DVX:
.list
.item System-level contributors maintaining libdvx itself.
.item Widget authors writing new .wgt DXE modules.
.item Application authors writing .app DXE modules.
.item Tool authors (e.g. the BASIC compiler/runtime) that sit on top of libdvx.
.endlist
All examples and signatures assume the DJGPP cross-compiler toolchain.
.h2 What's Covered
.list
.item Architecture -- Five-layer model, display pipeline, event model, build system.
.item API Reference -- Every public function, struct, enum, and constant documented with parameters and return values.
.endlist
Use the table of contents on the left to navigate. The API reference is organised by header file; each function has a one-line summary, parameter table, and (where useful) a working example.
.h2 Conventions
.list
.item Types end in a capital T (e.g. WindowT, DisplayT, BlitOpsT).
.item Enum types end in a capital E (e.g. ColorIdE, WallpaperModeE, ScrollbarOrientE).
.item Public functions use camelCase and prefixes that identify the subsystem: dvx* (application), wm* (window manager), wgt* (widget), prefs* (preferences), draw* / rect* (drawing), dirtyList* / flush* (compositor), video* / packColor / setClipRect (video), platform* (platform layer).
.item Constants use SCREAMING_SNAKE_CASE (e.g. HIT_CONTENT, MB_OK, CURSOR_ARROW).
.item Every header uses stdint.h types (int32_t, uint8_t) and stdbool.h types (bool).
.endlist
.h2 Getting Started
If you're new to DVX, read these topics in order:
.list
.item Architecture Overview -- The big picture, five-layer model, and the design philosophy.
.item Display Pipeline -- How the backbuffer, dirty list, and compositor work together.
.item Event Model -- How input becomes window/widget callbacks.
.item Widget System -- The retained-mode toolkit layered on the window manager.
.item DXE Module System -- How apps and widgets are loaded dynamically.
.endlist
Then dip into the API Reference to find specific functions. The public entry point for any application is dvxInit / dvxRun in dvxApp.h.
.h2 License
DVX is distributed under the MIT License (see the copyright notice at the top of every source file). Third-party code (stb_image, stb_ds, stb_image_write in thirdparty/) is used under its own permissive license.

View file

@ -237,7 +237,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
wclsOnKey(focus, key, mod);
// Fire user callbacks after the widget's internal handler
if (key >= 32 && key < 127 && focus->onKeyPress) {
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && focus->onKeyPress) {
focus->onKeyPress(focus, key);
}

View file

@ -1,118 +1,26 @@
# taskswitch -- Cooperative Task Switching Library
# libtasks -- Cooperative Task Switching
Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS
protected mode). Built as `libtasks.lib` -- a DXE3 module loaded by
the DVX loader.
A cooperative (non-preemptive) multitasking library. Tasks yield
voluntarily via `tsYield()`; the scheduler picks the next runnable task
by priority. Used by the DVX shell to run multiple DXE applications
concurrently.
## Documentation
## Why Cooperative
Full reference is in `libtasks.dhs` (this directory), compiled into the
DVX System Reference. Open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
DOS is single-threaded with no kernel scheduler. DPMI provides no
thread or timer-based preemption. The DVX GUI event model is
inherently single-threaded (one compositor, one input queue, one
window stack). Cooperative switching lets each task yield at safe
points, avoiding the need for synchronization primitives entirely.
Topics covered: `TaskT` struct, `tsInit` / `tsShutdown` / `tsSpawn` /
`tsYield` / `tsCurrent` / `tsKill` / `tsPause` / `tsSetPriority`, priority
model, stack allocation, and the exact return-code semantics for each
entry point.
## Source Files
## Scheduling Algorithm
Credit-based weighted fair-share, round-robin within a credit epoch.
Each task receives `(priority + 1)` credits per scheduling round.
Tasks run round-robin, consuming one credit per turn. When all
credits are spent, every ready task is refilled.
| Priority | Constant | Credits/Round | Relative Share |
|----------|----------|---------------|----------------|
| 0 | `TS_PRIORITY_LOW` | 1 | ~4% |
| 5 | `TS_PRIORITY_NORMAL` | 6 | ~22% |
| 10 | `TS_PRIORITY_HIGH` | 11 | ~41% |
Higher-priority tasks run proportionally more often but never starve
lower ones. A priority-10 task gets 11 turns per round while a
priority-0 task gets 1.
## Task States
| State | Description |
|-------|-------------|
| `TaskStateReady` | Eligible for scheduling |
| `TaskStateRunning` | Currently executing (cosmetic marker) |
| `TaskStatePaused` | Skipped until resumed |
| `TaskStateTerminated` | Slot available for reuse |
## API Reference
| Function | Description |
|----------|-------------|
| `tsInit()` | Initialize task system; calling context becomes task 0 (main) |
| `tsShutdown()` | Shut down and free all resources |
| `tsCreate(name, entry, arg, stackSize, priority)` | Create a new task; returns task ID |
| `tsYield()` | Yield CPU to next eligible task (credit-based round-robin) |
| `tsPause(taskId)` | Pause a task (implicit yield if self) |
| `tsResume(taskId)` | Resume a paused task (refills credits) |
| `tsSetPriority(taskId, priority)` | Set priority (refills credits immediately) |
| `tsGetPriority(taskId)` | Get task priority |
| `tsGetState(taskId)` | Get task state |
| `tsCurrentId()` | Get currently running task's ID |
| `tsGetName(taskId)` | Get task name |
| `tsExit()` | Terminate calling task (never returns) |
| `tsKill(taskId)` | Forcibly terminate another task |
| `tsRecoverToMain()` | Force scheduler back to task 0 after crash longjmp |
| `tsActiveCount()` | Count of non-terminated tasks |
### Error Codes
| Constant | Value | Description |
|----------|-------|-------------|
| `TS_OK` | 0 | Success |
| `TS_ERR_INIT` | -1 | Initialization failure |
| `TS_ERR_PARAM` | -2 | Invalid parameter |
| `TS_ERR_FULL` | -3 | No available task slots |
| `TS_ERR_NOMEM` | -4 | Stack allocation failed |
| `TS_ERR_STATE` | -5 | Invalid state transition |
### Constants
| Constant | Value | Description |
|----------|-------|-------------|
| `TS_DEFAULT_STACK_SIZE` | 32768 | Default stack size (32KB) |
| `TS_NAME_MAX` | 32 | Maximum task name length |
## Task 0 (Main Task)
Task 0 is special:
* Cannot be killed or paused
* Uses the process stack directly (no separate allocation)
* `tsRecoverToMain()` always returns control here after a crash
* The DVX shell runs as task 0 with `TS_PRIORITY_HIGH`
## Crash Recovery
`tsRecoverToMain()` is called after `longjmp` from a signal handler
that fired in a non-main task. It fixes the scheduler's `currentIdx`
to point back to task 0. The crashed task is NOT cleaned up by this
call -- `tsKill()` must be called separately afterward.
## Files
| File | Description |
|------|-------------|
| `taskswitch.h` | Public API header |
| `taskswitch.c` | Complete implementation (context switch, scheduler, stack management) |
| `Makefile` | Builds `bin/libs/libtasks.lib` |
- `taskSwch.h` -- public API
- `taskswitch.c` -- implementation (context save/restore, scheduler)
## Build
```
make # builds bin/libs/libtasks.lib
make clean # removes objects and library
```
No dependencies. Exports all symbols matching `_ts*`.
`make -C src/libs/kpunch/libtasks` (invoked by the top-level `make`).

View file

@ -33,12 +33,32 @@
Credit-based cooperative (non-preemptive) multitasking library for DOS protected mode (DJGPP/DPMI). Each task receives (priority + 1) credits per scheduling round. Tasks run round-robin, consuming one credit per turn. When all credits are exhausted, every ready task is refilled. Higher-priority tasks run proportionally more often but never starve lower ones.
Header: tasks/taskswitch.h
Header: libtasks/taskSwch.h
Loaded as: bin/libs/libtasks.lib
.h2 Why Cooperative
DOS is single-threaded with no kernel scheduler. DPMI provides no preemption primitives. The DVX GUI event model is inherently single-threaded: one compositor, one input queue, one window stack. Cooperative switching lets each task yield at safe points, avoiding synchronization primitives entirely.
.h2 Scheduling Semantics
Task storage is a stb_ds dynamic array. Terminated slots are recycled by tsCreate so the array does not grow unboundedly. The scheduler performs a linear scan for the next Ready task whose credit count is positive; when no task has credits left, every Ready task is refilled with (priority + 1) credits and the scan restarts.
.table
Priority Credits per round Approximate share
-------- ----------------- -----------------
TS_PRIORITY_LOW 1 ~4% (1 of 25 possible credits)
TS_PRIORITY_NORMAL 6 ~22%
TS_PRIORITY_HIGH 11 ~41%
.endtable
Actual share depends on the mix of priorities currently running. What the algorithm guarantees is that no ready task ever goes unscheduled -- even a priority-0 task always earns at least one turn per round.
.h3 Stack Allocation
Task 0 uses the caller's stack (no separate allocation). Every other task gets a heap-allocated stack of the size passed to tsCreate (or TS_DEFAULT_STACK_SIZE if 0 is passed). Stacks are freed at task termination, either via tsExit or tsKill.
.h2 Error Codes
.table
@ -95,7 +115,7 @@ int32_t tsInit(void);
Initialize the task system. The calling context becomes task 0 (the main task). Task 0 is special: it cannot be killed or paused, and tsRecoverToMain() always returns control here. No separate stack is allocated for task 0; it uses the process stack directly.
Returns: TS_OK on success, TS_ERR_INIT if already initialized.
Returns: TS_OK on success, TS_ERR_PARAM if already initialized.
.h2 tsShutdown
@ -123,7 +143,7 @@ Create a new task. Terminated task slots are recycled to avoid unbounded array g
priority Scheduling priority (0..10; use TS_PRIORITY_* constants)
.endtable
Returns: Task ID (>= 0) on success, or a negative error code (TS_ERR_INIT, TS_ERR_PARAM, TS_ERR_NOMEM).
Returns: Task ID (>= 0) on success, or a negative error code (TS_ERR_PARAM if not initialized or entry is NULL, TS_ERR_NOMEM if stack allocation failed).
.h2 tsYield
@ -147,7 +167,7 @@ Pause a task, removing it from scheduling. Cannot pause the main task (ID 0). If
taskId ID of the task to pause
.endtable
Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, TS_ERR_STATE if the task is not in a pausable state.
Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, or TS_ERR_STATE if the task is the main task or not in a pausable state (not Running or Ready).
.h2 tsResume
@ -180,7 +200,7 @@ Set a task's scheduling priority. Also refills credits so the change takes effec
priority New priority level (0..10)
.endtable
Returns: TS_OK on success, TS_ERR_PARAM on invalid ID or out-of-range priority.
Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, or TS_ERR_STATE if the task is terminated.
.h2 tsGetPriority
@ -262,7 +282,7 @@ Forcibly terminate another task. Cannot kill the main task (ID 0) or the current
taskId ID of the task to terminate
.endtable
Returns: TS_OK on success, TS_ERR_PARAM on invalid ID or illegal target (main task, self).
Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, or TS_ERR_STATE on illegal target (main task, self, or already terminated).
.h2 tsRecoverToMain

View file

@ -1,101 +1,24 @@
# listhelp -- Shared List/Dropdown Helper Library
# listhelp -- Shared List / Dropdown Helpers
Shared infrastructure for list and dropdown widgets, built as
`listhelp.lib` (DXE3 module). Provides dropdown arrow drawing, item
measurement, keyboard navigation, popup rectangle calculation, and
popup list painting.
Shared rendering and hit-testing infrastructure used by the ListBox,
ListView, ComboBox, and Dropdown widgets. Centralizes popup-list
painting, dropdown-arrow glyphs, and key-navigation helpers so the
widgets stay consistent and small.
Used by: Dropdown, ComboBox, ListBox, ListView, TreeView.
## Documentation
Full reference is in `listhelp.dhs` (this directory), compiled into the
DVX System Reference. Open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
## API Reference
Topics covered: dropdown arrow drawing, popup-list hit test and paint,
navigation keys (Up/Down/Home/End/PgUp/PgDn).
### Dropdown Arrow Glyph
```c
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops,
int32_t centerX, int32_t centerY, uint32_t color);
```
Draws the small downward-pointing triangle glyph used on dropdown
buttons.
### Item Measurement
```c
int32_t widgetMaxItemLen(const char **items, int32_t count);
```
Scans an array of item strings and returns the length of the longest
one. Used to size dropdown popups and list columns to fit their
content.
### Keyboard Navigation
```c
int32_t widgetNavigateIndex(int32_t key, int32_t current,
int32_t count, int32_t pageSize);
```
Maps arrow key presses to index changes for list navigation. Handles
Up, Down, Home, End, PageUp, and PageDown. Returns the new selected
index, clamped to valid range.
### Popup Rectangle Calculation
```c
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font,
int32_t contentH, int32_t itemCount,
int32_t *popX, int32_t *popY,
int32_t *popW, int32_t *popH);
```
Computes the screen rectangle for a dropdown popup overlay. Positions
the popup below the widget (or above if there is not enough room
below). Limits height to `DROPDOWN_MAX_VISIBLE` items.
### Popup List Painting
```c
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors,
int32_t popX, int32_t popY, int32_t popW, int32_t popH,
const char **items, int32_t itemCount,
int32_t hoverIdx, int32_t scrollPos);
```
Renders the popup overlay list with items, selection highlight, scroll
position, and beveled border. Used by Dropdown and ComboBox for their
popup overlays.
### Constants
| Name | Value | Description |
|------|-------|-------------|
| `DROPDOWN_BTN_WIDTH` | 16 | Width of dropdown arrow button area |
| `DROPDOWN_MAX_VISIBLE` | 8 | Maximum visible items in popup list |
## Exported Symbols
Matches prefixes: `_widgetDraw*`, `_widgetDropdown*`, `_widgetMax*`,
`_widgetNavigate*`, `_widgetPaint*`.
## Files
| File | Description |
|------|-------------|
| `listHelp.h` | Public API header |
| `listHelp.c` | Complete implementation |
| `Makefile` | Builds `bin/libs/listhelp.lib` + dep file |
## Source Files
- `listHelp.h` -- public API
- `listHelp.c` -- implementation
## Build
```
make # builds bin/libs/listhelp.lib + listhelp.dep
make clean # removes objects, library, and dep file
```
Depends on: `libtasks.lib`, `libdvx.lib` (via listhelp.dep).
`make -C src/libs/kpunch/listhelp` (invoked by the top-level `make`).

View file

@ -118,7 +118,7 @@ int32_t widgetMaxItemLen(const char **items, int32_t count) {
// Returns -1 for unrecognized keys so callers can check whether the
// key was consumed.
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
if (key == (0x50 | 0x100)) {
if (key == KEY_DOWN) {
// Down arrow
if (current < count - 1) {
return current + 1;
@ -127,7 +127,7 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t
return current < 0 ? 0 : current;
}
if (key == (0x48 | 0x100)) {
if (key == KEY_UP) {
// Up arrow
if (current > 0) {
return current - 1;
@ -136,23 +136,23 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t
return current < 0 ? 0 : current;
}
if (key == (0x47 | 0x100)) {
if (key == KEY_HOME) {
// Home
return 0;
}
if (key == (0x4F | 0x100)) {
if (key == KEY_END) {
// End
return count - 1;
}
if (key == (0x51 | 0x100)) {
if (key == KEY_PGDN) {
// Page Down
int32_t n = current + pageSize;
return n >= count ? count - 1 : n;
}
if (key == (0x49 | 0x100)) {
if (key == KEY_PGUP) {
// Page Up
int32_t n = current - pageSize;
return n < 0 ? 0 : n;

View file

@ -33,17 +33,20 @@
.h1 List Helper Library
Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView, TreeView). Provides dropdown arrow rendering, item measurement, keyboard navigation, popup geometry calculation, and popup list painting.
Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides dropdown arrow rendering, item measurement, keyboard navigation, popup geometry calculation, popup list painting, type-ahead search, and popup scrollbar hit testing.
Header: listhelp/listHelp.h
Loaded as: bin/libs/listhelp.lib
.h2 Constants
.table
Constant Value Description
-------- ----- -----------
DROPDOWN_BTN_WIDTH 16 Width of the dropdown arrow button in pixels.
DROPDOWN_MAX_VISIBLE 8 Maximum number of items visible in a popup list.
Constant Value Description
-------- ----- -----------
DROPDOWN_BTN_WIDTH 16 Width of the dropdown arrow button in pixels.
DROPDOWN_MAX_VISIBLE 8 Maximum number of items visible in a popup list before scrolling.
POPUP_SCROLLBAR_W SCROLLBAR_WIDTH Popup vertical scrollbar width (mirrors the core window-manager scrollbar width).
.endtable
.h2 widgetDrawDropdownArrow
@ -100,7 +103,7 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current,
pageSize Number of visible items (used for PgUp/PgDn step size).
.endtable
Returns the new index, clamped to [0, count-1].
Returns the new index, clamped to [0, count-1]. Returns -1 for unrecognized keys so callers can detect whether the key was consumed.
.h2 widgetDropdownPopupRect
@ -129,7 +132,7 @@ The popup is sized to show up to DROPDOWN_MAX_VISIBLE items.
.h2 widgetPaintPopupList
Render a popup item list with highlight, scrolling, and beveled border.
Render a popup item list with highlight, scrolling, and beveled border. When itemCount exceeds DROPDOWN_MAX_VISIBLE, draws a vertical scrollbar on the right edge sized according to scrollPos.
.code
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops,
@ -155,3 +158,50 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops,
hoverIdx Index of the highlighted (hovered/selected) item, or -1 for none.
scrollPos Index of the first visible item (scroll offset).
.endtable
.h2 widgetTypeAheadSearch
Case-insensitive type-ahead search. Scans items forward from currentIdx + 1, wrapping around, for the next item whose first character matches ch. Used by list and dropdown widgets to jump to an item based on a single keystroke.
.code
int32_t widgetTypeAheadSearch(char ch, const char **items,
int32_t itemCount, int32_t currentIdx);
.endcode
.table
Parameter Description
--------- -----------
ch Character to match (case-insensitive).
items Array of null-terminated item strings.
itemCount Number of items in the array.
currentIdx Currently selected index (search starts at currentIdx + 1 and wraps).
.endtable
Returns the index of the next matching item, or -1 if no match is found or itemCount is not positive.
.h2 widgetPopupScrollbarClick
Hit-test a popup scrollbar click. Tests whether the (x, y) coordinate falls on the popup's vertical scrollbar track; if so, updates scrollPos for up/down arrow clicks or page-up/page-down clicks in the trough. No-op on the thumb itself (callers handle drag separately).
.code
bool widgetPopupScrollbarClick(int32_t x, int32_t y,
int32_t popX, int32_t popY, int32_t popW, int32_t popH,
int32_t itemCount, int32_t visibleItems,
int32_t *scrollPos);
.endcode
.table
Parameter Description
--------- -----------
x Click X in the popup's coordinate space.
y Click Y in the popup's coordinate space.
popX Popup X origin.
popY Popup Y origin.
popW Popup width.
popH Popup height.
itemCount Total number of items.
visibleItems Number of visible rows in the popup.
scrollPos [in/out] Current scroll offset; updated in place when the click hits the scrollbar.
.endtable
Returns true if the click landed on the scrollbar (even if scrollPos was not changed, for example clicking the thumb), false if the click was in the item list area. When itemCount is less than or equal to DROPDOWN_MAX_VISIBLE, always returns false (there is no scrollbar).

View file

@ -1,428 +1,27 @@
# Packet -- Reliable Serial Transport with HDLC Framing
# packet -- Reliable HDLC Transport
Packetized serial transport providing reliable, ordered delivery over an
unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT
error detection, and a Go-Back-N sliding window protocol for automatic
retransmission.
A reliable, ordered serial-transport layer. HDLC-style byte stuffing
(FLAG=0x7E, ESC=0x7D), CRC-16-CCITT integrity, and Go-Back-N sliding-
window retransmission over any byte-oriented link. Sits on top of the
rs232 driver (or the Linux socket shim used by the proxy).
This layer sits on top of an already-open rs232 COM port. It does not
open or close the serial port itself.
## Documentation
Full reference is in the combined serial stack doc:
`../serial.dhs` (the `packet` section). It compiles into the DVX System
Reference -- open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
## Architecture
Topics covered: connection open/close, send/poll, frame format, window
size, retransmit timer, CRC polynomial and initial value, error codes,
callback contract.
```
Application
|
| pktSend() queue a packet for reliable delivery
| pktPoll() receive, process ACKs/NAKs, check retransmit timers
|
[Packet Layer] framing, CRC, sequencing, sliding window ARQ
|
[rs232] raw byte I/O via ISR-driven ring buffers
|
UART
```
## Source Files
The packet layer adds framing, error detection, and reliability to the
raw byte stream provided by rs232. The caller provides a receive callback
that is invoked synchronously from `pktPoll()` for each complete, CRC-verified,
in-order data packet.
- `packet.h` -- public API and error codes
- `packet.c` -- implementation (framing, CRC, sliding window)
## Build
## Frame Format
Before byte stuffing, each frame has the following layout:
```
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
```
| Field | Size | Description |
|-----------|-----------|----------------------------------------------|
| `0x7E` | 1 byte | Flag byte -- frame delimiter |
| `SEQ` | 1 byte | Sequence number (wrapping uint8_t, 0-255) |
| `TYPE` | 1 byte | Frame type (DATA, ACK, NAK, RST) |
| `LEN` | 1 byte | Payload length (0-255) |
| Payload | 0-255 | Application data |
| `CRC` | 2 bytes | CRC-16-CCITT, little-endian, over SEQ..payload |
The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the
minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte
payload) is 260 bytes before byte stuffing.
### Frame Types
| Type | Value | Description |
|--------|-------|----------------------------------------------------|
| `DATA` | 0x00 | Carries application payload |
| `ACK` | 0x01 | Cumulative acknowledgment -- next expected sequence |
| `NAK` | 0x02 | Negative ack -- request retransmit from this seq |
| `RST` | 0x03 | Connection reset -- clear all state |
### Byte Stuffing
HDLC transparency encoding ensures the flag byte (0x7E) and escape byte
(0x7D) never appear in the frame body. Within the frame data (everything
between flags), these two bytes are escaped by prefixing with 0x7D and
XORing the original byte with 0x20:
- `0x7E` becomes `0x7D 0x5E`
- `0x7D` becomes `0x7D 0x5D`
In the worst case, every byte in the frame is escaped, doubling the wire
size. In practice, these byte values are uncommon in typical data and the
overhead is minimal.
### Why HDLC Framing
HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit
variable-length frames on a raw byte stream. The 0x7E flag byte
unambiguously marks frame boundaries. This is proven, lightweight, and
requires zero buffering at the framing layer.
The alternative -- length-prefixed framing -- is fragile on noisy links
because a corrupted length field permanently desynchronizes the receiver.
With HDLC framing, the receiver can always resynchronize by hunting for
the next flag byte.
## CRC-16-CCITT
Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value
0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is
stored little-endian in the frame (CRC low byte first, then CRC high byte).
The CRC is computed via a 256-entry lookup table (512 bytes of `.rodata`).
Table-driven CRC is approximately 10x faster than bit-by-bit computation
on a 486 -- a worthwhile trade for a function called on every frame
transmitted and received.
## Go-Back-N Sliding Window Protocol
### Why Go-Back-N
Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not
need an out-of-order reassembly buffer and only tracks a single expected
sequence number. This works well for the low bandwidth-delay product of a
serial link. On a 115200 bps local connection, the round-trip time is
negligible, so the window rarely fills.
Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy
links, but serial links are nearly lossless. The CRC check is primarily a
safety net for electrical noise, not a routine error recovery mechanism.
### Protocol Details
The sliding window is configurable from 1 to 8 slots (default 4).
Sequence numbers are 8-bit unsigned integers that wrap naturally at 256.
The sequence space (256) is much larger than 2x the maximum window (16),
so there is no ambiguity between old and new frames.
**Sender behavior:**
- Assigns a monotonically increasing sequence number to each DATA frame
- Retains a copy of each sent frame in a retransmit slot until it is
acknowledged
- When the window is full (`txCount >= windowSize`), blocks or returns
`PKT_ERR_TX_FULL` depending on the `block` parameter
**Receiver behavior:**
- Accepts frames strictly in order (`seq == rxExpectSeq`)
- On in-order delivery, increments `rxExpectSeq` and sends an ACK
carrying the new expected sequence number (cumulative acknowledgment)
- Out-of-order frames within the window trigger a NAK for the expected
sequence number
- Duplicate and out-of-window frames are silently discarded
**ACK processing:**
- ACKs carry the next expected sequence number (cumulative)
- On receiving an ACK, the sender frees all retransmit slots with
sequence numbers less than the ACK's sequence number
**NAK processing:**
- A NAK requests retransmission from a specific sequence number
- The sender retransmits that frame AND all subsequent unacknowledged
frames (the Go-Back-N property)
- Each retransmitted slot has its timer reset
**RST processing:**
- Resets all sequence numbers and buffers to zero on both sides
- The remote side also sends a RST in response
### Timer-Based Retransmission
Each retransmit slot tracks the time it was last (re)transmitted. If
500ms elapses without an ACK, the slot is retransmitted and the timer
is reset. This handles the case where an ACK or NAK was lost on the
wire -- without this safety net, the connection would stall permanently.
The 500ms timeout is conservative for a local serial link (RTT is under
1ms) but accounts for the remote side being busy processing. On BBS
connections through the Linux proxy, the round-trip includes TCP latency,
making the generous timeout appropriate.
### Receive State Machine
Incoming bytes from the serial port are fed through a three-state HDLC
deframing state machine:
| State | Description |
|----------|------------------------------------------------------|
| `HUNT` | Discarding bytes until a flag (0x7E) is seen |
| `ACTIVE` | Accumulating frame bytes; flag ends frame, ESC escapes |
| `ESCAPE` | Previous byte was 0x7D; XOR this byte with 0x20 |
The flag byte serves double duty: it ends the current frame AND starts
the next one. Back-to-back frames share a single flag byte, saving
bandwidth. A frame is only processed if it meets the minimum size
requirement (5 bytes), so spurious flags produce harmless zero-length
"frames" that are discarded.
## API Reference
### Types
```c
// Receive callback -- called for each verified, in-order data packet
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
// Opaque connection handle
typedef struct PktConnS PktConnT;
```
### Constants
| Name | Value | Description |
|------------------------|-------|--------------------------------------|
| `PKT_MAX_PAYLOAD` | 255 | Maximum payload bytes per packet |
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
| `PKT_SUCCESS` | 0 | Success |
| `PKT_ERR_INVALID_PORT` | -1 | Invalid COM port |
| `PKT_ERR_NOT_OPEN` | -2 | Connection not open |
| `PKT_ERR_ALREADY_OPEN` | -3 | Connection already open |
| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block |
| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow |
| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter |
| `PKT_ERR_TX_FULL` | -7 | Transmit window full (non-blocking) |
| `PKT_ERR_NO_DATA` | -8 | No data available |
| `PKT_ERR_DISCONNECTED` | -9 | Serial port disconnected or error |
### Functions
#### pktOpen
```c
PktConnT *pktOpen(int com, int windowSize,
PktRecvCallbackT callback, void *callbackCtx);
```
Creates a packetized connection over an already-open COM port.
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `windowSize` -- sliding window size (1-8), or 0 for the default (4)
- `callback` -- called from `pktPoll()` for each received, verified,
in-order data packet. The `data` pointer is valid only during the
callback.
- `callbackCtx` -- user pointer passed through to the callback
Returns a connection handle, or `NULL` on failure (allocation error).
#### pktClose
```c
void pktClose(PktConnT *conn);
```
Frees the connection state. Does **not** close the underlying COM port.
The caller is responsible for calling `rs232Close()` separately.
#### pktSend
```c
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
```
Sends a data packet. `len` must be in the range 1 to `PKT_MAX_PAYLOAD`
(255). The data is copied into a retransmit slot before transmission, so
the caller can reuse its buffer immediately.
- `block = true` -- If the transmit window is full, polls internally
(calling `pktPoll()` in a tight loop) until an ACK frees a slot. Returns
`PKT_ERR_DISCONNECTED` if the serial port drops during the wait.
- `block = false` -- Returns `PKT_ERR_TX_FULL` immediately if the window
is full.
Returns `PKT_SUCCESS` on success.
#### pktPoll
```c
int pktPoll(PktConnT *conn);
```
The main work function. Must be called frequently (every iteration of
your main loop or event loop). It performs three tasks:
1. **Drain serial RX** -- reads all available bytes from the rs232 port
and feeds them through the HDLC deframing state machine
2. **Process frames** -- verifies CRC, handles DATA/ACK/NAK/RST frames,
delivers data packets to the callback
3. **Check retransmit timers** -- resends any slots that have timed out
Returns the number of DATA packets delivered to the callback this call,
or `PKT_ERR_DISCONNECTED` if the serial port returned an error, or
`PKT_ERR_INVALID_PARAM` if `conn` is NULL.
The callback is invoked synchronously, so the caller should be prepared
for re-entrant calls to `pktSend()` from within the callback.
#### pktReset
```c
int pktReset(PktConnT *conn);
```
Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST
frame to the remote side so it resets as well. Useful for recovering from
a desynchronized state.
#### pktCanSend
```c
bool pktCanSend(PktConnT *conn);
```
Returns `true` if there is room in the transmit window for another
packet. Useful for non-blocking send loops to avoid calling `pktSend()`
when it would return `PKT_ERR_TX_FULL`.
#### pktGetPending
```c
int pktGetPending(PktConnT *conn);
```
Returns the number of unacknowledged packets currently in the transmit
window. Ranges from 0 (all sent packets acknowledged) to `windowSize`
(window full). Useful for throttling sends and monitoring link health.
## Usage Example
```c
#include "packet.h"
#include "../rs232/rs232.h"
void onPacket(void *ctx, const uint8_t *data, int len) {
// process received packet -- data is valid only during this callback
}
int main(void) {
// Open serial port first (packet layer does not manage it)
rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
// Create packet connection with default window size (4)
PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);
// Send a packet (blocking -- waits for window space if needed)
uint8_t msg[] = "Hello, packets!";
pktSend(conn, msg, sizeof(msg), true);
// Main loop -- must call pktPoll() frequently
while (1) {
int delivered = pktPoll(conn);
if (delivered == PKT_ERR_DISCONNECTED) {
break;
}
// delivered = number of packets received this iteration
}
pktClose(conn);
rs232Close(RS232_COM1);
return 0;
}
```
### Non-Blocking Send Pattern
```c
// Send as fast as the window allows, doing other work between sends
while (bytesLeft > 0) {
pktPoll(conn); // process ACKs, free window slots
if (pktCanSend(conn)) {
int chunk = bytesLeft;
if (chunk > PKT_MAX_PAYLOAD) {
chunk = PKT_MAX_PAYLOAD;
}
if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) {
offset += chunk;
bytesLeft -= chunk;
}
}
// do other work here (update UI, check for cancel, etc.)
}
// Drain remaining ACKs
while (pktGetPending(conn) > 0) {
pktPoll(conn);
}
```
## Internal Data Structures
### Connection State (PktConnT)
The connection handle contains:
- **COM port index** and **window size** (configuration)
- **Callback** function pointer and context
- **TX state**: next sequence to assign, oldest unacked sequence, array
of retransmit slots, count of slots in use
- **RX state**: next expected sequence, deframing state machine state,
frame accumulation buffer
### Retransmit Slots (TxSlotT)
Each slot holds a copy of the sent payload, its sequence number, payload
length, and a `clock_t` timestamp of when it was last transmitted. The
retransmit timer compares this timestamp against the current time to
detect timeout.
## Building
```
make # builds ../lib/libpacket.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/packet/`, the library in `../lib/`.
Requires `librs232.a` at link time (for `rs232Read()` and `rs232Write()`).
## Files
- `packet.h` -- Public API header (types, constants, function prototypes)
- `packet.c` -- Complete implementation (framing, CRC, ARQ, state machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
- `rs232/` -- Serial port I/O (must be linked: `-lrs232`)
## Used By
- `seclink/` -- Secure serial link (adds channel multiplexing and encryption)
- `proxy/` -- Linux serial proxy (uses a socket-based adaptation)
`make -C src/libs/kpunch/serial/packet` (invoked by the top-level `make`
via the serial subdirectory).

View file

@ -73,14 +73,21 @@
// Maximum stuffed frame size (worst case: every byte stuffed + flag)
#define MAX_STUFFED_SIZE (1 + MAX_FRAME_SIZE * 2)
// Slack beyond one max-size stuffed frame -- absorbs short bursts arriving
// between pktPoll calls so we don't drop bytes while processing the current
// frame.
#define RX_BUF_SLACK 64
// Receive buffer must hold at least one max-size stuffed frame
#define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64)
#define RX_BUF_SIZE (MAX_STUFFED_SIZE + RX_BUF_SLACK)
// Retransmit timeout. 500ms is conservative for a local serial link (RTT
// is < 1ms) but accounts for the remote side being busy. On a real BBS
// connection through the proxy, the round-trip includes TCP latency.
#define RETRANSMIT_TIMEOUT_MS 500
// CRC-16-CCITT initial register value (RFC 1662 / HDLC convention).
#define CRC_INIT_VALUE 0xFFFF
// Receive state machine: three states for HDLC deframing.
// HUNT: discarding bytes until a flag (0x7E) is seen -- sync acquisition.
// ACTIVE: accumulating frame bytes, watching for flag (end of frame) or
@ -209,7 +216,7 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq);
// costs 512 bytes of .rodata -- a worthwhile trade for a function called
// on every frame received and transmitted.
static uint16_t crcCalc(const uint8_t *data, int len) {
uint16_t crc = 0xFFFF;
uint16_t crc = CRC_INIT_VALUE;
for (int i = 0; i < len; i++) {
crc = (crc << 8) ^ sCrcTable[(crc >> 8) ^ data[i]];

View file

@ -1,417 +1,28 @@
# RS232 -- ISR-Driven Serial Port Library for DJGPP
# rs232 -- Interrupt-Driven UART Driver
Interrupt-driven UART communication library supporting up to 4 simultaneous
COM ports with ring buffers and hardware/software flow control. Targets
486-class DOS hardware running under DJGPP/DPMI.
Low-level serial driver for up to 4 simultaneous COM ports. Handles
UART type detection (8250 / 16450 / 16550 / 16550A), IRQ sharing, ring
buffering, hardware and software flow control, and auto-detection of
port base address and IRQ from BIOS data. This is the bottom layer of
the DVX serial stack.
Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License),
stripped to DJGPP-only codepaths and restyled for the DVX project.
## Documentation
Full reference is in the combined serial stack doc:
`../serial.dhs` (the `rs232` section). It compiles into the DVX System
Reference -- open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
## Architecture
Topics covered: open/close, read/write, baud rates, handshake modes,
ring-buffer capacity, flow-control watermarks, UART register map,
error codes, and the DMA / polling performance trade-offs.
The library is built around a single shared ISR (`comGeneralIsr`) that
services all open COM ports. This design is necessary because COM1/COM3
typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that
polls all ports avoids the complexity of per-IRQ dispatch.
## Source Files
```
Application
|
| rs232Read() non-blocking drain from RX ring buffer
| rs232Write() blocking polled write directly to UART THR
| rs232WriteBuf() non-blocking write into TX ring buffer
|
[Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing
|
[ISR] comGeneralIsr -- shared handler for all open ports
|
[UART] 8250 / 16450 / 16550 / 16550A hardware
```
- `rs232.h` -- public API and error codes
- `rs232.c` -- implementation (ISRs, ring buffers, UART detection)
### ISR Design
## Build
The ISR follows a careful protocol to remain safe under DPMI while
keeping the system responsive:
1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry
2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through
3. **Loop** over all open ports, draining each UART's pending interrupt
conditions (data ready, TX hold empty, modem status, line status)
4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET
This mask-then-STI pattern is standard for slow device ISRs on PC
hardware. It prevents the same IRQ from re-entering while allowing the
system timer and keyboard to function during UART processing.
### Ring Buffers
Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that
head/tail wraparound is a single AND operation (bitmask indexing) rather
than an expensive modulo -- critical for ISR-speed code on a 486.
The buffers use a one-slot-wasted design to distinguish full from empty:
`head == tail` means empty, `(head + 1) & MASK == tail` means full.
### Flow Control
Flow control operates entirely within the ISR using watermark thresholds.
When the RX buffer crosses 80% full, the ISR signals the remote side to
stop sending; when it drops below 20%, the ISR allows the remote to
resume. This prevents buffer overflow without any application involvement.
Three modes are supported:
| Mode | Stop Signal | Resume Signal |
|------------|-------------------|-------------------|
| XON/XOFF | Send XOFF (0x13) | Send XON (0x11) |
| RTS/CTS | Deassert RTS | Assert RTS |
| DTR/DSR | Deassert DTR | Assert DTR |
On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR
modem status lines to pause and resume transmission from the TX ring
buffer.
### DPMI Memory Locking
The ISR code and all per-port state structures (`sComPorts` array) are
locked in physical memory via `__dpmi_lock_linear_region`. This prevents
page faults during interrupt handling -- a hard requirement for any ISR
running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.).
An IRET wrapper is allocated by DPMI to handle the real-mode to
protected-mode transition on hardware interrupt entry.
## UART Type Detection
`rs232GetUartType()` probes the UART hardware to identify the chip:
1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to
UART register 7 and reads them back. The 8250 lacks this register, so
readback fails. If both values read back correctly, the UART is at
least a 16450.
2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register),
then reads bits 7:6 of the IIR (Interrupt Identification Register):
- `0b11` = 16550A (working 16-byte FIFO)
- `0b10` = 16550 (broken FIFO -- present in hardware but unusable)
- `0b00` = 16450 (no FIFO at all)
The original FCR value is restored after probing.
| Constant | Value | Description |
|---------------------|-------|----------------------------------------|
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
| `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch |
| `RS232_UART_16450` | 2 | Scratch register present, no FIFO |
| `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) |
| `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) |
On 16550A UARTs, the FIFO trigger threshold is configurable via
`rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The
default is 14, which minimizes interrupt overhead at high baud rates.
## IRQ Auto-Detection
When `rs232Open()` is called without a prior `rs232SetIrq()` override,
the library auto-detects the UART's IRQ by:
1. Saving the current PIC interrupt mask registers (IMR)
2. Enabling all IRQ lines on both PICs
3. Generating a TX Hold Empty interrupt on the UART
4. Reading the PIC's Interrupt Request Register (IRR) to see which line
went high
5. Disabling the interrupt, reading IRR again to mask out persistent bits
6. Re-enabling once more to confirm the detection
7. Restoring the original PIC mask
If auto-detection fails (common on virtualized hardware that does not
model the IRR accurately), the library falls back to the default IRQ for
the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4).
The BIOS Data Area (at segment 0x0040) is read to determine each port's
I/O base address. Ports not configured in the BDA are unavailable.
## COM Port Support
| Constant | Value | Default IRQ | Default Base |
|--------------|-------|-------------|--------------|
| `RS232_COM1` | 0 | IRQ 4 | 0x3F8 |
| `RS232_COM2` | 1 | IRQ 3 | 0x2F8 |
| `RS232_COM3` | 2 | IRQ 4 | 0x3E8 |
| `RS232_COM4` | 3 | IRQ 3 | 0x2E8 |
Base addresses are read from the BIOS Data Area at runtime. The default
IRQ values are used only as a fallback when auto-detection fails. Both
the base address and IRQ can be overridden before opening with
`rs232SetBase()` and `rs232SetIrq()`.
## Supported Baud Rates
All standard rates from 50 to 115200 bps are supported. The baud rate
divisor is computed from the standard 1.8432 MHz UART crystal:
| Rate | Divisor | Rate | Divisor |
|--------|---------|--------|---------|
| 50 | 2304 | 4800 | 24 |
| 75 | 1536 | 7200 | 16 |
| 110 | 1047 | 9600 | 12 |
| 150 | 768 | 19200 | 6 |
| 300 | 384 | 38400 | 3 |
| 600 | 192 | 57600 | 2 |
| 1200 | 96 | 115200 | 1 |
| 1800 | 64 | | |
| 2400 | 48 | | |
| 3800 | 32 | | |
Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured
by writing the appropriate LCR (Line Control Register) bits.
## API Reference
### Error Codes
| Constant | Value | Description |
|-------------------------------|-------|--------------------------|
| `RS232_SUCCESS` | 0 | Success |
| `RS232_ERR_UNKNOWN` | -1 | Unknown error |
| `RS232_ERR_NOT_OPEN` | -2 | Port not open |
| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open |
| `RS232_ERR_NO_UART` | -4 | No UART detected at base |
| `RS232_ERR_INVALID_PORT` | -5 | Bad port index |
| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address |
| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number |
| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate |
| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) |
| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character |
| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) |
| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode |
| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold |
| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument |
| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ |
| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed |
### Handshaking Modes
| Constant | Value | Description |
|---------------------------|-------|----------------------|
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
| `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) |
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
### Open / Close
```c
int rs232Open(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake);
```
Opens a COM port. Reads the UART base address from the BIOS data area,
auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and
configures the UART for the specified parameters. Returns `RS232_SUCCESS`
or an error code.
- `com` -- port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
`'S'` (space)
- `stopBits` -- 1 or 2
- `handshake` -- `RS232_HANDSHAKE_*` constant
```c
int rs232Close(int com);
```
Closes the port, disables UART interrupts, removes the ISR, restores the
original interrupt vector, and unlocks DPMI memory (when the last port
closes).
### Read / Write
```c
int rs232Read(int com, char *data, int len);
```
Non-blocking read. Drains up to `len` bytes from the RX ring buffer.
Returns the number of bytes actually read (0 if the buffer is empty).
If flow control is active and the buffer drops below the low-water mark,
the ISR will re-enable receive from the remote side.
```c
int rs232Write(int com, const char *data, int len);
```
Blocking polled write. Sends `len` bytes directly to the UART THR
(Transmit Holding Register), bypassing the TX ring buffer entirely.
Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or
an error code.
```c
int rs232WriteBuf(int com, const char *data, int len);
```
Non-blocking buffered write. Copies as many bytes as will fit into the
TX ring buffer. The ISR drains the TX buffer to the UART automatically.
Returns the number of bytes actually queued. If the buffer is full, some
bytes may be dropped.
### Buffer Management
```c
int rs232ClearRxBuffer(int com);
int rs232ClearTxBuffer(int com);
```
Discard all data in the receive or transmit ring buffer by resetting
head and tail pointers to zero.
### Getters
```c
int rs232GetBase(int com); // UART I/O base address
int32_t rs232GetBps(int com); // Current baud rate
int rs232GetCts(int com); // CTS line state (0 or 1)
int rs232GetData(int com); // Data bits setting
int rs232GetDsr(int com); // DSR line state (0 or 1)
int rs232GetDtr(int com); // DTR line state (0 or 1)
int rs232GetHandshake(int com); // Handshaking mode
int rs232GetIrq(int com); // IRQ number
int rs232GetLsr(int com); // Line Status Register
int rs232GetMcr(int com); // Modem Control Register
int rs232GetMsr(int com); // Modem Status Register
char rs232GetParity(int com); // Parity setting ('N','O','E','M','S')
int rs232GetRts(int com); // RTS line state (0 or 1)
int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer
int rs232GetStop(int com); // Stop bits setting
int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer
int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
```
Most getters return cached register values from the per-port state
structure, avoiding unnecessary I/O port reads. `rs232GetUartType()`
actively probes the hardware (see UART Type Detection above).
### Setters
```c
int rs232Set(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake);
```
Reconfigure all port parameters at once. The port must already be open.
```c
int rs232SetBase(int com, int base); // Override I/O base (before open)
int rs232SetBps(int com, int32_t bps); // Change baud rate
int rs232SetData(int com, int dataBits); // Change data bits
int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14)
int rs232SetHandshake(int com, int handshake); // Change flow control mode
int rs232SetIrq(int com, int irq); // Override IRQ (before open)
int rs232SetMcr(int com, int mcr); // Write Modem Control Register
int rs232SetParity(int com, char parity); // Change parity
int rs232SetRts(int com, bool rts); // Assert/deassert RTS
int rs232SetStop(int com, int stopBits); // Change stop bits
```
## Usage Example
```c
#include "rs232.h"
int main(void) {
// Open COM1 at 115200 8N1, no flow control
int rc = rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
if (rc != RS232_SUCCESS) {
return 1;
}
// Identify the UART chip
int uartType = rs232GetUartType(RS232_COM1);
// uartType == RS232_UART_16550A on most 486+ systems
// Enable 16550A FIFO with trigger at 14 bytes
if (uartType == RS232_UART_16550A) {
rs232SetFifoThreshold(RS232_COM1, 14);
}
// Blocking send
rs232Write(RS232_COM1, "Hello\r\n", 7);
// Non-blocking receive loop
char buf[128];
int n;
while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) {
// process buf[0..n-1]
}
rs232Close(RS232_COM1);
return 0;
}
```
## Implementation Notes
- The single shared ISR handles all four COM ports. On entry it disables
UART interrupts for all open ports on the PIC, then re-enables CPU
interrupts (STI) so higher-priority devices (timer, keyboard) are
serviced promptly.
- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing
for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX).
- Flow control watermarks are at 80% (assert stop) and 20% (deassert
stop) of buffer capacity. These percentages are defined as compile-time
constants and apply to both RX and TX directions.
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring
buffers, and port state in physical memory. The ISR code region is
locked for 2048 bytes starting at the `comGeneralIsr` function address.
- `rs232Write()` is a blocking polled write that bypasses the TX ring
buffer entirely. It writes directly to the UART THR register, polling
LSR for readiness between each byte. `rs232WriteBuf()` is the
non-blocking alternative that queues into the TX ring buffer for ISR
draining.
- Per-port state is stored in a static array of `Rs232StateT` structures
(`sComPorts[4]`). This array is locked in physical memory alongside the
ISR code.
- The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's
far pointer API (`_farpeekw`) to obtain port base addresses at runtime.
## Building
```
make # builds ../lib/librs232.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/rs232/`, the library in `../lib/`.
## Files
- `rs232.h` -- Public API header
- `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O)
- `Makefile` -- DJGPP cross-compilation build rules
## Used By
- `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ)
- `seclink/` -- Secure serial link (opens and closes the COM port)
- `proxy/` -- Linux serial proxy (uses a socket-based shim of this API)
`make -C src/libs/kpunch/serial/rs232` (invoked by the top-level `make`
via the serial subdirectory).

View file

@ -103,6 +103,20 @@
#define IRQ_MAX 15
#define IRQ_NONE 0xFF
// Max valid UART port base (12-bit I/O address).
#define UART_MAX_BASE 0xFFF
// BIOS data area at 0040:0000 holds up to 4 COM port base addresses as
// 16-bit words at offsets 0x400-0x407 (linear, in DOS low memory).
#define BIOS_COM_DATA_AREA 0x400
// UART type detection constants.
#define UART_SCRATCH_TEST_A 0xA5 // first value written to UART_SCR
#define UART_SCRATCH_TEST_B 0x5A // second value written to UART_SCR
#define IIR_FIFO_STATUS_MASK 0xC0 // IIR bits 7:6 encode FIFO capability
#define IIR_FIFO_WORKING 0xC0 // 16550A: FIFO enabled and functional
#define IIR_FIFO_PRESENT 0x80 // 16550: FIFO advertised but broken
// PIC registers
#define INT_VECTOR_OFFSET 8
#define PIC_MASTER 0x20
@ -815,7 +829,7 @@ int rs232GetBase(int com) {
if (com < COM_MIN || com > COM_MAX) {
return RS232_ERR_INVALID_PORT;
}
if (port->base < 1 || port->base > 0xFFF) {
if (port->base < 1 || port->base > UART_MAX_BASE) {
return RS232_ERR_INVALID_BASE;
}
@ -1073,14 +1087,14 @@ int rs232GetUartType(int com) {
}
// Test scratch register -- 8250 lacks it
outportb(port->base + UART_SCR, 0xA5);
outportb(port->base + UART_SCR, UART_SCRATCH_TEST_A);
scratch = inportb(port->base + UART_SCR);
if (scratch != 0xA5) {
if (scratch != UART_SCRATCH_TEST_A) {
return RS232_UART_8250;
}
outportb(port->base + UART_SCR, 0x5A);
outportb(port->base + UART_SCR, UART_SCRATCH_TEST_B);
scratch = inportb(port->base + UART_SCR);
if (scratch != 0x5A) {
if (scratch != UART_SCRATCH_TEST_B) {
return RS232_UART_8250;
}
@ -1092,10 +1106,10 @@ int rs232GetUartType(int com) {
outportb(port->base + UART_FCR, port->fcr);
// Check IIR bits 7:6 for FIFO status
switch (iir & 0xC0) {
case 0xC0:
switch (iir & IIR_FIFO_STATUS_MASK) {
case IIR_FIFO_WORKING:
return RS232_UART_16550A;
case 0x80:
case IIR_FIFO_PRESENT:
return RS232_UART_16550;
default:
return RS232_UART_16450;
@ -1124,7 +1138,7 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
// stores up to 4 COM port base addresses as 16-bit words at 40:00-40:07.
// This is more reliable than hardcoding 0x3F8/0x2F8 because BIOS setup
// may have remapped ports or detected them in a different order.
if (rs232SetBase(com, _farpeekw(_dos_ds, 0x400 + (com << 1))) != RS232_SUCCESS) {
if (rs232SetBase(com, _farpeekw(_dos_ds, BIOS_COM_DATA_AREA + (com << 1))) != RS232_SUCCESS) {
return RS232_ERR_NO_UART;
}

View file

@ -1,450 +1,27 @@
# SecLink -- Secure Serial Link Library
# seclink -- Secure Serial Link
SecLink is the top-level API for the DVX serial/networking stack. It
composes three lower-level libraries into a single interface for reliable,
optionally encrypted, channel-multiplexed serial communication:
The top-level API for the DVX serial stack. Wraps the packet transport
and the security primitives to provide a multi-channel encrypted byte
link: per-packet encryption flag, up to 128 channels multiplexed on a
single physical port, forward-secret DH handshake, and bulk send with
retransmit handled underneath.
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
## Documentation
SecLink adds channel multiplexing and per-packet encryption control on
top of the packet layer's reliable delivery.
Full reference is in the combined serial stack doc:
`../serial.dhs` (the `seclink` section). It compiles into the DVX
System Reference -- open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
Topics covered: open/close, handshake state machine, send/poll, per-
channel callbacks, channel mask, error codes, typical end-to-end usage.
## Architecture
## Source Files
```
Application
|
| secLinkSend() send data on a channel, optionally encrypted
| secLinkPoll() receive, decrypt, deliver to callback
| secLinkHandshake() DH key exchange (blocking)
|
[SecLink] channel header, encrypt/decrypt, key management
|
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
[RS232] ISR-driven UART, 2048-byte ring buffers
|
UART Hardware
```
- `secLink.h` -- public API and error codes
- `secLink.c` -- implementation (handshake, channel mux, bulk send)
### Channel Multiplexing
## Build
SecLink prepends a one-byte header to every packet's payload before
handing it to the packet layer:
```
Bit 7 Bits 6..0
----- ---------
Encrypt Channel (0-127)
```
This allows up to 128 independent logical channels over a single serial
link. Each channel can carry a different type of traffic (terminal data,
file transfer, control messages, etc.) without needing separate framing
or sequencing per stream. The receive callback includes the channel
number so the application can dispatch accordingly.
The encrypt flag (bit 7) tells the receiver whether the payload portion
of this packet is encrypted. The channel header byte itself is always
sent in the clear.
### Mixed Clear and Encrypted Traffic
Unencrypted packets can be sent before or after the DH handshake. This
enables a startup protocol (version negotiation, capability exchange)
before keys are established. Encrypted packets require a completed
handshake -- attempting to send an encrypted packet before the handshake
returns `SECLINK_ERR_NOT_READY`.
On the receive side, encrypted packets arriving before the handshake is
complete are silently dropped. Cleartext packets are delivered regardless
of handshake state.
## Lifecycle
```
secLinkOpen() Open COM port and packet layer
secLinkHandshake() DH key exchange (blocks until both sides complete)
secLinkSend() Send data on a channel (encrypted or cleartext)
secLinkPoll() Receive and deliver packets to callback
secLinkClose() Tear down everything (ciphers, packet, COM port)
```
### Handshake Protocol
The DH key exchange uses the packet layer's reliable delivery, so lost
packets are automatically retransmitted. Both sides can send their public
key simultaneously -- there is no initiator/responder distinction.
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
2. Both sides send their 128-byte public key as a single packet
3. On receiving the remote's public key, each side immediately computes
the shared secret (`remote^private mod p`)
4. Each side derives separate TX and RX cipher keys from the master key
5. Cipher contexts are created and the link transitions to READY state
6. The DH context (containing the private key) is destroyed immediately
**Directional key derivation:**
The side with the lexicographically lower public key uses
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
side uses the reverse assignment. This is critical for CTR mode security:
if both sides used the same key and counter, they would produce identical
keystreams, and XORing two ciphertexts would reveal the XOR of the
plaintexts. The XOR-derived directional keys ensure each direction has a
unique keystream even though both sides start their counters at zero.
**Forward secrecy:**
The DH context (containing the private key and shared secret) is
destroyed immediately after deriving the session cipher keys. Even if
the application's long-term state is compromised later, past session
keys cannot be recovered from memory.
## Payload Size
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
(254 bytes). This is the packet layer's 255-byte maximum minus the
1-byte channel header that SecLink prepends.
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
automatically splits the data into 254-byte chunks and sends each one
with blocking delivery.
## API Reference
### Types
```c
// Receive callback -- delivers plaintext with channel number
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
uint8_t channel);
// Opaque connection handle
typedef struct SecLinkS SecLinkT;
```
The receive callback is invoked from `secLinkPoll()` for each incoming
packet. Encrypted packets are decrypted before delivery -- the callback
always receives plaintext regardless of whether encryption was used on
the wire. The `data` pointer is valid only during the callback.
### Constants
| Name | Value | Description |
|-------------------------|-------|---------------------------------------|
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
### Functions
#### secLinkOpen
```c
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake,
SecLinkRecvT callback, void *ctx);
```
Opens the COM port via rs232, creates the packet layer with default
window size (4), and returns a link handle. The callback is invoked from
`secLinkPoll()` for each received packet (decrypted if applicable).
Returns `NULL` on failure (serial port error, packet layer allocation
error, or memory allocation failure). On failure, all partially
initialized resources are cleaned up.
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
- `stopBits` -- 1 or 2
- `handshake` -- `RS232_HANDSHAKE_*` constant
- `callback` -- receive callback function
- `ctx` -- user pointer passed through to the callback
#### secLinkClose
```c
void secLinkClose(SecLinkT *link);
```
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
destroys the DH context if still present, closes the packet layer, closes
the COM port, zeroes the link structure, and frees memory.
#### secLinkHandshake
```c
int secLinkHandshake(SecLinkT *link);
```
Performs a Diffie-Hellman key exchange. Blocks until both sides have
exchanged public keys and derived cipher keys. The RNG must be seeded
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
Internally:
1. Creates a DH context and generates keys
2. Sends the 128-byte public key via the packet layer (blocking)
3. Polls the packet layer in a loop until the remote's public key arrives
4. Computes the shared secret and derives directional cipher keys
5. Destroys the DH context (forward secrecy)
6. Transitions the link to READY state
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
(DH key generation failure, send failure, or serial disconnect during
the exchange).
#### secLinkSend
```c
int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
uint8_t channel, bool encrypt, bool block);
```
Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel.
- `channel` -- logical channel number (0-127)
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
- `block` -- if `true`, waits for transmit window space. If `false`,
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
**Cipher counter safety:** The function checks transmit window space
BEFORE encrypting the payload. If it encrypted first and then the send
failed, the cipher counter would advance without the data being sent,
permanently desynchronizing the TX cipher state from the remote's RX
cipher. This ordering is critical for correctness.
The channel header byte is prepended to the data, and only the payload
portion (not the header) is encrypted.
Returns `SECLINK_SUCCESS` or an error code.
#### secLinkSendBuf
```c
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
uint8_t channel, bool encrypt);
```
Sends an arbitrarily large buffer by splitting it into
`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all
data is sent. The receiver sees multiple packets on the same channel and
must reassemble if needed.
Returns `SECLINK_SUCCESS` or the first error encountered.
#### secLinkPoll
```c
int secLinkPoll(SecLinkT *link);
```
Delegates to `pktPoll()` to read serial data, process frames, handle
ACKs and retransmits. Received packets are routed through an internal
callback that:
- During handshake: expects a 128-byte DH public key
- When ready: strips the channel header, decrypts the payload if the
encrypt flag is set, and forwards plaintext to the user callback
Returns the number of packets delivered, or a negative error code.
Must be called frequently (every iteration of your main loop).
#### secLinkGetPending
```c
int secLinkGetPending(SecLinkT *link);
```
Returns the number of unacknowledged packets in the transmit window.
Delegates directly to `pktGetPending()`. Useful for non-blocking send
loops to determine when there is room to send more data.
#### secLinkIsReady
```c
bool secLinkIsReady(SecLinkT *link);
```
Returns `true` if the DH handshake is complete and the link is ready for
encrypted communication. Cleartext sends do not require the link to be
ready.
## Usage Examples
### Basic Encrypted Link
```c
#include "secLink.h"
#include "../security/security.h"
void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
// handle received plaintext on 'channel'
}
int main(void) {
// Seed the RNG before handshake
uint8_t entropy[16];
secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, sizeof(entropy));
// Open link on COM1 at 115200 8N1, no flow control
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
RS232_HANDSHAKE_NONE, onRecv, NULL);
if (!link) {
return 1;
}
// DH key exchange (blocks until both sides complete)
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
secLinkClose(link);
return 1;
}
// Send encrypted data on channel 0
const char *msg = "Hello, secure world!";
secLinkSend(link, (const uint8_t *)msg, strlen(msg), 0, true, true);
// Main loop
while (1) {
secLinkPoll(link);
}
secLinkClose(link);
return 0;
}
```
### Mixed Encrypted and Cleartext Channels
```c
#define CHAN_CONTROL 0 // cleartext control channel
#define CHAN_DATA 1 // encrypted data channel
// Cleartext status message (no handshake needed)
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
// Encrypted payload (requires completed handshake)
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
```
### Non-Blocking File Transfer
```c
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
uint8_t channel, bool encrypt) {
int offset = 0;
int bytesLeft = fileSize;
while (bytesLeft > 0) {
secLinkPoll(link); // process ACKs, free window slots
if (secLinkGetPending(link) < 4) { // window has room
int chunk = bytesLeft;
if (chunk > SECLINK_MAX_PAYLOAD) {
chunk = SECLINK_MAX_PAYLOAD;
}
int rc = secLinkSend(link, fileData + offset, chunk,
channel, encrypt, false);
if (rc == SECLINK_SUCCESS) {
offset += chunk;
bytesLeft -= chunk;
}
// SECLINK_ERR_SEND means window full, retry next iteration
}
// Application can do other work here:
// update progress bar, check for cancel, etc.
}
// Drain remaining ACKs
while (secLinkGetPending(link) > 0) {
secLinkPoll(link);
}
return SECLINK_SUCCESS;
}
```
### Blocking Bulk Transfer
```c
// Send an entire file in one call (blocks until complete)
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
```
## Internal State Machine
SecLink maintains a three-state internal state machine:
| State | Value | Description |
|--------------|-------|----------------------------------------------|
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
Transitions:
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
- `HANDSHAKE -> READY`: when the remote's public key is received and
cipher keys are derived
- Any state -> cleanup: when `secLinkClose()` is called
Cleartext packets can be sent and received in any state. Encrypted
packets require `STATE_READY`.
## Building
```
make # builds ../lib/libseclink.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
Link against all four libraries in this order:
```
-lseclink -lpacket -lsecurity -lrs232
```
## Files
- `secLink.h` -- Public API header (types, constants, function prototypes)
- `secLink.c` -- Complete implementation (handshake, send, receive, state
machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
SecLink requires these libraries (all built into `../lib/`):
| Library | Purpose |
|------------------|---------------------------------------------|
| `librs232.a` | Serial port driver (ISR, ring buffers) |
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
`make -C src/libs/kpunch/serial/seclink` (invoked by the top-level `make`
via the serial subdirectory).

View file

@ -1,454 +1,26 @@
# Security -- Diffie-Hellman Key Exchange and XTEA-CTR Cipher
# security -- Crypto Primitives
Cryptographic library providing Diffie-Hellman key exchange, XTEA
symmetric encryption in CTR mode, and a DRBG-based pseudo-random number
generator. Optimized for 486-class DOS hardware running under DJGPP/DPMI.
Compact cryptographic primitives used by the secLink wrapper: 1024-bit
Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR
mode for session encryption, and an XTEA-CTR-based DRBG for session key
and IV generation. No external dependencies.
This library has no dependencies on the serial stack and can be used
independently for any application requiring key exchange, encryption,
or random number generation.
## Documentation
Full reference is in the combined serial stack doc:
`../serial.dhs` (the `security` section). It compiles into the DVX
System Reference -- open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
## Components
Topics covered: DH handshake API, XTEA-CTR encrypt/decrypt, DRBG seeding
and reseeding, remote-key validation range, threat model.
### 1. XTEA Cipher (CTR Mode)
## Source Files
XTEA (eXtended Tiny Encryption Algorithm) is a 64-bit block cipher with a
128-bit key and 32 Feistel rounds. In CTR (counter) mode, it operates as
a stream cipher: an incrementing counter is encrypted with the key to
produce a keystream, which is XOR'd with the plaintext. Because XOR is
its own inverse, the same operation encrypts and decrypts.
- `security.h` -- public API (DH, XTEA-CTR, DRBG)
- `security.c` -- implementation (bignum, Montgomery modexp, XTEA)
**Why XTEA instead of AES or DES:**
## Build
XTEA requires zero lookup tables, no key schedule, and compiles to
approximately 20 instructions per round (shifts, adds, and XORs only).
This makes it ideal for a 486 where the data cache is tiny (8KB) and
AES's 4KB S-boxes would thrash it. DES is similarly table-heavy and has
a complex key schedule. XTEA has no library dependencies -- the entire
cipher fits in about a dozen lines of C. At 32 rounds, XTEA provides
128-bit security with negligible per-byte overhead even on the slowest
target hardware.
**CTR mode properties:**
- Encrypt and decrypt are the same function (XOR is symmetric)
- No padding required -- operates on arbitrary-length data
- Random access possible (set the counter to any value)
- CRITICAL: the same counter value must never be reused with the same key.
Reuse reveals the XOR of two plaintexts. The secLink layer prevents this
by deriving separate TX/RX cipher keys for each direction.
**XTEA block cipher internals:**
The Feistel network uses the golden-ratio constant (delta = 0x9E3779B9)
as a round key mixer. Each round combines the two 32-bit halves using
shifts, additions, and XORs. The delta ensures each round uses a
different effective subkey, preventing slide attacks. No S-boxes or lookup
tables are involved anywhere in the computation.
### 2. Diffie-Hellman Key Exchange (1024-bit)
Uses the RFC 2409 Group 2 safe prime (1024-bit MODP group) with a
generator of 2. Private exponents are 256 bits for fast computation on
486-class hardware.
**Why 1024-bit DH with 256-bit private exponents:**
RFC 2409 Group 2 provides a well-audited, interoperable safe prime.
256-bit private exponents (versus full 1024-bit) reduce the modular
exponentiation from approximately 1024 squarings+multiplies to approximately
256 squarings + approximately 128 multiplies (half the exponent bits are 1 on
average). This makes key generation feasible on a 486 in under a second
rather than minutes. The security reduction is negligible -- Pollard's
rho on a 256-bit exponent requires approximately 2^128 operations, matching
XTEA's key strength.
**Key validation:**
`secDhComputeSecret()` validates that the remote public key is in the
range [2, p-2] to prevent small-subgroup attacks. Keys of 0, 1, or p-1
would produce trivially guessable shared secrets.
**Key derivation:**
The 128-byte shared secret is reduced to a symmetric key via XOR-folding:
each byte of the secret is XOR'd into the output key at position
`i % keyLen`. For a 16-byte XTEA key, each output byte is the XOR of
8 secret bytes, providing thorough mixing. A proper KDF (HKDF, etc.)
would be more rigorous but adds complexity and code size for marginal
benefit in this use case.
### 3. Pseudo-Random Number Generator
XTEA-CTR based DRBG (Deterministic Random Bit Generator). The RNG
encrypts a monotonically increasing 64-bit counter with a 128-bit XTEA
key, producing 8 bytes of pseudorandom output per block. The counter
never repeats (64-bit space is sufficient for any practical session
length), so the output is a pseudorandom stream as long as the key has
sufficient entropy.
**Hardware entropy sources:**
- PIT (Programmable Interval Timer) -- runs at 1.193182 MHz. Its LSBs
change rapidly and provide approximately 10 bits of entropy per read,
depending on timing jitter. Two readings with intervening code execution
provide additional jitter.
- BIOS tick count -- 18.2 Hz timer at real-mode address 0040:046C. Adds
a few more bits of entropy.
Total from hardware: roughly 20 bits of real entropy per call to
`secRngGatherEntropy()`. This is not enough on its own for
cryptographic use but is sufficient to seed the DRBG when supplemented
by user interaction timing (keyboard, mouse jitter).
**Seeding and mixing:**
The seed function (`secRngSeed()`) XOR-folds the entropy into the XTEA
key, derives the initial counter from the key bits, and then generates and
discards 64 bytes to advance past any weak initial output. This discard
step is standard DRBG practice -- it ensures the first bytes the caller
receives do not leak information about the seed material.
Additional entropy can be stirred in at any time via `secRngAddEntropy()`
without resetting the RNG state. This function XOR-folds new entropy into
the key and then re-mixes by encrypting the key with itself, diffusing
the new entropy across all key bits.
Auto-seeding: if `secRngBytes()` is called before `secRngSeed()`, it
automatically gathers hardware entropy and seeds itself as a safety net.
## BigNum Arithmetic
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
stored as 32 x `uint32_t` words in little-endian order. Operations:
| Function | Description |
|----------------|------------------------------------------------------|
| `bnAdd` | Add two bignums, return carry |
| `bnSub` | Subtract two bignums, return borrow |
| `bnCmp` | Compare two bignums (-1, 0, +1) |
| `bnBit` | Test a single bit by index |
| `bnBitLength` | Find the highest set bit position |
| `bnShiftLeft1` | Left-shift by 1, return carry |
| `bnClear` | Zero all words |
| `bnSet` | Set to a 32-bit value (clear upper words) |
| `bnCopy` | Copy from source to destination |
| `bnFromBytes` | Convert big-endian byte array to little-endian words |
| `bnToBytes` | Convert little-endian words to big-endian byte array |
| `bnMontMul` | Montgomery multiplication (CIOS variant) |
| `bnModExp` | Modular exponentiation via Montgomery multiply |
## Montgomery Multiplication
The CIOS (Coarsely Integrated Operand Scanning) variant computes
`a * b * R^(-1) mod m` in a single pass without explicit division by the
modulus. This replaces the expensive modular reduction step (division by a
1024-bit number) with cheaper additions and right-shifts.
For each of the 32 outer iterations (one per word of operand `a`):
1. Accumulate `a[i] * b` into the temporary product `t`
2. Compute the Montgomery reduction factor `u = t[0] * m0inv mod 2^32`
3. Add `u * mod` to `t` and shift right by 32 bits (implicit division)
After all iterations, the result is in the range [0, 2m), so a single
conditional subtraction brings it into [0, m).
**Montgomery constants** (computed once, lazily on first DH use):
- `R^2 mod p` -- computed via 2048 iterations of shift-left-1 with
conditional subtraction. This is the Montgomery domain conversion
factor.
- `-p[0]^(-1) mod 2^32` -- computed via Newton's method (5 iterations,
doubling precision each step: 1->2->4->8->16->32 correct bits). This
is the Montgomery reduction constant.
**Modular exponentiation** uses left-to-right binary square-and-multiply
scanning. For a 256-bit private exponent, this requires approximately 256
squarings plus approximately 128 multiplies (half the bits are 1 on average),
where each operation is a Montgomery multiplication on 32-word numbers.
## Secure Zeroing
Key material (private keys, shared secrets, cipher contexts) is erased
using a volatile-pointer loop:
```c
static void secureZero(void *ptr, int len) {
volatile uint8_t *p = (volatile uint8_t *)ptr;
for (int i = 0; i < len; i++) {
p[i] = 0;
}
}
```
The `volatile` qualifier prevents the compiler from optimizing away the
zeroing as a dead store. Without it, the compiler would see that the
buffer is about to be freed and remove the memset entirely. This is
critical for preventing sensitive key material from lingering in freed
memory where a later `malloc` could expose it.
## Performance
At serial port speeds, XTEA-CTR encryption overhead is minimal:
| Speed | Blocks/sec | CPU Cycles/sec | % of 33 MHz 486 |
|----------|------------|----------------|------------------|
| 9600 | 120 | ~240K | < 1% |
| 57600 | 720 | ~1.4M | ~4% |
| 115200 | 1440 | ~2.9M | ~9% |
DH key exchange takes approximately 0.3 seconds at 66 MHz or 0.6 seconds
at 33 MHz (256-bit private exponent, 1024-bit modulus, Montgomery
multiplication).
## API Reference
### Constants
| Name | Value | Description |
|---------------------|-------|-----------------------------------|
| `SEC_DH_KEY_SIZE` | 128 | DH public key size in bytes |
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size in bytes |
| `SEC_SUCCESS` | 0 | Success |
| `SEC_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived |
| `SEC_ERR_ALLOC` | -3 | Memory allocation failed |
### Types
```c
typedef struct SecDhS SecDhT; // Opaque DH context
typedef struct SecCipherS SecCipherT; // Opaque cipher context
```
### RNG Functions
```c
int secRngGatherEntropy(uint8_t *buf, int len);
```
Reads hardware entropy from the PIT counter and BIOS tick count. Returns
the number of bytes written (up to 8). Provides roughly 20 bits of true
entropy -- not sufficient alone, but enough to seed the DRBG when
supplemented by user interaction timing.
```c
void secRngSeed(const uint8_t *entropy, int len);
```
Initializes the DRBG with the given entropy. XOR-folds the input into
the XTEA key, derives the initial counter, and generates and discards 64
bytes to advance past weak initial output.
```c
void secRngAddEntropy(const uint8_t *data, int len);
```
Mixes additional entropy into the running RNG state without resetting it.
XOR-folds data into the key and re-mixes by encrypting the key with
itself. Use this to stir in keyboard timing, mouse jitter, or other
runtime entropy sources.
```c
void secRngBytes(uint8_t *buf, int len);
```
Generates `len` pseudorandom bytes. Auto-seeds from hardware entropy if
not previously seeded. Produces 8 bytes per XTEA block encryption of the
internal counter.
### Diffie-Hellman Functions
```c
SecDhT *secDhCreate(void);
```
Allocates a new DH context. Returns `NULL` on allocation failure. The
context must be destroyed with `secDhDestroy()` when no longer needed.
```c
int secDhGenerateKeys(SecDhT *dh);
```
Generates a 256-bit random private key and computes the corresponding
1024-bit public key (`g^private mod p`). Lazily initializes Montgomery
constants on first call. The RNG should be seeded before calling this.
```c
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
```
Exports the public key as a big-endian byte array into `buf`. On entry,
`*len` must be at least `SEC_DH_KEY_SIZE` (128). On return, `*len` is
set to 128.
```c
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
```
Computes the shared secret from the remote side's public key
(`remote^private mod p`). Validates the remote key is in range [2, p-2].
Both sides compute this independently and arrive at the same value.
```c
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
```
Derives a symmetric key by XOR-folding the 128-byte shared secret down
to `keyLen` bytes. Each output byte is the XOR of `128/keyLen` input
bytes.
```c
void secDhDestroy(SecDhT *dh);
```
Securely zeroes the entire DH context (private key, shared secret, public
key) and frees the memory. Must be called to prevent key material from
lingering in the heap.
### Cipher Functions
```c
SecCipherT *secCipherCreate(const uint8_t *key);
```
Creates an XTEA-CTR cipher context with the given 16-byte key. The
internal counter starts at zero. Returns `NULL` on allocation failure or
NULL key.
```c
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
```
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the same
function handles both directions. The internal counter advances by one
for every 8 bytes processed (one XTEA block). The counter must never
repeat with the same key; callers are responsible for ensuring this
(secLink handles it by using separate cipher instances per direction).
```c
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
```
Sets the 64-bit nonce/counter to a specific value. Both the nonce
(baseline) and the running counter are set to the same value. Call this
before encrypting if you need a deterministic starting point.
```c
void secCipherDestroy(SecCipherT *c);
```
Securely zeroes the cipher context (key and counter state) and frees the
memory.
## Usage Examples
### Full Key Exchange
```c
#include "security.h"
#include <string.h>
// Seed the RNG
uint8_t entropy[16];
secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, sizeof(entropy));
// Create DH context and generate keys
SecDhT *dh = secDhCreate();
secDhGenerateKeys(dh);
// Export public key to send to remote
uint8_t myPub[SEC_DH_KEY_SIZE];
int pubLen = SEC_DH_KEY_SIZE;
secDhGetPublicKey(dh, myPub, &pubLen);
// ... send myPub to remote, receive remotePub ...
// Compute shared secret and derive a 16-byte XTEA key
secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE);
uint8_t key[SEC_XTEA_KEY_SIZE];
secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE);
secDhDestroy(dh); // private key no longer needed
// Create cipher and encrypt
SecCipherT *cipher = secCipherCreate(key);
uint8_t message[] = "Secret message";
secCipherCrypt(cipher, message, sizeof(message));
// message is now encrypted
// Decrypt (reset counter first, then apply same operation)
secCipherSetNonce(cipher, 0, 0);
secCipherCrypt(cipher, message, sizeof(message));
// message is now plaintext again
secCipherDestroy(cipher);
```
### Standalone Encryption (Without DH)
```c
// XTEA-CTR can be used independently of Diffie-Hellman
uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ };
SecCipherT *c = secCipherCreate(key);
uint8_t data[1024];
// ... fill data ...
secCipherCrypt(c, data, sizeof(data)); // encrypt in place
secCipherDestroy(c);
```
### Random Number Generation
```c
// Seed from hardware
uint8_t hwEntropy[16];
secRngGatherEntropy(hwEntropy, sizeof(hwEntropy));
secRngSeed(hwEntropy, sizeof(hwEntropy));
// Stir in user-derived entropy (keyboard timing, etc.)
uint8_t userEntropy[4];
// ... gather from timing events ...
secRngAddEntropy(userEntropy, sizeof(userEntropy));
// Generate random bytes
uint8_t randomBuf[32];
secRngBytes(randomBuf, sizeof(randomBuf));
```
## Building
```
make # builds ../lib/libsecurity.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/security/`, the library in `../lib/`.
No external dependencies -- the library is self-contained. It uses only
DJGPP's `<pc.h>`, `<sys/farptr.h>`, and `<go32.h>` for hardware entropy
collection (PIT and BIOS tick count access).
## Files
- `security.h` -- Public API header (types, constants, function prototypes)
- `security.c` -- Complete implementation (bignum, Montgomery, DH, XTEA, RNG)
- `Makefile` -- DJGPP cross-compilation build rules
## Used By
- `seclink/` -- Secure serial link (DH handshake, cipher creation, RNG seeding)
`make -C src/libs/kpunch/serial/security` (invoked by the top-level
`make` via the serial subdirectory).

View file

@ -47,6 +47,10 @@
#define BN_WORDS (BN_BITS / 32)
#define BN_BYTES (BN_BITS / 8)
// R^2 mod m initialization uses repeated doubling (shift-left-by-1 + mod).
// Starting from 1, 2*BN_BITS doublings take us to 2^(2*BN_BITS) mod m = R^2.
#define R2_DOUBLE_ITERATIONS (2 * BN_BITS)
#define DH_PRIVATE_BITS 256
#define DH_PRIVATE_BYTES (DH_PRIVATE_BITS / 8)
@ -390,7 +394,7 @@ static uint32_t computeM0Inv(uint32_t m0) {
static void computeR2(BigNumT *r2, const BigNumT *m) {
bnSet(r2, 1);
for (int i = 0; i < 2 * BN_BITS; i++) {
for (int i = 0; i < R2_DOUBLE_ITERATIONS; i++) {
int carry = bnShiftLeft1(r2);
if (carry || bnCmp(r2, m) >= 0) {
bnSub(r2, r2, m);

View file

@ -44,10 +44,35 @@ The DVX serial/networking stack provides reliable, optionally encrypted communic
Loaded as: bin/libs/kpunch/serial/serial.lib
.h2 Layered Architecture
.code
+--------------------------------------------------+
| Application |
| |
| secLinkSend() -- send on channel, opt. encrypt |
| secLinkPoll() -- receive + dispatch |
+--------------------------------------------------+
| secLink (channel multiplex, TX/RX ciphers) |
+--------------------------------------------------+
| packet (HDLC framing, CRC-16, Go-Back-N ARQ) |
+--------------------------------------------------+
| security -- DH key exchange, XTEA-CTR, DRBG |
| (used by secLink during handshake) |
+--------------------------------------------------+
| rs232 (ISR-driven UART, 2048-byte rings) |
+--------------------------------------------------+
| UART hardware (8250 / 16450 / 16550 / 16550A) |
+--------------------------------------------------+
.endcode
The security layer is invoked by secLink during the Diffie-Hellman handshake and for per-packet cipher operations. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.
.link lib.serial.rs232 Layer 1: RS-232 UART Driver
.link lib.serial.packet Layer 2: Packet Transport
.link lib.serial.security Layer 3: Security (DH + XTEA)
.link lib.serial.seclink Layer 4: Secure Link
.link lib.serial.example End-to-End Example
.topic lib.serial.rs232
.title RS-232 UART Driver
@ -69,12 +94,13 @@ Header: rs232/rs232.h
.h2 Port Constants
.table
Constant Value Description
-------- ----- -----------
RS232_COM1 0 First serial port.
RS232_COM2 1 Second serial port.
RS232_COM3 2 Third serial port.
RS232_COM4 3 Fourth serial port.
Constant Value Description
-------- ----- -----------
RS232_COM1 0 First serial port.
RS232_COM2 1 Second serial port.
RS232_COM3 2 Third serial port.
RS232_COM4 3 Fourth serial port.
RS232_NUM_PORTS 4 Total number of supported COM ports.
.endtable
.h2 Handshake Modes
@ -176,13 +202,13 @@ Returns the number of bytes actually read (may be less than len).
.h2 rs232Write
Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Use for small, immediate writes.
Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte. Use for small, immediate writes where order matters (the packet layer uses this for frame transmission).
.code
int rs232Write(int com, const char *data, int len);
.endcode
Returns RS232_SUCCESS or a negative error code.
Returns the number of bytes actually written (may be less than len if software flow control is asserted), or a negative error code (RS232_ERR_INVALID_PORT, RS232_ERR_NOT_OPEN, RS232_ERR_NULL_PTR).
.h2 rs232WriteBuf
@ -196,7 +222,7 @@ Returns the number of bytes actually queued (may be less than len if the buffer
.h2 rs232Set
Change all line parameters on an open port.
Change all line parameters on an open port. Internally calls rs232SetBps, rs232SetData, rs232SetParity, rs232SetStop, and rs232SetHandshake in sequence. Returns RS232_SUCCESS on success or the first negative error code encountered.
.code
int rs232Set(int com, int32_t bps, int dataBits,
@ -209,15 +235,17 @@ int rs232Set(int com, int32_t bps, int dataBits,
Function Returns
-------- -------
rs232GetBase(com) I/O base address.
rs232GetBps(com) Current baud rate.
rs232GetData(com) Data bits setting.
rs232GetParity(com) Parity character ('N', 'O', 'E', 'M', 'S').
rs232GetStop(com) Stop bits setting.
rs232GetHandshake(com) Handshake mode.
rs232GetBps(com) Current baud rate (int32_t).
rs232GetData(com) Data bits setting (5-8).
rs232GetParity(com) Parity character: 'n', 'o', 'e', 'm', or 's' (lowercase).
rs232GetStop(com) Stop bits setting (1 or 2).
rs232GetHandshake(com) Handshake mode (RS232_HANDSHAKE_*).
rs232GetIrq(com) IRQ number.
rs232GetUartType(com) Detected UART type (RS232_UART_*).
.endtable
rs232GetParity returns a lowercase character on success. Callers comparing against uppercase constants must use either both cases or tolower().
.h2 Status Getters
.table
@ -299,10 +327,10 @@ Header: packet/packet.h
PKT_ERR_NOT_OPEN -2 Connection is not open.
PKT_ERR_WOULD_BLOCK -4 Operation would block.
PKT_ERR_OVERFLOW -5 Buffer overflow.
PKT_ERR_INVALID_PARAM -6 Invalid parameter.
PKT_ERR_TX_FULL -7 Transmit window is full.
PKT_ERR_INVALID_PARAM -6 Invalid parameter (NULL handle, bad data pointer, length out of range).
PKT_ERR_TX_FULL -7 Transmit window is full (non-blocking send only).
PKT_ERR_NO_DATA -8 No data available.
PKT_ERR_DISCONNECTED -9 Remote side disconnected.
PKT_ERR_DISCONNECTED -9 Serial port returned a negative read (disconnect/error).
.endtable
.h2 PktRecvCallbackT
@ -324,7 +352,7 @@ typedef void (*PktRecvCallbackT)(void *ctx,
.h2 pktOpen
Open a packetized connection over an already-open COM port.
Open a packetized connection over an already-open COM port. Only one packet connection may exist on a given COM port at a time; a second pktOpen call on the same port returns NULL to avoid silent framing corruption.
.code
PktConnT *pktOpen(int com, int windowSize,
@ -335,16 +363,16 @@ PktConnT *pktOpen(int com, int windowSize,
Parameter Description
--------- -----------
com COM port index (RS232_COM1..RS232_COM4). Must already be open via rs232Open.
windowSize Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW.
callback Called when a complete, CRC-verified packet is received.
windowSize Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW. Values greater than PKT_MAX_WINDOW are clamped.
callback Called when a complete, CRC-verified, in-order packet is received. The data pointer is valid only during the callback.
callbackCtx User pointer passed to the callback.
.endtable
Returns an opaque PktConnT handle, or NULL on failure.
Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure).
.h2 pktClose
Close a packetized connection. Does not close the underlying COM port.
Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed.
.code
void pktClose(PktConnT *conn);
@ -352,7 +380,7 @@ void pktClose(PktConnT *conn);
.h2 pktSend
Send a data packet.
Send a data packet. The payload is copied into an internal retransmit slot before transmission, so the caller may reuse the supplied buffer immediately.
.code
int pktSend(PktConnT *conn, const uint8_t *data,
@ -363,34 +391,36 @@ int pktSend(PktConnT *conn, const uint8_t *data,
Parameter Description
--------- -----------
conn Connection handle from pktOpen.
data Payload bytes (up to PKT_MAX_PAYLOAD).
data Payload bytes (1..PKT_MAX_PAYLOAD).
len Payload length.
block If true, block until transmit window has space. If false, return PKT_ERR_TX_FULL when the window is full.
block If true, poll pktPoll internally until transmit window has space. If false, return PKT_ERR_TX_FULL immediately when the window is full.
.endtable
Returns PKT_SUCCESS or a negative error code.
Returns PKT_SUCCESS on success, PKT_ERR_INVALID_PARAM for NULL/out-of-range input, PKT_ERR_TX_FULL for a non-blocking call with a full window, or PKT_ERR_DISCONNECTED if the serial port drops during a blocking wait.
.h2 pktPoll
Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop.
Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop. The callback is invoked synchronously for each complete, CRC-verified, in-order data frame, so the caller should be prepared for reentrant pktSend calls from within the callback.
.code
int pktPoll(PktConnT *conn);
.endcode
Returns the number of valid data packets delivered to the callback.
Returns the number of valid data packets delivered to the callback this call, or PKT_ERR_INVALID_PARAM if conn is NULL, or PKT_ERR_DISCONNECTED if the serial layer returned a negative read.
.h2 pktReset
Reset the connection state (sequence numbers, buffers) and send a RST frame to the remote side.
Reset the local connection state (sequence numbers, transmit slots, RX state machine) and send a RST frame to the remote side. The remote's RST handler clears its own state and echoes a RST back. Useful for recovering from a desynchronized connection.
.code
int pktReset(PktConnT *conn);
.endcode
Returns PKT_SUCCESS on success or PKT_ERR_INVALID_PARAM if conn is NULL.
.h2 pktCanSend
Check whether there is room in the transmit window for another packet.
Check whether there is room in the transmit window for another packet. Returns false when conn is NULL or the transmit window is full.
.code
bool pktCanSend(PktConnT *conn);
@ -398,12 +428,14 @@ bool pktCanSend(PktConnT *conn);
.h2 pktGetPending
Get the number of unacknowledged packets currently in the transmit window.
Get the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full).
.code
int pktGetPending(PktConnT *conn);
.endcode
Returns the pending count on success, or PKT_ERR_INVALID_PARAM if conn is NULL.
.topic lib.serial.security
.title Security (DH + XTEA)
.toc 1 Security (DH + XTEA)
@ -507,12 +539,14 @@ void secCipherDestroy(SecCipherT *c);
.table
Function Description
-------- -----------
secCipherCreate Allocate a cipher context with a 128-bit key.
secCipherSetNonce Set the initial counter value (nonce). Must be unique per session.
secCipherCrypt Encrypt or decrypt data in place. The counter increments automatically.
secCipherDestroy Free the cipher context.
secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
secCipherSetNonce Set the initial counter value (nonce). Must be unique per session. Both the stored nonce and the running counter are set to (nonceLo, nonceHi).
secCipherCrypt Encrypt or decrypt data in place. The counter increments by one every 8 bytes (one XTEA block).
secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.
.endtable
secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.
.topic lib.serial.seclink
.title Secure Link
.toc 1 Secure Link
@ -534,10 +568,12 @@ Header: seclink/secLink.h
.h2 Constants
.table
Constant Value Description
-------- ----- -----------
SECLINK_MAX_PAYLOAD 254 Maximum plaintext bytes per send (PKT_MAX_PAYLOAD minus 1-byte channel header).
SECLINK_MAX_CHANNEL 127 Highest valid channel number.
Constant Value Description
-------- ----- -----------
SECLINK_MAX_PAYLOAD 254 Maximum plaintext bytes per send (PKT_MAX_PAYLOAD minus 1-byte channel header).
SECLINK_MAX_CHANNEL 127 Highest valid channel number.
SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1).
SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read.
.endtable
.h2 Error Codes
@ -546,12 +582,12 @@ Header: seclink/secLink.h
Constant Value Description
-------- ----- -----------
SECLINK_SUCCESS 0 Operation succeeded.
SECLINK_ERR_PARAM -1 Invalid parameter.
SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length).
SECLINK_ERR_SERIAL -2 RS-232 layer error.
SECLINK_ERR_ALLOC -3 Memory allocation failed.
SECLINK_ERR_HANDSHAKE -4 DH handshake failed.
SECLINK_ERR_HANDSHAKE -4 DH handshake failed (key generation, send, or disconnect during exchange).
SECLINK_ERR_NOT_READY -5 Handshake not yet completed (encrypted send attempted).
SECLINK_ERR_SEND -6 Packet send failed.
SECLINK_ERR_SEND -6 Packet send failed, or non-blocking send when window is full.
.endtable
.h2 SecLinkRecvT
@ -565,7 +601,7 @@ typedef void (*SecLinkRecvT)(void *ctx,
.h2 secLinkOpen
Open a secure serial link. Opens the COM port and packet layer internally.
Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent.
.code
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits,
@ -579,18 +615,18 @@ SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits,
com COM port index (RS232_COM1..RS232_COM4).
bps Baud rate.
dataBits Data bits per character (5-8).
parity Parity mode ('N', 'O', 'E', 'M', 'S').
parity Parity mode ('N', 'O', 'E', 'M', 'S' or lowercase).
stopBits Stop bits (1 or 2).
handshake Flow control mode (RS232_HANDSHAKE_*).
callback Called when a packet is received (plaintext, with channel).
callback Called when a packet is received, after decryption if applicable. Plaintext data pointer is valid only during the callback.
ctx User pointer passed to the callback.
.endtable
Returns an opaque SecLinkT handle, or NULL on failure.
Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up.
.h2 secLinkClose
Close the link, free all resources, and close the COM port.
Close the link and free all associated resources in reverse of open order: securely destroy TX and RX ciphers (which scrubs key material), destroy any remaining DH context, close the packet layer, close the COM port, and finally free the link struct itself. Safe to call with a NULL link (no-op).
.code
void secLinkClose(SecLinkT *link);
@ -598,17 +634,36 @@ void secLinkClose(SecLinkT *link);
.h2 secLinkHandshake
Perform Diffie-Hellman key exchange. Blocks until both sides complete. The RNG must be seeded before calling this.
Perform a Diffie-Hellman key exchange and derive the session cipher keys. Blocks, polling the packet layer in a loop, until both sides have swapped public keys. The RNG must be seeded (via secRngSeed, secRngGatherEntropy + secRngSeed, or secRngAddEntropy) before calling this, or the generated private key will be predictable.
.code
int secLinkHandshake(SecLinkT *link);
.endcode
Returns SECLINK_SUCCESS or SECLINK_ERR_HANDSHAKE.
Internally:
.list
.item Creates a DH context via secDhCreate, generates 1024-bit keys via secDhGenerateKeys, and exports the 128-byte public key.
.item Sends the public key as a single packet via pktSend (blocking).
.item Polls pktPoll until the remote's public key arrives and the internal callback completes the handshake (computes the shared secret, derives directional TX and RX cipher keys, transitions to READY, destroys the DH context for forward secrecy).
.endlist
Returns SECLINK_SUCCESS on success, SECLINK_ERR_PARAM on NULL link, SECLINK_ERR_ALLOC on DH context allocation failure, or SECLINK_ERR_HANDSHAKE for DH key generation failure, packet send failure, or serial disconnect during the exchange.
.h3 Directional Key Derivation
To prevent CTR-mode keystream collision, the two sides never use the same TX key. After computing the shared secret, each side derives a master XTEA key via secDhDeriveKey and then XORs it with a direction byte:
.list
.item The side with the lexicographically lower public key uses masterKey XOR 0xAA for TX and masterKey XOR 0x55 for RX.
.item The other side uses the reverse assignment.
.endlist
Both sides initialize their cipher counters to zero and advance monotonically from there.
.h2 secLinkSend
Send data on a channel, optionally encrypted.
Send data on a channel, optionally encrypted. Prepends a one-byte channel header (bit 7 = encrypt flag, bits 6..0 = channel number), then encrypts the payload only (never the header) when encrypt is true.
.code
int secLinkSend(SecLinkT *link, const uint8_t *data,
@ -620,13 +675,15 @@ int secLinkSend(SecLinkT *link, const uint8_t *data,
--------- -----------
link Connection handle from secLinkOpen.
data Plaintext payload (1..SECLINK_MAX_PAYLOAD bytes).
len Payload length.
len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD.
channel Logical channel number (0..SECLINK_MAX_CHANNEL).
encrypt If true, encrypt before sending (requires completed handshake).
block If true, block until the transmit window has space.
block If true, block until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full.
.endtable
Returns SECLINK_SUCCESS or a negative error code.
Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND).
Cipher counter safety: the function checks transmit window space BEFORE encrypting the payload. Encrypting first and then failing to send would advance the TX cipher counter while the data never reached the wire, permanently desynchronizing the TX cipher state from the remote's RX cipher. This ordering is critical for correctness when non-blocking sends are used.
.h2 secLinkSendBuf
@ -641,26 +698,118 @@ Returns SECLINK_SUCCESS or the first error encountered.
.h2 secLinkPoll
Poll for incoming data. Decrypts encrypted packets and delivers plaintext to the receive callback.
Poll for incoming data. Delegates to pktPoll, which drains the RS-232 ring buffer, processes received frames, checks retransmit timers, and dispatches each complete packet to an internal callback that strips the channel header, decrypts when the encrypt flag is set, and forwards plaintext to the user callback.
.code
int secLinkPoll(SecLinkT *link);
.endcode
Returns the number of packets delivered, or a negative error code.
Returns the number of packets delivered to the callback this call, SECLINK_ERR_PARAM if link is NULL, or one of the pktPoll error codes (PKT_ERR_DISCONNECTED, PKT_ERR_INVALID_PARAM). Must be called frequently in the main loop.
.h2 secLinkIsReady
Check whether the handshake is complete and the link is ready for encrypted data.
Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready.
.code
bool secLinkIsReady(SecLinkT *link);
.endcode
Returns true only when link is non-NULL and the internal state is STATE_READY (that is, secLinkHandshake has completed). Returns false on NULL or before the handshake.
.h2 secLinkGetPending
Get the number of unacknowledged packets in the transmit window.
Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle.
.code
int secLinkGetPending(SecLinkT *link);
.endcode
Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL.
.topic lib.serial.example
.title End-to-End Example
.toc 1 End-to-End Example
.index Serial Example
.index BBS Client
.h1 End-to-End Example
Below is a complete secure BBS client that opens COM1, exchanges DH keys, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: open the link, seed the RNG, run the handshake, then enter a poll+send loop.
.code
#include "rs232/rs232.h"
#include "packet/packet.h"
#include "security/security.h"
#include "seclink/secLink.h"
#include <stdio.h>
#include <string.h>
#define CHAN_TERMINAL 0
static void onPacket(void *ctx, const uint8_t *data,
int len, uint8_t channel)
{
(void)ctx;
if (channel == CHAN_TERMINAL) {
fwrite(data, 1, len, stdout);
fflush(stdout);
}
}
int main(void) {
// Seed the RNG from hardware entropy plus any user-derived
// randomness (keyboard timing, etc.).
uint8_t entropy[16];
int got = secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, got);
// Open the link at 115200 8N1 with RTS/CTS flow control.
SecLinkT *link = secLinkOpen(
RS232_COM1, 115200, 8, 'N', 1,
RS232_HANDSHAKE_RTSCTS,
onPacket, NULL);
if (!link) {
fprintf(stderr, "secLinkOpen failed\n");
return 1;
}
// Run the DH key exchange. Blocks until both sides
// have swapped public keys and derived cipher keys.
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
fprintf(stderr, "Handshake failed\n");
secLinkClose(link);
return 1;
}
// Send an encrypted banner and then relay keyboard input.
const char *hello = "Hello, secure BBS!\r\n";
secLinkSend(link, (const uint8_t *)hello,
(int)strlen(hello), CHAN_TERMINAL, true, true);
while (!feof(stdin)) {
// Deliver any incoming packets to onPacket().
secLinkPoll(link);
// If the user typed something, forward it encrypted.
int ch = getchar();
if (ch == EOF) {
break;
}
uint8_t byte = (uint8_t)ch;
secLinkSend(link, &byte, 1, CHAN_TERMINAL, true, true);
}
secLinkClose(link);
return 0;
}
.endcode
Notes:
.list
.item secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.
.item The RNG must be seeded before secLinkHandshake. secRngGatherEntropy provides roughly 20 bits of hardware entropy; supplement with user interaction timing for cryptographic use.
.item secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.
.item For bulk transfers larger than SECLINK_MAX_PAYLOAD (254 bytes), use secLinkSendBuf which splits the data into chunks automatically.
.endlist

View file

@ -36,9 +36,37 @@ High-level wrapper around SQLite3 for DVX applications. Manages database connect
Header: sql/dvxSql.h
.h3 SQLite Wrapping
dvxSql wraps the bundled SQLite3 amalgamation (sql/thirdparty/sqlite/examples/sqlite3.h). It manages two internal dynamic tables keyed by 1-based handles:
.list
.item Database table: each slot holds a sqlite3 * plus a per-database error string and affected-row count.
.item Cursor table: each slot holds a sqlite3_stmt *, the owning database handle, and EOF tracking.
.endlist
Growing either table doubles its capacity. Closed slots are recycled by subsequent dvxSqlOpen or dvxSqlQuery calls, keeping handle values stable for the caller.
.h3 Handle Model
Database and cursor handles are int32_t values. A successful open or query returns a handle greater than zero. Handle 0 is reserved as the invalid/error sentinel. Closing a database automatically finalizes all cursors that belong to it.
Database and cursor handles are int32_t values. A successful open or query returns a handle greater than zero. Handle 0 is reserved as the invalid/error sentinel. Closing a database automatically finalizes all cursors that belong to it, so callers do not need to track cursor lifetimes per-database.
.h3 Connection Lifecycle
.list
.item dvxSqlOpen allocates a handle and calls sqlite3_open. The database file is created if it does not exist.
.item Use dvxSqlExec for DDL and non-query DML. On success, dvxSqlAffectedRows returns the row count for the last call on this handle.
.item Use dvxSqlQuery to obtain a cursor over a SELECT. Iterate with dvxSqlNext and read columns with dvxSqlFieldText, dvxSqlFieldInt, dvxSqlFieldDbl, or dvxSqlFieldByName.
.item Call dvxSqlFreeResult when the cursor is no longer needed. Call dvxSqlClose when the database is no longer needed; any cursors still open on that database are finalized automatically.
.endlist
.h3 Common Patterns
.list
.item Parameterize user input with dvxSqlEscape before interpolating into SQL strings.
.item Check dvxSqlError(db) for the last error on a handle; its message is stable until the next operation on the same database.
.item dvxSqlFieldByName matches column names case-insensitively, suitable for most real-world schemas.
.endlist
.link sql.db Database Operations
.link sql.cursor Cursor Operations

View file

@ -1,75 +1,20 @@
# DVX Task Manager (taskmgr.lib)
# taskmgr -- Task Manager
A separate DXE3 module that provides the system Task Manager window.
Built as `taskmgr.lib` and loaded alongside the other libs at startup.
The Task Manager is a shell-level component, not tied to any app -- it
persists even if the desktop app (Program Manager) is terminated.
Provides the system Task Manager window that lists running tasks and
lets the user switch to or terminate any task. Invoked from the shell
(usually via a system-menu item).
## Documentation
## Registration
Full reference is in `taskmgr.dhs` (this directory), compiled into the
DVX System Reference. Open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
The module uses a GCC `__attribute__((constructor))` function to
register itself with the shell at load time. The constructor sets the
`shellCtrlEscFn` function pointer (declared in `shellApp.h`) to
`shellTaskMgrOpen`. The shell calls this pointer when Ctrl+Esc is
pressed. If `taskmgr.lib` is not loaded, the pointer remains NULL and
Ctrl+Esc does nothing.
## Features
- **App list**: 6-column ListView showing Name, Title, File, Type,
Memory, and Status for all running apps
- **Memory column**: Per-app memory usage from `dvxMemGetAppUsage()`,
displayed in KB or MB
- **Switch To**: Raises and focuses the selected app's topmost window;
restores it if minimized. Also triggered by double-clicking a row.
- **End Task**: Force-kills the selected app via `shellForceKillApp()`
- **Run...**: Opens a file dialog to browse for and launch a `.app` file
- **Accelerator keys**: `&Switch To` (Alt+S), `&End Task` (Alt+E),
`&Run...` (Alt+R)
- **Status bar**: Shows running app count and system memory usage
- **Live refresh**: Registers a desktop update callback so the list
refreshes automatically when apps are loaded, reaped, or crash
## Window Behavior
- Owned by the shell (appId = 0), not by any app
- Single-instance: if already open, Ctrl+Esc raises and focuses it
- Closing the window unregisters the desktop update callback and
cleans up dynamic arrays
## API
```c
// Open or raise the Task Manager window.
void shellTaskMgrOpen(AppContextT *ctx);
// Refresh the task list (called by desktop update notification).
void shellTaskMgrRefresh(void);
```
## Dependencies
Loaded after: `dvxshell`, `libtasks`, `libdvx`, `texthelp`, `listhelp`
(via `taskmgr.dep`).
## Files
| File | Description |
|------|-------------|
| `shellTaskMgr.h` | Public API (open, refresh) |
| `shellTaskMgr.c` | Task Manager window, list, buttons, DXE constructor |
| `Makefile` | Builds `bin/libs/taskmgr.lib` + dep file |
## Source Files
- `shellTaskMgr.h` -- public entry point
- `shellTaskMgr.c` -- Task Manager window implementation
## Build
```
make # builds bin/libs/taskmgr.lib + taskmgr.dep
make clean # removes objects, library, and dep file
```
`make -C src/libs/kpunch/taskmgr` (invoked by the top-level `make`).

View file

@ -312,7 +312,7 @@ void shellTaskMgrOpen(AppContextT *ctx) {
tmCols[5].align = ListViewAlignLeftE;
sTmListView = wgtListView(root);
sTmListView->weight = 100;
sTmListView->weight = WGT_WEIGHT_FILL;
sTmListView->prefH = wgtPixels(TM_LIST_PREF_H);
sTmListView->onDblClick = onTmSwitchTo;
wgtListViewSetColumns(sTmListView, tmCols, TM_COL_COUNT);
@ -321,7 +321,7 @@ void shellTaskMgrOpen(AppContextT *ctx) {
btnRow->spacing = wgtPixels(TM_BTN_SPACING);
sTmStatusLbl = wgtLabel(btnRow, "");
sTmStatusLbl->weight = 100;
sTmStatusLbl->weight = WGT_WEIGHT_FILL;
WidgetT *switchBtn = wgtButton(btnRow, "&Switch To");
switchBtn->onClick = onTmSwitchTo;

View file

@ -30,12 +30,33 @@
.h1 Task Manager
System-level task manager for DVX. Displays running applications and per-app memory usage. Always accessible via Ctrl+Esc regardless of which application is focused. Persists independently of the desktop app (Program Manager).
System-level task manager for DVX. Displays all running applications with a six-column ListView (Name, Title, File, Type, Memory, Status) and exposes Switch To, End Task, and Run... operations. Always accessible via Ctrl+Esc regardless of which application is focused. Persists independently of the desktop app (Program Manager).
Header: taskmgr/shellTaskMgr.h
Loaded as: bin/libs/taskmgr.lib
.h2 Integration
The task manager is a shell-level library, not a regular DXE app. At DXE load time a GCC constructor function registers the Task Manager with the shell by setting the shellCtrlEscFn function pointer (declared extern in dvxshell/shellApp.h). The shell invokes shellCtrlEscFn on every Ctrl+Esc keystroke; if taskmgr.lib is not loaded the pointer stays NULL and Ctrl+Esc does nothing.
The Task Manager owns its window on behalf of the shell itself (appId = 0), not any running app, which is why it survives when the desktop app terminates. It registers shellTaskMgrRefresh with shellRegisterDesktopUpdate so the list refreshes live as apps load, reap, or crash.
.h2 Integration with libtasks
The Task Manager reads from the shell's app slot table (shellGetApp, shellAppSlotCount, shellRunningAppCount) rather than from the libtasks scheduler directly. Each ShellAppT slot carries its cooperative task ID when hasMainLoop is true; End Task calls shellForceKillApp, which in turn calls tsKill on that task ID (for main-loop apps) after destroying the app's windows.
.h2 Features
.list
.item Six-column ListView showing Name, Title, File, Type (Task or Callback), Memory (from dvxMemGetAppUsage), and Status for every running app.
.item Switch To button (Alt+S): raises and focuses the selected app's topmost window, restoring it if minimized. Also triggered by double-clicking a row.
.item End Task button (Alt+E): force-kills the selected app via shellForceKillApp.
.item Run... button (Alt+R): opens a file dialog to browse for and launch a .app file.
.item Status bar showing running app count plus total and used system memory.
.item Single-instance: reopening the Task Manager while it is already visible raises and focuses the existing window rather than creating a new one.
.endlist
.h2 shellTaskMgrOpen
Open the Task Manager window, or raise it to the front if already open.
@ -47,14 +68,14 @@ void shellTaskMgrOpen(AppContextT *ctx);
.table
Parameter Description
--------- -----------
ctx Application context (from dvxInit). Required for window creation and event registration.
ctx Application context (from dvxInit). Required for window creation, stack manipulation, and registering the desktop-update callback.
.endtable
Called by the shell's global Ctrl+Esc handler.
Called by the shell's global Ctrl+Esc handler via the shellCtrlEscFn hook.
.h2 shellTaskMgrRefresh
Refresh the task list display. Call this when applications are launched or terminated so the Task Manager reflects the current state.
Rebuild the task list display from the current shell app slot table and refresh the status bar. Registered with shellRegisterDesktopUpdate at open time so it is invoked whenever the shell detects app state changes.
.code
void shellTaskMgrRefresh(void);

View file

@ -1,133 +1,26 @@
# texthelp -- Shared Text Editing Helper Library
# texthelp -- Shared Text-Editing Helpers
Shared infrastructure for text editing widgets, built as
`texthelp.lib` (DXE3 module). Provides clipboard operations, cursor
blink management, word boundary detection, cross-widget selection
clearing, and a complete single-line text editing engine.
Shared text-editing infrastructure used by the TextInput, Spinner,
ComboBox, and AnsiTerm widgets. Centralizes cursor blinking, multi-click
detection, word boundary logic, cross-widget selection clearing, and the
single-line editing engine (key dispatch, mouse hit, drag-select,
undo buffer).
Used by: TextInput, TextArea, Spinner, ComboBox, AnsiTerm.
## Documentation
Full reference is in `texthelp.dhs` (this directory), compiled into the
DVX System Reference. Open via the Help Viewer app, or read
`docs/dvx_system_reference.html` after `make`.
## API Reference
Topics covered: cursor-blink update, word boundary functions, single-line
text edit mouse/key/paint entry points, undo snapshot API, and the
cross-widget selection clearing protocol.
### Cursor Blink
```c
void wgtUpdateCursorBlink(void);
```
Toggles the global cursor blink state (`sCursorBlinkOn`) based on
`clock()` timing. Called from the event dispatch layer during paint.
Widgets that show a text cursor check `sCursorBlinkOn` to decide
whether to draw or hide the cursor.
### Selection Management
```c
void clearOtherSelections(WidgetT *except);
```
Clears text/item selections in all widgets except the specified one.
Called when a widget gains focus or starts a new selection, ensuring
only one widget has an active selection at a time.
### Word / Character Helpers
```c
bool isWordChar(char c);
```
Returns true if `c` is a word character (alphanumeric or underscore).
Used for double-click word selection and Ctrl+arrow word navigation.
```c
int32_t wordStart(const char *buf, int32_t pos);
```
Scans backward from `pos` to find the start of the current word.
```c
int32_t wordEnd(const char *buf, int32_t len, int32_t pos);
```
Scans forward from `pos` to find the end of the current word.
### Single-Line Text Editing Engine
Four functions implement the complete editing behavior shared across
TextInput, Spinner, ComboBox, and other single-line text widgets:
```c
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod,
char *buf, int32_t bufSize, int32_t *pLen,
int32_t *pCursor, int32_t *pScrollOff,
int32_t *pSelStart, int32_t *pSelEnd,
char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor,
int32_t fieldWidth);
```
Handles all keyboard input: character insertion, deletion (Backspace,
Delete), cursor movement (arrows, Home, End), word navigation
(Ctrl+arrows), selection (Shift+arrows, Ctrl+Shift+arrows, Ctrl+A),
clipboard (Ctrl+C/X/V), and undo (Ctrl+Z).
```c
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy,
int32_t textLeftX, const BitmapFontT *font,
const char *buf, int32_t len, int32_t scrollOff,
int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd,
bool wordSelect, bool dragSelect);
```
Handles mouse click positioning: converts pixel coordinates to cursor
position, handles word selection (double-click), and drag-select.
```c
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge,
int32_t maxChars, const BitmapFontT *font, int32_t len,
int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd);
```
Updates cursor and selection during mouse drag operations.
```c
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors,
int32_t textX, int32_t textY, const char *buf,
int32_t visLen, int32_t scrollOff, int32_t cursorPos,
int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg,
bool showCursor, int32_t cursorMinX, int32_t cursorMaxX);
```
Renders a single line of text with selection highlighting and cursor.
### Constants
| Name | Value | Description |
|------|-------|-------------|
| `TEXT_INPUT_PAD` | 3 | Padding inside text input border |
## Exported Symbols
Matches prefixes: `_clipboard*`, `_clearOther*`, `_isWordChar*`,
`_multiClick*`, `_widgetText*`, `_wordStart*`, `_wordEnd*`.
## Files
| File | Description |
|------|-------------|
| `textHelp.h` | Public API header |
| `textHelp.c` | Complete implementation |
| `Makefile` | Builds `bin/libs/texthelp.lib` + dep file |
## Source Files
- `textHelp.h` -- public API
- `textHelp.c` -- implementation
## Build
```
make # builds bin/libs/texthelp.lib + texthelp.dep
make clean # removes objects, library, and dep file
```
Depends on: `libtasks.lib`, `libdvx.lib` (via texthelp.dep).
`make -C src/libs/kpunch/texthelp` (invoked by the top-level `make`).

View file

@ -296,7 +296,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
}
// Ctrl+C -- copy
if (key == 3) {
if (key == KEY_CTRL_C) {
if (hasSel) {
clipboardCopy(buf + selLo, selHi - selLo);
}
@ -305,7 +305,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
}
// Ctrl+V -- paste
if (key == 22) {
if (key == KEY_CTRL_V) {
int32_t cbLen = 0;
const char *cb = clipboardGet(&cbLen);
@ -354,7 +354,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
}
// Ctrl+X -- cut
if (key == 24) {
if (key == KEY_CTRL_X) {
if (hasSel) {
clipboardCopy(buf + selLo, selHi - selLo);
@ -373,7 +373,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
}
// Ctrl+Z -- undo
if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
if (key == KEY_CTRL_Z && undoBuf && pUndoLen && pUndoCursor) {
// Swap current and undo
char tmpBuf[*pLen + 1];
int32_t tmpLen = *pLen;
@ -414,7 +414,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
goto adjustScroll;
}
if (key >= 32 && key < 127) {
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) {
// Printable character
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
@ -461,7 +461,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
w->onChange(w);
}
}
} else if (key == (0x4B | 0x100)) {
} else if (key == KEY_LEFT) {
// Left arrow
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
@ -487,7 +487,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
(*pCursor)--;
}
}
} else if (key == (0x4D | 0x100)) {
} else if (key == KEY_RIGHT) {
// Right arrow
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
@ -559,7 +559,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
*pCursor = newPos;
}
} else if (key == (0x47 | 0x100)) {
} else if (key == KEY_HOME) {
// Home
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
@ -580,7 +580,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
*pCursor = 0;
}
} else if (key == (0x4F | 0x100)) {
} else if (key == KEY_END) {
// End
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
@ -601,7 +601,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
*pCursor = *pLen;
}
} else if (key == (0x53 | 0x100)) {
} else if (key == KEY_DELETE) {
// Delete
if (hasSel) {
if (undoBuf) {

View file

@ -35,6 +35,21 @@ Shared text editing infrastructure library for DVX widget DXEs. Provides cursor
Header: texthelp/textHelp.h
Loaded as: bin/libs/texthelp.lib
.h2 How Widgets Use This Library
Widget DXEs that implement text editing delegate to textHelp for the four high-cost behaviors:
.list
.item Cursor blink. The library tracks a 250 ms blink timer in a static global and the focused widget reads sCursorBlinkOn (exposed via libdvx) when repainting.
.item Selection clearing. When a widget gains focus it calls clearOtherSelections(self) so only one widget ever has an active text selection.
.item Word boundaries. isWordChar, wordStart/wordEnd, and wordBoundaryLeft/wordBoundaryRight implement the logic for double-click word selection and Ctrl+Left/Right navigation in a uniform way.
.item Single-line editing engine. widgetTextEditOnKey, widgetTextEditMouseClick, widgetTextEditDragUpdateLine, and widgetTextEditPaintLine form a pointer-parameterized implementation of keyboard, mouse, drag, and paint behaviors. Widgets (TextInput, Spinner, ComboBox, AnsiTerm) hand the library pointers to their internal buffer, cursor, scroll offset, and selection state.
.endlist
The engine is intentionally pointer-parameterized rather than struct-based so widgets can reuse it without adopting a shared state struct. Each widget owns its own buffer and state and passes pointers in on every call.
.h2 Constants
.table
@ -145,6 +160,82 @@ int32_t wordStart(const char *buf, int32_t pos);
Returns the index of the first character of the word containing pos.
.topic lib.texthelp.wordboundaryleft
.title wordBoundaryLeft
.toc 1 wordBoundaryLeft
.index wordBoundaryLeft
.index Word Boundary
.h2 wordBoundaryLeft
Finds the left word boundary for Ctrl+Left navigation. Skips non-word characters first, then skips word characters, leaving the cursor at the start of the previous word.
.code
int32_t wordBoundaryLeft(const char *buf, int32_t pos);
.endcode
.table
Parameter Type Description
--------- ---- -----------
buf const char * The text buffer to scan.
pos int32_t Starting position (character index).
.endtable
Returns the index of the previous word boundary. Returns 0 if pos is already at or before the start of the buffer.
.topic lib.texthelp.wordboundaryright
.title wordBoundaryRight
.toc 1 wordBoundaryRight
.index wordBoundaryRight
.index Word Boundary
.h2 wordBoundaryRight
Finds the right word boundary for Ctrl+Right navigation. Skips word characters first, then skips non-word characters, leaving the cursor at the start of the next word.
.code
int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
.endcode
.table
Parameter Type Description
--------- ---- -----------
buf const char * The text buffer to scan.
len int32_t Length of the text buffer.
pos int32_t Starting position (character index).
.endtable
Returns the index of the next word boundary. Returns len if pos is already at or past the end of the buffer.
.topic lib.texthelp.saveundo
.title textEditSaveUndo
.toc 1 textEditSaveUndo
.index textEditSaveUndo
.index Undo
.h2 textEditSaveUndo
Capture a snapshot of the current buffer contents and cursor position into an undo buffer. Called by widgetTextEditOnKey before every destructive operation (insert, delete, paste, cut). Truncates to bufSize - 1 characters so the null terminator always fits.
.code
void textEditSaveUndo(char *buf, int32_t len,
int32_t cursor, char *undoBuf,
int32_t *pUndoLen, int32_t *pUndoCursor,
int32_t bufSize);
.endcode
.table
Parameter Type Description
--------- ---- -----------
buf char * Source buffer to snapshot.
len int32_t Current length of source contents.
cursor int32_t Current cursor position to save.
undoBuf char * Destination undo buffer. If NULL the call is a no-op.
pUndoLen int32_t * [out] Length of saved undo contents.
pUndoCursor int32_t * [out] Saved cursor position.
bufSize int32_t Size of the destination undo buffer including the null terminator.
.endtable
.topic lib.texthelp.dragupdate
.title widgetTextEditDragUpdateLine
.toc 1 widgetTextEditDragUpdateLine

View file

@ -1,133 +1,190 @@
# DVX Loader
Bootstrap loader for the DVX desktop environment. Builds as `dvx.exe`
-- the only native executable in the system. Everything else is a
dynamically loaded DXE3 module.
Bootstrap loader for the DVX desktop environment. Builds as
`bin/dvx.exe` -- the only native executable in the system. Everything
else is a dynamically loaded DXE3 module: libraries (`.lib`), widget
plug-ins (`.wgt`), and applications (`.app`).
## What It Does
1. Changes working directory to the directory containing `dvx.exe`
2. Truncates `dvx.log` and initializes logging
3. Calls `platformInit()` to suppress Ctrl+C and install signal handlers
4. Calls `platformRegisterDxeExports()` to register platform and C
runtime symbols (libc, libm, libgcc) for DXE module resolution
5. Scans and loads all modules in two phases (see below)
6. Finds `shellMain()` via `dlsym` across loaded modules
7. Calls `shellMain()` -- the shell takes over from here
8. On return, closes all module handles in reverse load order
On launch, `dvx.exe` performs these steps in order:
1. `chdir` into the directory containing the executable. From that
point on, the working directory is the install root and all
relative paths (`LIBS/`, `WIDGETS/`, `APPS/`, `CONFIG/`, `SYSTEM/`)
resolve relative to it.
2. Truncate `dvx.log`, then use append-per-write thereafter.
3. Call `platformInit()` to suppress Ctrl+C, mask the default DOS
break handler, and prepare the signal-handling path for later.
4. Switch the display into VGA mode 13h and paint the splash screen
from `SYSTEM/SPLASH.RAW` (raw palette + pixels, as produced by
`bmp2raw`).
5. Call `platformRegisterDxeExports()` to publish platform entry
points and a curated set of libc / libm / libgcc symbols to the
DXE dynamic symbol resolver. This makes the loader the single
source of truth for these symbols so that every DXE module
resolves them consistently.
6. Scan and load all modules in two phases (see below), updating the
splash progress bar as each module loads.
7. Check whether help files need rebuilding (the module set changed
since the last boot); if so, run the integrated help compiler.
8. Find `shellMain` via `dlsym` across every loaded module and call
it. The shell takes over from here.
9. When `shellMain` returns, close every module handle in reverse
load order and exit.
## Two-Phase Module Loading
### Phase 1: Libraries (libs/*.lib)
### Phase 1: Libraries (`LIBS/*.lib`)
Recursively scans the `LIBS/` directory for `.lib` files. Each module may have a
`.dep` file (same base name, `.dep` extension) listing base names of
modules that must load first. The loader resolves the dependency graph
and loads in topological order.
Recursive scan of the `LIBS/` tree for `.lib` files. Each module may
have a `.dep` file (same base name, `.dep` extension) listing
base names of modules that must load first. The loader resolves the
dependency graph and loads in topological order.
Typical load order (determined entirely by `.dep` files):
Load order (via dep files):
```
libtasks.lib (no deps)
libdvx.lib (deps: libtasks)
texthelp.lib (deps: libtasks, libdvx)
listhelp.lib (deps: libtasks, libdvx)
dvxshell.lib (deps: libtasks, libdvx, texthelp, listhelp)
taskmgr.lib (deps: dvxshell, libtasks, libdvx, texthelp, listhelp)
libtasks.lib (no deps)
libdvx.lib (deps: libtasks)
texthelp.lib (deps: libtasks, libdvx)
listhelp.lib (deps: libtasks, libdvx)
dvxshell.lib (deps: libtasks, libdvx, texthelp, listhelp)
taskmgr.lib (deps: dvxshell, libtasks, libdvx, texthelp, listhelp)
serial.lib (deps: libtasks, libdvx)
dvxsql.lib (deps: libtasks, libdvx)
basrt.lib (deps: libtasks, libdvx)
```
### Phase 2: Widgets (widgets/*.wgt)
### Phase 2: Widgets (`WIDGETS/*.wgt`)
Recursively scans the `WIDGETS/` directory for `.wgt` files. Widget modules may
also have `.dep` files (e.g., `textinpt.dep` lists `texthelp`).
Loaded in topological order, same as libs.
Same algorithm applied to the `WIDGETS/` tree. Widget modules may
also declare dependencies (for example, `comboBox` depends on
`texthelp` and `listhelp`).
After loading each module, the loader checks for a `wgtRegister`
export. If present, it is called immediately so the widget registers
its class(es) and API with the core.
Immediately after each widget module loads, the loader looks up the
symbol `wgtRegister` and, if present, calls it. That entry point
registers the widget's class and API with the core so the shell and
apps can then create instances of it.
## Dependency File Format
Plain text, one dependency base name per line. Empty lines and lines
starting with `#` are ignored. Names are case-insensitive.
`.dep` files are plain text. Each line is one dependency base name
(case-insensitive, no extension). Blank lines and lines starting
with `#` are ignored.
Example (`comboBox.dep`):
Example (`combobox.dep`):
```
# combobox uses the shared text-editing helpers and the shared
# list/dropdown helpers.
texthelp
listhelp
```
Missing dependencies are detected before any module is loaded and
abort the boot with a clear fatal error, so a misconfigured
install fails early rather than crashing partway through.
## Hosted Components
### dvxLog()
### `dvxLog()`
The global logging function is defined in `loaderMain.c` and exported
to all DXE modules. It appends a line to `dvx.log`, opening and
closing the file per write so it is never held open.
to every DXE. It appends a single line to `dvx.log`, opening and
closing the file per call. This means the log is never held open and
its contents are safe to read even if the system crashes.
```c
void dvxLog(const char *fmt, ...);
```
### stb_ds
### `stb_ds`
The `STB_DS_IMPLEMENTATION` is compiled into the loader with
`STBDS_REALLOC` and `STBDS_FREE` overridden to use `dvxRealloc` and
`dvxFree`. This means all `arrput`/`hmput`/`arrfree` calls in DXE
code route through the per-app memory tracker, so stb_ds memory is
attributed correctly in the Task Manager's memory column. stb_ds
functions are exported to all DXE modules via the platform export
table.
`STB_DS_IMPLEMENTATION` is compiled into the loader so the
implementation is shared across the loader's translation units
(`loaderMain.o`, `dvxPrefs.o`). The loader uses plain `realloc` /
`free` for stb_ds; per-app memory tracking applies to DXE allocations
via the export table mappings described below.
### Platform Layer
`dvxPlatformDos.c` is compiled into the loader (not into libdvx.lib).
All platform functions are exported to DXE modules. This includes:
`dvxPlatformDos.c` is compiled into the loader (not into
`libdvx.lib`). All platform functions are exported to DXE modules via
the export table. This includes:
* Video: VESA VBE init, LFB mapping, mode enumeration
* Input: INT 33h mouse, INT 16h keyboard, CuteMouse wheel API
* Spans: `rep stosl`/`rep movsd` asm inner loops (8/16/32 bpp)
* DXE: symbol registration, symbol overrides
* Crash: signal handler installation, register dump logging
* System: memory info, directory creation, path utilities
* Video -- VESA VBE init, linear framebuffer mapping, mode
enumeration
* Input -- INT 33h mouse, INT 16h keyboard, CuteMouse wheel API
* Span fills / copies -- `rep stosl` / `rep movsd` asm inner loops at
8, 16, and 32 bpp
* DXE support -- symbol registration, symbol overrides
* Crash handling -- signal handler installation, register dump
logging
* System utilities -- memory info, directory creation, path helpers
### Per-App Memory Tracking
The DXE export table maps standard C allocation symbols to tracked
wrappers:
The DXE export table maps the standard C allocation symbols to
DVX-tracked wrappers:
| C Symbol | Mapped To | Effect |
|----------|-----------|--------|
| `malloc` | `dvxMalloc` | Allocations attributed to `currentAppId` |
| `calloc` | `dvxCalloc` | Same |
| `realloc` | `dvxRealloc` | Transfers attribution on resize |
| `free` | `dvxFree` | Decrements app's tracked usage |
| `strdup` | `dvxStrdup` | Tracks the duplicated string |
| C symbol | Mapped to | Behaviour |
|------------|-----------------|-----------|
| `malloc` | `dvxMalloc` | Allocation is attributed to the current app ID. |
| `calloc` | `dvxCalloc` | Same. |
| `realloc` | `dvxRealloc` | Attribution transfers with the resized block. |
| `free` | `dvxFree` | Decrements the app's tracked usage. |
| `strdup` | `dvxStrdup` | Tracks the duplicated buffer. |
This is transparent to DXE code -- apps call `malloc` normally and the
tracked wrapper runs instead. The Task Manager reads per-app usage via
`dvxMemGetAppUsage()`. When an app is reaped, `dvxMemResetApp()` zeroes
its counter.
This is transparent to DXE code -- apps call `malloc` normally and
the tracked wrapper runs instead. The Task Manager reads per-app
usage via `dvxMemGetAppUsage()`, and `dvxMemResetApp()` zeros the
counter when an app is reaped.
### Integrated Help Compilation
The help compiler (`dvxhlpc.c`) is built a second time as an object
file with `-DHLPC_NO_MAIN` and linked into the loader. On boot, the
loader compares the currently-loaded module set against the last-run
state recorded in `DVX.INI`. If it has changed, the loader
re-compiles the affected help files before starting the shell, so
newly-installed widgets and libraries contribute to the system help
without a manual step.
### Splash Screen
`SYSTEM/SPLASH.RAW` is a 320x200 256-colour image in raw
palette-plus-pixels format (as emitted by `bmp2raw`). The loader
drops to VGA mode 13h, paints the splash, and updates a progress bar
as modules load. The splash stays up until the shell switches into
its configured VESA mode.
## Files
| File | Description |
|------|-------------|
| `loaderMain.c` | Entry point, module scanner, dependency resolver, logger |
| `Makefile` | Builds `bin/dvx.exe` |
| `loaderMain.c` | Entry point, module scanner, dependency resolver, logger, splash, help-recompile hook. |
| `stddclmr.h` | Standard disclaimer. Included by `loaderMain.c`; leave it alone. |
| `Makefile` | Builds `bin/dvx.exe`. |
## Build
```
make # builds bin/dvx.exe
make clean # removes objects and binary
make # build bin/dvx.exe
make clean # remove the binary and intermediate objects
```
The loader links `loaderMain.c` + `dvxPlatformDos.c` into a native
DJGPP executable. The CWSDPMI stub is prepended via exe2coff +
CWSDSTUB.EXE for standalone execution.
The loader is cross-compiled with the DJGPP toolchain
(`i586-pc-msdosdjgpp-gcc`). The resulting `a.out` is converted with
`exe2coff`, and the CWSDPMI stub (`CWSDSTUB.EXE`) is prepended so the
final `dvx.exe` runs stand-alone on DOS without a separate DPMI host.
Linked translation units: `loaderMain.c` + `dvxPlatformDos.c` +
`dvxPlatformUtil.c` + `dvxPrefs.c` + `dvxhlpc.c` (as
`-DHLPC_NO_MAIN`).

View file

@ -1,17 +1,26 @@
# DVX Tools
Host-native utilities that run on the development machine (Linux or
DOS). These are not DXE modules and do not cross-compile with DJGPP --
they build with the system GCC.
Host-native utilities that run on the development machine during the
build. These tools are not DXE modules and do not cross-compile; they
build with the system GCC. They produce binaries, icons, resource
blocks, help files, and raw image data that get embedded into DXE
apps, widgets, and libraries.
All tools build into `bin/host/` with `make`. Two of them (the
resource tool and the help compiler) are also cross-compiled for DOS
and shipped to `bin/system/` as DOS executables so they can run on
the target as well.
## dvxres -- Resource Tool
Command-line tool for managing resource blocks appended to DXE3 files
(`.app`, `.wgt`, `.lib`). Resources are invisible to `dlopen` because
they are appended after the DXE3 content.
Manages resource blocks appended to DXE3 files (`.app`, `.wgt`,
`.lib`). Resources are invisible to `dlopen` because they sit after
the DXE3 image, in a separately indexed block. The runtime reads them
via `dvxResOpen`/`dvxResRead`/`dvxResClose` (see
`src/libs/kpunch/libdvx/dvxResource.c`).
### Commands
### Usage
```
dvxres add <file> <name> <type> <data|@file>
@ -23,105 +32,361 @@ dvxres strip <file>
| Command | Description |
|---------|-------------|
| `add` | Add or replace a single resource in a DXE file |
| `build` | Add all resources listed in a manifest file (replaces any existing resources) |
| `list` | List all resources in a DXE file |
| `get` | Extract a resource to a file or stdout |
| `strip` | Remove all appended resources, leaving only the DXE content |
| `add` | Add or replace a single resource in a DXE file. Existing entries with the same name are overwritten. |
| `build` | Replace all resources in a DXE file with the entries listed in a manifest. Any previously attached resources are removed first. |
| `list` | Dump a table of every resource in a DXE file (name, type, size). |
| `get` | Extract a named resource to a file, or to stdout if no output path is given. |
| `strip` | Truncate the file to remove the appended resource block. The DXE image itself is left unchanged. |
### Resource Types
| Type | Keyword | Description |
|------|---------|-------------|
| `DVX_RES_ICON` | `icon` or `image` | Image data (BMP icons, etc.) |
| `DVX_RES_TEXT` | `text` | Null-terminated string (author, copyright) |
| `DVX_RES_BINARY` | `binary` | Arbitrary binary data (app-specific) |
| Keyword | Type constant | Description |
|--------------------|--------------------|-------------|
| `icon` or `image` | `DVX_RES_ICON` | Image data (BMP for icons, PNG/JPEG/GIF for images used by apps). |
| `text` | `DVX_RES_TEXT` | Null-terminated string. Used for metadata: `author`, `copyright`, `publisher`, `description`, `version`, `name`. |
| `binary` | `DVX_RES_BINARY` | Arbitrary binary data, application-defined. |
For `add` with type `text`, the data argument is the string value
directly. For `icon` or `binary`, the data argument is a file path.
### Resource File Format
Resources are appended after the normal DXE3 content:
```
[DXE3 content] -- untouched, loaded by dlopen
[resource data entries] -- sequential, variable length
[resource directory] -- fixed-size 48-byte entries
[footer] -- 16 bytes: magic + dir offset + count
```
The footer is at the very end of the file. Reading starts from
`EOF - 16` bytes. The magic value is `0x52585644` ("DVXR" in
little-endian). The directory offset points to the start of the
directory entries, and the entry count gives the number of resources.
Each directory entry (48 bytes) contains:
- `name[32]` -- resource name (null-terminated)
- `type` (uint32) -- DVX_RES_ICON, DVX_RES_TEXT, or DVX_RES_BINARY
- `offset` (uint32) -- absolute file offset of data
- `size` (uint32) -- data size in bytes
- `reserved` (uint32) -- padding
For `add` with type `text`, the `<data>` argument is the literal
string value. For `icon` or `binary`, the `<data>` argument is a file
path; a leading `@` is stripped if present.
### Manifest File Format (.res)
Plain text, one resource per line:
Plain-text UTF-8 (ASCII preferred), one resource per line:
```
# Comment lines start with #
name type data
# Comments start with '#'
# Fields: name type data
name type data
# Examples:
icon32 icon icons/myapp32.bmp
icon16 icon icons/myapp16.bmp
author text "John Doe"
appdata binary data/config.bin
icon32 icon icon32.bmp
name text "My App"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
description text "Editor with syntax highlighting"
appdata binary data/config.bin
```
Each line has three fields: name, type, and data. Text data can be
quoted. Empty lines and lines starting with `#` are ignored.
Each non-empty, non-comment line has three whitespace-delimited
fields: `name`, `type`, `data`. For text values, double quotes around
the data are stripped. For icon/binary values, the data is a file
path relative to the current working directory when `dvxres build`
was invoked.
Names are limited to 31 characters (32 including the terminator) and
are truncated silently.
### Examples
```
# Attach a single icon to a freshly-built app
dvxres add bin/apps/kpunch/myapp/myapp.app icon32 icon icon32.bmp
# Rebuild the full resource block from a manifest
dvxres build bin/apps/kpunch/myapp/myapp.app myapp.res
# List every resource in a module
dvxres list bin/widgets/kpunch/button/button.wgt
# Extract an icon for inspection
dvxres get bin/apps/kpunch/myapp/myapp.app icon32 recovered.bmp
# Strip all resources from a file (useful for diffing the raw DXE)
dvxres strip bin/apps/kpunch/myapp/myapp.app
```
### File Format
Resources are appended after the DXE3 image:
```
[DXE3 image] <-- untouched, what dlopen sees
[resource data entries] <-- sequential, variable length
[resource directory] <-- fixed-size 48-byte entries
[footer] <-- 16 bytes: magic, dir offset, count
```
The footer sits at `EOF - 16` bytes. Magic value is `0x52585644`
(`"DVXR"` little-endian). Each directory entry (48 bytes) holds a
32-byte name, a `uint32_t` type, a `uint32_t` absolute file offset,
a `uint32_t` size, and a `uint32_t` reserved field.
### Binaries
* Host: `bin/host/dvxres`
* Target: `bin/system/DVXRES.EXE` (DOS executable, for use inside the
running system)
## mkicon -- Icon Generator
## dvxhlpc -- Help Compiler
Generates simple 32x32 24-bit BMP pixel-art icons for DVX apps.
Compiles hypertext help source files (`.dhs` = "DVX Help Source")
into the binary `.hlp` format consumed by the DVX help viewer. The
compiler also emits an equivalent HTML file so the same content can
be browsed outside the system.
### Usage
```
dvxhlpc -o <output.hlp> [-i <imagedir>] [--html <out.html>] [--quiet] <input.dhs> [@filelist] ...
```
| Flag | Purpose |
|---------------|---------|
| `-o <file>` | Required. Path of the compiled `.hlp` output. |
| `-i <dir>` | Optional. Directory where referenced images live. Defaults to the working directory. |
| `--html <file>` | Optional. Also emit a standalone HTML rendering of the help content. |
| `--quiet` | Suppress progress output. |
| `@<list>` | Response file: each line names an additional `.dhs` input file. Lines starting with `#` and blank lines are ignored. |
| `<input.dhs>` | One or more input source files. Order matters for topic discovery. |
### Five-Pass Compilation
1. Parse each source file; collect topics, table-of-contents entries,
index entries, and image references.
2. Word-wrap text and list-item records to a fixed width.
3. Build a deduplicated string table.
4. Generate a trigram-based search index for full-text search.
5. Serialize the binary `.hlp` file (and, if requested, the HTML).
### Source Format (.dhs)
`.dhs` is a line-oriented markup format. Documents are divided into
topics, each with a unique ID and title. Topics can link to one
another, include images, and be grouped into table-of-contents
sections. Widget and library help files use `.bhs` ("BASIC help
source") files with the same format but a BASIC-oriented syntax
vocabulary. See existing sources under
`src/libs/kpunch/libdvx/*.dhs` for examples.
### Examples
```
# Compile the system reference help file
dvxhlpc -o bin/apps/kpunch/progman/dvxhelp.hlp \
--html docs/dvx_system_reference.html \
-i src/libs/kpunch/libdvx \
src/libs/kpunch/libdvx/sysdoc.dhs \
src/libs/kpunch/libdvx/arch.dhs \
src/libs/kpunch/libdvx/apiref.dhs
# Response file (one filename per line)
dvxhlpc -o out.hlp @inputs.txt
```
### Binaries
* Host: `bin/host/dvxhlpc`
* Target: `bin/system/DVXHLPC.EXE` (DOS executable)
The help compiler is also linked into the loader as an object file so
help files can be rebuilt on first boot if the module set has
changed.
## mkicon -- Application Icon Generator
Generates 32x32 24-bit BMP pixel-art icons for the bundled
applications. Each icon is drawn procedurally by dedicated code so no
external image assets are needed.
### Usage
```
mkicon <output.bmp> <type>
```
Available icon types: `clock`, `notepad`, `cpanel`, `dvxdemo`, `imgview`,
`noicon`.
Available types:
| Type | Description |
|-----------|-------------|
| `noicon` | Grey square with a red diagonal X and a question mark. Used as a fallback. |
| `clock` | Clock face with hour and minute hands. |
| `notepad` | Paper with folded corner and text lines. |
| `cpanel` | Gear wheel with eight teeth. |
| `dvxdemo` | Coloured diamond shape. |
| `imgview` | Landscape in a picture frame. |
| `basic` | BASIC "B" glyph flanked by code brackets. |
| `help` | Blue book with a question mark. |
| `iconed` | Pixel grid with a pencil. |
| `resedit` | Document window with stacked resource rows. |
### Example
```
mkicon icon32.bmp clock
```
Used by app Makefiles to generate `icon32.bmp` when no hand-drawn
icon exists.
### Binary
`bin/host/mkicon`
## mktbicon -- Toolbar Icon Generator
Generates 16x16 24-bit BMP toolbar button icons.
Generates 16x16 24-bit BMP toolbar icons for the DVX BASIC IDE.
### Usage
```
mktbicon <output.bmp> <type>
```
Used to create toolbar button resources for DVX BASIC and other apps.
Available types: `open`, `save`, `run`, `stop`, `code`, `design`,
`debug`, `stepinto`, `stepover`, `stepout`, `runtocur`.
### Example
```
mktbicon tb_run.bmp run
```
### Binary
`bin/host/mktbicon`
## Files
## mkwgticon -- Widget Toolbox Icon Generator
| File | Description |
|------|-------------|
| `dvxres.c` | Resource tool implementation |
| `mkicon.c` | 32x32 icon generator |
| `mktbicon.c` | 16x16 toolbar icon generator |
| `Makefile` | Builds `bin/dvxres`, `bin/mkicon`, `bin/mktbicon` (host native) |
Generates 24x24 24-bit BMP icons representing each widget type in the
DVX BASIC visual form designer's toolbox.
### Usage
```
mkwgticon <output.bmp> <type>
```
Supported widget types (one BMP per widget):
```
button label textbox checkbox radio dropdown
combobox listbox listview treeview image imgbtn
canvas slider spinner progress timer frame
hbox vbox splitter scrollpane tabctrl toolbar
statusbar separator spacer terminal wrapbox datactrl
dbgrid
```
### Example
```
mkwgticon wgt_button.bmp button
```
### Binary
`bin/host/mkwgticon`
## bmp2raw -- VGA Splash Converter
Converts a 320x200 256-colour BMP into the raw VGA splash image
format used by the bootstrap loader. The loader displays this raw
image in VGA mode 13h before switching into the configured VESA mode.
### Usage
```
bmp2raw <input.bmp> <output.raw>
```
Input must be exactly 320x200 at 8 bits per pixel. Output is a flat
64768-byte file:
* 768 bytes of palette (256 entries of 3 bytes, VGA 6-bit RGB values)
* 64000 bytes of pixel data (320x200 in top-to-bottom scanline order)
### Example
```
bmp2raw assets/splash.bmp bin/system/SPLASH.RAW
```
### Binary
`bin/host/bmp2raw`
## bascomp -- DVX BASIC Compiler (host)
Command-line compiler for DVX BASIC projects. Takes a `.dbp` project
file and produces a standalone `.app` DXE binary. The compiler links
against `basstub.app`, which is the runtime stub that hosts the
compiled BASIC bytecode.
### Usage
```
BASCOMP <project.dbp> [-o <output.app>] [-release]
```
| Flag | Purpose |
|------------------|---------|
| `<project.dbp>` | Required. Path to the project file. |
| `-o <output>` | Output path. Defaults to the project name with `.app` extension. |
| `-release` | Strip debug information and obfuscate form and control identifiers before emitting the binary. |
The `-release` flag enables two internal passes inside the compiler:
* **strip** -- removes debug variable info and debug UDT definitions
from the emitted module, mangles procedure names that are not
needed for name-based dispatch.
* **obfuscate** -- renames form and control identifiers to short
generated tokens (`C1`, `C2`, ...) throughout the module and in
the raw `.frm` text resources.
These are implemented in
`src/apps/kpunch/dvxbasic/compiler/strip.{c,h}` and
`src/apps/kpunch/dvxbasic/compiler/obfuscate.{c,h}` and run
automatically when `-release` is passed.
### Example
```
BASCOMP iconed/iconed.dbp -o bin/apps/kpunch/iconed/iconed.app -release
```
The compiler lives under `src/apps/kpunch/dvxbasic/stub/bascomp.c`
(the main-less library form of the BASIC stack is shared with the
IDE).
### Binaries
* Host: `bin/host/bascomp`
* Target: `bin/system/BASCOMP.EXE` (DOS executable)
## proxy -- SecLink Serial Bridge
Separate Linux-side program; see `src/tools/proxy/README.md`.
## Shared Files
| File | Description |
|------------------|-------------|
| `bmpDraw.h` / `bmpDraw.c` | Minimal 24-bit BMP canvas and pixel-art primitives shared by mkicon, mktbicon, and mkwgticon. |
| `dvxResWrite.h` | Resource-writing helper used by `dvxres`. |
| `hlpcCompile.h` | Public interface of the help-compiler core, used both by the standalone `dvxhlpc` binary and by the loader (for boot-time recompilation). |
## Build
```
make # builds bin/dvxres
make clean # removes bin/dvxres
make # build every host tool
make clean # remove bin/host/ and bin/system/
```
Uses the system GCC, not the DJGPP cross-compiler. Links against
`core/dvxResource.c` for the runtime resource API (`dvxResOpen`,
`dvxResRead`, `dvxResClose`).
Uses the system GCC with `-O2 -Wall -Wextra -Werror`. The resource
tool, help compiler, and BASIC compiler are additionally cross-compiled
with the DJGPP toolchain (`i586-pc-msdosdjgpp-gcc`) so they can ship as
DOS executables inside `bin/system/`. (`bascomp` is built by the
`src/apps/kpunch/dvxbasic/Makefile`, not this directory's Makefile.)
The targets are host-native only; they do not need the DJGPP
toolchain for the non-DOS binaries.

View file

@ -1,9 +1,9 @@
# SecLink Proxy -- Linux Serial Bridge
Linux-hosted proxy that bridges an 86Box emulated serial port to a
Linux-hosted proxy that bridges an emulated DOS serial port to a
remote telnet BBS. Part of the DVX GUI project.
The 86Box side communicates using the SecLink protocol (HDLC packet
The DOS side communicates using the SecLink protocol (HDLC packet
framing, CRC-16, DH key exchange, XTEA-CTR encryption). The BBS side
is plain telnet over TCP. All crypto is transparent to the BBS -- it
sees a normal telnet client.
@ -12,7 +12,7 @@ sees a normal telnet client.
## Architecture
```
86Box (DOS terminal) Remote BBS
DOS side (terminal app) Remote BBS
| |
emulated modem telnet
| |
@ -23,11 +23,11 @@ sees a normal telnet client.
(encrypted, reliable)
```
The proxy accepts a single TCP connection from 86Box, performs the
SecLink handshake (Diffie-Hellman key exchange), then connects to the
BBS. All traffic between 86Box and the proxy is encrypted via XTEA-CTR
on channel 0. Traffic between the proxy and the BBS is unencrypted
telnet with IAC negotiation handling.
The proxy accepts a single TCP connection from the DOS side, performs
the SecLink handshake (Diffie-Hellman key exchange), then connects to
the BBS. All traffic between the DOS side and the proxy is encrypted
via XTEA-CTR on channel 0. Traffic between the proxy and the BBS is
unencrypted telnet with IAC negotiation handling.
## Usage
@ -36,11 +36,11 @@ telnet with IAC negotiation handling.
secproxy [listen_port] [bbs_host] [bbs_port]
```
| Argument | Default | Description |
|---------------|--------------|---------------------------------|
| `listen_port` | 2323 | TCP port for 86Box connection |
| `bbs_host` | 10.1.0.244 | BBS hostname or IP |
| `bbs_port` | 2023 | BBS TCP port |
| Argument | Default | Description |
|---------------|--------------|----------------------------------|
| `listen_port` | 2323 | TCP port for the DOS connection |
| `bbs_host` | 10.1.0.244 | BBS hostname or IP |
| `bbs_port` | 2023 | BBS TCP port |
Examples:
@ -55,7 +55,7 @@ secproxy --help # show usage
## Startup Sequence
1. Listen on the configured TCP port.
2. Wait for 86Box to connect (polls with Ctrl+C support).
2. Wait for the DOS side to connect (polls with Ctrl+C support).
3. Map the TCP socket to COM0 via the socket shim.
4. Seed the RNG from `/dev/urandom`.
5. Open SecLink and perform the DH handshake (blocks until the DOS
@ -71,13 +71,13 @@ secproxy --help # show usage
The main loop uses `poll()` with a 10ms timeout to multiplex between
the two TCP connections:
- **86Box -> BBS**: `secLinkPoll()` reads from the 86Box socket via
- **DOS -> BBS**: `secLinkPoll()` reads from the DOS-side socket via
the socket shim, decrypts incoming packets, and the receive callback
writes plaintext to the BBS socket.
- **BBS -> 86Box**: `read()` from the BBS socket, then the telnet
- **BBS -> DOS**: `read()` from the BBS socket, then the telnet
filter strips IAC sequences, then `secLinkSend()` encrypts and sends
to 86Box via the socket shim. If the send window is full, the loop
retries with ACK processing until the data goes through.
to the DOS side via the socket shim. If the send window is full, the
loop retries with ACK processing until the data goes through.
- **Maintenance**: `secLinkPoll()` also handles packet-layer
retransmit timers on every iteration.
@ -110,12 +110,12 @@ the DOS build. A socket shim (`sockShim.h` / `sockShim.c`) provides
rs232-compatible functions backed by TCP sockets instead of UART
hardware:
| rs232 function | Socket shim behavior |
|----------------|-----------------------------------------------|
| rs232 function | Socket shim behavior |
|----------------|--------------------------------------------------|
| `rs232Open()` | Validates socket assigned; ignores serial params |
| `rs232Close()` | Marks port closed (socket lifecycle is caller's) |
| `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` |
| `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` |
| `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` |
| `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` |
The shim maps COM port indices (0-3) to socket file descriptors via
`sockShimSetFd()`, which must be called before opening the SecLink
@ -126,30 +126,31 @@ secLink layers, which defines `RS232_H` to prevent the real `rs232.h`
from being included.
## DJGPP Stubs
## Platform Stubs
DOS-specific headers required by the security library are replaced by
minimal stubs in `stubs/`:
| Stub | Replaces DJGPP header | Contents |
|-------------------|------------------------|-------------------|
| `stubs/pc.h` | `<pc.h>` | No-op definitions |
| `stubs/go32.h` | `<go32.h>` | No-op definitions |
| `stubs/sys/farptr.h` | `<sys/farptr.h>` | No-op definitions |
| Stub | Replaces | Contents |
|----------------------|------------------------|-------------------|
| `stubs/pc.h` | `<pc.h>` | No-op definitions |
| `stubs/go32.h` | `<go32.h>` | No-op definitions |
| `stubs/sys/farptr.h` | `<sys/farptr.h>` | No-op definitions |
The security library's hardware entropy function returns zeros on
Linux, which is harmless since the proxy seeds the RNG from
`/dev/urandom` before the handshake.
## 86Box Configuration
## Emulator Configuration
Configure the 86Box serial port to connect to the proxy:
Configure the emulated DOS machine's serial port to connect to the
proxy:
1. In 86Box settings, set a COM port to TCP client mode pointing at
the proxy's listen port (default 2323).
2. Enable "No telnet negotiation" to send raw bytes.
3. The DOS terminal application running inside 86Box uses SecLink
1. Point a COM port at the proxy's listen port (default 2323) in TCP
client mode.
2. Disable any built-in telnet negotiation so raw bytes pass through.
3. The DOS terminal application running in the emulator uses SecLink
over this serial port.
@ -176,8 +177,8 @@ proxy/
sockShim.c socket shim implementation
Makefile Linux native build
stubs/
pc.h stub for DJGPP <pc.h>
go32.h stub for DJGPP <go32.h>
pc.h stub for <pc.h>
go32.h stub for <go32.h>
sys/
farptr.h stub for DJGPP <sys/farptr.h>
farptr.h stub for <sys/farptr.h>
```

View file

@ -20,16 +20,16 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// SecLink proxy -- bridges an 86Box serial connection to a telnet BBS
// SecLink proxy -- bridges a DOS serial connection to a telnet BBS
//
// Architecture:
// 86Box (DOS terminal) <-> TCP <-> proxy <-> TCP <-> BBS
// secLink protocol plain telnet
// DOS terminal <-> TCP <-> proxy <-> TCP <-> BBS
// secLink protocol plain telnet
//
// The proxy runs on Linux and sits between two TCP connections:
// 1. Left side: 86Box connects via its serial-over-TCP feature. The proxy
// speaks the full secLink protocol (packet framing, DH handshake,
// XTEA encryption) over this connection.
// 1. Left side: the DOS side connects via a serial-over-TCP feature.
// The proxy speaks the full secLink protocol (packet framing, DH
// handshake, XTEA encryption) over this connection.
// 2. Right side: plain telnet to a BBS. The proxy handles telnet IAC
// negotiation and strips control sequences before forwarding clean
// data to the DOS side.
@ -414,7 +414,7 @@ int main(int argc, char *argv[]) {
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
// Listen for 86Box connection
// Listen for incoming DOS-side connection
listenFd = createListenSocket(listenPort);
if (listenFd < 0) {
fprintf(stderr, "Failed to listen on port %d: %s\n", listenPort, strerror(errno));
@ -422,7 +422,7 @@ int main(int argc, char *argv[]) {
}
printf("Listening on port %d...\n", listenPort);
// Accept connection from 86Box (poll so Ctrl+C works)
// Accept connection from the DOS side (poll so Ctrl+C works)
struct pollfd listenPoll = {listenFd, POLLIN, 0};
while (sRunning) {
int pr = poll(&listenPoll, 1, 500);
@ -439,7 +439,7 @@ int main(int argc, char *argv[]) {
}
return sRunning ? 1 : 0;
}
printf("86Box connected.\n");
printf("DOS client connected.\n");
sClientFd = clientFd;
// Map the TCP socket to "COM0" so the secLink stack (which calls rs232Read/
@ -501,8 +501,8 @@ int main(int argc, char *argv[]) {
// Main proxy loop: poll both sockets with a short timeout so secLinkPoll
// runs frequently enough for ACK processing and retransmits. Data flows:
// 86Box -> secLink (decrypt) -> callback -> write to BBS
// BBS -> read -> telnet filter -> secLink (encrypt) -> 86Box
// DOS side -> secLink (decrypt) -> callback -> write to BBS
// BBS -> read -> telnet filter -> secLink (encrypt) -> DOS side
fds[0].fd = clientFd;
fds[0].events = POLLIN;
fds[1].fd = bbsFd;
@ -511,11 +511,11 @@ int main(int argc, char *argv[]) {
while (sRunning) {
poll(fds, 2, POLL_TIMEOUT_MS);
// Process incoming secLink packets from 86Box
// Process incoming secLink packets from the DOS side
// (callback forwards decrypted data to BBS)
secLinkPoll(link);
// Read from BBS, filter telnet, send clean data to 86Box
// Read from BBS, filter telnet, send clean data to the DOS side
if (fds[1].revents & POLLIN) {
uint8_t raw[64];
uint8_t clean[64];
@ -539,7 +539,7 @@ int main(int argc, char *argv[]) {
// Check for disconnects
if (fds[0].revents & (POLLERR | POLLHUP)) {
printf("86Box disconnected.\n");
printf("DOS client disconnected.\n");
break;
}
if (fds[1].revents & (POLLERR | POLLHUP)) {

View file

@ -1,430 +1,46 @@
# DVX Widget Modules
Individual widget type implementations, each built as a separate `.wgt`
DXE module. The loader recursively scans the `WIDGETS/` directory at startup and
loads every `.wgt` file it finds. Each module exports `wgtRegister()`,
which registers its widget class(es) and API struct with the core.
Individual widget types, each built as a separate plugin module loaded
at shell startup. The set is extensible: adding a new widget means
adding a new subdirectory with its `.c`, `.dhs`, `.bhs`, and Makefile
target -- no core changes required.
Core knows nothing about individual widgets. All per-widget state lives
in `w->data` (allocated by the widget DXE). All per-widget behavior is
dispatched through the `WidgetClassT` vtable. Each widget provides a
public header with its API struct, typed accessor, and convenience
macros.
## Documentation
The overall widget system -- `WidgetT`, `WidgetClassT`, `WgtIfaceT`, the
prop/method/event descriptor tables, registration, and the paint /
layout / event pipeline -- is documented in `wgtsys.dhs` in this
directory.
## ABI-Stable Handler Dispatch
Each widget has its own two-part documentation:
`WidgetClassT` contains a `handlers[WGT_METHOD_MAX]` array of function
pointers. Core dispatches via integer index:
`w->wclass->handlers[WGT_METHOD_PAINT]`, etc. Currently 21 methods are
defined (`WGT_METHOD_COUNT`), with room for 32 (`WGT_METHOD_MAX`).
Adding new methods does not break existing widget DXE binaries -- old
widgets simply have NULL in the new slots.
- `<widget>/<short>.dhs` -- SDK C reference (how to create the widget
from C: constructor, API struct, flags, handler functions).
- `<widget>/<short>.bhs` -- DVX BASIC reference (properties, methods,
events, example BASIC code).
Key method constants:
Both compile into the DVX System / BASIC References respectively. Open
via the Help Viewer app, or read `docs/dvx_system_reference.html` and
`docs/dvx_basic_reference.html` after `make`.
| Constant | Index | Signature |
|----------|-------|-----------|
| `WGT_METHOD_PAINT` | 0 | `(w, d, ops, font, colors)` |
| `WGT_METHOD_PAINT_OVERLAY` | 1 | `(w, d, ops, font, colors)` |
| `WGT_METHOD_CALC_MIN_SIZE` | 2 | `(w, font)` |
| `WGT_METHOD_LAYOUT` | 3 | `(w, font)` |
| `WGT_METHOD_ON_MOUSE` | 5 | `(w, root, vx, vy)` |
| `WGT_METHOD_ON_KEY` | 6 | `(w, key, mod)` |
| `WGT_METHOD_DESTROY` | 8 | `(w)` |
| `WGT_METHOD_ON_DRAG_UPDATE` | 15 | `(w, root, x, y)` |
| `WGT_METHOD_ON_DRAG_END` | 16 | `(w, root, x, y)` |
| `WGT_METHOD_QUICK_REPAINT` | 19 | `(w, outY, outH)` |
## Widget Directory
## Generic Drag Support
One subdirectory per widget. Each subdirectory contains:
Widgets that need drag behavior implement `WGT_METHOD_ON_DRAG_UPDATE`
and `WGT_METHOD_ON_DRAG_END`. Core tracks the drag widget and calls
these methods on mouse move/release during a drag. This replaces
per-widget drag hacks. Used by Slider, Splitter, ListBox (reorder),
ListView (reorder, column resize), and TreeView (reorder).
## Dynamic Limits
All widget child arrays, app slot tables, and callback lists use stb_ds
dynamic arrays. There are no fixed maximums for child count, app count,
or registered callbacks.
## Widget Summary
26 source files produce 26 `.wgt` modules containing 32 widget types:
| Source File | .wgt File | Types | Public Header | Dependencies |
|-------------|-----------|-------|---------------|--------------|
| `widgetBox.c` | `box.wgt` | VBox, HBox, Frame | `widgetBox.h` | -- |
| `widgetButton.c` | `button.wgt` | Button | `widgetButton.h` | -- |
| `widgetLabel.c` | `label.wgt` | Label | `widgetLabel.h` | -- |
| `widgetTextInput.c` | `textinpt.wgt` | TextInput, TextArea | `widgetTextInput.h` | texthelp |
| `widgetCheckbox.c` | `checkbox.wgt` | Checkbox | `widgetCheckbox.h` | -- |
| `widgetRadio.c` | `radio.wgt` | RadioGroup, Radio | `widgetRadio.h` | -- |
| `widgetDropdown.c` | `dropdown.wgt` | Dropdown | `widgetDropdown.h` | listhelp |
| `widgetComboBox.c` | `combobox.wgt` | ComboBox | `widgetComboBox.h` | texthelp, listhelp |
| `widgetListBox.c` | `listbox.wgt` | ListBox | `widgetListBox.h` | listhelp |
| `widgetListView.c` | `listview.wgt` | ListView | `widgetListView.h` | listhelp |
| `widgetTreeView.c` | `treeview.wgt` | TreeView, TreeItem | `widgetTreeView.h` | listhelp |
| `widgetSpinner.c` | `spinner.wgt` | Spinner | `widgetSpinner.h` | texthelp |
| `widgetSlider.c` | `slider.wgt` | Slider | `widgetSlider.h` | -- |
| `widgetProgressBar.c` | `progress.wgt` | ProgressBar | `widgetProgressBar.h` | -- |
| `widgetImage.c` | `image.wgt` | Image | `widgetImage.h` | -- |
| `widgetImageButton.c` | `imgbtn.wgt` | ImageButton | `widgetImageButton.h` | -- |
| `widgetCanvas.c` | `canvas.wgt` | Canvas | `widgetCanvas.h` | -- |
| `widgetSeparator.c` | `separatr.wgt` | Separator | `widgetSeparator.h` | -- |
| `widgetSpacer.c` | `spacer.wgt` | Spacer | `widgetSpacer.h` | -- |
| `widgetTabControl.c` | `tabctrl.wgt` | TabControl, TabPage | `widgetTabControl.h` | -- |
| `widgetScrollPane.c` | `scrlpane.wgt` | ScrollPane | `widgetScrollPane.h` | -- |
| `widgetSplitter.c` | `splitter.wgt` | Splitter | `widgetSplitter.h` | -- |
| `widgetToolbar.c` | `toolbar.wgt` | Toolbar | `widgetToolbar.h` | -- |
| `widgetStatusBar.c` | `statbar.wgt` | StatusBar | `widgetStatusBar.h` | -- |
| `widgetTimer.c` | `timer.wgt` | Timer | `widgetTimer.h` | -- |
| `widgetAnsiTerm.c` | `terminal.wgt` | AnsiTerm | `widgetAnsiTerm.h` | texthelp |
## Dependency Files (.dep)
Widgets that use shared helper libraries need a `.dep` file so the
loader loads the helper first. Dep files are in `config/` and copied
to `bin/widgets/` during build:
| Widget | Dep File | Dependencies |
|--------|----------|--------------|
| textinpt | `textinpt.dep` | texthelp |
| combobox | `combobox.dep` | texthelp, listhelp |
| spinner | `spinner.dep` | texthelp |
| terminal | `terminal.dep` | texthelp |
| dropdown | `dropdown.dep` | listhelp |
| listbox | `listbox.dep` | listhelp |
| listview | `listview.dep` | listhelp |
| treeview | `treeview.dep` | listhelp |
## Per-Widget API Details
### Box (VBox, HBox, Frame)
Container widgets that arrange children vertically or horizontally.
Frame adds a titled border around its children.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `vBox(parent)` | `wgtVBox(parent)` | Create vertical container |
| `hBox(parent)` | `wgtHBox(parent)` | Create horizontal container |
| `frame(parent, title)` | `wgtFrame(parent, title)` | Create titled frame container |
### Button
Push button with text label and accelerator key support.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, text)` | `wgtButton(parent, text)` | Create a button |
### Label
Static text label with optional alignment.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, text)` | `wgtLabel(parent, text)` | Create a label |
| `setAlign(w, align)` | `wgtLabelSetAlign(w, align)` | Set text alignment |
### TextInput / TextArea
Single-line text input, password input, masked input, and multiline
text area. Uses texthelp.lib for editing engine, cursor blink, word
boundaries, clipboard, and selection.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, maxLen)` | `wgtTextInput(parent, maxLen)` | Single-line text input |
| `password(parent, maxLen)` | `wgtPasswordInput(parent, maxLen)` | Password input (masked display) |
| `masked(parent, mask)` | `wgtMaskedInput(parent, mask)` | Masked input (e.g. phone number) |
| `textArea(parent, maxLen)` | `wgtTextArea(parent, maxLen)` | Multiline text editor |
### Checkbox
Toggle checkbox with text label.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, text)` | `wgtCheckbox(parent, text)` | Create a checkbox |
| `isChecked(w)` | `wgtCheckboxIsChecked(w)` | Get checked state |
| `setChecked(w, checked)` | `wgtCheckboxSetChecked(w, checked)` | Set checked state |
### Radio (RadioGroup, Radio)
Mutually exclusive radio buttons within a group container.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `group(parent)` | `wgtRadioGroup(parent)` | Create radio group container |
| `create(parent, text)` | `wgtRadio(parent, text)` | Create radio button in group |
| `groupSetSelected(w, idx)` | `wgtRadioGroupSetSelected(w, idx)` | Select radio by index |
| `getIndex(w)` | `wgtRadioGetIndex(w)` | Get selected index |
### Dropdown
Drop-down selection list with popup overlay.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtDropdown(parent)` | Create a dropdown |
| `setItems(w, items, count)` | `wgtDropdownSetItems(w, items, count)` | Set item list |
| `getSelected(w)` | `wgtDropdownGetSelected(w)` | Get selected index |
| `setSelected(w, idx)` | `wgtDropdownSetSelected(w, idx)` | Set selected index |
### ComboBox
Editable text input with dropdown suggestion list. Combines TextInput
and Dropdown behavior.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, maxLen)` | `wgtComboBox(parent, maxLen)` | Create a combo box |
| `setItems(w, items, count)` | `wgtComboBoxSetItems(w, items, count)` | Set suggestion list |
| `getSelected(w)` | `wgtComboBoxGetSelected(w)` | Get selected index (-1 if typed) |
| `setSelected(w, idx)` | `wgtComboBoxSetSelected(w, idx)` | Select item by index |
### ListBox
Scrollable list of text items with single or multi-select and optional
drag-reorder.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtListBox(parent)` | Create a list box |
| `setItems(w, items, count)` | `wgtListBoxSetItems(w, items, count)` | Set item list |
| `getSelected(w)` | `wgtListBoxGetSelected(w)` | Get selected index |
| `setSelected(w, idx)` | `wgtListBoxSetSelected(w, idx)` | Set selected index |
| `setMultiSelect(w, multi)` | `wgtListBoxSetMultiSelect(w, multi)` | Enable multi-select |
| `isItemSelected(w, idx)` | `wgtListBoxIsItemSelected(w, idx)` | Check if item is selected |
| `setItemSelected(w, idx, sel)` | `wgtListBoxSetItemSelected(w, idx, sel)` | Select/deselect item |
| `selectAll(w)` | `wgtListBoxSelectAll(w)` | Select all items |
| `clearSelection(w)` | `wgtListBoxClearSelection(w)` | Deselect all items |
| `setReorderable(w, r)` | `wgtListBoxSetReorderable(w, r)` | Enable drag reorder |
### ListView
Multi-column list with sortable headers, resizable columns, single or
multi-select, and optional drag-reorder.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtListView(parent)` | Create a list view |
| `setColumns(w, cols, count)` | `wgtListViewSetColumns(w, cols, count)` | Define columns |
| `setData(w, cellData, rowCount)` | `wgtListViewSetData(w, cellData, rowCount)` | Set row data |
| `getSelected(w)` | `wgtListViewGetSelected(w)` | Get selected row |
| `setSelected(w, idx)` | `wgtListViewSetSelected(w, idx)` | Set selected row |
| `setSort(w, col, dir)` | `wgtListViewSetSort(w, col, dir)` | Set sort column/direction |
| `setHeaderClickCallback(w, cb)` | `wgtListViewSetHeaderClickCallback(w, cb)` | Set header click handler |
| `setMultiSelect(w, multi)` | `wgtListViewSetMultiSelect(w, multi)` | Enable multi-select |
| `isItemSelected(w, idx)` | `wgtListViewIsItemSelected(w, idx)` | Check if row is selected |
| `setItemSelected(w, idx, sel)` | `wgtListViewSetItemSelected(w, idx, sel)` | Select/deselect row |
| `selectAll(w)` | `wgtListViewSelectAll(w)` | Select all rows |
| `clearSelection(w)` | `wgtListViewClearSelection(w)` | Deselect all rows |
| `setReorderable(w, r)` | `wgtListViewSetReorderable(w, r)` | Enable drag reorder |
Column definition uses `ListViewColT` with title, tagged width, and alignment
(`ListViewAlignLeftE`, `ListViewAlignCenterE`, `ListViewAlignRightE`).
Sort direction: `ListViewSortNoneE`, `ListViewSortAscE`, `ListViewSortDescE`.
### TreeView (TreeView, TreeItem)
Hierarchical tree with expand/collapse, single or multi-select, and
optional drag-reorder.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtTreeView(parent)` | Create a tree view |
| `getSelected(w)` | `wgtTreeViewGetSelected(w)` | Get selected item widget |
| `setSelected(w, item)` | `wgtTreeViewSetSelected(w, item)` | Set selected item |
| `setMultiSelect(w, multi)` | `wgtTreeViewSetMultiSelect(w, multi)` | Enable multi-select |
| `setReorderable(w, r)` | `wgtTreeViewSetReorderable(w, r)` | Enable drag reorder |
| `item(parent, text)` | `wgtTreeItem(parent, text)` | Create tree item |
| `itemSetExpanded(w, exp)` | `wgtTreeItemSetExpanded(w, exp)` | Expand/collapse item |
| `itemIsExpanded(w)` | `wgtTreeItemIsExpanded(w)` | Check if expanded |
| `itemIsSelected(w)` | `wgtTreeItemIsSelected(w)` | Check if selected |
| `itemSetSelected(w, sel)` | `wgtTreeItemSetSelected(w, sel)` | Select/deselect item |
### Spinner
Numeric input with up/down buttons.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, min, max, step)` | `wgtSpinner(parent, min, max, step)` | Create a spinner |
| `setValue(w, value)` | `wgtSpinnerSetValue(w, value)` | Set current value |
| `getValue(w)` | `wgtSpinnerGetValue(w)` | Get current value |
| `setRange(w, min, max)` | `wgtSpinnerSetRange(w, min, max)` | Set value range |
| `setStep(w, step)` | `wgtSpinnerSetStep(w, step)` | Set increment step |
### Slider
Horizontal track with draggable thumb for value selection.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, min, max)` | `wgtSlider(parent, min, max)` | Create a slider |
| `setValue(w, value)` | `wgtSliderSetValue(w, value)` | Set current value |
| `getValue(w)` | `wgtSliderGetValue(w)` | Get current value |
### ProgressBar
Horizontal or vertical progress indicator (0-100).
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtProgressBar(parent)` | Create horizontal progress bar |
| `createV(parent)` | `wgtProgressBarV(parent)` | Create vertical progress bar |
| `setValue(w, value)` | `wgtProgressBarSetValue(w, value)` | Set value (0-100) |
| `getValue(w)` | `wgtProgressBarGetValue(w)` | Get current value |
### Image
Displays a pixel buffer or image file in native display format.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, data, w, h, pitch)` | `wgtImage(parent, data, w, h, pitch)` | Create from pixel data |
| `fromFile(parent, path)` | `wgtImageFromFile(parent, path)` | Create from image file |
| `setData(w, data, imgW, imgH, pitch)` | `wgtImageSetData(w, data, imgW, imgH, pitch)` | Replace image data |
### ImageButton
Clickable button displaying an image instead of text.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, data, w, h, pitch)` | `wgtImageButton(parent, data, w, h, pitch)` | Create from pixel data |
| `fromFile(parent, path)` | `wgtImageButtonFromFile(parent, path)` | Create from image file |
| `setData(w, data, imgW, imgH, pitch)` | `wgtImageButtonSetData(w, data, imgW, imgH, pitch)` | Replace image data |
### Canvas
Drawable surface with pen tools, shapes, pixel access, and file I/O.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, w, h)` | `wgtCanvas(parent, w, h)` | Create a canvas |
| `clear(w, color)` | `wgtCanvasClear(w, color)` | Fill with solid color |
| `setPenColor(w, color)` | `wgtCanvasSetPenColor(w, color)` | Set drawing color |
| `setPenSize(w, size)` | `wgtCanvasSetPenSize(w, size)` | Set pen width |
| `setMouseCallback(w, cb)` | `wgtCanvasSetMouseCallback(w, cb)` | Set mouse event handler |
| `save(w, path)` | `wgtCanvasSave(w, path)` | Save canvas to file |
| `load(w, path)` | `wgtCanvasLoad(w, path)` | Load image into canvas |
| `drawLine(w, x0, y0, x1, y1)` | `wgtCanvasDrawLine(w, ...)` | Draw line |
| `drawRect(w, x, y, w, h)` | `wgtCanvasDrawRect(w, ...)` | Draw rectangle outline |
| `fillRect(w, x, y, w, h)` | `wgtCanvasFillRect(w, ...)` | Fill rectangle |
| `fillCircle(w, cx, cy, r)` | `wgtCanvasFillCircle(w, ...)` | Fill circle |
| `setPixel(w, x, y, color)` | `wgtCanvasSetPixel(w, x, y, color)` | Set individual pixel |
| `getPixel(w, x, y)` | `wgtCanvasGetPixel(w, x, y)` | Read pixel color |
### Separator (HSeparator, VSeparator)
Thin decorative line (horizontal or vertical).
| API Function | Macro | Description |
|--------------|-------|-------------|
| `hSeparator(parent)` | `wgtHSeparator(parent)` | Create horizontal separator |
| `vSeparator(parent)` | `wgtVSeparator(parent)` | Create vertical separator |
### Spacer
Invisible flexible space for layout padding.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtSpacer(parent)` | Create a spacer |
### TabControl (TabControl, TabPage)
Tabbed container with clickable tab headers.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtTabControl(parent)` | Create tab control |
| `page(parent, title)` | `wgtTabPage(parent, title)` | Add a tab page |
| `setActive(w, idx)` | `wgtTabControlSetActive(w, idx)` | Switch to tab by index |
| `getActive(w)` | `wgtTabControlGetActive(w)` | Get active tab index |
### ScrollPane
Scrollable container with internal scrollbars. Child content can
exceed the visible area; the ScrollPane handles clipping and scroll.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtScrollPane(parent)` | Create a scroll pane |
### Splitter
Resizable split between two child regions with a draggable divider.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, vertical)` | `wgtSplitter(parent, vertical)` | Create splitter |
| `setPos(w, pos)` | `wgtSplitterSetPos(w, pos)` | Set divider position (pixels) |
| `getPos(w)` | `wgtSplitterGetPos(w)` | Get divider position |
### Toolbar
Horizontal container for toolbar buttons (typically ImageButtons).
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtToolbar(parent)` | Create a toolbar |
### StatusBar
Single-line text display at the bottom of a window.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent)` | `wgtStatusBar(parent)` | Create a status bar |
Set text via `wgtSetText(statusBar, "text")`.
### Timer
Non-visual widget that fires callbacks at a specified interval.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, intervalMs, repeat)` | `wgtTimer(parent, intervalMs, repeat)` | Create a timer |
| `start(w)` | `wgtTimerStart(w)` | Start the timer |
| `stop(w)` | `wgtTimerStop(w)` | Stop the timer |
| `setInterval(w, intervalMs)` | `wgtTimerSetInterval(w, intervalMs)` | Change interval |
| `isRunning(w)` | `wgtTimerIsRunning(w)` | Check if running |
| `updateTimers()` | `wgtUpdateTimers()` | Process all pending timers |
### AnsiTerm
ANSI terminal emulator widget with scrollback buffer. Supports standard
escape sequences (cursor control, SGR colors, erase, scroll, DEC
private modes). Optionally connects to a comm channel for serial I/O.
| API Function | Macro | Description |
|--------------|-------|-------------|
| `create(parent, cols, rows)` | `wgtAnsiTerm(parent, cols, rows)` | Create terminal |
| `write(w, data, len)` | `wgtAnsiTermWrite(w, data, len)` | Write data to terminal |
| `clear(w)` | `wgtAnsiTermClear(w)` | Clear terminal screen |
| `setComm(w, ctx, readFn, writeFn)` | `wgtAnsiTermSetComm(w, ctx, readFn, writeFn)` | Set comm channel |
| `setScrollback(w, maxLines)` | `wgtAnsiTermSetScrollback(w, maxLines)` | Set scrollback depth |
| `poll(w)` | `wgtAnsiTermPoll(w)` | Poll comm channel |
| `repaint(w, outY, outH)` | `wgtAnsiTermRepaint(w, outY, outH)` | Fast incremental repaint |
- `widget<Name>.c` -- implementation (defines `sApi`, `sIface`,
`sProps`, `sMethods`, `sEvents`, `sClass`).
- `<name>.h` -- public C API (where applicable).
- `<short>.dhs` -- SDK C help source.
- `<short>.bhs` -- BASIC help source.
- Optional icons (`icon.bmp`, `icon32.bmp`) and resource file.
Widgets shipped: ansiTerm, box, button, canvas, checkbox, comboBox,
dataCtrl, dbGrid, dropdown, image, imageButton, label, listBox,
listView, progressBar, radio, scrollPane, separator, slider, spacer,
spinner, splitter, statusBar, tabControl, textInput, timer, toolbar,
treeView, wrapBox.
## Build
```
make # builds all 26 .wgt modules + dep files
make clean # removes objects, .wgt files, and dep files
```
Each widget is compiled to a single `.o` file, then packaged via
`dxe3gen` into a `.wgt` DXE module exporting only `wgtRegister`.
`make -C src/widgets` (invoked by the top-level `make`). Produces one
`.wgt` per subdirectory under `bin/widgets/kpunch/`.

View file

@ -31,9 +31,11 @@
.h2 AnsiTerm
A VT100/ANSI-compatible terminal emulator widget designed for connecting to BBS systems over the serial link. Uses a traditional text-mode cell buffer (character + attribute byte pairs) with the CP437 character set and 16-color CGA palette. Supports cursor movement, screen/line erase, scrolling regions, SGR colors, and scrollback history. Communication is abstracted through read/write function pointers, allowing the terminal to work with raw serial ports, the secLink encrypted channel, or any other byte-oriented transport.
A VT100/ANSI-compatible terminal emulator widget designed for connecting to BBS systems over the serial link. Uses a traditional text-mode cell buffer (character + attribute byte pairs) with the CP437 character set and 16-color CGA palette. Supports cursor movement, screen/line erase, scrolling regions, SGR colors, DEC private modes, and a configurable scrollback buffer. Communication is abstracted through read/write function pointers, allowing the terminal to work with raw serial ports, the secLink encrypted channel, or any other byte-oriented transport.
Header: widgets/widgetAnsiTerm.h
The widget renders through two paint paths: a full paint used by the normal widget pipeline, and a fast incremental repaint (wgtAnsiTermRepaint) that pushes dirty rows directly to the window content buffer for low-latency serial echo.
Header: widgets/ansiTerm.h
.h3 Creation
@ -41,7 +43,7 @@ Header: widgets/widgetAnsiTerm.h
WidgetT *term = wgtAnsiTerm(parent, 80, 25);
.endcode
.h3 Macros
.h3 API Functions
.index wgtAnsiTermWrite
.index wgtAnsiTermClear
@ -51,15 +53,31 @@ WidgetT *term = wgtAnsiTerm(parent, 80, 25);
.index wgtAnsiTermRepaint
.table
Macro Description
----- -----------
wgtAnsiTerm(parent, cols, rows) Create an ANSI terminal widget with the given column and row dimensions.
wgtAnsiTermWrite(w, data, len) Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer, len is the byte count.
wgtAnsiTermClear(w) Clear the terminal screen and reset the cursor to the home position.
wgtAnsiTermSetComm(w, ctx, readFn, writeFn) Attach a communication channel. readFn and writeFn are I/O callbacks; ctx is passed as their first argument.
wgtAnsiTermSetScrollback(w, maxLines) Set the maximum number of scrollback lines. Lines scrolled off the top are saved in a circular buffer.
wgtAnsiTermPoll(w) Poll the communication channel for incoming data and feed it into the ANSI parser.
wgtAnsiTermRepaint(w, outY, outH) Fast repaint path that renders dirty rows directly into the window's content buffer, bypassing the widget pipeline. Returns the dirty region via outY/outH.
Function Description
-------- -----------
WidgetT *wgtAnsiTerm(parent, cols, rows) Create an ANSI terminal widget with the given column/row dimensions.
void wgtAnsiTermWrite(w, data, len) Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer.
void wgtAnsiTermClear(w) Clear the terminal screen and reset the cursor to the home position.
void wgtAnsiTermSetComm(w, ctx, readFn, writeFn) Attach a communication channel. readFn and writeFn are I/O callbacks; ctx is passed as their first argument.
void wgtAnsiTermSetScrollback(w, maxLines) Set the maximum number of scrollback lines.
int32_t wgtAnsiTermPoll(w) Poll the communication channel for incoming data and feed it into the parser. Returns number of bytes consumed.
int32_t wgtAnsiTermRepaint(w, outY, outH) Fast repaint path that renders dirty rows directly into the window's content buffer, bypassing the widget pipeline. Returns the number of rows repainted and the dirty region via outY/outH.
.endtable
.h3 API Struct (wgtRegisterApi "ansiterm")
The sApi struct exposes these function pointers:
.table
Slot Function
---- --------
create wgtAnsiTerm
write wgtAnsiTermWrite
clear wgtAnsiTermClear
setComm wgtAnsiTermSetComm
setScrollback wgtAnsiTermSetScrollback
poll wgtAnsiTermPoll
repaint wgtAnsiTermRepaint
.endtable
.h3 Properties (BASIC Interface)
@ -69,7 +87,7 @@ WidgetT *term = wgtAnsiTerm(parent, 80, 25);
-------- ---- ------ -----------
Cols Integer Read-only Number of columns.
Rows Integer Read-only Number of rows.
Scrollback Integer Write-only Maximum scrollback lines.
Scrollback Integer Write-only Maximum number of scrollback lines.
.endtable
.h3 Methods (BASIC Interface)
@ -78,9 +96,24 @@ WidgetT *term = wgtAnsiTerm(parent, 80, 25);
Method Description
------ -----------
Clear Clear the terminal screen.
Write Write a string into the terminal.
Poll Process any pending bytes on the attached communication channel.
Write Write a string into the terminal (with ANSI escape processing).
.endtable
.h3 Events
AnsiTerm uses the common events only. No widget-specific events are defined.
.h3 Keyboard Shortcuts
Within the terminal widget:
.table
Key Action
--- ------
Ctrl+C Copy selection to the clipboard (when a selection exists).
Ctrl+V Send the clipboard contents to the attached write function.
Arrows Send VT100 cursor escape sequences.
Home/End Send VT100 Home/End sequences.
PgUp/PgDn/Del Send the corresponding VT100 escape sequences.
.endtable

View file

@ -30,9 +30,9 @@
.h1 Terminal
DVX Extension -- DVX Widget: ansiterm (ANSI terminal emulator)
DVX Extension -- DVX Widget: ansiterm
A VT100/ANSI terminal emulator widget. Supports ANSI escape sequences, scrollback buffer, and serial communication. Default size is 80 columns by 25 rows.
A VT100/ANSI terminal emulator with a text-mode cell buffer, 16-color CGA palette, and scrollback history. Processes standard ANSI escape sequences (cursor control, erase, scroll, SGR colors, DEC private modes) typically used by DOS BBS software. Default size is 80 columns by 25 rows. Supports text selection with the mouse, Ctrl+C to copy, and Ctrl+V to send clipboard text as input.
.h2 Type-Specific Properties
@ -41,23 +41,20 @@ A VT100/ANSI terminal emulator widget. Supports ANSI escape sequences, scrollbac
---------- ------- -------------------------------------------
Cols Integer Number of character columns (read-only).
Rows Integer Number of character rows (read-only).
Scrollback Integer Number of scrollback lines (write-only).
Scrollback Integer Maximum number of scrollback lines. Older lines are dropped (write-only; default 500).
.endtable
.index CommAttach
.index CommOpen
.h2 Type-Specific Methods
.table
Method Description
------ -----------
Clear Clear the terminal screen.
Poll Process pending comm data and update the display.
Write text$ Write text (with ANSI escape processing) to the terminal.
Clear Clear the terminal screen and reset the cursor to the home position.
Poll Poll the attached communication channel for incoming bytes and feed them into the ANSI parser.
Write text$ Write a string to the terminal with full ANSI escape processing.
.endtable
No default event.
No default event. Uses common events only.
.h2 Serial Communication
@ -72,4 +69,17 @@ CommAttach link, "Terminal1"
See comm.bas for the full communications API.
.h2 Example
.code
Begin Terminal Terminal1
MinWidth = 640
MinHeight = 400
End
Sub Form_Load ()
Terminal1.Write "Welcome!" & Chr$(13) & Chr$(10)
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods

View file

@ -76,6 +76,15 @@ static int32_t sTypeId = -1;
#define BLINK_MS 500
#define CURSOR_MS 250
// dirtyRows is a 32-bit bitmask, so ANSI_MAX_DIRTY_ROWS is fixed at 32.
// Rows beyond this still render correctly -- they just always repaint
// because we can't track their dirty state in the mask.
#define ANSI_MAX_DIRTY_ROWS 32
#define ANSI_DIRTY_ALL_ROWS 0xFFFFFFFF
// Clipboard buffer size for selection copy.
#define ANSI_CLIPBOARD_BUF 4096
typedef struct {
uint8_t *cells;
int32_t cols;
@ -109,6 +118,7 @@ typedef struct {
int32_t lastCursorCol;
uint32_t packedPalette[16];
bool paletteValid;
bool hasBlinkCells; // tracked to skip the per-blink O(rows*cols) scan
int32_t selStartLine;
int32_t selStartCol;
int32_t selEndLine;
@ -202,7 +212,7 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) {
static void ansiTermClearSelection(WidgetT *w) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data;
if (ansiTermHasSelection(w)) { at->dirtyRows = 0xFFFFFFFF; }
if (ansiTermHasSelection(w)) { at->dirtyRows = ANSI_DIRTY_ALL_ROWS; }
at->selStartLine = -1;
at->selStartCol = -1;
at->selEndLine = -1;
@ -217,7 +227,7 @@ static void ansiTermCopySelection(WidgetT *w) {
int32_t sLine, sCol, eLine, eCol;
ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol);
int32_t cols = at->cols;
char buf[4096];
char buf[ANSI_CLIPBOARD_BUF];
int32_t pos = 0;
for (int32_t line = sLine; line <= eLine && pos < 4095; line++) {
const uint8_t *lineData = ansiTermGetLine(w, line);
@ -264,7 +274,7 @@ static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) {
static void ansiTermDirtyRow(WidgetT *w, int32_t row) {
if (row >= 0 && row < 32) {
if (row >= 0 && row < ANSI_MAX_DIRTY_ROWS) {
((AnsiTermDataT *)w->data)->dirtyRows |= (1U << row);
}
}
@ -558,7 +568,7 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data;
switch (at->parseState) {
case PARSE_NORMAL:
if (ch == 0x1B) { at->parseState = PARSE_ESC; }
if (ch == KEY_ESCAPE) { at->parseState = PARSE_ESC; }
else if (ch == '\r') { at->cursorCol = 0; }
else if (ch == '\n') { ansiTermNewline(w); }
else if (ch == '\b') { if (at->cursorCol > 0) { at->cursorCol--; } }
@ -634,6 +644,9 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) {
int32_t idx = (row * cols + col) * 2;
at->cells[idx] = ch;
at->cells[idx + 1] = at->curAttr;
if (at->curAttr & ATTR_BLINK_BIT) {
at->hasBlinkCells = true;
}
ansiTermDirtyRow(w, row);
}
at->cursorCol++;
@ -652,7 +665,7 @@ static void ansiTermScrollDown(WidgetT *w) {
int32_t bytesPerRow = cols * 2;
if (bot > top) { memmove(at->cells + (top + 1) * bytesPerRow, at->cells + top * bytesPerRow, (bot - top) * bytesPerRow); }
ansiTermFillCells(w, top * cols, cols);
for (int32_t r = top; r <= bot && r < 32; r++) { at->dirtyRows |= (1U << r); }
for (int32_t r = top; r <= bot && r < ANSI_MAX_DIRTY_ROWS; r++) { at->dirtyRows |= (1U << r); }
}
@ -669,7 +682,7 @@ static void ansiTermScrollUp(WidgetT *w) {
}
if (bot > top) { memmove(at->cells + top * bytesPerRow, at->cells + (top + 1) * bytesPerRow, (bot - top) * bytesPerRow); }
ansiTermFillCells(w, bot * cols, cols);
for (int32_t r = top; r <= bot && r < 32; r++) { at->dirtyRows |= (1U << r); }
for (int32_t r = top; r <= bot && r < ANSI_MAX_DIRTY_ROWS; r++) { at->dirtyRows |= (1U << r); }
}
@ -705,7 +718,7 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
at->selEndLine = -1; at->selEndCol = -1;
at->blinkVisible = true; at->blinkTime = clock();
at->cursorOn = true; at->cursorTime = clock();
at->dirtyRows = 0xFFFFFFFF; at->lastCursorRow = -1; at->lastCursorCol = -1;
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; at->lastCursorRow = -1; at->lastCursorCol = -1;
for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; }
return w;
}
@ -755,17 +768,29 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
clock_t curInterval = (clock_t)CURSOR_MS * CLOCKS_PER_SEC / 1000;
if ((now - at->blinkTime) >= blinkInterval) {
at->blinkTime = now; at->blinkVisible = !at->blinkVisible;
int32_t cols = at->cols; int32_t rows = at->rows;
for (int32_t row = 0; row < rows && row < 32; row++) {
for (int32_t col = 0; col < cols; col++) {
if (at->cells[(row * cols + col) * 2 + 1] & ATTR_BLINK_BIT) { at->dirtyRows |= (1U << row); break; }
if (at->hasBlinkCells) {
int32_t cols = at->cols; int32_t rows = at->rows;
bool anyBlink = false;
for (int32_t row = 0; row < rows && row < ANSI_MAX_DIRTY_ROWS; row++) {
for (int32_t col = 0; col < cols; col++) {
if (at->cells[(row * cols + col) * 2 + 1] & ATTR_BLINK_BIT) {
at->dirtyRows |= (1U << row);
anyBlink = true;
break;
}
}
}
// Clear flag if a full scan found no blink cells (e.g. after a
// terminal clear). This keeps subsequent blink ticks free.
if (!anyBlink) {
at->hasBlinkCells = false;
}
}
}
if ((now - at->cursorTime) >= curInterval) {
at->cursorTime = now; at->cursorOn = !at->cursorOn;
int32_t cRow = at->cursorRow;
if (cRow >= 0 && cRow < 32) { at->dirtyRows |= (1U << cRow); }
if (cRow >= 0 && cRow < ANSI_MAX_DIRTY_ROWS) { at->dirtyRows |= (1U << cRow); }
}
if (!at->commRead) { return 0; }
uint8_t buf[256];
@ -872,7 +897,7 @@ static bool widgetAnsiTermClearSelection(WidgetT *w) {
at->selEndLine = -1;
at->selEndCol = -1;
at->selecting = false;
at->dirtyRows = 0xFFFFFFFF;
at->dirtyRows = ANSI_DIRTY_ALL_ROWS;
return true;
}
@ -916,7 +941,7 @@ static void widgetAnsiTermOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, in
int32_t lineIndex = at->scrollPos + row;
at->selEndLine = lineIndex;
at->selEndCol = col;
at->dirtyRows = 0xFFFFFFFF;
at->dirtyRows = ANSI_DIRTY_ALL_ROWS;
}
@ -930,20 +955,20 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (ansiTermHasSelection(w)) { ansiTermClearSelection(w); }
uint8_t buf[8];
int32_t len = 0;
if (key >= 32 && key < 127) { buf[0] = (uint8_t)key; len = 1; }
else if (key == 0x1B) { buf[0] = 0x1B; len = 1; }
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) { buf[0] = (uint8_t)key; len = 1; }
else if (key == KEY_ESCAPE) { buf[0] = 0x1B; len = 1; }
else if (key == 0x09) { buf[0] = 0x09; len = 1; }
else if (key == 13 || key == 10) { buf[0] = '\r'; len = 1; }
else if (key == 8) { buf[0] = 0x08; len = 1; }
else if (key == (0x48 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; len = 3; }
else if (key == (0x50 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; len = 3; }
else if (key == (0x4D | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; len = 3; }
else if (key == (0x4B | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; len = 3; }
else if (key == (0x47 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; len = 3; }
else if (key == (0x4F | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; len = 3; }
else if (key == (0x49 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; len = 4; }
else if (key == (0x51 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; len = 4; }
else if (key == (0x53 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; len = 4; }
else if (key == KEY_UP) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; len = 3; }
else if (key == KEY_DOWN) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; len = 3; }
else if (key == KEY_RIGHT) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; len = 3; }
else if (key == KEY_LEFT) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; len = 3; }
else if (key == KEY_HOME) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; len = 3; }
else if (key == KEY_END) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; len = 3; }
else if (key == KEY_PGUP) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; len = 4; }
else if (key == KEY_PGDN) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; len = 4; }
else if (key == KEY_DELETE) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; len = 4; }
else if (key >= 1 && key < 32) { buf[0] = (uint8_t)key; len = 1; }
if (len > 0) { at->commWrite(at->commCtx, buf, len); }
wgtInvalidatePaint(w);
@ -991,7 +1016,7 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
at->selEndLine = lineIndex; at->selEndCol = clickCol;
at->selecting = true; sDragWidget = hit;
}
at->dirtyRows = 0xFFFFFFFF;
at->dirtyRows = ANSI_DIRTY_ALL_ROWS;
return;
}
@ -1031,9 +1056,9 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t sbCount = at->scrollbackCount;
bool viewingLive = (at->scrollPos == sbCount);
uint32_t dirty = at->dirtyRows;
if (dirty == 0) { dirty = 0xFFFFFFFF; }
if (dirty == 0) { dirty = ANSI_DIRTY_ALL_ROWS; }
for (int32_t row = 0; row < rows; row++) {
if (row < 32 && !(dirty & (1U << row))) { continue; }
if (row < ANSI_MAX_DIRTY_ROWS && !(dirty & (1U << row))) { continue; }
int32_t lineIndex = at->scrollPos + row;
const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
int32_t curCol = -1;

View file

@ -28,7 +28,7 @@
.h1 Frame
VB Equivalent: Frame -- DVX Widget: frame (titled VBox container)
VB Equivalent: Frame -- DVX Widget: frame (titled container)
A container with a titled border. Child controls are placed inside the frame using VBox layout. In the .frm file, nest Begin/End blocks inside the Frame block.
@ -40,6 +40,8 @@ A container with a titled border. Child controls are placed inside the frame usi
Caption String The title displayed in the frame border.
.endtable
No type-specific methods or events.
Container: Yes
Default Event: Click
@ -69,13 +71,27 @@ End
DVX Extension -- DVX Widget: hbox (horizontal layout container)
A container that arranges its children horizontally, left to right. Use Weight on children to distribute extra space.
A container that arranges its children horizontally, left to right. No visible border. Use Weight on children to distribute extra space proportionally.
No type-specific properties, methods, or events.
Container: Yes
Default Event: Click
No type-specific properties.
.h2 Example
.code
Begin HBox ButtonRow
Spacing = 4
Begin CommandButton Command1
Caption = "OK"
End
Begin CommandButton Command2
Caption = "Cancel"
End
End
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.vbox VBox
@ -89,13 +105,26 @@ No type-specific properties.
DVX Extension -- DVX Widget: vbox (vertical layout container)
A container that arranges its children vertically, top to bottom. No title or border. Use Weight on children to distribute extra space.
A container that arranges its children vertically, top to bottom. No title or border. Use Weight on children to distribute extra space proportionally.
No type-specific properties, methods, or events.
Container: Yes
Default Event: Click
No type-specific properties.
.h2 Example
.code
Begin VBox Column1
Spacing = 4
Begin Label Label1
Caption = "Name:"
End
Begin TextBox Text1
End
End
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.hbox HBox

View file

@ -35,33 +35,63 @@
.h2 Box (VBox / HBox / Frame)
Container widgets that arrange their children in a vertical column (VBox), horizontal row (HBox), or a titled group box (Frame). These are the primary layout building blocks. Children are laid out using a flexbox-like algorithm with weight-based extra-space distribution.
Container widgets that arrange their children in a vertical column (VBox), horizontal row (HBox), or a titled group box (Frame). These are the primary layout building blocks. Children are laid out using a weight-based algorithm with configurable spacing, padding, and alignment.
Header: widgets/widgetBox.h
Frame is a labelled grouping container with a Motif-style beveled border. Its title text sits centered vertically on the top border line with a small background-filled gap behind the title, giving the classic Windows 3.1 / Motif group box appearance. Internally, Frame behaves like a VBox for layout purposes.
The widget DXE registers three separate interface entries ("vbox", "hbox", "frame") so the form designer can create each type independently.
Header: widgets/box.h
.h3 Creation
.code
WidgetT *row = wgtHBox(parent);
WidgetT *col = wgtVBox(parent);
WidgetT *group = wgtFrame(parent, "Options");
.endcode
.h3 API Functions
.table
Macro Description
----- -----------
wgtVBox(parent) Create a vertical box container. Children are stacked top to bottom.
wgtHBox(parent) Create a horizontal box container. Children are placed left to right.
wgtFrame(parent, title) Create a titled group box (a VBox with a border and label).
Function Description
-------- -----------
WidgetT *wgtVBox(parent) Create a vertical box container. Children stack top-to-bottom.
WidgetT *wgtHBox(parent) Create a horizontal box container. Children stack left-to-right.
WidgetT *wgtFrame(parent, title) Create a titled group box. Children stack vertically inside the bordered frame. The title string may include a '&' prefix for an accelerator key.
.endtable
.h3 API Struct (wgtRegisterApi "box")
.table
Slot Function
---- --------
vBox wgtVBox
hBox wgtHBox
frame wgtFrame
.endtable
The designer also registers per-type APIs: "vbox", "hbox", and "frame" each expose a single create slot.
.h3 Properties
Box containers use the common WidgetT fields for layout control:
Box containers use the common WidgetT fields for layout control. There are no widget-specific properties registered with the interface system.
.table
Property Description
-------- -----------
align Main-axis alignment of children. HBox: Start=left, Center=center, End=right. VBox: Start=top, Center=center, End=bottom.
spacing Gap between children (tagged size).
padding Internal padding around children (tagged size).
weight Controls how the box itself stretches within its parent.
Field Description
----- -----------
align Main-axis alignment of children. HBox: Start=left, Center=center, End=right. VBox: Start=top, Center=center, End=bottom.
spacing Gap between children (tagged size).
padding Internal padding around children (tagged size).
weight Controls how the box itself stretches within its parent.
.endtable
Frame text is managed via the standard wgtSetText() / wgtGetText() interface (the widget has WCLASS_HAS_TEXT). BASIC code can set its title via the generic "Caption" or "Text" property.
.h3 Events
Containers use the common events only. No widget-specific events.
Containers use the common events only. No widget-specific events or methods are registered.
.h3 Default Event
"Click" on all three types.

View file

@ -31,17 +31,17 @@
VB Equivalent: CommandButton -- DVX Widget: button | Name Prefix: Command
A push button that triggers an action when clicked. Created with wgtButton(parent, text).
A push button that triggers an action when clicked. The Caption may include a '&' before a character to mark an accelerator key (e.g. "&OK" underlines 'O', and Alt+O activates the button).
.h2 Type-Specific Properties
.table
Property Type Description
-------- ------ -------------------------------------------
Caption String The text displayed on the button. Use & for accelerator keys (e.g. "&OK").
Caption String The text displayed on the button. Use '&' for an accelerator key.
.endtable
No additional type-specific properties beyond common properties and Caption.
No type-specific methods or events.
Default Event: Click

View file

@ -29,32 +29,53 @@
.h2 Button
A push button with a text label. Fires onClick when pressed and released. Supports keyboard activation via accelerator keys.
A push button with a text label. Fires onClick when pressed and released. Uses a two-phase press model: the button visually depresses on mouse-down and fires onClick only when the mouse is released while still inside the button bounds -- dragging the mouse off cancels the press. Supports accelerator keys via '&' prefix in the text (e.g. "&OK" underlines 'O' and binds Alt+O).
Header: widgets/widgetButton.h
Disabled buttons use the classic "embossed" rendering (highlight text offset by +1,+1 and shadow text at 0,0) for a chiseled appearance.
Header: widgets/button.h
.h3 Creation
.code
WidgetT *btn = wgtButton(parent, "OK");
WidgetT *btn = wgtButton(parent, "&OK");
btn->onClick = onOkClicked;
.endcode
.h3 Macro
.h3 API Functions
.table
Macro Description
----- -----------
wgtButton(parent, text) Create a push button with the given label text.
Function Description
-------- -----------
WidgetT *wgtButton(parent, text) Create a push button with the given label text. Pass NULL for no text. Text is copied into the widget.
.endtable
.h3 API Struct (wgtRegisterApi "button")
.table
Slot Function
---- --------
create wgtButton
.endtable
.h3 Properties
Uses common WidgetT properties. Set accelKey for keyboard shortcut. Use wgtSetText() / wgtGetText() to change the label.
Uses common WidgetT properties. Label text is managed via wgtSetText() / wgtGetText(). Set accelKey for keyboard shortcut (automatically parsed from '&' prefix in the text). The button is focusable (WCLASS_FOCUSABLE) and draws a focus rectangle when it holds keyboard focus.
No widget-specific properties are registered with the interface system. BASIC code sets the label via the generic "Caption" or "Text" property.
.h3 Methods
No widget-specific methods.
.h3 Events
.table
Callback Description
-------- -----------
onClick Fires when the button is clicked (press + release).
onClick Fires when the button is clicked (mouse press and release inside, or Space/Enter when focused).
.endtable
.h3 Default Event
"Click" (VB basName: CommandButton, namePrefix: Command).

View file

@ -36,12 +36,15 @@
.index wgtCanvasDrawText
.index wgtCanvasSave
.index wgtCanvasLoad
.index wgtCanvasResize
.h2 Canvas
A freeform drawing surface with a fixed-size pixel buffer. Provides drawing primitives (lines, rectangles, circles, text, individual pixels) and supports save/load to BMP files. Mouse interaction is available via a callback.
A freeform drawing surface with a fixed-size pixel buffer stored in the display's native pixel format. Provides drawing primitives (lines, rectangles, circles, text, individual pixels) and supports save/load to image files (BMP/PNG/etc. via dvxLoadImage/dvxSaveImage). Mouse interaction is available via a callback. The buffer is rendered to screen as a straight blit with no per-pixel conversion.
Header: widgets/widgetCanvas.h
Canvas coordinates are independent of widget position: (0,0) is the top-left of the canvas content, not the widget. The widget frames the buffer with a 2-pixel sunken bevel.
Header: widgets/canvas.h
.h3 Creation
@ -49,44 +52,87 @@ Header: widgets/widgetCanvas.h
WidgetT *cv = wgtCanvas(parent, 320, 200);
.endcode
.h3 Macros
.h3 API Functions
.index wgtCanvasSetPenColor
.index wgtCanvasSetPenSize
.index wgtCanvasSetMouseCallback
.table
Macro Description
----- -----------
wgtCanvas(parent, w, h) Create a canvas with the given pixel dimensions.
wgtCanvasClear(w, color) Fill the entire canvas with a solid color.
wgtCanvasSetPenColor(w, color) Set the drawing pen color.
wgtCanvasSetPenSize(w, size) Set the drawing pen size in pixels.
wgtCanvasDrawLine(w, x0, y0, x1, y1) Draw a line from (x0,y0) to (x1,y1).
wgtCanvasDrawRect(w, x, y, width, height) Draw a rectangle outline.
wgtCanvasFillRect(w, x, y, width, height) Draw a filled rectangle.
wgtCanvasFillCircle(w, cx, cy, radius) Draw a filled circle.
wgtCanvasSetPixel(w, x, y, color) Set a single pixel to the given color.
wgtCanvasGetPixel(w, x, y) Get the color of a single pixel.
wgtCanvasDrawText(w, x, y, text) Draw text at the given position using the current pen color.
wgtCanvasSetMouseCallback(w, cb) Set a mouse interaction callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). Receives canvas-relative coordinates and whether the mouse is being dragged.
wgtCanvasSave(w, path) Save the canvas contents to a BMP file.
wgtCanvasLoad(w, path) Load a BMP file into the canvas.
Function Description
-------- -----------
WidgetT *wgtCanvas(parent, w, h) Create a canvas with the given pixel dimensions. Buffer is initialized to white.
void wgtCanvasClear(w, color) Fill the entire canvas with a packed color.
void wgtCanvasSetPenColor(w, color) Set the drawing pen color (packed display color).
void wgtCanvasSetPenSize(w, size) Set the drawing pen size in pixels (affects DrawLine dot thickness).
void wgtCanvasDrawLine(w, x0, y0, x1, y1) Draw a line using Bresenham's algorithm with the current pen color and size.
void wgtCanvasDrawRect(w, x, y, width, height) Draw a 1-pixel rectangle outline using the current pen color.
void wgtCanvasFillRect(w, x, y, width, height) Fill a rectangle using the current pen color.
void wgtCanvasFillCircle(w, cx, cy, radius) Fill a circle using the current pen color (integer sqrt; no FPU needed).
void wgtCanvasSetPixel(w, x, y, color) Set a single pixel to the given packed color.
uint32_t wgtCanvasGetPixel(w, x, y) Read a single pixel. Returns packed color.
void wgtCanvasDrawText(w, x, y, text) Draw text at the given canvas coordinates using the current pen color.
void wgtCanvasSetMouseCallback(w, cb) Set a mouse callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). cx/cy are canvas-relative, drag=true on mouse-move during press.
int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file. Returns 0 on success, -1 on error.
int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error.
void wgtCanvasResize(w, newW, newH) Resize the canvas buffer. New pixels are filled with white.
.endtable
.h3 API Struct (wgtRegisterApi "canvas")
.table
Slot Function
---- --------
create wgtCanvas
clear wgtCanvasClear
setPenColor wgtCanvasSetPenColor
setPenSize wgtCanvasSetPenSize
setMouseCallback wgtCanvasSetMouseCallback
save wgtCanvasSave
load wgtCanvasLoad
drawLine wgtCanvasDrawLine
drawRect wgtCanvasDrawRect
fillRect wgtCanvasFillRect
fillCircle wgtCanvasFillCircle
setPixel wgtCanvasSetPixel
getPixel wgtCanvasGetPixel
drawText wgtCanvasDrawText
resize wgtCanvasResize
.endtable
.h3 Properties
Canvas has no widget-specific properties registered with the interface system. Drawing state (pen color, pen size) is managed by the API functions above.
.h3 Methods (BASIC Interface)
.table
Method Description
------ -----------
Clear color% Fill the entire canvas with a 0x00RRGGBB color.
DrawLine x0%, y0%, x1%, y1%, color% Draw a line between two points.
DrawRect x%, y%, w%, h%, color% Draw a rectangle outline.
DrawText x%, y%, text$ Draw text at the given position.
FillCircle cx%, cy%, radius%, color% Fill a circle.
FillRect x%, y%, w%, h%, color% Fill a rectangle.
GetPixel(x%, y%) Returns the 0x00RRGGBB color at a pixel.
Load path$ Load an image file onto the canvas.
Resize w%, h% Resize the canvas (fills new area with white).
Save path$ Save the canvas to an image file.
SetPenColor color% Set the drawing color (0x00RRGGBB).
SetPenSize size% Set the pen size in pixels.
SetPixel x%, y%, color% Set a single pixel.
.endtable
.h3 Events
.table
Callback Description
-------- -----------
onClick Fires when the canvas is clicked.
Mouse callback (via wgtCanvasSetMouseCallback) Fires on mouse down and drag with canvas-local coordinates.
Callback Description
-------- -----------
onClick Fires when the canvas is clicked.
Mouse callback (via wgtCanvasSetMouseCallback) Fires on mouse down and drag with canvas-local coordinates.
.endtable
.h3 Methods (BASIC Interface)
.h3 Default Event
.table
Method Description
------ -----------
Clear Clear the canvas to a given color.
.endtable
"Click" (VB basName: PictureBox, namePrefix: Picture).

View file

@ -30,7 +30,7 @@
VB Equivalent: CheckBox -- DVX Widget: checkbox
A toggle control with a label. Checked state is exposed as a Boolean.
A toggle control with a label. The checked state is exposed as a Boolean. Supports accelerator keys (put '&' before a character in the Caption). Press Space or Enter to toggle when focused.
.h2 Type-Specific Properties
@ -41,6 +41,8 @@ A toggle control with a label. Checked state is exposed as a Boolean.
Value Boolean True if checked, False if unchecked.
.endtable
No type-specific methods or events.
Default Event: Click
.h2 Example

View file

@ -31,33 +31,36 @@
.h2 Checkbox
A toggle control with a text label. Clicking toggles between checked and unchecked states.
A toggle control with a text label. Clicking toggles between checked and unchecked states. Fires onChange each time the state flips. Supports accelerator keys (via '&' in the text) and keyboard toggle via Space or Enter when focused.
Header: widgets/widgetCheckbox.h
The check mark is drawn as an "X" pattern rather than a traditional checkmark glyph. The focus rectangle wraps the label text (not the box), matching the Windows 3.1 convention.
Header: widgets/checkbox.h
.h3 Creation
.code
WidgetT *cb = wgtCheckbox(parent, "Enable logging");
WidgetT *cb = wgtCheckbox(parent, "Enable &logging");
.endcode
.h3 Macros
.h3 API Functions
.table
Macro Description
----- -----------
wgtCheckbox(parent, text) Create a checkbox with the given label text.
wgtCheckboxIsChecked(w) Returns true if the checkbox is checked.
wgtCheckboxSetChecked(w, checked) Set the checked state programmatically.
Function Description
-------- -----------
WidgetT *wgtCheckbox(parent, text) Create a checkbox with the given label text.
bool wgtCheckboxIsChecked(w) Returns true if the checkbox is checked.
void wgtCheckboxSetChecked(w, checked) Set the checked state programmatically. Triggers a repaint but does NOT fire onChange.
.endtable
.h3 Events
.h3 API Struct (wgtRegisterApi "checkbox")
.table
Callback Description
-------- -----------
onClick Fires when clicked (after toggle).
onChange Fires when the checked state changes.
Slot Function
---- --------
create wgtCheckbox
isChecked wgtCheckboxIsChecked
setChecked wgtCheckboxSetChecked
.endtable
.h3 Properties (BASIC Interface)
@ -67,3 +70,22 @@ WidgetT *cb = wgtCheckbox(parent, "Enable logging");
-------- ---- ------ -----------
Value Boolean Read/Write Whether the checkbox is checked.
.endtable
Label text is managed via the standard wgtSetText() / wgtGetText() interface. BASIC code uses the generic "Caption" or "Text" property.
.h3 Methods
No widget-specific methods.
.h3 Events
.table
Callback Description
-------- -----------
onClick Fires when clicked (after the toggle has already applied).
onChange Fires when the checked state changes (mouse click or keyboard toggle).
.endtable
.h3 Default Event
"Click" (VB basName: CheckBox).

View file

@ -39,6 +39,9 @@
#define CHECKBOX_BOX_SIZE 12
#define CHECKBOX_GAP 4
// Check-glyph inset from the box edge on each side. Drawing the X this
// far inside leaves a consistent border between the mark and the bevel.
#define CHECKBOX_CHECK_INSET 3
static int32_t sTypeId = -1;
@ -164,9 +167,9 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
// for what's always a small fixed-size glyph (6x6 pixels). The 3px
// inset from the box edge keeps the mark visually centered.
if (cd->checked) {
int32_t cx = w->x + 3;
int32_t cy = boxY + 3;
int32_t cs = CHECKBOX_BOX_SIZE - 6;
int32_t cx = w->x + CHECKBOX_CHECK_INSET;
int32_t cy = boxY + CHECKBOX_CHECK_INSET;
int32_t cs = CHECKBOX_BOX_SIZE - 2 * CHECKBOX_CHECK_INSET;
uint32_t checkFg = w->enabled ? fg : colors->windowShadow;
for (int32_t i = 0; i < cs; i++) {

View file

@ -27,40 +27,59 @@
.index ComboBox
.index wgtComboBox
.index wgtComboBoxSetItems
.index wgtComboBoxAddItem
.index wgtComboBoxRemoveItem
.index wgtComboBoxClear
.index wgtComboBoxGetSelected
.index wgtComboBoxSetSelected
.h2 ComboBox
A combination of a text input and a dropdown list. The user can either type a value or select from a list of predefined options. Unlike Dropdown, the text field is editable.
A combination of a single-line text input and a drop-down list. The user can type a value or select one from the list. When the user picks a list item, its text is copied into the edit buffer. Supports full text editing (cursor movement, selection, clipboard, undo) via the texthelp library, and a popup overlay list via the listhelp library.
Header: widgets/widgetComboBox.h
Depends on "texthelp" and "listhelp" helper libraries (declared in combobox.dep).
Header: widgets/comboBox.h
.h3 Creation
.code
WidgetT *cb = wgtComboBox(parent, 128);
const char *items[] = { "Arial", "Courier", "Times" };
wgtComboBoxSetItems(cb, items, 3);
wgtComboBoxAddItem(cb, "Arial");
wgtComboBoxAddItem(cb, "Courier");
wgtComboBoxAddItem(cb, "Times");
.endcode
.h3 Macros
.h3 API Functions
.table
Macro Description
----- -----------
wgtComboBox(parent, maxLen) Create a combo box. maxLen is the maximum text input length.
wgtComboBoxSetItems(w, items, count) Set the dropdown items.
wgtComboBoxGetSelected(w) Get the index of the selected item (-1 if the text does not match any item).
wgtComboBoxSetSelected(w, idx) Set the selected item by index.
Function Description
-------- -----------
WidgetT *wgtComboBox(parent, maxLen) Create a combo box. maxLen is the maximum editable text length (0 => default 256).
void wgtComboBoxSetItems(w, items, count) Set the dropdown items from a const char ** array. Items are not copied -- caller owns them.
int32_t wgtComboBoxGetSelected(w) Get the index of the last selected item (-1 if the text was typed freely).
void wgtComboBoxSetSelected(w, idx) Select an item by index. Copies its text into the edit buffer.
void wgtComboBoxAddItem(w, text) Append an item to the owned list (strdup'd).
void wgtComboBoxRemoveItem(w, idx) Remove an owned item by index.
void wgtComboBoxClear(w) Remove all owned items and reset the selection.
const char *wgtComboBoxGetItem(w, idx) Get the text of an item by index.
int32_t wgtComboBoxGetItemCount(w) Get the total number of items.
.endtable
.h3 Events
.h3 API Struct (wgtRegisterApi "combobox")
.table
Callback Description
-------- -----------
onChange Fires when the text or selection changes.
Slot Function
---- --------
create wgtComboBox
setItems wgtComboBoxSetItems
getSelected wgtComboBoxGetSelected
setSelected wgtComboBoxSetSelected
addItem wgtComboBoxAddItem
removeItem wgtComboBoxRemoveItem
clear wgtComboBoxClear
getItem wgtComboBoxGetItem
getItemCount wgtComboBoxGetItemCount
.endtable
.h3 Properties (BASIC Interface)
@ -68,5 +87,31 @@ wgtComboBoxSetItems(cb, items, 3);
.table
Property Type Access Description
-------- ---- ------ -----------
ListIndex Integer Read/Write Index of the currently selected item.
ListIndex Integer Read/Write Index of the currently selected item (-1 if none).
.endtable
Editable text is accessible via the generic "Text" property. "ListCount" is available through the ListCount method.
.h3 Methods (BASIC Interface)
.table
Method Description
------ -----------
AddItem text$ Append an item to the dropdown list.
Clear Remove all items and clear the selection.
List(index%) Return the text of the item at the given index.
ListCount() Return the total number of items.
RemoveItem index% Remove the item at the given index.
.endtable
.h3 Events
.table
Callback Description
-------- -----------
onChange Fires when the selection or text changes.
.endtable
.h3 Default Event
"Click" (VB basName: ComboBox).

View file

@ -27,9 +27,9 @@
.h1 ComboBox
VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down list, max 256 chars)
VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down list, default maxLen 256)
A combination of a text input and a drop-down list. The user can type text or select from the list. Supports the same AddItem/RemoveItem/Clear/List methods as ListBox.
A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / List methods as ListBox.
.h2 Type-Specific Properties
@ -37,16 +37,39 @@ A combination of a text input and a drop-down list. The user can type text or se
Property Type Description
--------- ------- -------------------------------------------
Text String The text in the editable field.
ListIndex Integer Index of the currently selected list item (-1 = none).
ListCount Integer Number of items in the drop-down list (read-only).
ListIndex Integer Index of the currently selected list item (-1 = user typed something not in the list).
.endtable
.h2 Type-Specific Methods
Same as ListBox: AddItem, RemoveItem, Clear, List.
.link ctrl.listbox See ListBox for details
.table
Method Description
------ -----------
AddItem text$ Append an item to the drop-down list.
Clear Remove all items.
List(index%) Return the text of the item at the given index.
ListCount() Return the number of items in the list.
RemoveItem index% Remove the item at the given index.
.endtable
Default Event: Click
.h2 Example
.code
Begin ComboBox Combo1
End
Sub Form_Load ()
Combo1.AddItem "Small"
Combo1.AddItem "Medium"
Combo1.AddItem "Large"
End Sub
Sub Combo1_Change ()
Label1.Caption = "Selected: " & Combo1.Text
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.listbox See ListBox for details

View file

@ -142,7 +142,7 @@ WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
d->selStart = -1;
d->selEnd = -1;
d->selectedIdx = -1;
w->weight = 100;
w->weight = WGT_WEIGHT_FILL;
}
return w;
@ -337,7 +337,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (d->open) {
if (key == (0x48 | 0x100)) {
if (key == KEY_UP) {
if (d->hoverIdx > 0) {
d->hoverIdx--;
@ -350,7 +350,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
return;
}
if (key == (0x50 | 0x100)) {
if (key == KEY_DOWN) {
if (d->hoverIdx < d->itemCount - 1) {
d->hoverIdx++;
@ -410,7 +410,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
// Down arrow on closed combobox opens the popup
if (!d->open && key == (0x50 | 0x100)) {
if (!d->open && key == KEY_DOWN) {
d->open = true;
d->hoverIdx = d->selectedIdx;
sOpenPopup = w;

View file

@ -32,9 +32,11 @@
.h2 DataCtrl
A VB3-style Data control for database binding. Displays a visible navigation bar that connects to a SQLite database via dvxSql* functions. Reads all rows from the RecordSource query into an in-memory cache for bidirectional navigation. Fires Reposition events when the cursor moves so bound controls can update. Supports master-detail linking between Data controls.
A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads all rows from the RecordSource query into an in-memory cache for bidirectional navigation. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATEs use the KeyColumn to identify the row.
Header: widgets/widgetDataCtrl.h
The widget depends on the "dvxsql" library DXE; the navigation logic caches the full result set so there is no round-trip for MoveNext/MovePrev.
Header: widgets/dataCtrl.h
.h3 Creation
@ -42,38 +44,65 @@ Header: widgets/widgetDataCtrl.h
WidgetT *data = wgtDataCtrl(parent);
.endcode
.h3 Macros
Set properties (DatabaseName, RecordSource, KeyColumn, etc.) and then call dataCtrlRefresh to populate.
.index wgtDataCtrlRefresh
.index wgtDataCtrlMoveFirst
.index wgtDataCtrlMoveNext
.index wgtDataCtrlGetField
.index wgtDataCtrlSetField
.index wgtDataCtrlUpdate
.index wgtDataCtrlAddNew
.index wgtDataCtrlDelete
.h3 API Functions
.index dataCtrlRefresh
.index dataCtrlMoveFirst
.index dataCtrlMoveNext
.index dataCtrlGetField
.index dataCtrlSetField
.index dataCtrlUpdate
.index dataCtrlAddNew
.index dataCtrlDelete
.table
Macro Description
----- -----------
wgtDataCtrl(parent) Create a Data control.
wgtDataCtrlRefresh(w) Re-execute the RecordSource query and rebuild the row cache.
wgtDataCtrlMoveFirst(w) Move the cursor to the first row.
wgtDataCtrlMovePrev(w) Move the cursor to the previous row.
wgtDataCtrlMoveNext(w) Move the cursor to the next row.
wgtDataCtrlMoveLast(w) Move the cursor to the last row.
wgtDataCtrlGetField(w, colName) Get the value of a column in the current row. Returns const char *.
wgtDataCtrlSetField(w, colName, value) Set the value of a column in the current row (marks the row dirty).
wgtDataCtrlUpdateRow(w) Write the current row's pending changes back to the database.
wgtDataCtrlUpdate(w) Flush all pending changes to the database.
wgtDataCtrlAddNew(w) Begin a new row. Sets dirty state; call Update to commit.
wgtDataCtrlDelete(w) Delete the current row from the database.
wgtDataCtrlSetMasterValue(w, val) Set the master-detail filter value for this control.
wgtDataCtrlGetRowCount(w) Get the total number of cached rows.
wgtDataCtrlGetColCount(w) Get the number of columns in the result set.
wgtDataCtrlGetColName(w, col) Get the name of a column by index. Returns const char *.
wgtDataCtrlGetCellText(w, row, col) Get the text of a specific cell by row and column index. Returns const char *.
wgtDataCtrlSetCurrentRow(w, row) Set the current row by index (0-based).
Function Description
-------- -----------
WidgetT *dataCtrlCreate(parent) Create a Data control. Exposed as create in the API struct.
void dataCtrlRefresh(w) Re-execute the RecordSource query and rebuild the row cache.
void dataCtrlMoveFirst(w) Move the cursor to the first row.
void dataCtrlMovePrev(w) Move the cursor to the previous row.
void dataCtrlMoveNext(w) Move the cursor to the next row.
void dataCtrlMoveLast(w) Move the cursor to the last row.
const char *dataCtrlGetField(w, colName) Get the value of a column in the current row (case-insensitive).
void dataCtrlSetField(w, colName, value) Set the value of a column in the current row. Marks the row dirty.
void dataCtrlUpdate(w) Flush pending changes (INSERT or UPDATE) to the database.
void dataCtrlUpdateRow(w) Legacy wrapper around dataCtrlUpdate.
void dataCtrlAddNew(w) Append a blank row and move the cursor to it. The row is dirty and new until Update is called.
void dataCtrlDelete(w) Delete the current row from the cache and the database.
void dataCtrlSetMasterValue(w, val) Set the master-detail filter value. Normally managed by the form runtime.
int32_t dataCtrlGetRowCount(w) Get the total number of cached rows.
int32_t dataCtrlGetColCount(w) Get the number of columns in the result set.
const char *dataCtrlGetColName(w, col) Get the name of a column by index.
const char *dataCtrlGetCellText(w, row, col) Get the text of a specific cell.
void dataCtrlSetCurrentRow(w, row) Set the current row by index. Auto-saves the previous row if dirty.
.endtable
.h3 API Struct (wgtRegisterApi "data")
.table
Slot Function
---- --------
create dataCtrlCreate
refresh dataCtrlRefresh
moveFirst dataCtrlMoveFirst
movePrev dataCtrlMovePrev
moveNext dataCtrlMoveNext
moveLast dataCtrlMoveLast
getField dataCtrlGetField
setField dataCtrlSetField
updateRow dataCtrlUpdateRow
update dataCtrlUpdate
addNew dataCtrlAddNew
delete dataCtrlDelete
setMasterValue dataCtrlSetMasterValue
getRowCount dataCtrlGetRowCount
getColCount dataCtrlGetColCount
getColName dataCtrlGetColName
getCellText dataCtrlGetCellText
setCurrentRow dataCtrlSetCurrentRow
.endtable
.h3 Properties (BASIC Interface)
@ -87,9 +116,9 @@ WidgetT *data = wgtDataCtrl(parent);
MasterSource String Read/Write Name of the master Data control for master-detail linking.
MasterField String Read/Write Column in the master control to read for the filter value.
DetailField String Read/Write Column in this table to filter by the master value.
Caption String Read/Write Text displayed on the navigation bar.
BOF Boolean Read-only True when the cursor is before the first row.
EOF Boolean Read-only True when the cursor is past the last row.
Caption String Read/Write Text displayed on the navigator bar.
BOF Boolean Read-only True when the cursor is at the first row (or no rows).
EOF Boolean Read-only True when the cursor is past the last row (or no rows).
.endtable
.h3 Methods (BASIC Interface)
@ -97,12 +126,12 @@ WidgetT *data = wgtDataCtrl(parent);
.table
Method Description
------ -----------
AddNew Begin a new row.
AddNew Append a blank row and move to it.
Delete Delete the current row.
MoveFirst Move to the first row.
MoveLast Move to the last row.
MoveNext Move to the next row.
MovePrevious Move to the previous row.
MoveFirst Move the cursor to the first row.
MoveLast Move the cursor to the last row.
MoveNext Move the cursor to the next row.
MovePrevious Move the cursor to the previous row.
Refresh Re-execute the query and rebuild the cache.
Update Write pending changes to the database.
.endtable
@ -112,6 +141,6 @@ WidgetT *data = wgtDataCtrl(parent);
.table
Event Description
----- -----------
Reposition Fires when the current row changes (navigation, refresh, etc.). Default event.
Validate Fires before writing changes. Return false to cancel.
Reposition Fires when the current row changes. Default event.
Validate Fires before writing. Return false to cancel the write.
.endtable

View file

@ -37,26 +37,26 @@
.h1 Data
VB Equivalent: Data -- DVX Widget: data (database record navigator)
VB Equivalent: Data -- DVX Widget: data | Name Prefix: Data
A data access control that connects to a SQLite database and provides record navigation. Other controls can bind to a Data control via their DataSource and DataField properties.
A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations auto-save the current row if dirty.
.link ctrl.databinding See Data Binding for details
.h2 Type-Specific Properties
.table
Property Type R/W Description
------------ ------- --- -------------------------------------------
DatabaseName String R/W Path to the SQLite database file.
RecordSource String R/W Table name or SQL SELECT query for the recordset.
KeyColumn String R/W Primary key column name (used for UPDATE/DELETE operations).
Caption String R/W Text displayed on the navigator bar.
BOF Boolean R True if the current position is before the first record (read-only).
EOF Boolean R True if the current position is past the last record (read-only).
MasterSource String R/W Name of a master Data control (for master-detail binding).
MasterField String R/W Column in the master recordset to filter by.
DetailField String R/W Column in this recordset that matches the master field.
Property Type Description
------------ ------- -------------------------------------------
DatabaseName String Path to the SQLite database file.
RecordSource String Table name or SQL SELECT query for the recordset.
KeyColumn String Primary key column name (used for UPDATE/DELETE operations).
Caption String Text displayed on the navigator bar.
BOF Boolean True if the current position is the first record or there are no records (read-only).
EOF Boolean True if the current position is past the last record or there are no records (read-only).
MasterSource String Name of a master Data control (for master-detail binding).
MasterField String Column in the master recordset to read for the filter value.
DetailField String Column in this recordset that matches the master field.
.endtable
.h2 Type-Specific Methods
@ -68,7 +68,7 @@ A data access control that connects to a SQLite database and provides record nav
MoveLast (none) Navigate to the last record.
MoveNext (none) Navigate to the next record.
MovePrevious (none) Navigate to the previous record.
AddNew (none) Add a new blank record.
AddNew (none) Append a blank record and move to it.
Delete (none) Delete the current record.
Refresh (none) Re-query the database and reload records.
Update (none) Write pending changes to the database.
@ -79,10 +79,31 @@ A data access control that connects to a SQLite database and provides record nav
.table
Event Parameters Description
---------- ----------------- -------------------------------------------
Reposition (none) Fires after the current record changes (navigation). This is the default event.
Validate Cancel As Integer Fires before writing a record. Set Cancel = 1 to abort.
Reposition (none) Fires after the current record changes. Default event.
Validate Cancel As Integer Fires before writing. Set Cancel = 1 to abort.
.endtable
Default Event: Reposition
.h2 Example
.code
Begin Data Data1
DatabaseName = "books.db"
RecordSource = "titles"
KeyColumn = "id"
End
Begin TextBox Text1
DataSource = "Data1"
DataField = "title"
End
Sub Form_Load ()
Data1.Refresh
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.databinding Data Binding
.link ctrl.dbgrid DBGrid

View file

@ -26,40 +26,55 @@
.toc 0 DbGrid
.index DbGrid
.index wgtDbGrid
.index wgtDbGridSetDataWidget
.index Database Grid
.h2 DbGrid
A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized, and renamed by the application. Clicking a column header sorts the display. Selecting a row syncs the Data control's cursor position. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data.
A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. Selecting a row syncs the Data control's cursor position. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data.
Header: widgets/widgetDbGrid.h
The grid supports alternating row shading, vertical and horizontal scrollbars, keyboard navigation (arrows, Page Up/Down, Home, End), and double-click activation (fires onDblClick).
Header: widgets/dbGrid.h
.h3 Creation
.code
WidgetT *grid = wgtDbGrid(parent);
wgtDbGridSetDataWidget(grid, dataCtrl);
dbGridSetDataWidget(grid, dataCtrl);
.endcode
.h3 Macros
.h3 API Functions
.index wgtDbGridRefresh
.index wgtDbGridSetColumnVisible
.index wgtDbGridSetColumnHeader
.index wgtDbGridSetColumnWidth
.index wgtDbGridGetSelectedRow
.index dbGridRefresh
.index dbGridSetColumnVisible
.index dbGridSetColumnHeader
.index dbGridSetColumnWidth
.index dbGridGetSelectedRow
.table
Macro Description
----- -----------
wgtDbGrid(parent) Create a database grid widget.
wgtDbGridSetDataWidget(w, dataWidget) Bind the grid to a Data control. The grid reads rows from this widget.
wgtDbGridRefresh(w) Re-read the Data control's state and repaint the grid.
wgtDbGridSetColumnVisible(w, fieldName, visible) Show or hide a column by field name.
wgtDbGridSetColumnHeader(w, fieldName, header) Set a display header for a column (overrides the field name).
wgtDbGridSetColumnWidth(w, fieldName, width) Set the width of a column by field name (tagged size, 0 = auto).
wgtDbGridGetSelectedRow(w) Get the index of the currently selected data row (-1 if none).
Function Description
-------- -----------
WidgetT *dbGridCreate(parent) Create a database grid widget. Exposed as create in the API struct.
void dbGridSetDataWidget(w, dataWidget) Bind the grid to a Data control. Auto-populates columns from the Data control's column names.
void dbGridRefresh(w) Re-read the Data control's state and repaint the grid. Rebuilds the sort index if sorting is active.
void dbGridSetColumnVisible(w, fieldName, visible) Show or hide a column by field name.
void dbGridSetColumnHeader(w, fieldName, header) Set a display header for a column (overrides the field name).
void dbGridSetColumnWidth(w, fieldName, width) Set a column's width (tagged size, 0 = auto).
int32_t dbGridGetSelectedRow(w) Get the index of the currently selected data row (-1 if none).
.endtable
.h3 API Struct (wgtRegisterApi "dbgrid")
.table
Slot Function
---- --------
create dbGridCreate
setDataWidget dbGridSetDataWidget
refresh dbGridRefresh
setColumnVisible dbGridSetColumnVisible
setColumnHeader dbGridSetColumnHeader
setColumnWidth dbGridSetColumnWidth
getSelectedRow dbGridGetSelectedRow
.endtable
.h3 Properties (BASIC Interface)
@ -67,7 +82,7 @@ wgtDbGridSetDataWidget(grid, dataCtrl);
.table
Property Type Access Description
-------- ---- ------ -----------
GridLines Boolean Read/Write Whether to draw grid lines between cells.
GridLines Boolean Read/Write Whether to draw grid lines between cells (default: true).
.endtable
.h3 Methods (BASIC Interface)
@ -83,6 +98,10 @@ wgtDbGridSetDataWidget(grid, dataCtrl);
.table
Event Description
----- -----------
Click Fires when a row is clicked.
Click Fires when a row is clicked (selection change).
DblClick Fires when a row is double-clicked. Default event.
.endtable
.h3 Default Event
"DblClick" (VB basName: DBGrid, namePrefix: DBGrid).

View file

@ -28,9 +28,9 @@
.h1 DBGrid
VB Equivalent: DBGrid -- DVX Widget: dbgrid
VB Equivalent: DBGrid -- DVX Widget: dbgrid | Name Prefix: DBGrid
A data-bound grid that displays records from a Data control in a tabular format. Columns are auto-generated from the query results. Bind it using the DataSource property.
A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. Selecting a row moves the bound Data control's cursor to that row.
.h2 Type-Specific Properties
@ -38,7 +38,7 @@ A data-bound grid that displays records from a Data control in a tabular format.
Property Type Description
---------- ------- -------------------------------------------
DataSource String Name of the Data control that supplies records.
GridLines Boolean Show or hide grid lines between cells.
GridLines Boolean Show or hide grid lines between cells (default: True).
.endtable
.h2 Type-Specific Methods
@ -54,10 +54,34 @@ A data-bound grid that displays records from a Data control in a tabular format.
.table
Event Parameters Description
-------- ---------- -------------------------------------------
Click (none) Fires when a cell is clicked.
DblClick (none) Fires when a cell is double-clicked. This is the default event.
Click (none) Fires when a row is clicked.
DblClick (none) Fires when a row is double-clicked. Default event.
.endtable
Default Event: DblClick
.h2 Example
.code
Begin Data Data1
DatabaseName = "books.db"
RecordSource = "titles"
End
Begin DBGrid DBGrid1
DataSource = "Data1"
GridLines = True
End
Sub Form_Load ()
Data1.Refresh
End Sub
Sub DBGrid1_DblClick ()
MsgBox "Opening row " & Str$(Data1.BOF)
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.data Data
.link ctrl.databinding Data Binding

View file

@ -57,6 +57,19 @@
#define DBGRID_MAX_NAME 64
#define DBGRID_RESIZE_ZONE 3
// Vertical padding added to font->charHeight for header and rows.
#define DBGRID_HEADER_VPAD 4
#define DBGRID_ROW_VPAD 2
// Alternating-row background shade: darken the content bg by this per-
// channel amount so every other row is subtly distinct.
#define DBGRID_ALT_ROW_SHADE 0x080808
// Horizontal cull margin: columns whose rect is within this many pixels
// of the visible area are still rendered. Gives scroll movement a bit
// of pre-draw buffer so columns fade in instead of popping.
#define DBGRID_HORIZ_CULL_MARGIN 20
// ============================================================
// Sort direction (matches ListView for consistency)
// ============================================================
@ -177,10 +190,10 @@ static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy) {
static void computeLayout(WidgetT *w, const BitmapFontT *font, int32_t *outInnerW, int32_t *outInnerH, int32_t *outHeaderH, int32_t *outVisRows, bool *outNeedV, bool *outNeedH) {
DbGridDataT *d = (DbGridDataT *)w->data;
int32_t headerH = font->charHeight + 4;
int32_t headerH = font->charHeight + DBGRID_HEADER_VPAD;
int32_t innerW = w->w - DBGRID_BORDER * 2;
int32_t innerH = w->h - DBGRID_BORDER * 2 - headerH;
int32_t rowH = font->charHeight + 2;
int32_t rowH = font->charHeight + DBGRID_ROW_VPAD;
int32_t rowCount = getDataRowCount(d);
int32_t visRows = innerH / rowH;
@ -294,7 +307,7 @@ static void dbGridBuildSortIndex(WidgetT *w) {
static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = font->charWidth * 20 + DBGRID_BORDER * 2;
w->calcMinH = (font->charHeight + 2) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2;
w->calcMinH = (font->charHeight + DBGRID_ROW_VPAD) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2;
}
@ -502,17 +515,17 @@ static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t newDisp = dispRow;
// Up arrow
if (key == (0x48 | 0x100)) {
if (key == KEY_UP) {
newDisp = dispRow > 0 ? dispRow - 1 : 0;
}
// Down arrow
if (key == (0x50 | 0x100)) {
if (key == KEY_DOWN) {
newDisp = dispRow < rowCount - 1 ? dispRow + 1 : rowCount - 1;
}
// Page Up
if (key == (0x49 | 0x100)) {
if (key == KEY_PGUP) {
newDisp = dispRow - visRows;
if (newDisp < 0) {
@ -521,7 +534,7 @@ static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
// Page Down
if (key == (0x51 | 0x100)) {
if (key == KEY_PGDN) {
newDisp = dispRow + visRows;
if (newDisp >= rowCount) {
@ -530,12 +543,12 @@ static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
// Home
if (key == (0x47 | 0x100)) {
if (key == KEY_HOME) {
newDisp = 0;
}
// End
if (key == (0x4F | 0x100)) {
if (key == KEY_END) {
newDisp = rowCount - 1;
}
@ -749,7 +762,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
bool needV, needH;
computeLayout(w, font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
int32_t rowH = font->charHeight + 2;
int32_t rowH = font->charHeight + DBGRID_ROW_VPAD;
int32_t rowCount = getDataRowCount(d);
// Outer sunken border
@ -777,7 +790,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
int32_t cw = d->resolvedW[c];
if (hx + cw > contentX - 20 && hx < contentX + innerW + 20) {
if (hx + cw > contentX - DBGRID_HORIZ_CULL_MARGIN && hx < contentX + innerW + DBGRID_HORIZ_CULL_MARGIN) {
BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
drawBevel(disp, ops, hx, contentY, cw, headerH, &hdrBevel);
@ -828,7 +841,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
// Alternating row background
if (i % 2 == 1) {
uint32_t altBg = colors->contentBg - 0x080808;
uint32_t altBg = colors->contentBg - DBGRID_ALT_ROW_SHADE;
rectFill(disp, ops, contentX, ry, innerW, rowH, altBg);
}
@ -840,7 +853,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
}
uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg;
uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? colors->contentBg - 0x080808 : colors->contentBg);
uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? colors->contentBg - DBGRID_ALT_ROW_SHADE : colors->contentBg);
// Draw cells
int32_t cx = contentX - d->scrollPosH;

View file

@ -29,23 +29,47 @@
DVX Extension -- DVX Widget: dropdown (non-editable drop-down list)
A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem/RemoveItem/Clear/List.
A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the arrow keys to navigate and Enter to commit when the list is open.
.h2 Type-Specific Properties
.table
Property Type Description
--------- ------- -------------------------------------------
ListIndex Integer Index of the currently selected item.
ListCount Integer Number of items (read-only).
ListIndex Integer Index of the currently selected item (-1 = none).
.endtable
.h2 Type-Specific Methods
Same as ListBox: AddItem, RemoveItem, Clear, List.
.link ctrl.listbox See ListBox for details
.table
Method Description
------ -----------
AddItem text$ Append an item to the drop-down list.
Clear Remove all items.
List(index%) Return the text of the item at the given index.
ListCount() Return the number of items in the list.
RemoveItem index% Remove the item at the given index.
.endtable
Default Event: Click
.h2 Example
.code
Begin DropDown DropDown1
End
Sub Form_Load ()
DropDown1.AddItem "Small"
DropDown1.AddItem "Medium"
DropDown1.AddItem "Large"
DropDown1.ListIndex = 1
End Sub
Sub DropDown1_Click ()
Label1.Caption = "Picked: " & DropDown1.List(DropDown1.ListIndex)
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.listbox See ListBox for details

View file

@ -27,32 +27,79 @@
.index Dropdown
.index wgtDropdown
.index wgtDropdownSetItems
.index wgtDropdownAddItem
.index wgtDropdownRemoveItem
.index wgtDropdownClear
.index wgtDropdownGetSelected
.index wgtDropdownSetSelected
.h2 Dropdown
A drop-down list that displays a single selected item and expands to show all options when clicked. Read-only selection (the user cannot type into it).
A drop-down list that displays a single selected item and expands to a popup list when clicked. Unlike ComboBox, the user cannot type free text -- only list items can be selected. Keyboard navigation: Down/Up to move, Enter to select, type-ahead search on printable keys. The popup overlay is rendered above all other widgets via the widget paint overlay mechanism.
Header: widgets/widgetDropdown.h
Depends on the "listhelp" helper library (declared in dropdown.dep).
Header: widgets/dropdown.h
.h3 Creation
.code
WidgetT *dd = wgtDropdown(parent);
const char *items[] = { "Red", "Green", "Blue" };
wgtDropdownSetItems(dd, items, 3);
wgtDropdownAddItem(dd, "Red");
wgtDropdownAddItem(dd, "Green");
wgtDropdownAddItem(dd, "Blue");
.endcode
.h3 Macros
.h3 API Functions
.table
Macro Description
----- -----------
wgtDropdown(parent) Create a dropdown list.
wgtDropdownSetItems(w, items, count) Set the list of items. items is a const char ** array.
wgtDropdownGetSelected(w) Get the index of the selected item (-1 if none).
wgtDropdownSetSelected(w, idx) Set the selected item by index.
Function Description
-------- -----------
WidgetT *wgtDropdown(parent) Create a dropdown list.
void wgtDropdownSetItems(w, items, count) Set the list from a caller-owned const char ** array.
int32_t wgtDropdownGetSelected(w) Get the index of the selected item (-1 if none).
void wgtDropdownSetSelected(w, idx) Set the selected item by index.
void wgtDropdownAddItem(w, text) Append an owned item (strdup'd).
void wgtDropdownRemoveItem(w, idx) Remove an owned item by index.
void wgtDropdownClear(w) Remove all owned items.
const char *wgtDropdownGetItem(w, idx) Get the text of an item by index.
int32_t wgtDropdownGetItemCount(w) Get the total number of items.
.endtable
.h3 API Struct (wgtRegisterApi "dropdown")
.table
Slot Function
---- --------
create wgtDropdown
setItems wgtDropdownSetItems
getSelected wgtDropdownGetSelected
setSelected wgtDropdownSetSelected
addItem wgtDropdownAddItem
removeItem wgtDropdownRemoveItem
clear wgtDropdownClear
getItem wgtDropdownGetItem
getItemCount wgtDropdownGetItemCount
.endtable
.h3 Properties (BASIC Interface)
.table
Property Type Access Description
-------- ---- ------ -----------
ListIndex Integer Read/Write Index of the currently selected item (-1 if none).
.endtable
.h3 Methods (BASIC Interface)
.table
Method Description
------ -----------
AddItem text$ Append an item to the drop-down list.
Clear Remove all items.
List(index%) Return the text of the item at the given index.
ListCount() Return the total number of items.
RemoveItem index% Remove the item at the given index.
.endtable
.h3 Events
@ -63,10 +110,6 @@ wgtDropdownSetItems(dd, items, 3);
onChange Fires when the selected item changes.
.endtable
.h3 Properties (BASIC Interface)
.h3 Default Event
.table
Property Type Access Description
-------- ---- ------ -----------
ListIndex Integer Read/Write Index of the currently selected item.
.endtable
"Click" (VB basName: DropDown).

View file

@ -49,6 +49,9 @@
#include <string.h>
#include "stb_ds_wrap.h"
// Focus-rect inset inside the dropdown's text area (pixels on each side).
#define DROPDOWN_FOCUS_INSET 3
static int32_t sTypeId = -1;
typedef struct {
@ -278,7 +281,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (d->open) {
// Popup is open -- navigate items
if (key == (0x48 | 0x100)) {
if (key == KEY_UP) {
if (d->hoverIdx > 0) {
d->hoverIdx--;
@ -286,7 +289,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
d->scrollPos = d->hoverIdx;
}
}
} else if (key == (0x50 | 0x100)) {
} else if (key == KEY_DOWN) {
if (d->hoverIdx < d->itemCount - 1) {
d->hoverIdx++;
@ -331,7 +334,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
} else if (key == (0x50 | 0x100)) {
} else if (key == KEY_DOWN) {
// Down arrow: cycle selection forward (wheel-friendly)
if (d->selectedIdx < d->itemCount - 1) {
d->selectedIdx++;
@ -340,7 +343,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->onChange(w);
}
}
} else if (key == (0x48 | 0x100)) {
} else if (key == KEY_UP) {
if (d->selectedIdx > 0) {
d->selectedIdx--;
@ -464,7 +467,7 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const
widgetDrawDropdownArrow(disp, ops, arrowX, arrowY, arrowFg);
if (w == sFocusedWidget) {
drawFocusRect(disp, ops, w->x + 3, w->y + 3, textAreaW - 6, w->h - 6, fg);
drawFocusRect(disp, ops, w->x + DROPDOWN_FOCUS_INSET, w->y + DROPDOWN_FOCUS_INSET, textAreaW - 2 * DROPDOWN_FOCUS_INSET, w->h - 2 * DROPDOWN_FOCUS_INSET, fg);
}
}

View file

@ -31,18 +31,34 @@
VB Equivalent: Image -- DVX Widget: image
A static image display control. Loads BMP images from file. Cannot be placed via the designer toolbox (requires pixel data at creation time); typically created in code or loaded via the Picture property.
A static image display control. Loads bitmap images from file. Set the Picture property at design-time (in the .frm file) or at runtime to load a file.
.h2 Type-Specific Properties
.table
Property Type Description
----------- ------- -------------------------------------------
Picture String Path to a BMP file to load (write-only).
Picture String Path to an image file to load.
Stretch Boolean When True, the image stretches to fill the widget; when False, shown at natural size.
ImageWidth Integer Width of the loaded image in pixels (read-only).
ImageHeight Integer Height of the loaded image in pixels (read-only).
.endtable
No type-specific methods or events.
Default Event: Click
.h2 Example
.code
Begin Image Image1
Picture = "assets/logo.bmp"
Stretch = True
End
Sub Image1_Click ()
MsgBox "Size: " & Str$(Image1.ImageWidth) & "x" & Str$(Image1.ImageHeight)
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods

View file

@ -30,30 +30,60 @@
.index wgtImageFromFile
.index wgtImageSetData
.index wgtImageLoadFile
.index wgtImageSetTransparent
.h2 Image
Displays a bitmap image. Can be created from raw pixel data or loaded from a BMP file on disk. The image is rendered at its natural size within the widget bounds.
Displays a bitmap image. Can be created from raw pixel data or loaded from a file on disk (BMP/PNG/etc. via dvxLoadImage). Supports optional stretching to fill the widget and transparent color keys for chroma-key style masking.
Header: widgets/widgetImage.h
Header: widgets/image.h
.h3 Creation
.code
WidgetT *img = wgtImageFromFile(parent, "logo.bmp");
// or from raw pixels:
// WidgetT *img = wgtImage(parent, pixelData, w, h, pitch);
.endcode
.h3 API Functions
.table
Macro Description
----- -----------
wgtImage(parent, data, w, h, pitch) Create an image widget from raw pixel data. data is a uint8_t * pixel buffer, w/h are dimensions, pitch is bytes per row.
wgtImageFromFile(parent, path) Create an image widget by loading a BMP file from disk.
Function Description
-------- -----------
WidgetT *wgtImage(parent, data, w, h, pitch) Create from raw pixel data (in display's native format).
WidgetT *wgtImageFromFile(parent, path) Create by loading an image file.
void wgtImageSetData(w, data, imgW, imgH, pitch) Replace the displayed image with new raw pixel data.
void wgtImageLoadFile(w, path) Replace by loading a new file. Static but exposed via the API struct.
void wgtImageSetTransparent(w, hasTransparency, keyColor) Set chroma-key color. Pixels matching keyColor are not drawn.
.endtable
.h3 API Struct (wgtRegisterApi "image")
.table
Slot Function
---- --------
create wgtImage
fromFile wgtImageFromFile
setData wgtImageSetData
loadFile wgtImageLoadFile
setTransparent wgtImageSetTransparent
.endtable
.h3 Properties (BASIC Interface)
.table
Property Type Access Description
-------- ---- ------ -----------
Picture String Read/Write Setting writes a file path (loads image). Reading returns the current path.
Stretch Boolean Read/Write When true, the image stretches to fill the widget; when false, shown at natural size.
ImageWidth Integer Read-only Width of the currently loaded image in pixels.
ImageHeight Integer Read-only Height of the currently loaded image in pixels.
.endtable
.h3 Methods
.table
Macro Description
----- -----------
wgtImageSetData(w, data, imgW, imgH, pitch) Replace the displayed image with new raw pixel data.
wgtImageLoadFile(w, path) Replace the displayed image by loading a new file.
.endtable
No widget-specific BASIC methods. Setting Picture loads a new image.
.h3 Events
@ -63,12 +93,6 @@ Header: widgets/widgetImage.h
onClick Fires when the image is clicked.
.endtable
.h3 Properties (BASIC Interface)
.h3 Default Event
.table
Property Type Access Description
-------- ---- ------ -----------
Picture String Write-only Load an image from a file path.
ImageWidth Integer Read-only Width of the currently loaded image in pixels.
ImageHeight Integer Read-only Height of the currently loaded image in pixels.
.endtable
"Click" (VB basName: Image).

View file

@ -29,18 +29,37 @@
DVX Extension -- DVX Widget: imagebutton
A button that displays an image instead of text. Like Image, it requires pixel data at creation time and is typically loaded via the Picture property.
A button that displays an image instead of text. Has the same press/release behavior as CommandButton, but shows a bitmap. Typically used inside a Toolbar.
.h2 Type-Specific Properties
.table
Property Type Description
----------- ------- -------------------------------------------
Picture String Path to a BMP file to load (write-only).
Picture String Path to an image file to load.
ImageWidth Integer Width of the loaded image in pixels (read-only).
ImageHeight Integer Height of the loaded image in pixels (read-only).
.endtable
No type-specific methods or events.
Default Event: Click
.h2 Example
.code
Begin Toolbar Toolbar1
Begin ImageButton ImageButton1
Picture = "icons/save.bmp"
End
Begin ImageButton ImageButton2
Picture = "icons/open.bmp"
End
End
Sub ImageButton1_Click ()
MsgBox "Save clicked"
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods

View file

@ -32,34 +32,37 @@
.h2 ImageButton
A clickable button that displays an image instead of text. Has press/release visual feedback like a regular button. Can be created from raw pixel data or a BMP file.
A clickable button that displays an image instead of text. Has the same press/release visual feedback and focus handling as a regular Button. Can be created from raw pixel data or loaded from an image file. Typically used to populate a Toolbar.
Header: widgets/widgetImageButton.h
Header: widgets/imgBtn.h
.h3 Creation
.code
WidgetT *btn = wgtImageButtonFromFile(parent, "icons/save.bmp");
btn->onClick = onSaveClicked;
.endcode
.h3 API Functions
.table
Macro Description
----- -----------
wgtImageButton(parent, data, w, h, pitch) Create an image button from raw pixel data.
wgtImageButtonFromFile(parent, path) Create an image button by loading a BMP file.
Function Description
-------- -----------
WidgetT *wgtImageButton(parent, data, w, h, pitch) Create from raw pixel data (in display's native format).
WidgetT *wgtImageButtonFromFile(parent, path) Create by loading an image file.
void wgtImageButtonSetData(w, data, imgW, imgH, pitch) Replace the image with new raw pixel data.
void wgtImageButtonLoadFile(w, path) Replace the image by loading a new file.
.endtable
.h3 Methods
.h3 API Struct (wgtRegisterApi "imagebutton")
.table
Macro Description
----- -----------
wgtImageButtonSetData(w, data, imgW, imgH, pitch) Replace the image with new raw pixel data.
wgtImageButtonLoadFile(w, path) Replace the image by loading a new file.
.endtable
.h3 Events
.table
Callback Description
-------- -----------
onClick Fires when the image button is clicked (press + release).
Slot Function
---- --------
create wgtImageButton
fromFile wgtImageButtonFromFile
setData wgtImageButtonSetData
loadFile wgtImageButtonLoadFile
.endtable
.h3 Properties (BASIC Interface)
@ -67,9 +70,23 @@ Header: widgets/widgetImageButton.h
.table
Property Type Access Description
-------- ---- ------ -----------
Picture String Write-only Load an image from a file path.
Picture String Read/Write Setting writes a file path (loads image). Reading returns the current path.
ImageWidth Integer Read-only Width of the currently loaded image in pixels.
ImageHeight Integer Read-only Height of the currently loaded image in pixels.
.endtable
.hr
.h3 Methods
No widget-specific BASIC methods. Setting Picture loads a new image.
.h3 Events
.table
Callback Description
-------- -----------
onClick Fires when the image button is clicked (press and release inside).
.endtable
.h3 Default Event
"Click" (VB basName: ImageButton).

View file

@ -42,6 +42,9 @@
#include "dvxWgtP.h"
// 2px bevel on every side -- min size and draw inset both derive from this.
#define IMAGEBUTTON_BEVEL_W 2
static int32_t sTypeId = -1;
typedef struct {
@ -185,8 +188,8 @@ void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
// Bevel border only, no extra padding
w->calcMinW = d->imgW + 4;
w->calcMinH = d->imgH + 4;
w->calcMinW = d->imgW + 2 * IMAGEBUTTON_BEVEL_W;
w->calcMinH = d->imgH + 2 * IMAGEBUTTON_BEVEL_W;
}
@ -212,7 +215,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, con
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
bevel.face = bgFace;
bevel.width = 2;
bevel.width = IMAGEBUTTON_BEVEL_W;
drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);
if (d->pixelData) {

Some files were not shown because too many files have changed in this diff Show more