I wrote a programming language, then wrote an operating system in it. MORT OS is a graphical x86 OS with a shell, a file browser, a filesystem that survives reboots and a TCP/IP stack that serves web pages, and every line above the boot stub is written in Mort, a language I built from scratch that compiles to C. Its interface is not a desktop: it is a geological cross-section you drill through.
A real boot in QEMU: the start screen. Four strata spell M-O-R-T.
You need uv, git and QEMU (qemu-system-i386), on Linux or Windows. One command builds the kernel with the pinned Mort compiler and boots it to a shell on your terminal:
git clone https://github.com/0xmortuex/MortOS && cd MortOS
uv run --no-project --with ziglang --with cryptography python build.py bootThe build fetches the Mort compiler at the commit pinned in build.py (MORT_COMMIT), so a clean checkout always builds with the same compiler. At the ~ $ prompt try help, ls /, write hello.txt hi, cat hello.txt and exec /bin/hello.bin; Ctrl+A then X quits QEMU. Replace boot with boot-check to build, boot, and exit 0 as soon as the shell prompt is up.
To see the graphical Borehole interface instead, build a bootable ISO and boot it in a QEMU window (build.py iso fetches the Limine bootloader and, on Windows, xorriso; on Linux, install xorriso first):
uv run --no-project --with ziglang --with cryptography python build.py run-isoIt boots to the Borehole start screen (see docs/borehole-concept.md): press Enter for a shell, then try help, ls, write hello.txt hi and cat hello.txt. Esc climbs back out; M, O, R and T drill to the Machine, Objects, Run and Terminate strata. The ISO is a BIOS/UEFI hybrid built to be written to a USB stick, but I have only tested it in QEMU.
An operating-system kernel written in Mort — my own programming language. It boots in QEMU (and the ISO is built to boot from a USB stick, though I have only tested QEMU), runs in 32-bit protected mode, and draws the Borehole interface (docs/borehole-concept.md) to a linear framebuffer in a bitmap font. It has a real filesystem — write a file, reboot the machine, and it's still there — it runs real, interactive compiled programs (a .mx program compiled to a flat binary, loaded off the disk, talking to the kernel through int 0x80 syscalls), and it speaks TCP/IP: the mortnet stack is vendored into the kernel, so MORT OS gets its own IP over DHCP and runs a web server. Everything above the boot stub — the framebuffer renderer, PS/2 keyboard driver, interrupt handlers, ATA disk driver, the filesystem, the syscall layer, the RTL8139 network driver, the shell — is written in Mort.
The bottom stratum, T, holds Lock, Sleep, Restart and Shut down. Lock seals the screen in granite until the account password is given (mort); Sleep leaves one banked ember until a key is pressed; Shut down powers the machine off (via ACPI on emulators, or an "It is now safe to turn off your computer" halt on bare metal). The clock at the top right reads the CMOS RTC. Everything is a shell command too: lock, sleep, restart, shutdown, power.
net brings up the RTL8139 NIC and leases an address over DHCP; httpd then serves an HTML page on port 80. This is the mortnet stack — NIC driver, ARP, IPv4, ICMP, UDP, DHCP, DNS, TCP, HTTP, all written from scratch in Mort — vendored into net/ and compiled into the kernel. See docs/networking.md for which file implements each layer and how net/httpd actually drive it.
A web server, on an OS, over a TCP/IP stack — every layer in one language. Networking needs an RTL8139 NIC (QEMU emulates one). The rest of the OS does not need a network card.
- The Borehole interface (
ui/borehole.mx) — a multiboot linear framebuffer, an 8×16 bitmap font renderer, and one idea: everything is depth. Four strata hold the Machine (live readings and hardware actions), Objects (MortFS: directories are cavities you bore into, files are beds whose thickness grows with their size), Run (the shell, in a gallery cut from the rock) and Terminate. Opening a file pulls a core sample. Up/Down, Enter and Esc reach everything;M,O,RandTdrill straight to a stratum. Scenes are composed off-screen and revealed behind the drill bit. Seedocs/borehole-concept.md. The shell renders through the same four console calls in both modes, and falls back to VGA text when no framebuffer is present (the bare-kernelpath).
- A real filesystem (MortFS) — an ATA PIO disk driver and an on-disk format, both written in Mort.
ls,cat <file>,write <file> <text>,rm <file>, andrun <file>(execute a file of shell commands). Files persist across reboots — write a note, reboot QEMU,catit back. - Runs real, interactive compiled programs —
exec <file>loads a Mort program (compiled to a flat binary) off the disk to0x00A00000and runs it. Programs share no symbols with the kernel; they call it throughint 0x80syscalls (args passed via a fixed mailbox, since Mort'sasm()takes no operands). A read-line syscall polls the keyboard directly, so programs can take input too —exec /bin/ask.binasks your name and greets you. Sample programs are inprograms/. Seedocs/memory-map.mdfor every fixed physical address the kernel uses. - Users, login, and file permissions — two built-in accounts,
rootandmortuex, with djb2-hashed passwords (acct_init,kmain.mx:2904); the shell auto-logs-in as the normal user and starts in/home/mortuex(login_default/cwd_init,kmain.mx:2910/2916), withwhoami,su,sudo, andpasswdon top. Every MortFS entry carries an owner uid and Unix-style mode bits, enforced on entry creation,write, andrmbycan_write(kmain.mx:2978). Seedocs/accounts.mdfor the model, and for which bits are enforced versus only displayed. - A real bootable ISO — a BIOS+UEFI hybrid ISO (Limine bootloader) built to be written to a USB stick, not just QEMU's
-kernelshortcut (tested in QEMU only; not yet verified on physical hardware) - Interrupt-driven keyboard — a flat GDT, an IDT, remapped PICs; IRQ1 fires into a Mort handler (no polling)
- A shell — command parsing, Backspace line editing, Shift-aware scancode→ASCII, and command history (Up/Down arrows, decoded from 0xE0 extended scancodes). See
docs/shell.mdfor the full command reference. - PIT timer on IRQ0 (~100 Hz) with an
uptimecommand - CPU exception handlers — per-vector stubs that report which fault occurred (try the
crashcommand) - Terminal scrolling and a cursor that tracks input (a drawn underline in graphics, the hardware cursor in text mode)
- Multiboot modules — the bootloader passes ISO files as modules;
readmeprints one, and a second acts as a boot script the kernel runs at startup like an/etc/rc - Tested in CI-style headless runs —
test.pyandtest_fs.pyboot the real kernel in QEMU, inject keystrokes through the monitor, and assert on VGA memory (including the write→reboot→cat persistence path) - PCI hardware detection, USB, and audio —
hw_scan_pci(net/hardware.mx:12) walks the PCI bus for Ethernet/Wi-Fi/audio/USB controllers; a UHCI USB 1.1 host-controller driver resets the controller and enumerates one root-port device at boot (usb_boot_init,net/hci_usb.mx:195, called fromkmain()atkmain.mx:3620), setting a Bluetooth-HCI flag if the enumerated device's class matches (net/hci_usb.mx:155); an Intel AC'97 driver does PCI discovery, mixer volume, and PCM-out DMA (ac97_init,net/audio.mx:9), alongside a PIT-driven legacy PC-speaker beeper (speaker_start/speaker_stop,net/hardware.mx:37/47). All of it is surfaced live in the Machine stratum (docs/settings.md). Seedocs/hardware.mdfor how the PCI scan, USB enumeration, and AC'97 init actually work.
See docs/architecture.md for the full boot chain,
execution model, and how the subsystems below connect, cited to source.
kmain.mx the kernel, in Mort ─┐
│ mortc --freestanding │ Mort -> freestanding C -> 32-bit object
▼ │
boot.s multiboot header + _start ─┤ assembled to a 32-bit object
│ │
linker.ld places it at 1 MB ─┘ linked -> build/kernel.elf
│
▼
qemu-system-i386 -kernel build/kernel.elf
Why does a hobby language compile to C instead of running on an interpreter? Because an interpreter can't boot. Mort emits freestanding-friendly C, so hello.mx and this kernel go through the exact same compiler.
Requirements: Python 3, pip install ziglang cryptography (ziglang is the C cross-compiler; the Mort compiler imports cryptography), and QEMU for booting. The Mort compiler is fetched automatically into .mort/ at the commit pinned by MORT_COMMIT in build.py (or set MORT_HOME, or keep a ../Mort checkout, to build with your own compiler instead; the build prints a note when that differs from the pin).
python build.py check # build + verify it's a valid 32-bit multiboot ELF
python build.py run # build, then boot it fullscreen in QEMU (with the disk)
python build.py window # same as run, but in a resizable window instead of fullscreenThe first thing on screen is the boot banner, printed at kmain.mx:3603, then
the shell prompt — nothing else runs automatically on this bare -kernel
path, since run_script(1) (kmain.mx:884) finds no multiboot module without
an ISO and returns immediately:
MORT OS -- interrupt-driven. type 'help', Enter
~ $
(~ $ is the auto-logged-in user's home-directory prompt, drawn by
draw_prompt, kmain.mx:709-727.)
That banner and prompt are what you're reading in the QEMU display window
itself. All console output goes through either VGA text memory (put_index)
or the drawn framebuffer console (draw_glyph), both funneled through
put_cell_at, which also mirrors it to COM1 (a 16550 UART at 0x3F8, 115200
8N1). Add -serial stdio or a -serial tcp: port to QEMU to get the same
shell on a serial line; the serial command prints its loss counters.
For scripted serial input on Windows, pace stdio byte by byte: QEMU's Windows
stdio backend drops bytes from a one-shot write. Over TCP, a burst over about
512 bytes (the receive ring) can lose bytes unless the sender paces itself or
waits for the prompt after each command; serial counts the drops. See
ARCHITECTURE.md.
run auto-creates build/disk.img (16 MiB, MortFS) and attaches it, so files
you write in the OS survive reboots. python mkfs.py build/disk.img wipes it
clean (add --add host.txt:name.txt to seed files). Try it:
> write notes.txt hello from mort os
> cat notes.txt
> write job.txt echo hi
> write job.txt uptime
> run job.txt
MORT OS runs real compiled programs, not just shell scripts. Sources live in
programs/. The first time any disk-creating command runs —
run, window, disk, or run-iso — ensure_disk() (build.py:341-365)
compiles every programs/*.mx and seeds the resulting binaries onto
build/disk.img, so after your first python build.py run the shell can
already see them:
> exec /bin/hello.bin # a real Mort program prints via syscall, then returns
> exec /bin/count.bin # a second program, proving more than one builds and runs
> exec /bin/ask.bin # an interactive program: it asks your name and greets you
python build.py prog compiles programs/*.mx into loose build/*.bin
files for inspection, but — as the note below explains — it never touches
disk.img, so it's not what makes exec able to find a program:
python build.py prog # compile programs/*.mx -> build/*.binNote: program binaries are seeded onto build/disk.img only once, when
ensure_disk() first creates it (build.py:341-365) — editing a program
and re-running python build.py prog alone won't update what exec sees
inside a running OS, since prog never touches the disk image. Delete
build/disk.img to get a rebuilt program back onto disk; see
docs/programs.md for the details.
python build.py disk (disk(), build.py:368-374) creates and seeds
build/disk.img the same way run/window/run-iso do, but without
building the kernel or starting QEMU — the standalone way to get (or
rebuild, after deleting it) the disk image on its own:
python build.py disk # create build/disk.img (seeded with programs) without bootingIf the image already exists, it prints a one-line notice and leaves it
alone rather than overwriting it, pointing at python mkfs.py build/disk.img
as the explicit wipe path (build.py:369-372) — the same no-clobber
behavior ensure_disk() has everywhere else.
See docs/programs.md for the syscall ABI and how to
write your own program.
Automated tests (all drive the real kernel headless in QEMU):
python test.py smoke # boot + shell basics (text-mode -kernel path)
python test_fs.py # the disk stack, incl. write-reboot-cat persistence
python test_exec.py # build programs, seed, boot, exec, check syscall output
python test_gfx.py # boot the ISO, drive the Borehole interface, assert on screendumps and guest memorySee docs/testing.md for what each script actually
checks and the keyboard/screen-scrape harness they share.
The Borehole interface comes up on the ISO path (run-iso), where Limine
provides the framebuffer. The bare -kernel path has no framebuffer, so the
kernel falls back to VGA text mode there — which is why the text-driven tests
above still work.
python build.py iso # -> build/mort.iso (BIOS + UEFI hybrid, Limine + xorriso)
python build.py run-iso # build the ISO, then boot it in QEMUThese two commands, and the ISO paths of test_gfx.py and test_serial.py,
work on Windows and Linux; CI builds the ISO and boots it on windows-latest
and ubuntu-latest. iso() in build.py fetches Limine at a pinned commit.
On Windows it also fetches a pinned xorriso.exe and runs both .exe tools
directly. On Linux, install xorriso first (apt install xorriso); Limine's
host tool is built with Zig from the limine.c in the same download. macOS
is untested. build/check/run/window/disk/prog only need Zig (via
ziglang) and QEMU.
This path boots to the Borehole start screen (ui_start, ui/borehole.mx)
instead of a bare prompt — press Enter to bore into the shell, where the
banner and the startup script baked into the ISO (STARTUP_TXT,
build.py:88-90, run via run_script(1)) have already executed:
MORT OS -- interrupt-driven. type 'help', Enter
~ $ echo running startup script loaded from disk...
running startup script loaded from disk...
~ $ about
MORT OS -- written in Mort, a language built from scratch
~ $
run-iso's QEMU window also behaves differently from run/window's, which
this README never pointed out: _run() (build.py:403-421), used by both
run and window, always passes -display gtk,zoom-to-fit=on,show-menubar=off
(build.py:410-412) — a borderless window that scales the console to fit —
and adds -full-screen for run specifically (build.py:413-414).
run_iso() (build.py:377-384) passes neither: its QEMU invocation is just
[qemu, "-cdrom", ISO, "-hda", DISK] (build.py:384), with no -display
argument at all, so it boots in QEMU's own default display backend and
window size, menu bar included, and never fullscreen. test_gfx.py boots
the same ISO a third way — headless, with its own -display none (see
docs/testing.md) — so this gap only affects a human
running python build.py run-iso directly.
Write mort.iso byte-for-byte to a USB stick (e.g. Rufus in "DD image" mode) and it is built to boot from it. Not yet verified on physical hardware (QEMU only so far), so treat that as untested.
- Everything above (the Borehole interface, ATA driver, MortFS,
exec-ing real programs) - Space reclamation for
rm(the fixed-extent bump allocator never reclaims, in v1 or v2; re-mkfs to compact) - More syscalls (file I/O from programs, spawn) and a richer program ABI
- A mouse (PS/2 IRQ12) for the Borehole interface
- Mort — the language this kernel is written in: lexer, parser, type checker, and C code generator, from scratch in Python with zero libraries.





