UnoDOS pc64

Sample programs

Complete, working programs to read, run and take apart. Every one of them ships on the device in SDK\ - open it in Studio, press Ctrl-B to build and Ctrl-R to run. Each sample earns its place by teaching something the one before it didn't.

SampleLanguageWhat it teaches
SAMPLE.C / SAMPLE.PYUnoC / Python The app skeleton: the vtable / the uno.App class, drawing, keys, ticks.
TIMER.CUnoCReal timekeeping, number formatting without printf, sound, deliberate repainting.
LIFE.CUnoCArrays and computation, randomness, pacing a simulation.
TODO.PYPythonFiles that survive reboots, both kinds of key, the idle idiom.
CHART.PYPythonParsing a file, scaling to the canvas, graceful fallbacks.
GOODNITE.PYPythonAutomation under the permission model - three calls at three tiers.
DOSTRIS.CUnoCA complete shipped game, unchanged - the big worked example.
bench_snapshot.pyPython, on your PCA genuinely scheduled task: your PC's scheduler drives the box over the remote link.

TIMER.C - a kitchen timer

The natural second program after SAMPLE.C, because it forces the three habits a real UnoC app needs. The frame is the timebase - count your own ticks. tick() arrives once per shell frame, ~60 a second, and the timer counts those calls; pausing works by simply not counting. It deliberately does not use TickCount() - on pc64 that is a call counter shared by every app, not a clock. Repaint deliberately: a tick-driven app calls repaint_all(), and only when the displayed state changed - once a second here, not sixty times. And numbers without printf: UnoC has no varargs, so put2() composes the MM:SS readout and fmt_u() does general unsigned decimal.

Also in there: one-shot notes with music_open_chan() + music_note_on() for the alarm, and a countdown that rounds up (one remaining tick still reads 0:01) while the stopwatch rounds down - the small honesty every clock UI owes its user.

TIMER.C mid-countdown: 4:56 left, the bar draining, and the hint line switched from start to pause. The window title is the app's own win_title.
/* TIMER.C - a kitchen timer and stopwatch for the UnoDOS desktop.
 *
 * Space starts and pauses.  + and - (or the Up/Down arrows) dial the
 * countdown a minute at a time, M switches between countdown and
 * stopwatch, N resets.  When the countdown reaches zero the window
 * flashes and beeps until you press any key.
 *
 * What it demonstrates, beyond SAMPLE.C: keeping time by counting your
 * own tick() calls (the shell ticks you ~60 times a second - and that,
 * not TickCount(), is the platform's timebase: TickCount() counts CALLS,
 * shared by every app, so pace on your own frames), formatting numbers
 * with put2() - there is no printf - playing notes with music_note_on(),
 * and the repaint_all() redraw idiom.  Build with Ctrl-B, run with
 * Ctrl-R.
 */
#include "UNO.H"

#define BAR_W     240
#define MAX_MIN   95
#define START_T   18000              /* 5 minutes, in 60 Hz frames */
#define CAP_T     359940             /* put2 shows 2 digits: 99:59 */

static long  set_frames = START_T;   /* the dialled countdown duration */
static long  left       = START_T;   /* countdown frames remaining     */
static long  elapsed;                /* stopwatch frames accumulated   */
static Boolean running   = false;
static Boolean stopwatch  = false;   /* false = countdown mode         */
static long  ring_frames;            /* nonzero while the alarm rings  */
static long  beep_in;                /* frames until the next beep     */
static long  shown = -1;             /* last second painted            */
static Boolean flash;

static const GameRGB kFace   = {  24,  26,  40, C_BLUE };
static const GameRGB kRing   = { 200,  40,  40, C_MAG  };
static const GameRGB kBarOn  = {  90, 210, 120, C_CYAN };
static const GameRGB kBarOff = {  46,  50,  72, C_BLUE };

