HamsterOS Developer SDK
App SDK
Synced Jul 6, 2026
HamsterOS App Developer Guide
Revision: v1 (June 2026) — HAMA App Format v1
Overview
HamsterOS apps are standalone 32-bit i386 binaries loaded from the floppy at runtime. Shipped apps live in four standard folders:
| Directory | Purpose |
|---|---|
/APPS/ | System apps converted from built-in code (e.g. NOTEPAD.APP) |
/TOOLS/ | Hardware tool apps — Disk Copier, CD Player, Floppy Diag, CD ROM Diag, IDE Diag, Format, GFX Diag |
/USER/ | User-installed third-party apps and bundled user apps |
The toolbar buttons open /APPS/, /TOOLS/, /USER/, and /GAMES/ as normal file manager windows. Double-clicking a .APP in those folders reads the file from disk, allocates heap memory, copies the code into it, applies relocations, and calls your app_entry() function. When the app closes the heap is freed automatically.
There is no kernel reboot, no relink, no persistent process list. Apps are purely event-driven: the OS calls your draw/input/update functions on demand.
Because floppy and FAT reads are synchronous in the current shell, file-manager launch paths and system-app loaders should paint a small immediate "Loading <name>" toast before starting blocking disk I/O. This keeps the UI from feeling dead during external app loads. While the kernel is in the protected .APP read path, the mouse pointer overlay is intentionally hidden and service callbacks are temporarily suspended so floppy reads are not disturbed. The pointer returns on the next redraw when the app opens or an error dialog appears.
For apps that need large temporary scratch buffers only during one operation (for example BMP load/save staging or a flood-fill queue), HamsterOS also exposes a shared transient workspace API. This lets short-lived work areas come from one reusable pool instead of forcing every app to keep a dedicated persistent heap reservation.
Quick-start: three required files
Every app needs three source files: myapp.c (your logic), myapp_stubs.c (OS call wrappers), and myapp_ext_entry.c (the app_entry() function). See apps/piano_stubs.c and apps/piano_ext_entry.c for complete examples.
myapp_ext_entry.c (always the same shape)
c
#include "app_abi.h"
#include <stddef.h>
/* Forward-declare functions from myapp.c */
void myapp_stubs_set_host(HamsterHostAPI *h);
void myapp_init(void); void myapp_open(void); void myapp_close(void);
bool myapp_is_open(void); bool myapp_is_active(void); bool myapp_has_modal(void);
void myapp_set_active(bool);
bool myapp_scancode(uint8_t);
bool myapp_ptr_down(int16_t, int16_t);
bool myapp_ptr_move(int16_t, int16_t);
bool myapp_ptr_up(int16_t, int16_t);
void myapp_draw(void);
void myapp_bounds(int16_t*, int16_t*, int16_t*, int16_t*);
static AppDescriptor g_desc = {
myapp_init, myapp_open,
NULL, NULL, myapp_close, /* open_file, open_file_at, close */
myapp_is_open, myapp_is_active, myapp_has_modal, myapp_set_active,
myapp_scancode,
myapp_ptr_down, myapp_ptr_move, myapp_ptr_up,
NULL, NULL, /* handle_menu, handle_edit */
myapp_draw, NULL, /* draw, draw_content */
myapp_bounds, NULL, NULL, /* get_bounds, content_bounds, status_bounds */
NULL, NULL, /* consume_content_redraw, consume_row */
NULL, NULL, /* update, consume_redraw_request */
NULL, NULL, /* interaction_active, live_interaction_active */
NULL /* open_launch_at */
};
AppDescriptor *app_entry(HamsterHostAPI *host) {
myapp_stubs_set_host(host);
return &g_desc;
}Build toolchain
Requirements: hamsteros-builder Docker image (same one used to build the OS).
sh
CC_FLAGS="-m32 -march=i386 -std=c11 -ffreestanding -fno-builtin -fno-pic -fno-pie \
-fno-stack-protector -Ikernel -Idrivers -Iapps -Ifs -Wall -Wextra -O2"
gcc $CC_FLAGS -c myapp.c -o myapp.o
gcc $CC_FLAGS -c myapp_stubs.c -o myapp_stubs.o
gcc $CC_FLAGS -c myapp_ext_entry.c -o myapp_ext.o
ld -T tools/app.ld -melf_i386 --emit-relocs -nostdlib \
myapp_ext.o myapp.o myapp_stubs.o -o myapp.elf
python3 tools/mkapp.py --elf myapp.elf --name MYAPP --output MYAPP.APPCopy MYAPP.APP to /USER/ on the floppy. It appears in the USER file manager folder. For Makefile integration see the PIANO_APP and SLOTS_APP rules in Makefile.
--emit-relocs is required. mkapp.py reads every R_386_32 entry from the ELF .rel.* sections and writes those patch offsets into the HAMA relocation table. These relocations include obvious static data pointers and less-obvious function pointers stored in AppDescriptor. If descriptor relocations are missing, the app may load successfully and then soft-crash when the OS calls init(), open(), or another descriptor callback.
HAMA v1 file format
Offset Size Field Description
------ ---- ----- -----------
0 4 magic 0x414D4148 ("HAMA" little-endian)
4 2 version 1 (loader rejects anything else)
6 2 flags Bit 0 = HAS_ICON, bit 1 = COMPRESSED, bit 2 = ALIAS
8 4 code_size Unpacked bytes of code+rodata+data after optional icon
12 4 bss_size Bytes of zeroed BSS allocated at runtime
16 4 entry_offset Byte offset of app_entry() in code blob
20 4 reloc_count Number of uint32 patch offsets after code
24 8 name Display name: ASCII, null-padded
32 256 icon Optional 16x16 VGA icon when HAS_ICON is set
32/288 4 packed_size Present only when COMPRESSED is set
... ... code blob code_size bytes when uncompressed, packed_size bytes when compressed
... ... reloc table reloc_count x uint32_t patch offsetsIf HAS_ICON is set, the 256-byte icon is stored immediately after the header and before the code blob. If COMPRESSED is set, a little-endian packed_size field sits after the optional icon and before the packed blob. The loader must skip both the icon block and the optional size field before copying or unpacking code and before locating the relocation table.
Load sequence (kernel/app_loader.c): 1. heap_alloc(file_size) staging buffer, load whole file. 2. Validate magic and version. 3. heap_alloc(code_size + bss_size) runtime region. 4. Skip the optional icon block, then either copy the code blob directly or unpack it into the runtime buffer, then zero BSS. 5. Apply relocations: *(uint32_t*)(base + offset) += (uint32_t)base. 6. heap_free(staging) — staging freed immediately. 7. Call app_entry(host) at base + entry_offset. 8. The owner slot/loader calls desc->init() before first desc->open(). 9. On close: heap_free(code_buf) — all RAM reclaimed.
If the app is launched from a shell surface that performs blocking disk reads on the UI thread, draw and present a loading toast before step 1. Current file-manager and lazy-loader code uses a centered 170x34 light-gray box with a dark-gray border, bold text like Loading SNAKE, and an immediate vga_present_framebuffer(). The pointer overlay is suppressed during the blocking read so users do not see a frozen cursor. This is a visual busy state only; the mouse position is still kept and is redrawn after loading completes.
Load failures must be nonfatal. Missing files, malformed headers, invalid descriptor callbacks, bad 8.3 filenames, out-of-memory conditions, and disk read errors should call shell_show_error() or return an error that the owning shell surface converts into a dialog. Never leave a loading toast on screen as the final state.
Compression and aliases
tools/mkapp.py now supports two build-time space-saving features:
sh
python3 tools/mkapp.py --elf myapp.elf --name MYAPP --output MYAPP.APP
python3 tools/mkapp.py --elf myapp.elf --name MYAPP --no-compress --output MYAPP.APP
python3 tools/mkapp.py --alias-target /TOOLS/MYAPP.APP --name MYAPP --output MYAPP.APP- Normal
.APPbuilds automatically compress the code blob when that makes the
file smaller enough to be worth it. The loader inflates the blob after reading it from disk, so floppy launch I/O drops without changing the in-memory app.
--no-compressforces the older uncompressed layout.--alias-targetcreates a tiny shortcut.APP. Alias apps are for shell
surfaces such as /DESKTOP; they point at the real app path and are not executable payloads themselves.
Relocation offsets are flat offsets in the runtime code/data blob because app ELFs are linked as ET_EXEC at address 0. Do not add the target section base again when packing relocations. Doing so drops relocations outside .text, including descriptor callback pointers.
Host services reference (HamsterHostAPI v1)
All services via the HamsterHostAPI* passed to app_entry(). Never call OS functions directly — only through this pointer.
Drawing (VGA)
| Field | Signature | Notes |
|---|---|---|
fill_rect | (x, y, w, h, color) | Fill rectangle |
draw_text | (x, y, str, color, scale) | Bitmap text; scale 1 or 2 |
draw_border | (x, y, w, h, color) | 1-pixel border |
Colors: VGA_COLOR_* constants 0-15 from vga.h.
Filesystem (FAT)
| Field | Notes |
|---|---|
fat_load(drive, dir, name, buf, max, &sz, &trunc) | Load file into buffer |
fat_save(drive, dir, name, buf, sz) | Save buffer to file |
fat_error() | Last FAT error string |
drive: FAT_DRIVE_BOOT = A:, FAT_DRIVE_B = B:, FAT_DRIVE_HOST = C: (first IDE), FAT_DRIVE_RAM = R:. dir = 0 for root directory.
Loading from a subdirectory — dir is a FAT cluster number, not a path string. To reach a nested folder directly, resolve the cluster chain with fat_list_dir. Apps that need assets beside their executable should implement open_launch_at(drive, cluster, name), store that drive/cluster as their asset base, then run their normal open path. Pack those apps with mkapp.py --open-launch-at; older apps without that HAMA flag continue to receive open(). open_file_at(drive, cluster, name) is for opening a user-selected document.
`.APP` files on non-boot drives — the OS shell now supports double-clicking a .APP on any drive (B:, C:, etc.). The loader calls app_loader_load_drive(drive, cluster, filename) so the binary is read from the correct device. No special handling needed in your app.
File dialog
| Field | Notes |
|---|---|
fd_init(state, x, y) | Init a FileDialogState |
fd_open(state, mode, drive, default_name, cfg, ctx) | Open LOAD or SAVE dialog |
fd_close(state) | Close without accepting |
fd_draw(state, cfg) | Draw dialog |
fd_scancode(state, sc, shift, cfg, ctx) | Returns ACCEPT or CANCEL |
fd_pointer_down(state, x, y, cfg, ctx) | Mouse click |
fd_translate_key(sc, shift) | Scancode to ASCII for filename input |
fd_current_cluster(state) | Directory cluster for fat_load/fat_save |
fd_filename_83_valid(name) | Validate 8.3 filename |
Window manager
Every normal app should use these for its main window. All title bar styling, bevel buttons, active/inactive states, and future OS chrome changes propagate automatically.
| Field | Notes |
|---|---|
wnd_init(f, x, y, w, h, min_w, min_h) | Init WindowFrame |
wnd_draw_frame(f, title) | Draw title bar and chrome — always use this, never roll your own |
wnd_content_rect(f, inset, &x,&y,&w,&h) | Get content area (use WINDOW_INSET=8) |
wnd_display_bounds(f, &x,&y,&w,&h) | Full window bounds |
wnd_hit_test(f, x, y) | Returns WINDOW_HIT_* (low-level; prefer wnd_handle_pointer_down) |
wnd_handle_pointer_down(f, x, y) | Dispatch pointer-down on chrome; starts drag/resize, sets chrome_pressed |
wnd_handle_pointer_up(f, x, y) | Dispatch pointer-up; ends drag/resize, returns hit type for close/maximize |
wnd_begin_drag(f, px, py) | Start drag (called internally by wnd_handle_pointer_down) |
wnd_begin_resize(f, px, py) | Start resize (called internally by wnd_handle_pointer_down) |
wnd_update_pointer(f, px, py) | Call on pointer_move during drag/resize |
wnd_end_interaction(f) | End drag or resize (called internally by wnd_handle_pointer_up) |
wnd_toggle_maximize(f) | Toggle maximised state |
Window chrome pattern (all apps must follow this):
c
/* pointer_down */
WindowHit hit = window_handle_pointer_down(&frame, x, y);
if (hit == WINDOW_HIT_CLOSE || hit == WINDOW_HIT_MAXIMIZE ||
hit == WINDOW_HIT_TITLE || hit == WINDOW_HIT_RESIZE)
return true;
if (hit != WINDOW_HIT_BODY) return false;
/* ... handle body clicks ... */
/* pointer_move */
if (window_update_pointer(&frame, x, y)) return true;
/* pointer_up */
WindowHit release = window_handle_pointer_up(&frame, x, y);
if (release == WINDOW_HIT_CLOSE) { /* close app */ return true; }
if (release == WINDOW_HIT_MAXIMIZE) { window_toggle_maximize(&frame); return true; }
return release != WINDOW_HIT_NONE;Standard OS scrollbar
Use draw_scrollbar for any scrollable content. Never implement a custom scrollbar — this ensures consistent look and keyboard/mouse behaviour across all apps.
draw_scrollbar(x, y, height, total, visible, position, up_pressed, down_pressed)
x, y, height— position and total height of the 16-px-wide striptotal— total scrollable units (e.g. line count)visible— units visible at once (determines thumb size)position— current scroll offset (0 = top)up_pressed / down_pressed— true while the respective arrow button is held
c
g_host->draw_scrollbar(scroll_x, content_y, content_height,
total_lines, visible_lines, scroll_offset,
up_btn_pressed, down_btn_pressed);Constants: VGA_SCROLLBAR_WIDTH = 16, VGA_SCROLLBAR_ARROW_HEIGHT = 16.
Scrollbar hit-testing for clicks:
c
VgaScrollbarMetrics sb;
VgaScrollbarHit hit;
g_host->scrollbar_metrics(scroll_x, content_y, content_height,
total_lines, visible_lines, scroll_offset, &sb);
hit = g_host->scrollbar_hit_test(&sb, pointer_x, pointer_y);For automatic mouse-wheel support, expose the scrollable list as the app's primary scroll area in AppDescriptor. The window manager will route wheel events to the focused app. If your custom handle_wheel() is missing or returns false, HamsterOS will use these two hooks to update the scroll position automatically.
c
static bool my_get_primary_scroll(AppScrollArea *out)
{
if (total_lines <= visible_lines) return false;
out->total = total_lines;
out->visible = visible_lines;
out->position = scroll_offset;
out->wheel_step = 3; /* 0 also means the OS default of 3 */
return true;
}
static bool my_set_primary_scroll(uint32_t position)
{
uint32_t max = total_lines > visible_lines ? total_lines - visible_lines : 0;
if (position > max) position = max;
if (position == scroll_offset) return false;
scroll_offset = position;
return true;
}
static AppDescriptor g_desc = {
/* required callbacks ... */
.get_primary_scroll = my_get_primary_scroll,
.set_primary_scroll = my_set_primary_scroll
};Positive wheel deltas mean wheel-up. Apps with several independent scroll areas or modal lists may still implement handle_wheel() directly; return true after consuming the wheel event, or false to let the primary scroll fallback run.
Timing and audio
| Field | Signature | Notes |
|---|---|---|
pit_millis() | -> uint32_t | Milliseconds since boot |
speaker_note_on(hz) | uint16_t hz | Play square-wave tone on PC speaker |
speaker_note_off() | none | Stop PC speaker tone |
sb16_card_present() | -> bool | True if SoundBlaster 16 was detected at boot |
sb16_play_pcm(pcm, len, hz) | const uint8_t*, uint32_t, uint16_t | Play 8-bit unsigned mono PCM non-blocking via DMA |
sb16_audio_stop() | none | Halt DMA playback immediately |
sb16_opl3_note_on(midi_note) | uint8_t midi_note | Play one OPL3 FM note using a MIDI note number |
sb16_opl3_note_off() | none | Stop the active OPL3 FM note |
SB16 audio notes for app authors:
- Always call
sb16_card_present()before using SB16 PCM or OPL3 services. Fall back tospeaker_note_on/offon machines without a card. - Playback is non-blocking —
sb16_play_pcmreturns immediately; DMA runs in background. Do not free the PCM buffer until playback finishes or you have calledsb16_audio_stop. - Use sounds sparingly. A single WAV file can easily consume 20–40 KB on a 1.44 MB floppy. Keep app sounds under 2 seconds. Prefer 11025 Hz, 8-bit unsigned mono; that yields ~11 KB per second.
- Call
sb16_audio_stop()before starting a new sound so the previous DMA transfer does not overlap. - For simple musical notes, prefer
sb16_opl3_note_on/offover PCM samples; it costs much less floppy space.
Misc
| Field | Notes |
|---|---|
prefs_sound() | System confirmation beep |
serial_str(s) | Debug string to serial port |
serial_char(ch) | Single character to serial |
serial_dec16(v) | int16_t as decimal to serial |
shell_files_changed() | Notify shell to refresh file lists |
set_toolbar_hidden(bool) | Hide/show only the floating tray (no other changes) |
set_game_mode(bool) | Game Mode — see section below |
Game Mode
Game Mode gives an app exclusive ownership of the screen and input. Call it in open() and undo it in close(). One call replaces `set_toolbar_hidden` for games — do not call both.
c
void mm_port_open(void) {
g_open = g_active = true;
g_host->set_game_mode(true); /* enter Game Mode */
/* load game data, etc. */
}
void mm_port_close(void) {
g_host->set_game_mode(false); /* restore full OS UI */
g_open = g_active = false;
}What `set_game_mode(true)` does:
| Effect | Detail |
|---|---|
| Toolbar tray hidden | Tray not drawn; clicks in tray area fall through to game window |
| Menu-bar header hidden | The 20 px OS header strip (menus, clock, LED) is not drawn; reclaimed for the game window |
| Exclusive keyboard input | All scancodes go only to the front WM app. shell_handle_scancode is completely bypassed — F1, Alt+F, Alt+H, Ctrl+Q, volume keys, and every other shell shortcut are suppressed. The app's handle_scancode() return value is not used as a fallback; the shell never sees the key. |
| Screensaver disabled | The idle timer is not checked while game mode is active; the screensaver never fires during gameplay |
Your `handle_scancode` in Game Mode:
Return true for keys you handle and false for keys you don't — but note that returning false in Game Mode does not pass the key to the shell. It is simply discarded. The only way to exit Game Mode is to call close() (which should call set_game_mode(false)).
Recommended quit pattern — catch ESC or whatever quit key the game uses:
c
bool mm_port_scancode(uint8_t sc) {
if (sc == 0x01) { /* ESC — quit game */
mm_port_close();
return true;
}
/* ... game input handling ... */
return true; /* consume everything in game mode */
}Other ideas that combine well with Game Mode:
- Position your
WindowFrameat(0, 0)withw=640, h=480to cover the full screen (header is hidden so the top 20 px are available) - Call
update()returningtrueevery frame for continuous animation - Save game state in
close()— this fires whether the user closes from your quit key or from a programmatic close set_toolbar_hidden(bool)still exists independently for apps that want only tray suppression without the full Game Mode restrictions
Shared transient workspace
| Field | Signature | Notes |
|---|---|---|
workspace_acquire | (size, owner, &out_size) | Request exclusive scratch space for one transient operation |
workspace_release | (ptr) | Release the shared scratch block when the operation is done |
Rules:
- Treat the workspace as a temporary, exclusive loan, not app-owned memory.
- Only use it for scratch data that can be discarded and recreated.
- Release it as soon as the blocking operation or calculation completes.
- If
workspace_acquire()returnsNULL, show a normal user-facing low-memory
or busy status and abort that operation cleanly.
- Do not store live document state, window state, undo history, or any
pointer that must survive beyond the current operation in the workspace.
Shared clipboard
Plain text clipboard v1 is exposed through appended HamsterHostAPI fields:
| Field | Signature | Notes |
|---|---|---|
clipboard_set_text | (text, length, owner) | Copies ASCII text into the OS clipboard; bounded and heap-backed |
clipboard_get_text | (buffer, max_size) | Copies text out; returns the full clipboard size even if truncated |
clipboard_text_size | () | Current text payload length |
clipboard_version | () | Monotonic change counter for apps that cache clipboard state |
clipboard_owner | () | Short owner label from the last writer |
clipboard_has_text | () | True when a non-empty plain-text payload exists |
Keep clipboard use small and transient. Larger typed payloads are planned later; v1 apps should treat it as plain text only.
AppDescriptor fields
Return from app_entry(). Set unused fields to NULL.
| Field | Required | When called |
|---|---|---|
init() | Yes | After app_entry() — init state, call wnd_init() |
open() | Yes | User launches the app when open_launch_at is not implemented |
open_file(drive, name) | No | Root-dir file association |
open_file_at(drive, cluster, name) | No | File browser document double-click |
open_launch_at(drive, cluster, name) | No | App launch with executable folder context; called instead of open() only when the .APP was packed with --open-launch-at, so store context and enter the normal open path. name is the .APP filename |
close() | No | Programmatic close |
is_open() | Yes | Every frame — return true while window exists |
is_active() | Yes | Every frame — return true while window has focus |
has_modal() | Yes | Every frame — true if a modal dialog is open |
set_active(bool) | Yes | When another window gains focus |
handle_scancode(sc) | Yes | Key press/release — return true if consumed |
handle_pointer_down/move/up(x,y) | Yes | Mouse events — return true if consumed |
handle_menu_command(cmd) | No | Menu item (uint32_t — cast to your enum) |
handle_edit_command(cmd) | No | Edit menu / clipboard |
draw() | Yes | Full window redraw |
draw_content() | No | Partial redraw (content only) |
get_bounds(x,y,w,h) | Yes | Window bounds for hit-testing and damage tracking |
update() | No | Animation/audio tick — return true if redraw needed |
consume_redraw_request() | No | Animation dirty flag poll |
handle_wheel(wheel) | No | Signed wheel input; positive means wheel-up. Return true if consumed |
get_primary_scroll(out) | No | Describe the main scrollable list for automatic wheel support |
set_primary_scroll(position) | No | Apply a clamped scroll position for automatic wheel support |
cursor_style_at(x,y) | No | Return VGA_CURSOR_RUNTIME_DEFAULT for the normal pointer, or a transient VGA_CURSOR_* shape for the point under the mouse |
Use cursor_style_at() for local interaction affordances only. It should not change the user's saved pointer preference; the window manager applies the returned style only while drawing the cursor. Current transient styles include busy, text/I-beam, pencil, eraser, fill, and crosshair.
Writing a stubs file
Every OS function your app calls must be wrapped. The linker reports the missing ones. See apps/piano_stubs.c for the full pattern:
c
/* myapp_stubs.c */
#include "app_abi.h"
#include "window.h"
#include "vga.h"
#include <stdbool.h>
#include <stdint.h>
static HamsterHostAPI *g_host;
void myapp_stubs_set_host(HamsterHostAPI *h) { g_host = h; }
void vga_fill_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t c)
{ g_host->fill_rect(x, y, w, h, c); }
void vga_draw_text(int16_t x, int16_t y, const char *s, uint8_t c, uint8_t sc)
{ g_host->draw_text(x, y, s, c, sc); }
void vga_draw_border(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t c)
{ g_host->draw_border(x, y, w, h, c); }
void window_init(WindowFrame *f, int16_t x, int16_t y, int16_t w, int16_t h,
int16_t mw, int16_t mh) { g_host->wnd_init(f,x,y,w,h,mw,mh); }
void window_draw_frame(const WindowFrame *f, const char *t)
{ g_host->wnd_draw_frame(f, t); }
void window_content_rect(const WindowFrame *f, int16_t i,
int16_t *x, int16_t *y, int16_t *w, int16_t *h)
{ g_host->wnd_content_rect(f, i, x, y, w, h); }
void window_display_bounds(const WindowFrame *f,
int16_t *x, int16_t *y, int16_t *w, int16_t *h)
{ g_host->wnd_display_bounds(f, x, y, w, h); }
WindowHit window_hit_test(const WindowFrame *f, int16_t x, int16_t y)
{ return g_host->wnd_hit_test(f, x, y); }
void window_begin_drag(WindowFrame *f, int16_t px, int16_t py)
{ g_host->wnd_begin_drag(f, px, py); }
void window_begin_resize(WindowFrame *f, int16_t px, int16_t py)
{ g_host->wnd_begin_resize(f, px, py); }
bool window_update_pointer(WindowFrame *f, int16_t px, int16_t py)
{ return g_host->wnd_update_pointer(f, px, py); }
void window_end_interaction(WindowFrame *f) { g_host->wnd_end_interaction(f); }
void window_toggle_maximize(WindowFrame *f) { g_host->wnd_toggle_maximize(f); }ABI stability contract
HamsterHostAPIandAppDescriptorare append-only. New fields are
always added at the end. Never reorder existing fields — doing so breaks every compiled .APP.
- HAMA
versionis incremented on breaking changes. Currently: 1. - HAMA
flagsbit 0 means a bundled 16x16 VGA icon is present immediately
after the header. Unknown flag bits should be treated as unsupported until a future version defines them.
- Shared structs (
WindowFrame,FileDialogState) must be compiled from the
OS source tree headers — never copy headers manually.
- The
name[8]field in the HAMA header is the display name used by app
launch surfaces and icon view. Set it with mkapp.py --name MYAPP (max 8 chars).
Memory rules
- Static variables live in BSS, zeroed at load time.
- Persistent app state should still be static/BSS-backed in v1.
- General
heap_alloc()/heap_free()are not part of the public app ABI. - For large transient scratch work only, use the shared workspace API
instead of inventing a private permanent buffer.
- BSS is freed when
is_open()returns false. Do not store cross-open pointers. - Total loaded app footprint = code + BSS. Current examples: Piano is about
5 KB, Notepad about 23 KB, and Paint about 23 KB before their runtime heap buffers.
- The staging buffer used to load the .APP file is freed before
app_entry()
is called, so only your runtime region occupies heap.
Conventions
- Prefix all symbols with your app name. The linker combines all objects;
duplicate names cause link errors.
- No libc. Use only
<stdbool.h>,<stdint.h>,<stddef.h>. - No direct I/O. Never access VGA memory, I/O ports, or kernel globals.
Every OS interaction goes through HamsterHostAPI.
- Uppercase 8.3 filenames on the FAT12 filesystem.
- One USER app, game, or tool slot at a time. Loading another closes the
current one and releases the previous .APP image.
- Show load feedback before blocking reads. For file-manager launches and lazy-loaded
system apps, draw a loading toast before fat_load_file() or app_loader_load() begins. The file manager does this automatically for .APP files double-clicked in icon or list view.
- Fail visibly and return control. If a load cannot complete, show a
user-facing error dialog and return to the desktop or file manager. Do not spin, panic, or leave the loading toast as the last painted state.
- Keep `AppDescriptor` initializers explicit. Fill every field through
update and consume_redraw_request, using NULL for unsupported hooks. Short initializers compile, but they hide ABI drift and make relocation review harder.
- Provide `close()` when the app owns heap buffers or hardware state. The
slots call it before replacing a loaded app/tool, and lazy system-app loaders use it to make teardown idempotent before releasing the .APP image.
File manager icon view, .ICO sidecars, and bundled icons
The file manager supports two view modes per window: By Name (list) and By Icon (grid). Users toggle between them via VIEW → BY NAME / BY ICON in the top menu bar.
Generic file-type icons
drivers/vga.c provides a centralized icon library via:
c
void vga_draw_file_icon(FileIconKind kind, int16_t x, int16_t y, uint8_t color);FileIconKind values (from drivers/vga.h):
| Constant | Shown for |
|---|---|
FILE_ICON_FOLDER | Directories |
FILE_ICON_APP | .APP files (fallback when no bundled icon) |
FILE_ICON_TEXT | .TXT files |
FILE_ICON_IMAGE | .BMP files |
FILE_ICON_UNKNOWN | Everything else |
Preferred fast path: .ICO sidecars
For built-in apps/tools/games shipped on the floppy, HamsterOS prefers a separate NAME.ICO sidecar next to NAME.APP. This keeps folder browsing fast: the file manager can batch-load a fixed 256-byte icon file without touching the app binary at all.
Use sidecars for anything that appears in normal folder listings on the shipping disk. This is the default pattern for /APPS, /GAMES, /TOOLS, /USER, and desktop shortcuts.
Bundled .APP icons
When an .APP file has HAMA_FLAG_HAS_ICON set in its header, HamsterOS can still read and display that icon by loading only the fixed header/icon block via app_loader_read_icon(). The full app code is never loaded just for icon display.
This path is best treated as compatibility or metadata support, not the primary folder-browsing strategy. If you expect users to browse a directory full of apps, ship .ICO sidecars so icon view stays snappy.
Icon data format: 256 bytes, 16×16 pixels, one byte per pixel (VGA palette index). Color 0 is treated as transparent by vga_draw_palette_icon().
To include an icon in your app, pass --icon assets/myapp_icon.bin to mkapp.py:
sh
python3 tools/mkapp.py --elf myapp.elf --name MYAPP --icon assets/myapp_icon.bin --output MYAPP.APPThe icon binary must be exactly 256 bytes (16×16 VGA palette indices, row-major). See existing game apps (assets/snake_icon.bin etc.) as references.
Existing apps as reference implementations
| App | Source | Format | Notes |
|---|---|---|---|
| Notepad | apps/notepad.c + apps/notepad_stubs.c | /APPS/NOTEPAD.APP | File I/O, file dialog, complex state |
| Paint | apps/paint.c + apps/paint_stubs.c | /APPS/PAINT.APP | External system app, heap canvas, close hook |
| Piano | apps/piano.c + apps/piano_stubs.c | /USER/PIANO.APP | Audio (speaker), animation tick |
| Slots | apps/slot_machine.c + apps/slot_stubs.c | /USER/SLOTS.APP | Animation, consume_redraw_request |
| Games | apps/*_gentry.c + apps/*_gstubs.c | /GAMES/*.APP | Bundled 16x16 icons via HAMA HAS_ICON |
| Disk Copier | apps/diskcopier.c + apps/diskcopier_tool_stubs.c | /TOOLS/DISKCOPY.APP | Floppy driver API stubs |
| CD Player | apps/cdplayer.c + apps/cdplayer_tool_stubs.c | /TOOLS/CDPLAY.APP | CDROM/ATAPI API stubs, animation tick |
| Floppy Diag | apps/floppydiag.c + apps/floppydiag_tool_stubs.c | /TOOLS/FLOPDIAG.APP | Full floppy + FAT + shell stubs |
| CD ROM Diag | apps/cdromdiag.c + apps/cdromdiag_tool_stubs.c | /TOOLS/CDDIAG.APP | CDROM diagnostic stubs |
| IDE Diag | apps/idediag.c + apps/idediag_tool_stubs.c | /TOOLS/IDEDIAG.APP | IDE diagnostic stubs |
| Format | apps/format.c + apps/format_tool_stubs.c | /TOOLS/FORMAT.APP | FAT format + floppy stubs |
| GFX Diag | apps/gfxdiag.c + apps/gfxdiag_tool_stubs.c | /TOOLS/GFXDIAG.APP | Multi-mode VGA benchmark |
HamsterHostAPI — extended fields (tool apps, June 2026)
The following fields were appended after prefs_failure_sound to support the tool apps in /TOOLS/. All new fields follow the same append-only rule.
| Field | Maps to | Used by |
|---|---|---|
draw_text_bold | vga_draw_text_bold | All tool apps |
present_framebuffer | vga_present_framebuffer | GFX Diag |
get_video_mode | vga_current_video_mode | GFX Diag |
pit_accumulate | pit_accumulate | Floppy Diag, GFX Diag |
serial_hex32 | serial_write_hex32 | Floppy Diag |
fat_delete … fat_file_trace | FAT extension functions | Floppy Diag, Format |
shell_set_boot_ready … shell_set_cdrom_ready | Shell state notifications | Floppy Diag, Format, CD ROM Diag |
kernel_service | kernel_service | Disk Copier |
floppy_a_ready … floppy_last_failure | Floppy driver functions | Disk Copier, Floppy Diag, Format |
cdrom_present … cdrom_eject | CDROM driver functions | CD Player, CD ROM Diag |
ide_diag | ide_run_diagnostic | IDE Diag |
ata_drive_present, ata_read_raw | ATA raw sector access | Partition Table Viewer |
sb16_card_present, sb16_play_pcm, sb16_audio_stop, sb16_opl3_note_on/off | SoundBlaster 16 | Piano (OPL3), any app that wants digital audio |
Hardware access and driver architecture
How drivers work in HamsterOS
All drivers in drivers/ are compiled directly into the kernel binary (hamster-dos.bin). There is no dynamic driver loading. External .APP files cannot install new kernel drivers — they access hardware exclusively through the HamsterHostAPI function table passed to app_entry().
text
Kernel binary (hamster-dos.bin)
└─ drivers/floppy.c ──┐
└─ drivers/ata.c ├─ Storage Registry (drivers/storage.*)
└─ drivers/atapi.c │ storage_device_count() / storage_get_device_info()
└─ drivers/cdrom.c ──┘ storage_read_sector512() / storage_write_sector512()
└─ drivers/sb16.c
└─ drivers/vga.c ↑ exposed to external apps via HamsterHostAPI
└─ ... ↓
External .APP file
└─ myapp_stubs.c ──── calls g_host->sb16_card_present(), g_host->floppy_a_ready(), etc.If your app needs hardware that isn't exposed yet, the path is: add the kernel function → append a new field to HamsterHostAPI (always at the end) → add a stub in your app's stubs file. Never reorder existing fields.
Driver registry
The driver registry (drivers/driver_model.h) provides a read-only view of every driver detected at boot.
c
/* Enumerate all drivers */
uint8_t count = driver_registry_count(); /* via g_host->... not exposed yet — use tool-app APIs directly */
DriverInfo info;
for (uint8_t i = 0; i < count; i++) {
driver_registry_get(i, &info);
/* info.id, info.name, info.driver_class, info.present, info.instance_count */
}DriverClass values: DRIVER_CLASS_STORAGE, DRIVER_CLASS_INPUT, DRIVER_CLASS_AUDIO, DRIVER_CLASS_VIDEO, DRIVER_CLASS_TIMER, DRIVER_CLASS_SYSTEM.
Capability flags (bitfield in info.capability_flags): DRIVER_CAP_BLOCK_IO, DRIVER_CAP_REMOVABLE, DRIVER_CAP_READ_ONLY, DRIVER_CAP_POINTER, DRIVER_CAP_KEYBOARD, DRIVER_CAP_PCM_OUTPUT, DRIVER_CAP_FRAMEBUFFER, DRIVER_CAP_RTC_CLOCK, DRIVER_CAP_TIMER_TICKS, DRIVER_CAP_DIAGNOSTIC.
> The driver registry is informational only. It does not route I/O — use the storage registry or direct hardware APIs for actual reads/writes.
Storage registry
The storage registry (drivers/storage.h) enumerates every block device and abstracts reads/writes behind a single index-based API. The FAT layer uses this internally; apps can use it directly for raw sector access.
c
/* StorageDeviceInfo fields */
typedef struct {
bool present; /* device detected */
bool media_present; /* removable media loaded (floppy/CD-ROM) */
bool read_only; /* write-protected */
bool removable; /* can eject/swap media */
StorageDeviceClass device_class; /* FLOPPY / HARDDISK / CDROM */
StorageBackend backend; /* FLOPPY / ATA / ATAPI / MKE_CDROM */
uint8_t backend_index; /* drive ID within backend (0–3) */
uint8_t instance_number;
uint16_t block_size; /* bytes per sector: 512 for floppy/HDD, 2048 for CD */
const char *controller_name; /* "IDE Primary", "Floppy Controller", … */
const char *device_name; /* "Primary Master", "Floppy Drive A", … */
} StorageDeviceInfo;Enumerate and read:
c
uint8_t n = storage_device_count();
for (uint8_t i = 0; i < n; i++) {
StorageDeviceInfo info;
if (!storage_get_device_info(i, &info)) continue;
if (!info.present || !info.media_present) continue;
/* read sector 0 */
uint8_t buf[512];
if (storage_read_sector512(i, 0u, buf)) {
/* buf contains first 512 bytes of the device */
}
}> storage_read/write_sector512 is not yet exposed via HamsterHostAPI. Use the specific driver APIs below for now.
Hardware APIs available to apps
All hardware access goes through HamsterHostAPI. Use these via your stubs file:
Floppy:
c
g_host->floppy_a_ready() /* true if drive A has media */
g_host->floppy_b_ready() /* true if drive B has media */
g_host->floppy_a_read(lba, buf) /* read 512-byte sector */
g_host->floppy_a_write(lba, buf)
g_host->floppy_b_read(lba, buf)
g_host->floppy_b_write(lba, buf)
g_host->floppy_batch_begin(drive) /* hold motor on for multi-sector ops */
g_host->floppy_batch_end()
g_host->floppy_cmos_types() /* CMOS nibbles for A/B drive types */
g_host->floppy_get_timing() / set_timing() /* FloppyTimingMode */
g_host->floppy_last_failure(out) /* FloppyFailureInfo */CD-ROM / ATAPI audio:
c
g_host->cdrom_present() /* any CD-ROM controller found */
g_host->cdrom_mke() /* true = Matsushita/MKE interface */
g_host->cdrom_audio_ok() /* true = ATAPI audio transport available */
g_host->cdrom_read_toc(out) /* AtapiToc — track list and lengths */
g_host->cdrom_play(sm,ss,sf, em,es,ef) /* play MSF range */
g_host->cdrom_pause(resume)
g_host->cdrom_stop()
g_host->cdrom_status(out) /* AtapiAudioStatus */
g_host->cdrom_eject()
g_host->cdrom_probe_port(base) /* probe one MKE port; returns true if device responds */ATA raw sector access (primary master only):
c
g_host->ata_drive_present()
g_host->ata_read_raw(lba, buf) /* 512-byte sector, no partition translation */
g_host->ata_write_raw(lba, buf)
g_host->ata_sector_count_raw() /* total sectors on device */SoundBlaster 16:
c
g_host->sb16_card_present() /* SB16 detected at boot */
g_host->sb16_get_base_port() /* I/O base (0x220, 0x240, …) */
g_host->sb16_get_irq_num() /* IRQ number */
g_host->sb16_play_pcm(buf, len, hz) /* 8-bit unsigned mono PCM */
g_host->sb16_play_wav_buf(wav, sz) /* play a WAV buffer from memory */
g_host->sb16_audio_stop()
g_host->sb16_opl3_note_on(midi_note) /* FM synthesis via YMF262 */
g_host->sb16_opl3_note_off()PC Speaker:
c
g_host->speaker_note_on(hz) /* start tone at frequency */
g_host->speaker_note_off()Writing a hardware tool app — checklist
1. Add stubs for every g_host-> field you use in myapp_stubs.c (see apps/piano_stubs.c). 2. Check g_host->sb16_card_present() / g_host->floppy_a_ready() / g_host->cdrom_present() before using a device — never assume hardware is present. 3. Call g_host->kernel_service() inside long I/O loops to keep the mouse cursor moving. 4. For CD-ROM reads through FAT, use g_host->fat_load with FAT_DRIVE_CDROM — the FAT layer handles the MKE/ATAPI routing. Only use cdrom_probe_port or the direct ATAPI APIs if you are writing a diagnostic. 5. To add a new hardware API: add the kernel function, append one field to HamsterHostAPI in kernel/app_abi.h, wire it in kernel/app_loader.c, add a stub. Never insert between existing fields.
Writing kernel drivers (.DRV)
HamsterOS supports external loadable kernel drivers via the HAMD v1 format. Unlike .APP files (user-space apps), .DRV files are loaded early in boot and run as trusted kernel-level code with direct port I/O access.
Drop a `.DRV` into `/DRIVERS/` on the floppy and reboot — the driver is live.
The complete driver writing guide is in `docs/HAMSTEROS_DRIVER_SDK.md`, covering:
- How HAMD differs from HAMA (magic, entry point, no compression,
tools/driver.ld) DriverHostAPI— the host services table drivers receive (port I/O, heap, serial)DriverDescriptor— what your driver returns (storage, audio, and input callbacks)- Two-file structure (
mydrv_entry.c+mydrv.c) and build recipe StorageDeviceInfofields for storage-class drivers- Safety rules and the
apps/example_driver/nulldiskreference implementation
Loader troubleshooting notes
- If an external app soft-crashes immediately after
app_loader: loaded NAME.APP,
inspect descriptor relocations first. AppDescriptor contains function pointers and must have R_386_32 relocations applied like any other static pointer table.
- If an icon-bearing app crashes before
app_entry(), verify the loader skips
the optional 256-byte icon block before copying code.
- If a window appears at
(0,0)with broken bounds or does not open, confirm
the owner slot/loader calls desc->init() before desc->open().
- If an app works as built-in code but not as
.APP, check the stubs file for
any direct OS calls that were not routed through HamsterHostAPI.
- If the screen stays at
Loading NAME, the loader is probably blocked inside
the disk read path. Check serial output for app_loader: header load failed, app_loader: load failed, the FAT trace, and the byte/truncation counters.
- If a load fails cleanly, the expected user experience is an error dialog such
as "File not found.", "Invalid application file.", "Not enough memory to open application.", or "Application file could not be read."
The quickest runtime smoke test is to boot build/hamster.img, launch an app, and watch serial output for app_loader: loaded NAME.APP followed by the app-specific loader message, with no fault/panic output afterward.
*HamsterOS — 32-bit protected-mode OS for 386/486 hardware.* *App Developer Guide — June 2026.*