School project to create a DOS-based GUI shell from scratch
  • C++ 60%
  • Clean 23.4%
  • Roff 11.1%
  • C 3.3%
  • CMake 1.1%
  • Other 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sandeep Vangipuram 215fc6d5a3
All checks were successful
/ print-content (push) Successful in 17s
Remove MSDOS button; remove exit button for WASM build
2026-09-03 15:35:59 +01:00
.forgejo/workflows Add docker build 2026-09-03 10:10:05 +01:00
.vscode Make icon drawing faster. Use fixed text size for now. 2026-08-31 16:56:45 +01:00
src Remove MSDOS button; remove exit button for WASM build 2026-09-03 15:35:59 +01:00
tests Fix overall build using AI 2026-08-31 16:09:38 +01:00
.dockerignore Add docker build 2026-09-03 10:10:05 +01:00
.gitignore Implement WASM build 2026-09-02 22:45:46 +01:00
CMakeLists.txt Remove MSDOS button; remove exit button for WASM build 2026-09-03 15:35:59 +01:00
Dockerfile Add docker build 2026-09-03 10:10:05 +01:00
nginx.conf Add docker build 2026-09-03 10:10:05 +01:00
README.md Add docker build 2026-09-03 10:10:05 +01:00
shell.html Scale canvas with browser window 2026-09-02 23:18:37 +01:00

LYNX Desktop Shell

A retro desktop shell originally written in Turbo C++ using the Borland Graphics Interface (BGI), ported to SDL2 for Linux, with an upcoming WASM build target.

Project Structure

lynx/
├── CMakeLists.txt                  # Build system (CMake)
├── src/
│   │
│   │ ── Core Shim Layer (SDL2 abstraction) ──
│   ├── bgi.h                       # Unified BGI/conio/DOS compatibility header (305 lines)
│   ├── bgi.cpp                     # SDL2 implementation of all BGI/conio/DOS functions (1161 lines)
│   ├── graphics.h                  # Thin wrapper: #include "bgi.h"
│   ├── dos.h                       # Thin wrapper: #include "bgi.h"
│   ├── conio.h                     # Thin wrapper: #include "bgi.h"
│   ├── mouse.h                     # Mouse API header (includes <SDL_mouse.h>)
│   ├── mouse.cpp                   # Mouse implementation (rewritten for SDL2, 78 lines)
│   │
│   │ ── Application Infrastructure ──
│   ├── lynx.h                      # Master app header: constants, grinit(), inline helpers
│   ├── lynx_api.h                  # Cross-module function declarations
│   ├── grph.h                      # grph_settings class (save/restore graphics state)
│   ├── cookie.h                    # proxy class: read/write settings to lynx.ini
│   ├── box.h                       # Text-mode message box and input box (conio shim)
│   ├── button.h                    # button class (GUI button widget using BGI shim)
│   ├── icon.h                      # icon class (reads system.icl, draws with BGI shim)
│   ├── windows.h                   # msgbox() and input_box() (graphical dialog widgets)
│   ├── cal_eval.h                  # Calculator expression evaluator interface
│   │
│   │ ── Application Source Files ──
│   ├── lynx.cpp                    # Main desktop shell entry point (160 lines)
│   ├── calculat.cpp                # Calculator application (183 lines)
│   ├── clock.cpp                   # Analog clock application (61 lines)
│   ├── grafiti.cpp                 # Paint/graphics editor (736 lines)
│   ├── intro.cpp                   # Intro animation (189 lines)
│   ├── stellar.cpp                 # "Stellar Evade" text-mode game (88 lines)
│   ├── tic.cpp                     # Tic-Tac-Toe game logic (313 lines)
│   ├── tic_main.cpp                # Standalone Tic-Tac-Toe entry point (11 lines)
│   ├── dot.cpp                     # Standalone "DOTS" board game (369 lines)
│   ├── viewer.cpp                  # Text file viewer (104 lines)
│   ├── options.cpp                 # Options menu (55 lines)
│   ├── opt_bkg.cpp                 # Background options + drawing (210 lines)
│   ├── opt_scr.cpp                 # Screen saver selector (69 lines)
│   ├── opt_time.cpp                # Time setting (22 lines)
│   ├── cal_eval.cpp                # Calculator expression evaluator (157 lines)
│   │
│   │ ── Screen Saver Source Files ──
│   ├── scr1.cpp                    # Screen saver 1: circle of life (25 lines)
│   ├── scr2.cpp                    # Screen saver 2: going around (33 lines)
│   ├── matrix.cpp                  # Screen saver 3: matrix text effect (27 lines)
│   ├── pipe.cpp                    # Screen saver 4: pipe line (38 lines)
│   ├── marquee.cpp                 # Screen saver 5: scrolling marquee (73 lines)
│   ├── marline.cpp                 # Screen saver 6: 3D building walkthrough (295 lines)
│   ├── marl_3d.cpp                 # 3D building drawing helper (83 lines)
│   │
│   │ ── Data Files ──
│   ├── system.icl                  # Icon image data (binary)
│   ├── lynx.ini                    # Settings file (binary, read/written by cookie.h)
│   ├── lynx.txt                    # Credits text
│   ├── egavga.bgi                  # Original BGI driver (unused in SDL port)
│   ├── goth.chr ... trip.chr       # BGI font files (stubs in SDL port)
│   └── aparna.pic                  # Sample paint file
│
└── tests/
    └── calc_button_test.cpp        # Visual smoke test for calculator buttons

