UnoDOS pc64

Python SDK reference

Every call a Python app can make, with parameters, return values, remarks and a working snippet for each. The app model itself is on Python apps; the machine-readable stub ships on the device as SDK\uno.pyi.

The application object

A Python app is one .py file that defines a class and a module-global instance named exactly app. The runtime (PYRT.UNO) executes your file top to bottom, looks up app, and binds whichever of the six callbacks you defined. There is no event loop to write and no main(): the desktop calls you.

import uno

class MyApp(uno.App):

    def build(self, cv):        # once, when the window opens
        self.w, self.h = cv.width(), cv.height()

    def draw(self, cv):         # every repaint
        cv.clear(uno.rgb(16, 18, 34))
        cv.text(8, 8, "hello", uno.rgb(214, 218, 232))

    def tick(self):             # ~60 times a second
        return False            # nothing changed - let the shell sleep

    def key(self, uni, scan, ctrl):
        return False            # not ours - let the shell have it

app = MyApp()                   # the runtime looks for this exact name
Bound at loadThe binding happens once, at load. Adding or swapping a method on the instance afterwards has no effect, and if the name app is missing the run fails with No `app` object. Define: app = MyApp() in Studio's output pane.

Callbacks

CallbackWhen it runsReturn
build(self, cv)Once, when the window opens. The canvas is already valid, so cv.width()/cv.height() give your drawable size - read them here, never hardcode a size (the shell picks your window).ignored
draw(self, cv)Every repaint.ignored
tick(self)~60 times a second while the app is open.None (or nothing) repaints; an explicit falsey value such as False skips the repaint - the idle idiom below.
key(self, uni, scan, ctrl)A key was pressed while your window is focused. uni is the character's codepoint (0 if none), scan the special-key code (0 if none), ctrl nonzero while Ctrl is held. Exactly one of uni/scan is nonzero per event.truthy = you consumed it
action(self, id)A toolkit widget action (only relevant to apps hosted with widgets).truthy = consumed
opened(self) / closed(self)The window was shown / is closing. Only bound if they exist at load.ignored

Key codes

Keyscanuni
Up / Down / Right / Left1 / 2 / 3 / 40
Delete (forward)80
F1..F120x0B..0x160
Esc0x170
Enter / Backspace / Tab / Space013 / 8 / 9 / 32
Letters, digits, symbols0the ASCII code, shift-aware

Keys the shell owns never reach you: Ctrl+Esc, Alt+Tab, Ctrl+W, Ctrl+M, Ctrl+Tab, F2, Alt+arrows, and Esc while a popup or fullscreen app is up.

The idle idiom

An app that returns None from tick() repaints sixty times a second whether anything changed or not. A static app should keep a dirty flag instead:

def key(self, uni, scan, ctrl):
    ...change something...
    self.dirty = True
    return True

def tick(self):
    if self.dirty:
        self.dirty = False
        return None                 # repaint this frame
    return False                    # idle - skip the repaint

Canvas

The drawing surface handed to build and draw. Coordinates are canvas-relative - (0, 0) is the top-left of your content area, below the title bar - and draws are clipped to your window. All coordinates must be ints: pass int(x) if you animate with floats, or the call raises TypeError.

MethodEffect
cv.width() / cv.height()Drawable size in pixels. Read it every draw if you want live resize behaviour.
cv.clear(color)Fill the whole canvas.
cv.fill_rect(x, y, w, h, color)Filled rectangle.
cv.rect(x, y, w, h, color)1-pixel outline.
cv.pixel(x, y, color)One pixel.
cv.hline(x, y, w, color) / cv.vline(x, y, h, color)Horizontal / vertical line.
cv.text(x, y, s, color)Draw a string, transparent background.

The remaining Canvas methods (wall_col, wall_span, mask_span, flat_span, duum_frame, seg_cols) are the textured-column fast paths built for Duum's renderer; their exact contracts are in SDK\uno.pyi.

uno.rgb

uno.rgb(r, g, b) -> int packs three 0-255 channels into the colour value every drawing call takes. Build your palette once at module level, not per frame.

RED = uno.rgb(255, 96, 96)      # channels are 0-255, masked & 0xFF
cv.fill_rect(10, 10, 40, 40, RED)

Time and held keys

uno.ticks() -> int - the Toolbox tick counter. Honesty first: this is a call counter shared with every C app (it advances once per call, from anywhere), so treat it as frame pacing only - call it exactly once per tick() and use deltas - or skip it and count your own tick() invocations. For real time, use unoauto.uptime(): milliseconds since boot, ungated in every build - the clock the automation samples pace on.

import uno, unoauto

def build(self, cv):
    self.frames = 0                     # your own frame counter...
    self.t0 = unoauto.uptime()          # ...and the real clock, in ms