static void timer_draw(UnoWin *w)
{
    Rect r;
    char t[8];
    short x0 = w->bounds.left + 14;
    short y0 = w->bounds.top + TBAR_H + 10;
    long frames_now = stopwatch ? elapsed : left;
    /* a countdown rounds up (1 frame left still reads 0:01); a stopwatch
     * rounds down, like every stopwatch you have ever held */
    long secs = stopwatch ? frames_now / 60 : (frames_now + 59) / 60;
    short fill;

    SetRect(&r, x0 - 4, y0 - 4, x0 + BAR_W + 4, y0 + 96);
    fill_rgb(&r, (ring_frames && flash) ? &kRing : &kFace);
    uno_box(&r, C_WHITE);

    text_at(x0, y0 + 6, stopwatch ? "STOPWATCH" : "COUNTDOWN",
            C_CYAN, C_BLUE, false);
    put2(secs / 60, t);
    t[2] = ':';
    put2(secs % 60, t + 3);
    text_at(x0 + 170, y0 + 6, t, C_WHITE, C_BLUE, false);

    /* the bar drains as the countdown runs */
    SetRect(&r, x0, y0 + 28, x0 + BAR_W, y0 + 44);
    fill_rgb(&r, &kBarOff);
    if (!stopwatch && set_frames > 0) {
        fill = (short)((long)BAR_W * (left / 60) / (set_frames / 60));
        if (fill > 0) {
            SetRect(&r, x0, y0 + 28, x0 + fill, y0 + 44);
            fill_rgb(&r, &kBarOn);
        }
    }

    text_at(x0, y0 + 56, running ? "Space: pause" : "Space: start",
            C_WHITE, C_BLUE, false);
    text_at(x0 + 110, y0 + 56, "M: mode   N: reset", C_MAG, C_BLUE, false);
    text_at(x0, y0 + 74, "+ / - or Up / Down: dial the minutes",
            C_CYAN, C_BLUE, false);
}

static void timer_reset(void)
{
    running = false;
    left = set_frames;
    elapsed = 0;
    shown = -1;
}

static Boolean timer_key(char ch, short code, Boolean cmd)
{
    if (cmd) return false;
    if (ring_frames) {                /* any key silences the alarm */
        ring_frames = 0;
        music_quiet();
        repaint_all();
        return true;
    }
    if (ch == ' ') {
        running = !running;
        repaint_all();
        return true;
    }
    if (ch == 'n' || ch == 'N') {
        timer_reset();
        repaint_all();
        return true;
    }
    if (ch == 'm' || ch == 'M') {
        stopwatch = !stopwatch;
        timer_reset();
        repaint_all();
        return true;
    }
    if (ch == '+' || ch == '=' || ch == 0x1E) {        /* Up arrow */
        if (set_frames < (long)MAX_MIN * 60 * 60)
            set_frames += 60 * 60;
        if (!running) left = set_frames;
        repaint_all();
        return true;
    }
    if (ch == '-' || ch == 0x1F) {                     /* Down arrow */
        if (set_frames > 60 * 60)
            set_frames -= 60 * 60;
        if (!running) left = set_frames;
        repaint_all();
        return true;
    }
    (void)code;
    return false;
}

/* tick() arrives once per shell frame, ~60/s: the frame IS the clock.
 * Pausing works by not counting - a paused timer simply lets frames by. */
static void timer_tick(void)
{
    long secs;

    if (ring_frames) {
        ring_frames--;
        if (!ring_frames) {           /* rang long enough on its own */
            music_quiet();
            repaint_all();
            return;
        }
        if (--beep_in <= 0) {
            music_note_on(96, 9);
            beep_in = 24;
        }
        if (((ring_frames / 12) & 1) != (long)flash) {
            flash = (Boolean)((ring_frames / 12) & 1);
            repaint_all();
        }
        return;
    }

    if (!running) return;

    if (stopwatch) {
        if (elapsed < CAP_T) elapsed++;
        secs = elapsed / 60;
    } else {
        left--;
        if (left <= 0) {
            left = 0;
            running = false;
            ring_frames = 300;        /* ring for five seconds */
            beep_in = 1;
            repaint_all();
            return;
        }
        secs = (left + 59) / 60;
    }
    if (secs != shown) {              /* repaint once a second, not 60x */
        shown = secs;
        repaint_all();
    }
}

static void timer_opened(void)
{
    music_open_chan();
}

static const AppInterface kIface = {
    timer_draw, timer_key, 0, timer_tick, timer_opened, 0,
    "Timer", { 40, 40, 328, 194 }
};