Build System

Native Linux Build (CMake + SDL2)

mkdir build && cd build
cmake ..
make

Produces:

  • lynx — main desktop shell
  • tic — standalone Tic-Tac-Toe
  • dot — standalone DOTS game
  • calc_button_test — visual smoke test

Dependencies (via pkg-config): sdl2, SDL2_ttf

WASM Build (Emscripten)

mkdir build-wasm && cd build-wasm
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release
make

Produces lynx.html / lynx.js / lynx.wasm. Uses the WASM shim layer (bgi_wasm.cpp, mouse_wasm.cpp) which replaces SDL2 entirely with direct canvas rendering via Emscripten APIs.

Architecture

Abstraction Layers

The codebase has four distinct abstraction layers:

Layer 1: The BGI Shim (bgi.h + bgi.cpp) The primary abstraction. Provides 100+ functions that reimplement the Borland Graphics Interface, Borland <conio.h>, and Borland <dos.h> on top of SDL2. No application code ever calls SDL functions directly (except mouse.cpp).

Layer 2: Application Widgets (grph.h, button.h, icon.h, windows.h, box.h) Build on the BGI shim to provide higher-level UI constructs:

  • grph_settings: RAII-like save/restore of line/fill/text/color state
  • button: Clickable GUI button with 3D shading and press/release animation
  • icon: Desktop icon reading pixel data from system.icl
  • msgbox() / input_box(): Modal dialog boxes using getimage/putimage for save/restore

Layer 3: Mouse Abstraction (mouse.h + mouse.cpp) Bridges between SDL events and the original DOS INT 0x33 mouse API. The public API in mouse.h is unchanged from the original DOS version.

Layer 4: Cross-Module API (lynx_api.h) Flat declarations of all application entry points for linker resolution.

SDL2 Rendering Pipeline

Only two files call SDL functions directly: bgi.cpp and mouse.cpp. The rendering pipeline works as follows:

Application calls BGI function (e.g. line(), bar(), outtextxy())
    │
    ▼
bgi.cpp writes pixels directly into E.gsurf (640×480 SDL_Surface)
  - gput(x,y,color) writes one pixel via SDL_MapRGB()
  - bar() fills rectangle rows in surface pixel memory
  - line() uses Bresenham's algorithm with gput() per pixel
  - circle() uses midpoint circle algorithm with gput() per pixel
  - floodfill() uses stack-based 4-connected fill on raw pixels
  - outtextxy() renders text via TTF_RenderUTF8_Blended() then SDL_BlitSurface()
    │
    ▼
present_active() pushes the surface to screen:
  SDL_UpdateTexture → SDL_RenderClear → SDL_RenderCopy → SDL_RenderPresent

Two-Window Architecture

The original DOS program switched between VGA graphics mode (640×480) and 80×25 text mode. This is emulated with two independent SDL windows:

Mode Window Size Shown When
Graphics "LYNX" 640×480 setgraphmode()
Text "LYNX (text mode)" 640×400 restorecrtmode()

BGI Colour Palette

The 16-colour EGA palette is mapped to 24-bit RGB at drawing time:

Index Color RGB
0 BLACK (0, 0, 0)
1 BLUE (0, 0, 170)
2 GREEN (0, 170, 0)
3 CYAN (0, 170, 170)
4 RED (170, 0, 0)
5 MAGENTA (170, 0, 170)
6 BROWN (170, 85, 0)
7 LIGHTGRAY (170, 170, 170)
8 DARKGRAY (85, 85, 85)
9 LIGHTBLUE (85, 85, 255)
10 LIGHTGREEN (85, 255, 85)
11 LIGHTCYAN (85, 255, 255)
12 LIGHTRED (255, 85, 85)
13 LIGHTMAGENTA (255, 85, 255)
14 YELLOW (255, 255, 85)
15 WHITE (255, 255, 255)

Drawing Primitives

All primitives use software rasterization — no SDL2 draw calls:

  • line(): Bresenham's line algorithm, writes via gput()
  • circle(): Midpoint circle algorithm, writes via gput()
  • ellipse(): Parametric angle stepping, calls line() for segments
  • fillellipse(): Row-by-row horizontal span fill via gput()
  • rectangle(): Four calls to line()
  • bar(): Direct row-by-row pixel write into surface memory
  • bar3d(): Calls bar() + line() for 3D edges
  • fillpoly(): Even-odd scanline polygon fill
  • drawpoly(): N calls to line()
  • pieslice(): Arc + polygon fill
  • floodfill(): Stack-based 4-connected seed fill on raw pixels
  • putpixel() / getpixel(): Single pixel read/write
  • getimage() / putimage(): Pixel-level blit with COPY/XOR/OR/AND/NOT operations
  • outtextxy(): Text rendered via SDL_ttf, blitted with SDL_BlitSurface()

