UnoDOS pc64

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.

Start with StudioThe friendliest way to write an app is Studio, the built-in IDE: edit, press Ctrl+B to compile it to a .UNO, and Ctrl+R to run it - no PC or toolchain needed.

1. Pick a language

UnoCPython
Runsnative x86-64, compiled on the device by Studioon the PYRT runtime (MicroPython)
Feels likeclassic C against a Toolbox-style APImodern Python with a small, flat uno module
Best forgames and tools that want every cyclealmost everything else - less ceremony, floats, exceptions, big ints
Watch out forno 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 in draw(); 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 - in closed(), 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->bounds plus TBAR_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() from tick() when something moved. Python: return False from tick() 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 .UNO modules 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_u or 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

LimitValue
Source file (Studio editor)192 KB
Built .UNO (Studio)256 KB
UnoC user-app slot512 KB, one resident app
Python heap16 MB
Function parameters/arguments (UnoC)8
App descriptor1024 bytes; id 15 chars, name 31
Icon32x32 QOI, 16 KB file, 12 custom slots system-wide
Installed apps48 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 AppInterface app 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.