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.
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):
| Method | When it runs | Use it for |
|---|---|---|
build(self, cv) | once, as the window opens | set up state; cv.width()/height() are valid here |
draw(self, cv) | whenever the window repaints | paint one frame |
tick(self) | ~60 times a second | advance animation or game state |
key(self, uni, scan, ctrl) | on a key press | handle input; return True if you consumed it |
opened(self) / closed(self) | window shown / closing | acquire / 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.
Limits (v1)
- One Python app runs at a time; launching another replaces it.
- No
importof other.pyfiles yet - keep an app to a single file (it can be large). The standardmathmodule and the built-ins are available. - The
unomodule is an app's door to the platform - its window, canvas, sound anduno.read/uno.writefiles. To script the machine (drive the UI, launch apps, user-scoped files, and more, all behind a permission gate) a script usesunoscriptinstead; there is no generalos/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.
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.