const AppInterface *uno_app_main(const KernelApi *k)
{
    gK = k;
    return &kIface;
}

LIFE.C - Conway's Game of Life

Real computation in UnoC: 2-D unsigned char arrays, a double buffer flipped with one memcpy, and a wrapping neighbourhood via modular arithmetic. Three details worth stealing: the simulation is paced (a generation every 6 frames, counted by the app itself - the shell stays responsive and the motion reads as motion); the draw paints only what is alive over one background fill; and the board seeds defensively, stirring the cell coordinates into Random() so even a weak generator yields a live board. Note opened() can run again after a close and reopen - the board seeds only once.

LIFE.C after a minute: 66 generations in, 209 cells alive. Cells that have survived a while cool from green to blue.
/* LIFE.C - Conway's Game of Life.
 *
 * The classic cellular automaton on a wrapping grid.  Space pauses,
 * S steps one generation while paused, N reseeds the board, C clears it.
 * Cells that survive a while cool from green to blue.
 *
 * What it demonstrates: 2-D arrays and real computation in UnoC, seeding
 * randomness with Random(), pacing a simulation by counting tick() calls
 * (the shell's ~60 Hz frame is the platform's timebase), and painting
 * only what is alive instead of every cell.  Build with Ctrl-B, run with
 * Ctrl-R.
 */
#include "UNO.H"

#define COLS 44
#define ROWS 30
#define CELL 6

static unsigned char g[ROWS][COLS];   /* 0 dead, else generations alive */
static unsigned char nx[ROWS][COLS];
static Boolean running = true;
static Boolean seeded  = false;
static long gen;
static long pop;
static short acc;                     /* frames since the last step */

static const GameRGB kBack  = {  12,  14,  24, C_BLUE };
static const GameRGB kYoung = { 120, 220, 140, C_CYAN };
static const GameRGB kOld   = {  70, 140, 220, C_CYAN };

static void life_seed(void)
{
    short r, c;
    pop = 0;
    for (r = 0; r < ROWS; r++)
        for (c = 0; c < COLS; c++) {
            /* stir the coordinates in so even a weak Random() seeds life */
            short v = (short)(((Random() + r * 31 + c * 17) & 0x7FFF) % 100);
            g[r][c] = (unsigned char)(v < 30 ? 1 : 0);
            if (g[r][c]) pop++;
        }
    gen = 0;
}

static short life_neigh(short r, short c)
{
    short dr, dc, n = 0;
    for (dr = -1; dr <= 1; dr++)
        for (dc = -1; dc <= 1; dc++) {
            short rr, cc;
            if (dr == 0 && dc == 0) continue;
            rr = (short)((r + dr + ROWS) % ROWS);   /* the grid wraps */
            cc = (short)((c + dc + COLS) % COLS);
            if (g[rr][cc]) n++;
        }
    return n;
}

static void life_step(void)
{
    short r, c, n;
    pop = 0;
    for (r = 0; r < ROWS; r++)
        for (c = 0; c < COLS; c++) {
            n = life_neigh(r, c);
            if (g[r][c])
                nx[r][c] = (unsigned char)
                    ((n == 2 || n == 3) ? (g[r][c] < 200 ? g[r][c] + 1 : 200) : 0);
            else
                nx[r][c] = (unsigned char)(n == 3 ? 1 : 0);
            if (nx[r][c]) pop++;
        }
    memcpy(g, nx, sizeof(g));
    gen++;
}