Event Handling

pump_events() calls SDL_PollEvent() and translates:

SDL Event Internal Mapping
SDL_KEYDOWN DOS scan codes pushed to E.keys deque
SDL_MOUSEMOTION E.mx, E.my, E.moved
SDL_MOUSEBUTTONDOWN/UP E.mb bitmask (using SDL_BUTTON())
SDL_QUIT / SDL_WINDOWEVENT_CLOSE E.quit = true

Key Patterns

Pattern 1: Mouse visibility toggling Almost every drawing function hides the mouse cursor before drawing and shows it after, to prevent the OS cursor from corrupting rendered pixels.

Pattern 2: present_active() called selectively Not after every draw call — batched until the next mouse read or explicit present. Called after: putpixel(), putimage(), clearviewport() (full screen), outtextxy(), pump_events().

Pattern 3: grph_settings RAII guard Used in virtually every application function to save and restore drawing state:

grph_settings gr;
gr.get_settings();
gr.reset_settings();
// ... do work ...
gr.set_settings();

Pattern 4: Save/restore for modal dialogs msgbox() and input_box() use imagesize() + getimage() to save the screen region, draw the dialog, run the event loop, then putimage() to restore.

Font Rendering

BGI .chr font files are stubs — installuserfont() returns grOk without loading them. All text rendering uses SDL_ttf with a system TrueType font, selected at startup by probing standard font paths or $LYNX_FONT. Font size is controlled by settextstyle() which scales the TTF point size (size × 8).

Text Mode

The 80×25 text mode maintains a character grid (E.cells[]) with per-cell foreground/background colour arrays. text_redraw() renders this grid to a surface by calling TTF_RenderText_Shaded() per cell and blitting with SDL_BlitSurface().

Applications

Application Entry Point Description
Desktop Shell lynx.cpp:main() Icon-based desktop with clock, screen savers, options
Calculator calculat.cpp:calculator() GUI calculator with expression evaluator
Clock clock.cpp:lynx_clock() Analog clock with hour/minute/second hands
Paint grafiti.cpp:paint() Drawing editor with pencil, line, rectangle, circle, spray tools
Stellar Evade stellar.cpp:stellar() Text-mode space game
Tic-Tac-Toe tic.cpp:tictactoe() Two-player tic-tac-toe with mouse input
Text Viewer viewer.cpp:viewer() Text file reader with pagination
DOTS dot.cpp:main() Standalone board game
Options options.cpp:options() Background, screen saver, and time settings

WASM Build Target

The WASM build replaces SDL2 entirely with a shim layer that draws to an HTML canvas element directly via Emscripten APIs. All application source files are shared unchanged — they only ever go through the bgi.h/mouse.h shim.

Key differences from the SDL2 build:

  • bgi_wasm.cpp replaces bgi.cpp — same software pixel buffer (ABGR), blitted to the <canvas> as RGBA via putImageData
  • mouse_wasm.cpp replaces mouse.cpp — Emscripten event callbacks
  • No SDL2 or SDL2_ttf dependencies
  • Uses Emscripten's -sASYNCIFY for blocking-call compatibility
  • Font rendering via the browser canvas fillText()/measureText() API
  • Single canvas element (no dual-window mode switching)

Building

emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm      # produces lynx_wasm.html/.js/.wasm/.data

Running in a browser

Serve the build directory over HTTP (Emscripten data files and the .wasm cannot be loaded from file://), then open lynx_wasm.html:

emrun build-wasm/lynx_wasm.html
# or
python3 -m http.server --directory build-wasm
# browse to http://localhost:8000/lynx_wasm.html

Docker

A multi-stage Dockerfile builds the WASM output using the official Emscripten image and serves it with nginx:alpine. No local Emscripten installation required.

docker build -t lynx-wasm .
docker run --rm -p 8080:80 lynx-wasm
# browse to http://localhost:8080

Custom HTML shell (shell.html)

A minimal hand-written shell (shell.html, wired via --shell-file=) is used instead of the default Emscripten shell. It:

  • sizes a 640x480 canvas that fits the graphics framebuffer
  • omits the default "Lock/hide mouse pointer" checkbox, avoiding Emscripten's automatic requestPointerLock() call (which throws WrongDocumentError) when the app blocks on input under ASYNCIFY
  • gives the canvas focus so keyboard events reach the app

The canvas 2D context is created with {willReadFrequently: true} (see js_canvas_init in bgi_wasm.cpp) because the shim calls getImageData for every glyph rendered; this removes the Canvas2D readback performance warning.