UnoDOS pc64

Writing apps in Python

UnoDOS treats Python 3 as a first-class app language, right alongside UnoC. Write real Python - classes, functions, floats, the math module - in Studio, press Ctrl+B to package it and Ctrl+R to run it in its own window, all on the machine.

The Python runtime is a moduleThe interpreter is MicroPython, shipped as an optional module APPS\PYRT.UNO (the Python runtime). If it is not installed, Studio still edits and builds UnoC apps normally; running a .py app reports Python runtime not installed. A build that wants a smaller image can leave PYRT.UNO out.

The app model

A Python app is a class that subclasses uno.App, plus a module-global app holding an instance of it. The runtime finds app and calls its methods for you:

import uno

class Hello(uno.App):
    def draw(self, cv):
        cv.clear(uno.rgb(0, 0, 0))
        cv.text(8, 8, "Hello from Python", uno.rgb(255, 255, 255))

# the runtime looks up `app` and drives its methods for you
app = Hello()

Every method is optional (though an app with no draw shows nothing):

MethodWhen it runsUse it for
build(self, cv)once, as the window opensset up state; cv.width()/height() are valid here
draw(self, cv)whenever the window repaintspaint one frame
tick(self)~60 times a secondadvance animation or game state
key(self, uni, scan, ctrl)on a key presshandle input; return True if you consumed it
opened(self) / closed(self)window shown / closingacquire / release resources

cv is a Canvas; colours come from uno.rgb(r, g, b). The whole platform - the framebuffer, sound and files - is one uno. call away.

The sample: a bouncing ball

Studio greets you with SDK\SAMPLE.PY, the Python counterpart of the UnoC sample. It shows the whole shape: build sets the ball's position and velocity, tick moves it and bounces it off the walls, draw paints the background and the ball, and key speeds it up on the space bar. Note the real floats - something UnoC does not have.

import uno

class Bouncer(uno.App):
    def build(self, cv):                 # once, as the window opens
        self.w, self.h = cv.width(), cv.height()
        self.x, self.y = 20.0, 24.0      # real floats - UnoC has none
        self.vx, self.vy = 3.2, 2.3
        self.d = 22

    def draw(self, cv):                  # paint one frame
        cv.clear(uno.rgb(16, 18, 34))
        cv.text(8, 8, "Hello from Python", uno.rgb(210, 214, 230))
        cv.fill_rect(int(self.x), int(self.y), self.d, self.d, uno.rgb(255, 96, 96))

    def tick(self):                      # ~60 times a second
        self.x += self.vx; self.y += self.vy
        if self.x < 0 or self.x + self.d > self.w: self.vx = -self.vx
        if self.y < 0 or self.y + self.d > self.h: self.vy = -self.vy

    def key(self, uni, scan, ctrl):      # space = speed the ball up
        if uni == 32:
            self.vx *= 1.25; self.vy *= 1.25
            return True
        return False

app = Bouncer()

Press Ctrl+B and the output pane reports Packed SAMPLE.UNO; press Ctrl+R and the ball bounces in its own window.

The uno module

Everything a Python app reaches on the platform goes through import uno. The full reference is in the API reference and in SDK\uno.pyi (a stub for editors); here is the whole surface at a glance:

import uno

uno.rgb(r, g, b)                 # pack a colour (0-255 each) -> int

# --- Canvas (passed to build() and draw(); coords are canvas-relative) ---
cv.width(); cv.height()          # your drawable size, in pixels
cv.clear(color)                  # fill the whole canvas
cv.fill_rect(x, y, w, h, color)  # filled / outline rectangle
cv.rect(x, y, w, h, color)
cv.pixel(x, y, color)
cv.hline(x, y, w, color); cv.vline(x, y, h, color)
cv.text(x, y, "string", color)

# --- Sound (the shared UnoSound voice) ---
uno.beep(midi, ticks)            # square-wave note, ticks ~= 1/60 s
uno.quiet()

# --- Files (vol defaults to 0, the boot volume) ---
uno.read(name) -> bytes                 # whole (small) file
uno.read_at(vol, name, off, n) -> bytes # stream a big file a slice at a time
uno.size(name) -> int                   # bytes, or -1 if missing
uno.write(name, data) -> bool           # to a writable volume
uno.mkdir(vol, name) -> bool            # create one directory (parent must exist)

# --- Devices: what hardware is on this machine (read-only) ---
uno.devices() -> str                    # the device tree, one line per PCI function:
                                        #   "bb:dd.f ven:dev cc/ss class driver|UNCLAIMED"
uno.pci() -> list                       # the same, parsed for filtering:
                                        #   [(loc, ven, dev, cls, subcls, progif, driver_or_None), ...]

Reading files without loading them whole