def tick(self):
    self.frames += 1                    # ~60/s: fine for animation pacing
    elapsed_s = (unoauto.uptime() - self.t0) // 1000   # honest seconds

uno.keys_down() -> int - the navigation/action keys held right now, as a bitmask: 1 Up, 2 Down, 4 Right, 8 Left, 16 fire (F or Ctrl), 32 use (Space/E), 64 comma, 128 period. For movement, polling this beats buffering key() events.

held = uno.keys_down()
if held & 1:  self.y -= 2           # Up is held right now
if held & 4:  self.x += 2           # Right
# 0 on the firmware input path (no key-up events there):
# fall back to timing out your own key() events, like Duum does.

Sound

uno.beep(midi, ticks) plays a square-wave note at a MIDI pitch (60 = middle C, 69 = A440) for ticks sixtieths of a second; uno.quiet() stops it. These two always work, on any machine.

uno.beep(69, 30)                    # A440 for half a second
uno.quiet()                         # cut it short

The sampled-audio calls need a DAC (HD Audio or AC'97) and raise OSError when the machine has none - always wrap them and fall back to beep:

CallEffect
uno.sfx_load(slot, pcm, rate) -> boolLoad unsigned 8-bit mono PCM into an effect slot.
uno.sfx_play(slot, vol, sep) -> boolPlay a loaded slot: volume 0-255, stereo position 0 left / 128 centre / 255 right. False means that one play didn't start.
uno.mus_play(smf, loop=0) -> boolPlay a standard MIDI file from bytes.
uno.mus_stop()Stop music.
try:
    ok = uno.sfx_load(0, pcm_bytes, 11025)   # slot, 8-bit PCM, rate
    uno.sfx_play(0, 200, 128)                # slot, volume 0-255, pan 0/128/255
    uno.mus_play(smf_bytes)                  # standard MIDI file; loop=1 repeats
except OSError:                              # no DAC on this machine
    uno.beep(60, 10)                         # the square wave always works

Files

Volumes are numbered; 0 is the boot volume and the one-argument forms default to it. Names are 8.3 on FAT volumes and case-insensitive. There is no listdir and no file objects - the API is whole-file and offset reads, which is what a small app actually needs.

CallReturnsRemarks
uno.read(name) / uno.read(vol, name)bytes, or None if the file does not existReads the whole file - fine for documents, wrong for a 4 MB WAD (stream those with read_at).
uno.read_at(vol, name, off, n)bytesReads n bytes at offset off. vol is required here.
uno.size(name) / uno.size(vol, name)int, -1 if missingThe cheap existence test.
uno.write(name, data) / uno.write(vol, name, data)bool False on a read-only volume - surface that state to the user, don't swallow it.
uno.mkdir(path) / uno.mkdir(vol, path)bool One level at a time; the parent must exist.
data = uno.read("NOTES.TXT")        # the boot volume; None if missing
big  = uno.read(1, "DATA.BIN")      # an explicit volume index

# stream a large file instead of loading it whole:
hdr = uno.read_at(0, "GAME.WAD", 0, 12)     # vol, name, offset, length

n = uno.size("NOTES.TXT")           # -1 if it does not exist
ok = uno.write("SCORES.TXT", text.encode())
if not ok:
    ...                             # read-only volume - tell the user

uno.mkdir("SAVES")                  # one level; the parent must exist
uno.write("SAVES/SLOT1.DAT", blob)

Preferences and key bindings

Small per-app settings without inventing a file format: uno.pref_get(name) -> str|None and uno.pref_set(name, value) -> bool persist strings (values capped at 32 bytes) in the shell's preference store. uno.bind_name(action), uno.bind_set(action, uni, scan) and uno.bind_reset() read and remap the shared game-action key bindings.

name = uno.pref_get("player") or "YOU"   # None on first run
uno.pref_set("player", name)             # values are capped at 32 bytes

The system log

uno.log(sev, fac, text) writes a line to the system log (severities follow syslog; fac is a numeric facility - apps are 6). The viewer app, the log file sink and the remote link all see it - prefer this to inventing your own trace file. The reading half (log_read/log_span/log_stat/...) is documented in SDK\uno.pyi.

uno.log(6, 6, "level loaded")   # severity, facility, text - both ints
# severities follow syslog: 3 err, 4 warning, 5 notice, 6 info, 7 debug.
# facilities: 0 kernel, 1 net, 2 storage, 3 browser, 4 ui, 5 security,
# 6 app (yours), 7 remote.  Read it back in the System Log viewer.

Hardware introspection

uno.devices() -> str and uno.pci() -> list expose the device tree (location, IDs, class, claiming driver) - the same data the devices remote verb reports. Read-only.

Language and library limits

The runtime is MicroPython at its core feature level, plus the modules above. What that means in practice:

You haveYou don't have
classes, generators, comprehensions, set, bytearray, big ints, floats (single precision), % and .format() formatting, walrus, async/await f-strings, frozenset, memoryview, __del__, reverse special methods, NotImplemented
math, struct, array, collections (namedtuple), sys, gc, micropython time, random, json, os, io, re, typing - and math.pi (write 3.14159265)
one .py file up to 192 KB, a 16 MB heap, an 8 KB print() buffer shown in Studio importing a second .py file, threads, and the @micropython.native/viper decorators (they fault on real hardware - never use them)

unoauto: drive the machine

import unoauto is the automation half of unoautomate, available to any Python app in every build: each call checks the caller's privilege and, when the capability is absent, returns its inert value (False, [], None) instead of raising - so the same script degrades gracefully on a machine that trusts it less. uptime() and deadline_left() are ungated.

FunctionNeedsReturns / effect
available()observeTrue when observation is granted.
log(text)observeEmit on the script log channel (streams to the dev PC).
probe()observeSystem snapshot: (name, kind, state, v1, v2) rows - kind 0 module, 1 window, 2 subsystem.
key(scan, uni, ctrl=0)driveInject a key. Argument order is scan-first - the mirror image of the key() callback. Lands on the next frame.
pointer(x, y, btn)driveInject a pointer event (btn 1 press, 0 release).
apps() / launch(i) / close_top()drive Count, open, close apps. Never close_top() your own window from an automation script.
uptime()-Milliseconds since boot. Pace scripts on this.
deadline_left()-ms left in the current test budget, -1 if none.
poweroff()systemShut down (unattended runs end themselves).
remote_active() / remote_send(s) / remote_recv() / remote_stop()observe/systemTalk to the dev PC over the URC link.

The load-bearing pattern - actions land on the next shell frame, so run your steps from a generator and yield between step and check:

import uno, unoauto

class Bot(uno.App):
    def build(self, cv):
        self.steps = self.script()
        self.until = 0

    def script(self):
        unoauto.key(0x17, 0, 1)     # Ctrl+Esc: open the Start menu...
        yield 30                    # ...which lands on the NEXT frame
        unoauto.key(0, 13)          # Enter
        yield 30
        wins = [r[0] for r in unoauto.probe() if r[1] == 1]
        unoauto.log("open now: %s" % ", ".join(wins))

    def tick(self):
        if self.steps is None:
            return False
        now = unoauto.uptime()
        if now >= self.until:
            try:
                self.until = now + next(self.steps)
            except StopIteration:
                self.steps = None

app = Bot()

unoscript: the permission-gated OS surface

import unoscript is the production automation surface: UI, apps, files, processes, memory, ports and power, each call gated by a capability tier (0 ambient, 1 user, 2 admin, 3 kernel). Denied calls raise OSError; request() asks for a grant (consent sheet, role, or signed manifest) and answers False rather than raising. The model, the manifest story and the full catalog are on Remote control & automation.

CallTierEffect
available() / whoami() / secured()- Presence, acting uid (0xFFFFFFFF when nobody is signed in), whether the real adjudicator is in.
cap_tier(name) / request(name[, scope[, ttl]])- Look up a capability's tier; ask for it.
ui.click(x, y[, btn]) / ui.move(x, y) / ui.key(scan[, uni[, mods]]) 0Synthetic input on the real device path.
ui.screen()0The window tree as text, focused window marked.
ui.clip_get() / ui.clip_set(s)0 / 1Clipboard.
app.count() / app.launch(i) / app.close_top() / app.message(i, verb)0 / 1App control; verb is "info", "focus" or "close".
fs.read(path) / fs.write(path, data)1 / 2 A bare relative path is your home (USERS\<uid>\..., parents auto-created, reads capped at 4 KB); an absolute /label/rest names a volume and is tier 2. .. is rejected.
proc.list()2(pid, tid, state, name, owner) rows; state bit 0 = focused.
mem.read/write, io.in_/out2-3Raw memory and port I/O; always audited.
sys.power(n)20 shutdown, 1 reboot.
import unoscript as u

u.cap_tier("power")                  # -> 2: needs an explicit grant
if u.request("power"):               # consent sheet, role, or manifest
    u.sys.power(0)                   # clean shutdown

u.fs.write("notes/todo.txt", b"buy milk")   # tier 1: USERS/<uid>/notes/...
print(u.fs.read("notes/todo.txt"))          # b'buy milk'  (4 KB read cap)

for pid, tid, state, name, owner in u.proc.list():   # tier 2
    print(pid, name, "focused" if state & 1 else "")

Worked examples: the sample programs. The app model: Python apps. The permission model and unattended grants: Remote control & automation.