static void life_draw(UnoWin *w)
{
    Rect f, q;
    short r, c, x, y;
    char num[16];
    short x0 = w->bounds.left + 8;
    short y0 = w->bounds.top + TBAR_H + 6;
    short by = (short)(y0 + ROWS * CELL);

    SetRect(&f, x0 - 2, y0 - 2, x0 + COLS * CELL + 2, by + 2);
    uno_box(&f, C_WHITE);
    SetRect(&f, x0, y0, x0 + COLS * CELL, by);
    fill_rgb(&f, &kBack);

    for (r = 0; r < ROWS; r++)
        for (c = 0; c < COLS; c++)
            if (g[r][c]) {
                x = (short)(x0 + c * CELL);
                y = (short)(y0 + r * CELL);
                SetRect(&q, x, y, x + CELL - 1, y + CELL - 1);
                fill_rgb(&q, g[r][c] > 4 ? &kOld : &kYoung);
            }

    /* Below the field the background is the window's own light chrome, so
     * these draw in BLUE - C_WHITE is invisible off the dark playfield. */
    text_at(x0, by + 8, "Gen:", C_CYAN, C_BLUE, false);
    fmt_u(gen, num);
    text_at(x0 + 38, by + 8, num, C_BLUE, C_BLUE, false);
    text_at(x0 + 100, by + 8, "Pop:", C_CYAN, C_BLUE, false);
    fmt_u(pop, num);
    text_at(x0 + 138, by + 8, num, C_BLUE, C_BLUE, false);
    text_at(x0, by + 24,
            running ? "Space: pause   N: reseed   C: clear"
                    : "PAUSED - Space runs, S steps once",
            C_MAG, C_BLUE, false);
}

static Boolean life_key(char ch, short code, Boolean cmd)
{
    if (cmd) return false;
    if (ch == ' ') {
        running = !running;
        repaint_all();
        return true;
    }
    if (ch == 's' || ch == 'S') {
        running = false;
        life_step();
        repaint_all();
        return true;
    }
    if (ch == 'n' || ch == 'N') {
        life_seed();
        repaint_all();
        return true;
    }
    if (ch == 'c' || ch == 'C') {
        memset(g, 0, sizeof(g));
        gen = 0;
        pop = 0;
        running = false;
        repaint_all();
        return true;
    }
    (void)code;
    return false;
}

/* tick() arrives once per shell frame, ~60/s - count frames yourself
 * rather than trusting TickCount(), which counts calls, not time. */
static void life_tick(void)
{
    if (!running) return;
    if (++acc < 6) return;                /* ~10 generations a second */
    acc = 0;
    life_step();
    repaint_all();
}

static void life_opened(void)
{
    if (!seeded) {                        /* opened() can run again after a
                                           * close/reopen - keep the board */
        life_seed();
        seeded = true;
    }
}

static const AppInterface kIface = {
    life_draw, life_key, 0, life_tick, life_opened, 0,
    "Life", { 30, 30, 320, 280 }
};

const AppInterface *uno_app_main(const KernelApi *k)
{
    gK = k;
    return &kIface;
}

TODO.PY - a to-do list that survives reboots

Persistence is two calls: uno.read() returns None if the file doesn't exist (a fine first run), and uno.write() returns False on a read-only volume - the app shows that state instead of hiding it. The format is plain text ([x] task / [ ] task), so Notepad can edit the same file. The key() handler routes both kinds of key - printable characters arrive in uni, special keys in scan - and tick() returns False whenever nothing changed, so a static window costs the machine nothing.

TODO.PY with two tasks typed in and one checked off - and the green line confirming the list reached the disk.
# TODO.PY - a to-do list that survives reboots.
#
# Type a task and press Enter to add it.  Up/Down pick a task, Tab checks
# it off, Delete removes it, Esc clears what you were typing.  The list is
# written to TODO.TXT on the boot volume after every change and loaded
# back the next time the app opens - it is plain text, so Notepad can read
# and edit the same file.
#
# What it demonstrates, beyond SAMPLE.PY: persistence with uno.read() and
# uno.write(), telling printable keys (uni) from special keys (scan) apart,
# and the idle idiom - tick() returns False whenever nothing changed, so
# the shell doesn't repaint a static window 60 times a second.

import uno

FILE = "TODO.TXT"

BG   = uno.rgb(20, 22, 36)
INK  = uno.rgb(214, 218, 232)
DIM  = uno.rgb(120, 126, 150)
HEAD = uno.rgb(120, 200, 255)
GOOD = uno.rgb(110, 210, 130)
BAD  = uno.rgb(240, 120, 120)
SELB = uno.rgb(44, 50, 80)