uno.read_at(vol, name, off, n) reads a slice of a file at a byte offset, so a Python app can work with data far larger than memory - a level, a .wav, a multi-megabyte WAD - a piece at a time. This is the same call the Duum engine uses to stream a Doom WAD:

import uno

class WadInfo(uno.App):
    def build(self, cv):
        # read only the 12-byte header of a multi-MB file, not the whole thing
        n = uno.size("DOOM1.WAD")                 # -1 if it is not there
        head = uno.read_at(0, "DOOM1.WAD", 0, 12) if n > 0 else b""
        self.line = "no WAD" if n < 0 else \
            "%s  %d lumps  %d bytes" % (head[0:4].decode(), head[4], n)

    def draw(self, cv):
        cv.clear(uno.rgb(0, 0, 0))
        cv.text(8, 8, self.line, uno.rgb(120, 230, 160))

app = WadInfo()

How "building" a Python app works

Pressing Ctrl+B on a .py file does not translate it to machine code. Studio wraps your source in a small .UNO container - the same format .c apps compile to, flagged as a Python app - and writes NAME.UNO beside it. Running it hands that source to PYRT.UNO, which compiles and executes it with the uno module bound in. Studio routes purely by extension: a .py file is packaged for the runtime, a .c file is compiled by the UnoC compiler. Everything else - the editor, the project pane, build output, the AI assistant - is identical.

Giving a Python app an icon

A packaged Python app runs, but by default the only way to reach it is to open its .UNO file in Files. To put it on the desktop and in the Start menu, ship a descriptor beside the source and name it when packaging:

id: myapp
name: My App
icon: file:MYAPP.QOI
cat: tools
rank: 20
python3 tools/mkuno.py pyapp apps/MYAPP.PY APPS/MYAPP.UNO apps/MYAPP.DESC

The shell reads that block off the disk at startup - two sector reads, executing nothing - and the app gets a row of its own. icon: takes either the name of one of the system emblems or file:NAME.QOI, a 32x32 image you ship next to the module; the shell has to draw the icon before it would load a byte of your code, which is why the artwork is a separate file rather than something the app draws. Duum is packaged exactly this way, and tools/mkicon.py will author the QOI for you.

Beside the source, not inside itThe descriptor is a file beside the source rather than a comment inside it, and that is deliberate: an app whose source is generated or vendored from somewhere else would lose a magic comment at the next update. Keeping it separate means the launcher metadata belongs to whoever packages the app, not to whoever wrote the code.

Limits (v1)

  • One Python app runs at a time; launching another replaces it.
  • No import of other .py files yet - keep an app to a single file (it can be large). The standard math module and the built-ins are available.
  • The uno module is an app's door to the platform - its window, canvas, sound and uno.read/uno.write files. To script the machine (drive the UI, launch apps, user-scoped files, and more, all behind a permission gate) a script uses unoscript instead; there is no general os/sys.

Duum: Doom, in Python

Duum (SDK\DUUM.PY) is a Doom engine written entirely in Python - the proof that Python is a first-class app language here. It loads a real Doom IWAD, parses the map, and renders a first-person, BSP-traversed view of the level you can walk around, all through the uno API. It exercises the whole platform at once: file I/O (it streams the multi-MB WAD with uno.read_at, never loading it whole), heavy compute (the BSP walk and the column renderer), the framebuffer, keyboard input, and floating-point math.

Forty-eight seconds of Duum on the x86-64 build, recorded from the running system: the start room, a walk down the corridor drawn from the WAD, a firefight, and the status bar built from the game's own artwork. The film streams from the UnoDOS website, so playing it needs a connection.
Bring your own WADDuum needs a Doom-format IWAD on the disk as DOOM1.WAD - none ships with UnoDOS, game data belongs to its makers. Use Freedoom (freedoom.github.io, a free BSD-licensed IWAD; rename freedoom1.wad to DOOM1.WAD) or the freely distributable id Software shareware DOOM1.WAD. Put it next to the apps on the boot disk. Without a WAD, Duum opens and says it is missing.

Walls, floors, ceilings and sky are all texture-mapped from the WAD (perspective-correct, distance- and orientation-shaded), with sprites for monsters, items and the weapon. It is a complete game rather than a demo: monster AI, hitscan and projectile weapons, doors, lifts, switches, teleporters, keycards, pickups, exploding barrels, damage, the real STBAR status bar, and E1M1 through E1M9 in order. Three inner loops run in C (cv.wall_span, cv.mask_span, cv.flat_span - one call per rendered column) so the per-pixel work does not go through the interpreter, and input comes through the live held-key level (uno.keys_down()) with the 60 Hz clock from uno.ticks(). Move with the arrows, strafe with , and ., fire with F, use with Space.

Next: complete programs to take apart on Sample programs, and every call with its contract in the Python SDK reference.