Writing apps
Everything between "I have an idea" and "it's in the Start menu", in the order you'll meet it: pick a language, learn the lifecycle the desktop drives you through, draw, react, keep time, save, make sound, then package. The per-call contracts live in the UnoC and Python SDK references, and complete programs to take apart are on Sample programs - this page is the map. The second half covers the deeper tiers the built-in apps use.
.UNO, and Ctrl+R to run it - no PC or toolchain needed.1. Pick a language
| UnoC | Python | |
|---|---|---|
| Runs | native x86-64, compiled on the device by Studio | on the PYRT runtime (MicroPython) |
| Feels like | classic C against a Toolbox-style API | modern Python with a small,
flat uno module |
| Best for | games and tools that want every cycle | almost everything else - less ceremony, floats, exceptions, big ints |
| Watch out for | no floating point, no printf, LLP64 (long is 4 bytes) |
no f-strings, no time/random/json, one file per app |
Both compile and run entirely on the device: open Studio, write, Ctrl-B builds, Ctrl-R runs. Errors land in the output pane with a line number; click one and the caret jumps there.
2. The lifecycle: the desktop calls you
There is no main() and no event loop to write. Your app hands the desktop a small set of
callbacks and the desktop drives them: build/opened once at open, draw on every repaint,
tick ~60 times a second, key/click when input arrives for your window,
closed on the way out. The two smallest complete apps:
#include "UNO.H"
static void my_draw(UnoWin *w)
{
short x0 = w->bounds.left + 10;
short y0 = w->bounds.top + TBAR_H + 10;
text_at(x0, y0, "hello, UnoDOS", C_WHITE, C_BLUE, false);
}
static const AppInterface kIface = {
my_draw, 0, 0, 0, 0, 0,
"Hello", { 40, 40, 260, 140 }
};
const AppInterface *uno_app_main(const KernelApi *k)
{
gK = k;
return &kIface;
}
import uno
class Hello(uno.App):
def draw(self, cv):
cv.clear(uno.rgb(16, 18, 34))
cv.text(8, 8, "hello, UnoDOS", uno.rgb(214, 218, 232))
app = Hello()
Rules that hold in both languages:
- Never block. Every callback runs on the shell's frame loop; a busy-wait freezes the whole
desktop. Structure long work as increments driven from
tick(). - Draw only in draw. State changes in
tick()/key(), pixels indraw(); ask for a repaint rather than painting eagerly. - Closing is not the end. Treat
opened()as "again", not "first time", and clean up - stop your music, free your memory - inclosed(), because nobody does it for you.
3. Drawing
You draw into your window's content area - below the title bar - through the calls in the references
(UnoC: rect fills, ovals, lines, text_at;
Python: the Canvas). Two disciplines carry every
sample:
- Read your size, don't assume it. UnoC:
w->boundsplusTBAR_H; Python:cv.width()/cv.height(). The shell picks window sizes and the user picks resolutions and fonts. - Repaint on change, not on schedule. UnoC: call
repaint_all()fromtick()when something moved. Python: returnFalsefromtick()on idle frames. A static window should cost nothing.
4. Input
Keys arrive as events; return true to consume one. Printable characters and special keys are
distinguished for you (UnoC: ch vs code; Python: uni vs
scan - full tables in the references). Some chords never reach you - Ctrl+Esc, Alt+Tab and
friends belong to the shell. For held-key movement in games, Python has uno.keys_down();
UnoC games time out their own key events.
5. Time
Your tick() callback arrives once per shell frame, nominally 60 a second - the frame
is the platform's timebase, so count your own tick() calls. Do not reach for
TickCount()/uno.ticks() expecting a clock: it is a
call counter shared by every app, so it only behaves like frame
pacing if you call it exactly once per tick and nothing else is calling it.
TIMER.C is the worked example of frame counting. The one real
clock an app can read is unoauto.uptime() (milliseconds since boot, available in every
build) - automation scripts pace on it.
6. Files and settings
Volume 0 is the boot volume; names are 8.3. Whole-file read/write plus offset reads for streaming -
the contracts (missing files, read-only volumes, size probes) are in the references, and
TODO.PY shows the polite way to surface a failed save. For small
settings, Python apps have uno.pref_get/pref_set; log through
uno.log() so your traces land in the system log with everyone
else's.
7. Sound
The square-wave voice (music_note_on / uno.beep) works on every machine.
Sampled effects and MIDI music need a DAC and fail detectably when there is none - always keep the beep
fallback. Looping game music in UnoC is gm_start; stop it in closed(),
because the shell won't.
8. Packaging: from the user slot to the Start menu
What Studio builds runs immediately - the user slot - and that is the whole story for a
personal tool: the .UNO lands next to your source, Files can launch it, one such app is
resident at a time.
To make an installed app - a Start-menu row, a desktop icon, a durable name - the module must carry an app descriptor, and that is a host-side step today (Studio doesn't write descriptors):
# on your PC, in pc64/:
python3 tools/mkicon.py myicon.ppm MYAPP.QOI
python3 tools/mkuno.py pyapp MYAPP.PY APPS/MYAPP.UNO MYAPP.DESC
# MYAPP.DESC:
# id: myapp
# name: My App
# icon: file:MYAPP.QOI
# cat: tools
# rank: 50
Drop the result into APPS\ on the device (Files' Copy, or put over the
remote link) and press Rescan in the Control Panel - no reboot. The
descriptor's id: is your app's durable identity: window geometry, session restore and
launch <id> all key off it. Icons are 32x32 QOI with hard-edged alpha; categories
order the Start menu; the user's APPS.CFG gets the last word over your name and placement.
The full descriptor grammar and limits are in the
UnoC SDK reference.
9. When it goes wrong
- Build errors land in Studio's output pane with file and line; UnoC checks every call you make against the kernel's export table at build time, so a typo'd function is a compile error, never a mysterious crash.
- Python tracebacks (with line numbers) print to the output pane; a raising callback is fenced - it never takes the desktop down.
- A
draw()that raises paints its own traceback into the app's window, so the window that would otherwise have been blank tells you what went wrong and which line did it. Drawing then stops rather than raising sixty times a second; fix the code and run again (Ctrl+R) and it starts clean. The exception line also goes to the system log, which is how a machine you are not sitting in front of reports it. - Studio will not open a binary file as text. The project pane lists
.UNOmodules on purpose, so you can see what you just built, but picking one now says so in the output pane and leaves your open document alone. It used to load the module as mojibake, and saving that back truncated the module at its first zero byte and destroyed it. - print() works in Python (an 8 KB buffer shown by Studio); UnoC apps put numbers on
screen with
fmt_uor into the system log. - For anything deeper, the remote link streams logs to your PC and can drive the app with injected input while you watch.
10. The numbers
| Limit | Value |
|---|---|
| Source file (Studio editor) | 192 KB |
Built .UNO (Studio) | 256 KB |
| UnoC user-app slot | 512 KB, one resident app |
| Python heap | 16 MB |
| Function parameters/arguments (UnoC) | 8 |
| App descriptor | 1024 bytes; id 15 chars, name 31 |
| Icon | 32x32 QOI, 16 KB file, 12 custom slots system-wide |
| Installed apps | 48 registry rows |
Beyond the SDK: how the built-ins are made
Everything above is the classic tier - the SDK Studio compiles for. The built-in apps use two deeper tiers that today require the PC toolchain (see Building & tooling). There are a few UnoC styles; the native widget app is the normal path for a built-in.
- Native widget app: build a window out of toolkit widgets and let the shell run the event loop.
- Canvas app: the toolkit owns the window chrome, focus, dragging and z-order; your app owns the pixels inside a canvas rectangle and receives raw input. Games, Paint, Tracker, the browser and Runner3D are canvas apps.
- Legacy bridge app: an existing
AppInterfaceapp hosted inside a canvas through the compatibility bridge.
A native widget app
An app is simply a builder function that populates a window. It uses only unoui.h calls,
and every widget is reachable by pointer or keyboard for free.
#include "unoui.h"
enum { ID_HELLO_BTN = 100 };
/* An app is just a builder that populates a window.
The shell owns the event loop and calls this once. */
void build_hello(unoui_window *w)
{
/* title, x, y, width, height (screen coords, includes the title bar) */
unoui_window_init(w, "Hello", 160, 60, 200, 110);
/* a static label at content-relative (12, 10) */
unoui_add_label(w, 12, 10, "Hello, UnoDOS pc64!");
/* a default push button; its id is echoed back on click */
unoui_widget *b = unoui_add_button(w, 12, 40, 80, "OK", UI_F_DEFAULT);
b->id = ID_HELLO_BTN;
}
The shell turns each input event into an unoui_action and dispatches by the widget id
you assigned:
unoui_ui_init(&ui, &theme_unodos, FB_W, FB_H);
unoui_ui_add(&ui, &window);
for (;;) {
unoui_event ev = port_next_event(); /* platform input adapter */
unoui_action a = unoui_handle(&ui, &ev); /* portable widget behavior */
if (a.changed && a.id == ID_HELLO_BTN) { /* the OK button was pressed */ }
if (a.changed && a.kind == UI_ACT_CLOSE) { /* close box; value = z-index */ }
unoui_render_ui(&ui);
port_present(fb); /* platform present adapter */
}
Widget positions are relative to the window content origin (inside the frame and title bar); the toolkit computes that origin from the active theme, so hit-testing always matches what is drawn.
How an app reaches the desktop
Each app (the games, Paint, Tracker, Music, Photos, Studio) is a .UNO file in the
APPS folder of the disk, loaded the first time you open it. A .UNO is a small
relocatable code module: the loader (pc64_modload.c) reads it, checks it, places it in memory,
and resolves the kernel functions it calls by name against an export table - so the app carries no kernel
code and the kernel carries no app code. The shell keeps a table of window builders and opens an app's
window on demand:
static void open_app(int a) {
if (!g_built[a]) {
if (a < NNATIVE) g_build[a](&g_win[a]); /* native builder */
else build_legacy(a); /* bridged legacy app */
g_built[a] = 1;
}
if (!g_open[a]) {
unoui_ui_add(&UI, &g_win[a]); /* add window to the UI */
g_open[a] = 1;
}
}
The core shell windows (Control Panel, the Editor, Files) are built by function-pointer builders in
g_build[] (pc64_uui.c); a desktop icon or programs-menu row calls
open_app(index). Loadable apps are compiled once each with a distinct entry symbol via
-DUNO_APP_SYM=uno_app_main_<name>, then flattened into a .UNO by
tools/mkuno.py. Studio builds exactly this format on the machine itself.
A canvas app
Give the toolkit a unoui_canvas (a draw callback plus an event callback) and it manages the window
around your pixels:
#include "unoui.h"
/* the app owns every pixel inside its canvas rectangle */
static void hello_draw(struct unoui_widget *w, unoui_rect r, void *ctx) {
fb_fill_rect(r.x, r.y, r.w, r.h, FB_RGB(20, 30, 60));
fb_text(r.x + 8, r.y + 8, "these pixels are mine", UNO_WHITE, -1);
}
/* return 1 if the event was consumed */
static int hello_event(struct unoui_widget *w, const void *ev, void *ctx) {
const unoui_event *e = ev;
if (e->kind == UI_EV_KEY && e->key == UI_KEY_ESC) return 1;
return 0;
}
static unoui_canvas g_hello = { hello_draw, hello_event, 0 };
void build_hello_canvas(unoui_window *w) {
unoui_window_init(w, "Canvas", 120, 40, 280, 180);
unoui_add_canvas(w, 6, 20, 256, 120, &g_hello);
}
For a game or a 3D view, call unoui_fullscreen(&ui, win) to make the canvas fill the screen with
all input routed to it (Esc returns), and unoui_fullscreen(&ui, NULL) to restore the
desktop. Mark widgets that should stretch on resize with unoui_widget_fill and set the
UI_WIN_RESIZE window flag so canvas apps reflow.
The legacy bridge
The older family apps implement a small shared ABI in pc64/uno_app.h. Each exports one entry that
returns a vtable of callbacks; the kernel dispatches purely through the pointers, with no per-app
switch.
/* pc64/uno_app.h -- the legacy shared ABI (for bridged apps) */
typedef struct AppInterface {
void (*draw)(UnoWin *w);
Boolean (*key)(char ch, short code, Boolean cmd);
void (*click)(UnoWin *w, Point p);
void (*tick)(void);
void (*opened)(void);
void (*closed)(void);
const char *win_title;
short win_rect[4];
} AppInterface;
/* every legacy app module exports exactly one entry: */
const AppInterface *uno_app_main(const KernelApi *k); /* UNO_APP_ENTRY_NAME */
On pc64 these apps are handed a KernelApi callback table (drawing primitives, a FAT reader, and the
music engine) and hosted inside a canvas, so they run unchanged inside the modern desktop.