UnoC SDK reference
Every call a UnoC app can make, with parameters, return values, remarks and a working
snippet for each. SDK\UNO.H is the whole SDK - the one header you include - and Studio's
compiler checks every call against the kernel's export table at build time, so an unavailable function
is a compile error with a line number, never a mystery crash. The app model is on
Writing apps; the language subset is summarised at the end.
The entry point and the vtable
An app defines exactly one entry, uno_app_main, stashes the KernelApi
pointer in gK (the UNO.H prelude macros expand through it), and returns an
AppInterface:
#include "UNO.H"
static void my_draw(UnoWin *w) { ... }
static const AppInterface kIface = {
my_draw, /* draw - required */
my_key, /* key - or 0 */
0, /* click - or 0 */
my_tick, /* tick - or 0 */
my_opened, /* opened - or 0 */
my_closed, /* closed - or 0 */
"My App", { 40, 40, 360, 240 } /* title; l,t,r,b (only w x h used) */
};
const AppInterface *uno_app_main(const KernelApi *k)
{
gK = k; /* the UNO.H prelude macros expand through this */
return &kIface;
}
| Member | Contract |
|---|---|
void draw(UnoWin *w) | Required - the load is refused without it.
Called once per repaint of the whole scene, in z-order, even when your window is occluded; not called
when closed or on another virtual desktop. The canvas is not cleared for you - paint everything
you own. w->bounds is in absolute screen pixels; content starts at
bounds.top + TBAR_H (18 px). |
Boolean key(char ch, short code, Boolean cmd) | Focused window only. Printable
ASCII (32..126) arrives in ch with code 0. Exactly six special keys are
delivered: arrows (ch 0x1C left, 0x1D right, 0x1E up, 0x1F down, with code
0x7B-0x7E), Enter (0x0D) and Backspace (0x08). Esc, Tab, Delete, Home/End and the function keys never
reach an app. cmd is the Ctrl flag for those six keys only - printable characters
always arrive with cmd == false, so Ctrl-letter accelerators are not receivable. Return
true to consume (which also marks the screen dirty). |
void click(UnoWin *w, Point p) | Left-button press only - no release,
no motion, no right button. p is in screen pixels, not window pixels (see below).
Only clicks inside your canvas arrive; the title bar belongs to the shell. A delivered click always
triggers a repaint. |
void tick(void) | Once per shell frame while the app is open - focused or
not - nominally 60/s, slower under load. Does not repaint: call repaint_all()
yourself when something visible changed. |
void opened(void) / void closed(void) | opened runs
once per Run, right after uno_app_main. Closing the window calls closed() but
keeps the module resident; the next Run replaces it wholesale and re-zeroes all globals, so every
opened() starts from pristine state. Nothing is cleaned up for you on close - stop
your music and free your memory in closed(). |
const char *win_title | Window title, taskbar chip and launcher label. The pointer is borrowed - use a string literal. NULL falls back to "My App". |
short win_rect[4] | {left, top, right, bottom} - only the
width and height are used; the shell places the window itself. Height includes the 18 px title
bar you never draw, so the canvas is w x (h-18). Minimum window 140x100; the window is not
resizable. |
Coordinates and the window
static void my_draw(UnoWin *w)
{
/* your content area starts below the title bar the shell draws */
short x0 = w->bounds.left;
short y0 = w->bounds.top + TBAR_H;
short cw = w->bounds.right - w->bounds.left; /* canvas width */
short ch = w->bounds.bottom - (w->bounds.top + TBAR_H); /* canvas height */
...
}
All drawing is clipped to your canvas - with two exceptions noted below. Rects are half-open:
SetRect(&r, 0, 0, 10, 10) covers pixels 0..9. An inverted or empty rect draws
nothing.
Drawing
void uno_fill(Rect *r, short c) / void uno_box(Rect *r, short c) /
void uno_invert(Rect *r)
| Parameter | Meaning |
|---|---|
r | The rectangle, screen coordinates, half-open. |
c | A palette index: C_BLUE(0) C_CYAN(1)
C_MAG(2) C_WHITE(3). Not bounds-checked - anything else reads past a
4-entry table. |
Remarks. uno_fill fills, uno_box draws a 1-pixel outline,
uno_invert XORs the pixels (colour channels only). All three are clipped to your canvas
and treat an empty rect as a no-op. The fill and box honour the global pen mode - after
PenMode(patXor) they invert instead of painting - and both leave the global fore colour
set to black on return.
Rect r;
SetRect(&r, x0, y0, x0 + 100, y0 + 40); /* half-open: [l,r) x [t,b) */
uno_fill(&r, C_BLUE); /* palette fill */
uno_box(&r, C_WHITE); /* 1px outline */
uno_invert(&r); /* XOR the pixels */
void fill_rgb(Rect *q, const GameRGB *c)
True-colour fill: GameRGB carries 0-255 r,g,b and a mono
palette fallback used by the 1-bit ports (ignored on pc64 - set it to the nearest C_* so
the same source works everywhere). Clipped; empty rect is a no-op; leaves fore colour black.
static const GameRGB kGold = { 240, 200, 40, C_WHITE };
/* r, g, b are 0-255; .mono is the palette fallback for 1-bit ports -
* unused on pc64, but fill it in for portability */
Rect q;
SetRect(&q, x0, y0, x0 + 32, y0 + 32);
fill_rgb(&q, &kGold);
void text_at(short x, short y, const char *s, short fg, short bg, Boolean opaque)
| Parameter | Meaning |
|---|---|
x, y | Screen coordinates. y is neither the cell top nor the
baseline: with the default font the 16-px text cell occupies roughly [y-7, y+9) and the
baseline lands at y+5. Space rows at least 16 px apart (the samples use 18). |
fg, bg | Palette indices 0..3, unchecked. bg is used only
when opaque is true. |
opaque | false: only glyph pixels are written.
true: each character's advance cell is filled with bg first. |
Remarks. The default face is a proportional Chicago-style TrueType at 15 px - there is
no fixed character width, and the user can change both the face and the UI scale in the Control
Panel (at 150% your hardcoded layout will collide). Measure with TextWidth (exported;
declare short TextWidth(Ptr, short, short); yourself) or bound with text_at_max.
Glyphs cover ASCII 32..126; anything else draws as a blank advance. Clipped to the canvas. On return the
global text state is reset (fore black, back white, mode transparent).
void text_at_max(short x, short y, const char *s, short fg, short maxw)
As text_at (always transparent), but drops trailing characters until the run fits in
maxw pixels - a hard cut, no ellipsis. If not even one character fits, nothing is drawn.
text_at(x0, y, "Score:", C_CYAN, C_BLUE, false); /* transparent */
text_at(x0, y, "MENU", C_WHITE, C_MAG, true); /* opaque cell */
text_at_max(x0, y, longname, C_WHITE, 120); /* hard-cut at 120 px */
Toolbox shapes: PaintRect FrameRect InvertRect PaintOval FrameOval MoveTo
LineTo
The classic QuickDraw slice. All paint with the global fore colour - set it with
RGBForeColor(&kPalette[C_CYAN]) or any RGBColor immediately before
drawing, because every uno_*/text_at/fill_rgb call resets it
to black. PenMode supports exactly two behaviours: patXor/srcXor
invert, everything else copies. PenNormal() resets size and mode (not colour).
MoveTo positions the pen; LineTo draws from the pen and moves it, so calls
chain into a polyline.
PaintOval, FrameOval and MoveTo/LineTo bypass the canvas clip: with coordinates outside your window they will happily paint over other windows and the taskbar. The rect and text calls are properly clipped; clip your own line and oval geometry.Geometry
| Call | Effect |
|---|---|
void SetRect(Rect *r, short l, short t, short rt, short b) | Pure assignment - does not normalize; an inverted rect is treated as empty by every drawing call. |
void OffsetRect(Rect *r, short dh, short dv) | Translate. 16-bit arithmetic, wraps silently past +/-32767. |
void InsetRect(Rect *r, short dh, short dv) | Shrink by dh/dv on
each side (negative values grow). Over-insetting past the centre inverts the rect - no clamp. |
PtInRect is deliberately absent (UnoC cannot pass a Point by
value in that form) - compare p.h/p.v against the rect yourself.
Numbers to text
void fmt_u(long v, char *out)
Unsigned decimal, no padding, NUL-terminated. v <= 0 prints "0" - negatives are
not rendered; format a sign yourself. out needs 11 bytes.
void put2(long v, char *out)
Exactly two zero-padded digits + NUL (out needs 3 bytes): the value modulo 100, so
123 prints "23". Never pass a negative - the digits come out as
garbage characters, not numbers.
char num[11]; /* fmt_u: up to 10 digits + NUL */
fmt_u(score, num);
text_at(x0 + 60, y, num, C_WHITE, C_BLUE, false);
char t[8]; /* put2: "MM:SS" from two calls */
put2(secs / 60, t); t[2] = ':'; put2(secs % 60, t + 3);
Windows and repainting
| Call | Effect |
|---|---|
void repaint_all(void) | Mark the screen dirty; the shell repaints every
window at the end of the current frame. This is the redraw call - cheap, deferred, never
re-entrant. Animation from tick() must call it or nothing moves. |
void draw_window(UnoWin *w) | Identical effect to repaint_all()
(the argument is ignored). Kept for source compatibility with the other ports. |
UnoWin *find_app_window(short proc) | Your own window answers
find_app_window(APP_NAPPS) - and only after the first paint (before that its bounds are
zero). The APP_* enum values name the built-in apps; only the five shipped games/tools
can answer, and only while open. Do not use APP_RUNNER: it always returns NULL for
a user app (older SDK examples got this wrong). |
void launch_app(short proc) | A no-op on pc64 - the shell owns launching. |
short topmost_proc(void) (via
gK->topmost_proc()) | The focused built-in app's proc, or -1 - including whenever your window is the focused one. Of little use to a user app. |
static long frame;
static void my_tick(void) /* ~60/s while the app is open */
{
frame++;
if (frame % 6) return; /* pace: ~10 updates a second */
advance_state();
repaint_all(); /* deferred: painted at frame end */
}
Input
static Boolean my_key(char ch, short code, Boolean cmd)
{
if (ch == ' ') { toggle(); repaint_all(); return true; }
if (ch == 0x1E) { move_up(); repaint_all(); return true; } /* Up */
if (ch == 0x1F) { move_down(); repaint_all(); return true; } /* Down */
if (ch == 0x1C) { move_left(); repaint_all(); return true; } /* Left */
if (ch == 0x1D) { move_right(); repaint_all(); return true; } /* Right*/
if (ch == 0x0D) { confirm(); repaint_all(); return true; } /* Enter*/
(void)code; (void)cmd;
return false; /* not ours */
}
static void my_click(UnoWin *w, Point p)
{
/* p is in SCREEN pixels - convert to canvas coordinates yourself */
short cx = p.h - w->bounds.left;
short cy = p.v - (w->bounds.top + TBAR_H);
...
}
void GetMouse(Point *p) / Boolean StillDown(void)
GetMouse fills p with the pointer position in screen pixels
(p.h horizontal, p.v vertical) - subtract your window origin yourself. On pc64
it also polls the hardware and presents the frame, which is what makes a classic blocking drag loop
track live - but it is not free, so don't spam it. StillDown() reports whether a button is
held, as of the last GetMouse call - poll GetMouse in the loop or it
never changes.
Point m;
GetMouse(&m); /* screen coords; also refreshes buttons */
while (StillDown()) { /* a classic drag loop */
GetMouse(&m); /* GetMouse also presents the frame */
track(m.h - x0, m.v - y0);
}
Time: long TickCount(void)
Read this one carefully: on pc64 TickCount() is a call counter, not a clock. It
returns a global counter incremented once per call - by any caller, in any app - and nothing else
advances it. Called exactly once per tick() it approximates a 60 Hz frame counter;
called twice, your animation runs at half speed; called while another app is also calling it, your
deltas inflate. The samples therefore pace on their own frame counters (see
TIMER.C), and so should you. There is no wall clock in the classic
SDK; a Python app can read unoauto.uptime() (real milliseconds).
Randomness: short Random(void)
A shared linear-congruential generator returning the full signed 16-bit range -32768..32767 (mask
with & 0x7FFF for non-negative). The seed is a compile-time constant and there is no
seeding call: the sequence is identical on every boot, shifted only by however many times anything
else has called it. Mix your own entropy in (LIFE.C stirs the cell coordinates into the seed for
exactly this reason).
Memory
Ptr NewPtr(long byteCount) / void DisposePtr(Ptr p)
NewPtr is malloc: 16-byte aligned, NULL on failure (always check), contents
not zeroed. A non-positive size allocates one byte. The arena is a single 32 MB heap shared
with the shell and Studio - there is no per-app quota, so leaks degrade the whole machine until the next
Run (which reloads your module and frees nothing for you: free in closed()).
DisposePtr is free: NULL-safe and double-free-safe.
Ptr p = NewPtr(4096);
if (!p) return; /* NULL on failure - always check */
memset(p, 0, 4096); /* NewPtr does not zero */
...
DisposePtr(p); /* NULL-safe */
The libc slice - memcpy memmove memset memcmp strlen strcpy strncpy strcat strcmp
strncmp - behaves exactly as C requires; sizes are unsigned long (4 bytes,
LLP64).
Sound
One voice, shared with the shell's own apps: the PC speaker, or the machine's DAC when one exists. Durations are in shell frames (~60/s).
| Call | Effect |
|---|---|
void music_open_chan(void) | No-op on pc64 (the voice is always ready);
call it once in opened() for portability. |
void music_note_on(short midi, short durTicks) | One square-wave note at a
MIDI pitch (60 = middle C, 69 = A440; practical range ~24..108 - below ~16 nothing sounds).
durTicks <= 0 plays 6 frames. A note interrupts a playing score, which resumes
after. |
void music_quiet(void) / void music_stop(void) /
void gm_stop(void) | All three do the same thing: stop the score and silence the
voice. music_start() is a no-op. |
void gm_start(const Note *notes, short count, short owner) | Loop a score
forever. Note is {midi, dur} bytes; midi 0 is a rest;
owner is ignored on pc64. The array is borrowed, not copied - make it
static const. Not stopped when your window closes: call gm_stop() in
closed(). |
static const Note kTune[] = { {76,16},{72,16},{69,16},{0,8} }; /* 0 = rest */
music_note_on(84, 12); /* one beep: MIDI 84, 12 frames */
gm_start(kTune, 3, 0); /* loop a score (owner ignored) */
gm_stop(); /* stop it - also do this in closed() */
Files
The classic file slice is a small, session-scoped store - fine for a settings file or a saved game,
wrong for anything bigger (a Python app's uno.read/write is the fuller API).
| Call | Contract |
|---|---|
Boolean fat12_mount(void) | Always true on pc64; call once for portability. |
void fat12_list(void) | Fills the exported arrays with the root directory of
every volume: gFatCount (capped at 16), gFatNames (12-char names + NUL).
gFatSizes is always 0 on pc64 - don't display it. |
long fat12_read(const char *name, unsigned char *buf, long max) | Read from
offset 0, up to max bytes. Returns the byte count or -1 for: not found, empty file,
or no free handle. Names are exact, case-sensitive, up to 31 chars. Reads the session store only:
a name listed by fat12_list from a real disk is not readable here. |
Boolean fat12_write(const char *name, const unsigned char *buf, long len) |
Creates the file if absent. Writing an existing name APPENDS - there is no truncate and no delete, so save under a versionless single name only if you write it once per session, or build the whole content and write once. On success the file is also mirrored to the first writable FAT volume, so your save is visible in Files and to Python apps. Returns false only when the store is full (24 files / 256 KB per file); a failed mirror is not reported. |
static unsigned char buf[4096];
long n = fat12_read("NOTES.TXT", buf, sizeof buf);
if (n < 0) { /* missing, empty, or unreadable */ }
/* WARNING: writing an existing name APPENDS (see remarks) */
fat12_write("SCORES.TXT", data, len);
fat12_read does not). A game that must reload yesterday's save should be a Python app today.Display modes
gK->display_res_count() / display_res_get(idx, &w, &h, &zoom,
&active) / display_res_set(idx) enumerate and change the desktop size
(the output is scaled to the panel; zoom is always 0 on pc64). Call count
first - it rebuilds the list the other two index. set is immediate, relayouts every window,
and bypasses the Control Panel's 15-second revert countdown - a wrong mode stays wrong. Almost no
app should call it.
The language, on one screen
| You have | You don't have |
|---|---|
char/short/int/long/long long + unsigned, pointers, multi-dim arrays, struct/union/enum/typedef,
function pointers, the full operator set, if/while/do/for/switch, static globals + locals, brace
initializers with address constants, char s[] = "text", string-literal concatenation,
sizeof, recursion |
floating point (not even the keywords), varargs (so no printf), function-like macros, bitfields,
goto, VLAs, #if arithmetic, <system> includes, more than 8
parameters/arguments, struct pass-by-value above 8 bytes |
LLP64: long is 4 bytes, long long and pointers are 8. The ABI is
MS x64. #include "FILE.H" searches the file's folder, then SDK\. char
is signed. Full grammar: DOCS\LANG.MD on the device.
Shipping it: the app descriptor
What Studio builds runs in the user slot (one at a time, 512 KB). To give your app a Start-menu row, a desktop icon and a durable identity, wrap it with an app descriptor on your PC - the full walkthrough is in Writing apps §8, and the grammar is:
| Key | Meaning | Limit |
|---|---|---|
id: | durable identity - geometry, session restore and launch
<id> all key on it | 15 chars, [a-z0-9._-] |
name: / short: | launcher label / desktop-icon label | 31 / 15 chars |
icon: | a named emblem, or file:NAME.QOI (32x32 QOI beside the
module, hard-edged alpha) | 15 chars total - the filename part fits 10 |
cat: / rank: | Start-menu section (system net tools media games other) and sort key | rank 0-255 |
flags: | singleton hidden game nosession | |
min: | preferred window size WxH | < 8192 |
The whole block is capped at 1024 bytes; unknown keys are ignored forever (the
extension point), unknown values fail the build. The user's APPS.CFG can rename, re-file
or hide your app - the module is the authority for what's inside the window, the user for what it's
called.
Worked examples: the sample programs. The Python side:
Python SDK reference. The wider export table (the kernel exports far
more than UNO.H declares - fb_*, malloc, the filesystem) is discoverable in
pc64_modload.c's KX() tables, undocumented and unguaranteed: stay inside
UNO.H unless you enjoy archaeology.