class Todo(uno.App):

    def build(self, cv):
        self.items = []              # [done, text] pairs
        self.sel = 0
        self.edit = ""
        self.saved = True
        self.dirty = True
        data = uno.read(FILE)        # None if the file doesn't exist yet
        if data:
            for ln in data.decode("latin1").split("\n"):
                ln = ln.strip()
                if ln.startswith("[x] "):
                    self.items.append([True, ln[4:]])
                elif ln.startswith("[ ] "):
                    self.items.append([False, ln[4:]])

    def save(self):
        out = ""
        for done, text in self.items:
            out += ("[x] " if done else "[ ] ") + text + "\n"
        # write() returns False on a read-only volume - show it, don't hide it
        self.saved = uno.write(FILE, out.encode())

    def key(self, uni, scan, ctrl):
        if scan == 1 and self.sel > 0:                    # Up
            self.sel -= 1
        elif scan == 2 and self.sel + 1 < len(self.items):  # Down
            self.sel += 1
        elif scan == 8 and self.items:                    # Delete: remove
            self.items.pop(self.sel)
            if self.sel and self.sel >= len(self.items):
                self.sel -= 1
            self.save()
        elif scan == 0x17:                                # Esc: drop the entry
            self.edit = ""
        elif uni == 13 and self.edit:                     # Enter: add
            self.items.append([False, self.edit])
            self.sel = len(self.items) - 1
            self.edit = ""
            self.save()
        elif uni == 9 and self.items:                     # Tab: toggle done
            self.items[self.sel][0] = not self.items[self.sel][0]
            self.save()
        elif uni == 8:                                    # Backspace
            self.edit = self.edit[:-1]
        elif 32 <= uni < 127 and len(self.edit) < 48:     # typing
            self.edit += chr(uni)
        else:
            return False
        self.dirty = True
        return True

    def tick(self):
        if self.dirty:
            self.dirty = False
            return None              # repaint this frame...
        return False                 # ...and sleep the rest of the time

    def draw(self, cv):
        cv.clear(BG)
        done = 0
        for it in self.items:
            if it[0]:
                done += 1
        cv.text(10, 8, "TO-DO", HEAD)
        cv.text(cv.width() - 110, 8, "%d of %d done" % (done, len(self.items)), DIM)
        cv.hline(10, 24, cv.width() - 20, DIM)

        # the list (the last 14 entries fit; older ones scroll away)
        shown = self.items[-14:]
        base = len(self.items) - len(shown)
        y = 32
        for i in range(len(shown)):
            if base + i == self.sel:
                cv.fill_rect(6, y - 2, cv.width() - 12, 15, SELB)
            mark, text = shown[i][0], shown[i][1]
            cv.text(10, y, "[x]" if mark else "[ ]", GOOD if mark else DIM)
            cv.text(42, y, text, DIM if mark else INK)
            y += 16
        if not self.items:
            cv.text(42, 40, "nothing yet - type a task, press Enter", DIM)

        # the entry line and the save state
        y = cv.height() - 44
        cv.hline(10, y, cv.width() - 20, DIM)
        cv.text(10, y + 6, "> " + self.edit + "_", INK)
        cv.text(10, y + 22, "Enter add   Tab done   Del remove", DIM)
        cv.text(10, y + 34,
                "saved to " + FILE if self.saved else "NOT SAVED - volume is read-only",
                GOOD if self.saved else BAD)


app = Todo()

CHART.PY - a bar chart from a file

A small, genuinely useful tool: drop a DATA.CSV on the boot volume - one label,value per line - and it draws the chart, scaled to fit, with the biggest bar called out. Press R after editing the file in Notepad. It parses without a csv module (there isn't one): split(","), strip(), and a try/except ValueError that makes header lines and junk simply vanish. Everything is scaled from cv.width()/cv.height() with integer math - never hardcode the canvas size, the shell picks your window. With no file it charts built-in demo data and says so, so the first launch teaches you what to do next.

CHART.PY on a machine with no DATA.CSV: it charts its built-in demo data and says so in the header, rather than showing an empty plot.
# CHART.PY - a bar chart drawn from a text file.
#
# Put a DATA.CSV on the boot volume - one "label,value" line each, like
#
#     MON,12
#     TUE,19
#     WED,7
#
# and this app charts it, scaled to fit the window, with the biggest bar
# called out.  Edit the file in Notepad and press R here to reload it.
# With no file it charts built-in demo data so you can see the shape first.
#
# What it demonstrates: reading and parsing a file with uno.read(), integer
# scaling to a variable-size canvas (read cv.width()/height(), never
# hardcode - the shell picks your window size), and a static app that only
# repaints when its data changes.

import uno

FILE = "DATA.CSV"

BG   = uno.rgb(18, 20, 34)
INK  = uno.rgb(214, 218, 232)
DIM  = uno.rgb(110, 116, 140)
BAR  = uno.rgb(96, 170, 255)
TOP  = uno.rgb(255, 170, 90)

DEMO = [["MON", 12], ["TUE", 19], ["WED", 7], ["THU", 23],
        ["FRI", 17], ["SAT", 4], ["SUN", 9]]


class Chart(uno.App):

    def build(self, cv):
        self.load()

    def load(self):
        self.rows = []
        self.demo = False
        data = uno.read(FILE)
        if data:
            for ln in data.decode("latin1").split("\n"):
                parts = ln.strip().split(",")
                if len(parts) < 2:
                    continue
                try:
                    v = int(parts[1].strip())
                except ValueError:
                    continue             # a header line, or junk - skip it
                self.rows.append([parts[0].strip(), v if v > 0 else 0])
                if len(self.rows) == 16:  # more bars than that won't fit
                    break
        if not self.rows:
            self.rows = DEMO
            self.demo = True
        self.dirty = True

    def key(self, uni, scan, ctrl):
        if uni in (114, 82):             # r / R: reload the file
            self.load()
            return True
        return False

    def tick(self):
        if self.dirty:
            self.dirty = False
            return None
        return False

    def draw(self, cv):
        cv.clear(BG)
        head = ("%s not found - demo data (R reloads)" % FILE) if self.demo else FILE
        cv.text(10, 8, head, DIM if self.demo else INK)

        x0, y0 = 34, 30                  # plot origin (top-left)
        ph = cv.height() - y0 - 34       # plot height
        pw = cv.width() - x0 - 10
        base = y0 + ph                   # the x axis
        mx = 1
        for r in self.rows:
            if r[1] > mx:
                mx = r[1]

        cv.vline(x0, y0, ph, DIM)
        cv.hline(x0, base, pw, DIM)
        cv.text(4, y0 - 4, "%d" % mx, DIM)
        cv.text(4, base - 10, "0", DIM)

        n = len(self.rows)
        step = pw // n
        bw = step - 6 if step > 12 else step - 2
        for i in range(n):
            label, v = self.rows[i][0], self.rows[i][1]
            h = v * ph // mx
            x = x0 + 4 + i * step
            cv.fill_rect(x, base - h, bw, h, TOP if v == mx else BAR)
            cv.text(x, base - h - 12, "%d" % v, INK)
            cv.text(x, base + 6, label[:5], DIM)


app = Chart()

GOODNITE.PY - an end-of-day automation app

The bridge from apps to automation: one launch journals what you were working on, then offers to shut the machine down. It is the permission model made concrete - three calls at three tiers:

CallCapabilityTier
u.ui.screen() - read the window treeui.read0, ambient
u.fs.write("journal/...") - into your homefs.user 1
u.sys.power(0) - shut downpower2, consent

Two things to take away. Denial is not an error state: denied calls raise OSError (or u.request() answers False), the app handles both, and the same script runs everywhere - it just does less on a machine that trusts it less. And the generator/tick pattern is load-bearing: an injected action lands on the next shell frame, so an automation script yields between step and check, pacing those yields on unoauto.uptime() - the wall clock in milliseconds, and the only real clock an app can read. For unattended runs, a signed manifest (.MFT sidecar) grants the declared capabilities at launch, no prompts.

GOODNITE.PY with nobody signed in: the tier-0 window read works, and the two capabilities it was not granted are reported rather than raised.
# GOODNITE.PY - an end-of-day automation app.
#
# One launch journals what you were working on, then offers to shut the
# machine down.  It is a worked example of `unoscript`, the production
# automation surface, doing real work under the permission model:
#
#   - reading the window tree     u.ui.screen()      tier 0, ambient
#   - writing into your home      u.fs.write()       tier 1, fs.user
#   - powering the machine off    u.sys.power(0)     tier 2, `power` -
#                                 granted only through an explicit consent
#                                 (or a role, or a signed manifest)
#
# A capability you don't hold never crashes the app: denied calls raise
# OSError, and u.request() simply answers False.  Handle both and the same
# script runs everywhere - it just does less on a machine that trusts it
# less.
#
# The steps run from a generator paced by tick() on unoauto.uptime(),
# the wall clock in milliseconds (ungated, real time in every build).
# That pattern is load-bearing for every automation app: an injected
# action lands on the NEXT shell frame, so a script must yield before it
# looks at the result.

import uno
import unoauto
import unoscript as u

JOURNAL = "journal/GOODNITE.LOG"
KEEP = 3000              # stay under unoscript's 4 KB read cap

BG   = uno.rgb(16, 18, 34)
HEAD = uno.rgb(120, 200, 255)
INK  = uno.rgb(214, 218, 232)
WARN = uno.rgb(255, 170, 90)


class GoodNight(uno.App):

    def build(self, cv):
        self.lines = []
        self.armed = False
        self.until = 0
        self.gen = self.script()
        self.dirty = True

    def say(self, s):
        self.lines.append(s)
        self.dirty = True

    # ---- the workflow; each yield is a pause in milliseconds -------------
    def script(self):
        uid = u.whoami()                 # 0xFFFFFFFF when nobody is signed in
        self.say("Good night." if uid in (-1, 0xFFFFFFFF)
                 else "Good night, user %d." % uid)
        yield 500

        # 1. what is open right now?  (ui.read - ambient, tier 0)
        try:
            wins = [ln.strip() for ln in u.ui.screen().split("\n") if ln.strip()]
        except OSError:
            wins = []
        self.say("open right now: %d window(s)" % len(wins))
        for w in wins[:6]:
            self.say("  " + w[:44])
        yield 1000

        # 2. journal it into your home  (fs.user, tier 1)
        entry = "-- session, uptime %d min, %d window(s)\n" % (
            unoauto.uptime() // 60000, len(wins))
        for w in wins[:6]:
            entry += "   " + w[:60] + "\n"
        journaled = False
        try:
            try:
                old = u.fs.read(JOURNAL).decode("latin1")
            except OSError:
                old = ""                     # first run: no journal yet
            u.fs.write(JOURNAL, (old + entry)[-KEEP:].encode())
            self.say("journaled to home:" + JOURNAL)
            journaled = True
        except (OSError, NotImplementedError):
            self.say("journal skipped - fs.user not granted")
        yield 500

        # 3. ask permission to power off  (power, tier 2)
        if u.request("power"):
            self.say("")
            self.say("Enter shuts the machine down.  Esc keeps it on.")
            self.armed = True
        else:
            self.say("")
            self.say("`power` not granted - leaving the machine on.")
            if journaled:
                self.say("(journal is written; close me when you're done)")

    def tick(self):
        if self.gen:
            now = unoauto.uptime()       # wall-clock milliseconds
            if now >= self.until:
                try:
                    self.until = now + next(self.gen)
                except StopIteration:
                    self.gen = None
        if self.dirty:
            self.dirty = False
            return None
        return False

    def key(self, uni, scan, ctrl):
        if self.armed and uni == 13:         # Enter: do it
            self.say("shutting down...")
            u.sys.power(0)                   # does not return when granted
            return True
        if self.armed and scan == 0x17:      # Esc: stand down
            self.armed = False
            self.say("cancelled - staying on.")
            return True
        return False

    def draw(self, cv):
        cv.clear(BG)
        cv.text(10, 8, "GOODNITE - end-of-day automation", HEAD)
        cv.hline(10, 24, cv.width() - 20, uno.rgb(60, 66, 96))
        y = 32
        for s in self.lines[-14:]:
            cv.text(10, y, s, WARN if self.armed and s.startswith("Enter") else INK)
            y += 16


app = GoodNight()

bench_snapshot.py - a genuinely scheduled task

There is no cron on the device - deliberately: pc64 has no preemptive scheduler, and background scripts are a reserved capability for a facility that doesn't exist yet. The honest scheduled-task story is your PC schedules, the box answers. Task Scheduler or cron runs this script nightly; it waits for the box to dial in over the remote link, authenticates with the Remote-control code, appends one CSV row of uptime/heap/fs/net counters, saves a screenshot, and exits. A month later the folder answers "when did that start?" for free.

Arm only Watch on the device - the script only observes, so give it only that - and put the 6-digit code in UNO_PIN on the PC. The standing caveat applies: the link is plaintext and LAN-only by intent; the code proves who may drive the box, it does not encrypt the wire.

#!/usr/bin/env python3
"""bench_snapshot.py - a scheduled health snapshot of a UnoDOS machine.

Run this from your PC's scheduler each night.  It waits for the box to
dial in over the URC link, authenticates, and files one snapshot: a
screenshot, plus a CSV row of uptime, heap, filesystem and network
counters.  A month later you have a folder that answers "when did that
start" for free.

Everything it needs ships with UnoDOS: tools/unoauto_remote.py is the
client library - keep this script next to it, or point PYTHONPATH at it.

On the machine: Control Panel -> Remote control..., tick Watch (and
nothing else - this script only observes), note the 6-digit code, and
choose discover so the box finds this PC by broadcast.  Put the code in
the UNO_PIN environment variable on the PC.  Debug builds skip the code.

Schedule it:
  Windows:  schtasks /Create /TN UnoSnapshot /SC DAILY /ST 23:00 ^
            /TR "py C:\\unodos\\pc64\\tools\\bench_snapshot.py"
  Linux:    0 23 * * *  python3 /home/you/unodos/pc64/tools/bench_snapshot.py
"""
import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from unoauto_remote import UnoAutoLink

PIN = os.environ.get("UNO_PIN", "")
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "snapshots")
CSV = os.path.join(OUT, "health.csv")


def main():
    os.makedirs(OUT, exist_ok=True)
    stamp = time.strftime("%Y%m%d-%H%M%S")

    link = UnoAutoLink(port=5099)
    link.listen()                          # the box dials us
    if not link.wait_connected(timeout=300):
        print("no box dialled in - is it on, and is discover armed?")
        return 1
    link.wait_hello(timeout=30)            # booted and draining its link
    if PIN:                                # production builds authenticate
        link.command("auth", PIN)

    # one CSV row of the numbers that drift
    row = {"uptime_s": link.uptime() // 1000}
    for p in link.probe():                 # kind 2 = subsystem rows
        if p["kind"] == 2 and p["name"] in ("heap", "fs", "net"):
            row[p["name"] + "_v1"] = p["v1"]
            row[p["name"] + "_v2"] = p["v2"]
    new = not os.path.exists(CSV)
    keys = sorted(row)
    with open(CSV, "a") as f:
        if new:
            f.write("stamp," + ",".join(keys) + "\n")
        f.write(stamp + "," + ",".join(str(row[k]) for k in keys) + "\n")

    # and one screenshot, as a portable PPM
    w, h, rgba = link.screen_grab()
    shot = os.path.join(OUT, stamp + ".ppm")
    rgb = bytearray()
    for i in range(0, len(rgba), 4):
        rgb += rgba[i:i + 3]
    with open(shot, "wb") as f:
        f.write(b"P6\n%d %d\n255\n" % (w, h))
        f.write(bytes(rgb))

    link.close()
    print("snapshot %s: %dx%d screen, %s" % (stamp, w, h,
          ", ".join("%s=%s" % (k, row[k]) for k in keys)))
    return 0


if __name__ == "__main__":
    sys.exit(main())
The scheduler is your PC'sSchedule it with the tools your PC already has: schtasks /Create /TN UnoSnapshot /SC DAILY /ST 23:00 /TR "py ...\bench_snapshot.py" on Windows, or 0 23 * * * python3 .../bench_snapshot.py in a crontab.

DOSTRIS.C and the shipped sources

SDK\DOSTRIS.C is the shipped game, ported to UnoC unchanged - the big worked example for game state, a piece table in nested initializers, line clearing and a looping gm_start song. SDK\DUUM.PY is the whole Doom-style engine in Python, streaming a real WAD with uno.read_at. Both reward reading with the C and Python references open beside them.

Next: Writing apps for the app model these all share, and the UnoC SDK reference / Python SDK reference for every call they make.