Files
MontaukOS/template/docs/gui-apps.md
T

28 KiB

Writing GUI Applications for MontaukOS

This guide covers how to build graphical applications for MontaukOS, from standalone Window Server clients to desktop-integrated apps.

Table of Contents


App Types

MontaukOS supports two kinds of GUI applications:

Standalone Apps Desktop Apps
Location programs/src/<appname>/ programs/src/desktop/apps/
Window Own process, shared-memory pixel buffer Embedded in desktop compositor
Event loop win_poll() syscall Callback-driven (on_draw, on_mouse, on_key)
Drawing Direct pixel buffer writes Canvas abstraction
Examples Spreadsheet, Music, Wikipedia File Manager, System Info, Settings

Choose standalone when you need a separate process (e.g., networking, heavy computation, isolation). Choose desktop-integrated for lightweight tools that benefit from tight compositor integration.


Standalone Window Server Apps

Standalone apps are separate ELF binaries that communicate with the Window Server through syscalls. They get a shared-memory pixel buffer and manage their own event loop.

Minimal Example

#include <montauk/syscall.h>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>

static TrueTypeFont* g_font;
static int g_win_w, g_win_h;

static void render(uint32_t* pixels) {
    // Clear background
    for (int i = 0; i < g_win_w * g_win_h; i++)
        pixels[i] = Color::from_rgb(0xFF, 0xFF, 0xFF).to_pixel();

    // Draw text
    if (g_font)
        g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
                               20, 30, "Hello, MontaukOS!",
                               Color::from_rgb(0x33, 0x33, 0x33), 18);
}

extern "C" void _start() {
    // Load font
    g_font = new TrueTypeFont();
    g_font->init("0:/fonts/Roboto-Medium.ttf");

    // Create window
    g_win_w = 400;
    g_win_h = 300;
    montauk::abi::WinCreateResult wres;
    montauk::win_create("My App", g_win_w, g_win_h, &wres);
    int win_id = wres.id;
    uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;

    // Initial render
    render(pixels);
    montauk::win_present(win_id);

    // Event loop
    while (true) {
        montauk::abi::WinEvent ev;
        int r = montauk::win_poll(win_id, &ev);

        if (r < 0) break;           // Window destroyed externally
        if (r == 0) {
            montauk::sleep_ms(16);   // ~60 FPS idle
            continue;
        }

        if (ev.type == 3) break;     // Close event

        if (ev.type == 2) {          // Resize
            g_win_w = ev.resize.w;
            g_win_h = ev.resize.h;
            pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
        }

        if (ev.type == 0) {          // Keyboard
            // ev.key.ascii, ev.key.scancode, ev.key.pressed
        }

        if (ev.type == 1) {          // Mouse
            // ev.mouse.x, ev.mouse.y, ev.mouse.buttons
        }

        render(pixels);
        montauk::win_present(win_id);
    }

    montauk::win_destroy(win_id);
    montauk::exit(0);
}

Window Lifecycle

win_create()    Create window, get pixel buffer pointer
    |
    v
win_present()   Push current pixel buffer to screen
    |
    v
win_poll()      Receive events (returns 0 if none, <0 if closed)
    |
    v
win_resize()    Handle resize, get new pixel buffer pointer
    |
    v
win_destroy()   Clean up window

Event Types

Events are delivered via win_poll() into a WinEvent struct:

ev.type Event Fields
0 Keyboard ev.key.scancode, ev.key.ascii, ev.key.pressed, ev.key.shift, ev.key.ctrl, ev.key.alt
1 Mouse ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll
2 Resize ev.resize.w, ev.resize.h
3 Close (none)

Drawing Helpers

For new standalone apps, prefer the shared helpers in #include <gui/standalone.hpp>. It provides:

  • gui::WsWindow for win_create() / win_poll() / win_resize() / win_present()
  • gui::Canvas for drawing into the window buffer
  • immediate-mode helpers like draw_text(), draw_button(), and fill_circle()

Older apps in the tree still define local px_* helpers, but new code should not need to.

gui::WsWindow win;
win.create("My App", 400, 300);

gui::Canvas c = win.canvas();
c.fill(gui::colors::WHITE);
draw_text(c, g_font, 20, 30, "Hello", gui::colors::TEXT_COLOR, 18);
draw_button(c, g_font, 20, 60, 96, 28, "OK",
            gui::colors::ACCENT, gui::colors::WHITE, 6, 16);

Rounded Rectangles

static void px_fill_rounded(uint32_t* px, int bw, int bh,
                            int x, int y, int w, int h, int r, uint32_t color) {
    for (int row = y; row < y + h && row < bh; row++) {
        for (int col = x; col < x + w && col < bw; col++) {
            int dx = 0, dy = 0;
            if (col < x + r && row < y + r)         { dx = x + r - col; dy = y + r - row; }
            else if (col >= x+w-r && row < y + r)    { dx = col - (x+w-r-1); dy = y + r - row; }
            else if (col < x + r && row >= y+h-r)    { dx = x + r - col; dy = row - (y+h-r-1); }
            else if (col >= x+w-r && row >= y+h-r)   { dx = col - (x+w-r-1); dy = row - (y+h-r-1); }
            if (dx * dx + dy * dy <= r * r)
                px[row * bw + col] = color;
            else if (dx == 0 && dy == 0)
                px[row * bw + col] = color;
        }
    }
}

Desktop-Integrated Apps

Desktop apps are compiled into the desktop binary itself. They register callback functions that the compositor calls during its event loop.

Creating a Desktop App

Step 1: Define your app state

// In apps/app_myapp.cpp
struct MyAppState {
    int counter;
    char label[64];
};

Step 2: Implement callbacks

static void myapp_on_draw(Window* win, Framebuffer& fb) {
    MyAppState* state = (MyAppState*)win->app_data;
    Canvas c(win);

    c.fill(colors::WINDOW_BG);

    // Draw toolbar
    c.fill_rect(0, 0, win->content_w, 36, Color::from_rgb(0xF5, 0xF5, 0xF5));
    c.hline(0, 36, win->content_w, colors::BORDER);

    // Draw content
    c.text(20, 60, state->label, colors::TEXT_COLOR);
}

static void myapp_on_mouse(Window* win, MouseEvent& ev) {
    MyAppState* state = (MyAppState*)win->app_data;
    if (ev.left_pressed()) {
        state->counter++;
        win->dirty = true;  // Request redraw
    }
}

static void myapp_on_key(Window* win, const montauk::abi::KeyEvent& key) {
    if (!key.pressed) return;
    MyAppState* state = (MyAppState*)win->app_data;
    // Handle keystrokes...
    win->dirty = true;
}

static void myapp_on_close(Window* win) {
    MyAppState* state = (MyAppState*)win->app_data;
    montauk::mfree(state);
}

Step 3: Write the open function

void open_myapp(DesktopState* ds) {
    int idx = desktop_create_window(ds, "My App", 400, 300, 320, 400);
    if (idx < 0) return;
    Window* win = &ds->windows[idx];

    MyAppState* state = (MyAppState*)montauk::malloc(sizeof(MyAppState));
    montauk::memset(state, 0, sizeof(MyAppState));

    win->app_data = state;
    win->on_draw  = myapp_on_draw;
    win->on_mouse = myapp_on_mouse;
    win->on_key   = myapp_on_key;
    win->on_close = myapp_on_close;
    win->dirty    = true;
}

Step 4: Register in the app menu

Add an entry in desktop_init() (main.cpp) to the app menu so users can launch it. Include the open_myapp function in apps_common.hpp or similar.

Callback Reference

Callback Signature When Called
on_draw void(Window*, Framebuffer&) Every frame when win->dirty is true
on_mouse void(Window*, MouseEvent&) Mouse event within window content area
on_key void(Window*, const KeyEvent&) Keyboard event while window is focused
on_close void(Window*) Window close button clicked
on_poll void(Window*) Every frame, for background processing

The dirty Flag

Desktop apps use a dirty flag to control redraws. Set win->dirty = true after any state change that requires a visual update. The compositor skips on_draw for non-dirty windows.


Drawing and Rendering

Canvas API (Desktop Apps)

The Canvas wraps a window's pixel buffer with drawing primitives:

Canvas c(win);  // Construct from Window*

// Fills
c.fill(Color c);                                          // Entire buffer
c.fill_rect(int x, int y, int w, int h, Color c);        // Rectangle
c.fill_rounded_rect(int x, int y, int w, int h, int r, Color c);  // Rounded rect

// Lines
c.hline(int x, int y, int len, Color c);                 // Horizontal
c.vline(int x, int y, int len, Color c);                 // Vertical
c.rect(int x, int y, int w, int h, Color c);             // Outline

// Text
c.text(int x, int y, const char* str, Color c);          // TrueType or bitmap
c.text_2x(int x, int y, const char* str, Color c);       // 2x scaled bitmap
c.text_mono(int x, int y, const char* str, Color c);     // Monospace

// UI elements
c.button(int x, int y, int w, int h, const char* label,
         Color bg, Color fg, int radius);                 // Styled button
c.icon(int x, int y, const SvgIcon& ic);                 // SVG icon

// Layout helpers
c.kv_line(int x, int* y, const char* line, Color c, int line_h);  // Key-value line
c.separator(int x_start, int x_end, int* y, Color c, int spacing); // Horizontal separator

Framebuffer API (Low-Level)

For direct framebuffer access (used by the compositor itself and fullscreen apps):

Framebuffer fb;
fb.put_pixel(x, y, color);
fb.put_pixel_alpha(x, y, color);     // With alpha blending
fb.fill_rect(x, y, w, h, color);
fb.fill_rect_alpha(x, y, w, h, color);
fb.blit(x, y, w, h, pixels);         // Copy pixel region
fb.blit_alpha(x, y, w, h, pixels);   // With alpha blending
fb.clear(color);
fb.flip();                            // Swap to hardware

Drawing Primitives

From gui/draw.hpp, available for both Framebuffer-based rendering:

draw_hline(fb, x, y, w, color);
draw_vline(fb, x, y, h, color);
draw_rect(fb, x, y, w, h, color);
fill_rounded_rect(fb, x, y, w, h, radius, color);
fill_circle(fb, cx, cy, r, color);
draw_circle(fb, cx, cy, r, color);
draw_line(fb, x0, y0, x1, y1, color);     // Bresenham's
draw_shadow(fb, x, y, w, h, offset, color);

Text and Fonts

TrueType Fonts (Preferred)

MontaukOS uses stb_truetype for font rendering. Fonts are loaded from the VFS:

TrueTypeFont* font = new TrueTypeFont();
font->init("0:/fonts/Roboto-Medium.ttf");

// Render text to a pixel buffer
font->draw_to_buffer(pixels, buf_w, buf_h, x, y, "Hello", color, 18);

// Measure text width before drawing
int width = font->measure_text("Hello", 18);

// Get line height for layout
int line_h = font->get_line_height(18);

System Fonts

The desktop initializes a set of global fonts:

fonts::init();  // Call once at startup

// Available fonts
fonts::system_font   // Roboto-Medium.ttf    (UI text)
fonts::system_bold   // Roboto-Bold.ttf      (headings)
fonts::mono          // JetBrainsMono-Regular.ttf (code/terminal)
fonts::mono_bold     // JetBrainsMono-Bold.ttf

// Standard sizes
fonts::UI_SIZE       // 18  (body text)
fonts::TITLE_SIZE    // 18  (window titles)
fonts::LARGE_SIZE    // 28  (headings)
fonts::TERM_SIZE     // 18  (terminal)

Glyph Caching

TrueType fonts cache rasterized glyphs per pixel size. Up to 4 size caches are maintained per font. Access the cache directly for advanced metrics:

GlyphCache* cache = font->get_cache(18);
// cache->ascent, cache->descent — for line-height calculation

Bitmap Font (Fallback)

An 8x8 bitmap font is always available for basic text rendering when TrueType is not loaded:

draw_char(fb, x, y, 'A', color);
draw_text(fb, x, y, "Hello", color);
int w = text_width("Hello");

Input Handling

Keyboard

// In standalone apps (via win_poll)
if (ev.type == 0) {
    montauk::abi::KeyEvent& key = ev.key;
    if (!key.pressed) { /* key release */ }

    if (key.ascii >= 0x20 && key.ascii < 0x7F) {
        // Printable character
    }

    // Special keys by scancode
    switch (key.scancode) {
        case 0x01: /* Escape */     break;
        case 0x0E: /* Backspace */  break;
        case 0x1C: /* Enter */      break;
        case 0x0F: /* Tab */        break;
        case 0x53: /* Delete */     break;
        case 0x48: /* Up */         break;
        case 0x50: /* Down */       break;
        case 0x4B: /* Left */       break;
        case 0x4D: /* Right */      break;
        case 0x47: /* Home */       break;
        case 0x4F: /* End */        break;
        case 0x49: /* Page Up */    break;
        case 0x51: /* Page Down */  break;
    }

    // Modifiers
    if (key.ctrl) { /* Ctrl held */ }
    if (key.shift) { /* Shift held */ }
    if (key.alt) { /* Alt held */ }
}

Mouse

// In standalone apps (via win_poll)
if (ev.type == 1) {
    int mx = ev.mouse.x;
    int my = ev.mouse.y;

    // Button state
    bool left_down  = ev.mouse.buttons & 0x01;
    bool right_down = ev.mouse.buttons & 0x02;

    // Detect clicks (press edge)
    bool left_pressed  = (ev.mouse.buttons & 0x01) && !(ev.mouse.prev_buttons & 0x01);
    bool left_released = !(ev.mouse.buttons & 0x01) && (ev.mouse.prev_buttons & 0x01);

    // Scroll wheel
    int scroll = ev.mouse.scroll;  // Positive = up, negative = down
}

In desktop apps, the MouseEvent struct provides convenience methods:

void myapp_on_mouse(Window* win, MouseEvent& ev) {
    if (ev.left_pressed()) { /* click start */ }
    if (ev.left_released()) { /* click end */ }
    if (ev.left_held()) { /* dragging */ }
    if (ev.right_pressed()) { /* context menu */ }
    if (ev.scroll != 0) { /* scroll */ }
}

Hit Testing

A common pattern for clickable UI regions:

struct ButtonRect { int x, y, w, h; };

bool hit_test(ButtonRect& btn, int mx, int my) {
    return mx >= btn.x && mx < btn.x + btn.w &&
           my >= btn.y && my < btn.y + btn.h;
}

Widgets

MontaukOS provides built-in widget types in gui/widgets.hpp:

Button

Button btn;
btn.init(x, y, width, height, "Click Me");
btn.bg = colors::ACCENT;
btn.fg = colors::WHITE;
btn.on_click = [](void* data) { /* handle click */ };
btn.userdata = my_state;

// In draw callback
btn.draw(fb);

// In mouse callback
btn.handle_mouse(ev);

TextBox

TextBox tb;
tb.init(x, y, width, height);

// In draw callback
tb.draw(fb);

// In mouse callback (sets focus)
tb.handle_mouse(ev);

// In key callback (text input)
tb.handle_key(key);

// Read value
const char* value = tb.text;

Scrollbar

Scrollbar sb;
sb.init(x, y, width, height);
sb.content_height = 2000;  // Total content height
sb.view_height = 400;      // Visible area height

// In draw callback
sb.draw(fb);

// In mouse callback
sb.handle_mouse(ev);

// Use scroll_offset for content positioning
int offset = sb.scroll_offset;

Colors and Theming

Color Construction

Color c1 = Color::from_rgb(0xFF, 0x00, 0x00);     // Red
Color c2 = Color::from_rgba(0x00, 0x00, 0xFF, 0x80); // Semi-transparent blue
Color c3 = Color::from_hex(0x367BF0);               // From hex
uint32_t pixel = c3.to_pixel();                      // ARGB for pixel buffer

System Colors

Defined in gui/gui.hpp under the colors namespace:

Constant Hex Usage
WINDOW_BG #FFFFFF Window content background
TEXT_COLOR #333333 Primary text
ACCENT #367BF0 Links, selections, active elements
BORDER #D0D0D0 Window/widget borders
PANEL_BG #2B3E50 Taskbar/panel background
PANEL_TEXT #FFFFFF Panel text
TITLEBAR_BG #F5F5F5 Window titlebar
DESKTOP_BG #E0E0E0 Desktop background
CLOSE_BTN #FF5F57 Close button (red)
MAX_BTN #28CA42 Maximize button (green)
MIN_BTN #FFBD2E Minimize button (yellow)
TERM_BG #2D2D2D Terminal background
TERM_FG #CCCCCC Terminal text
SCROLLBAR_BG Scrollbar track
SCROLLBAR_FG Scrollbar thumb

Toolbar Convention

Standard toolbar pattern used across desktop apps:

// 36px tall, light gray background, thin bottom border
c.fill_rect(0, 0, win->content_w, 36, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, 36, win->content_w, colors::BORDER);

// 24x24 icon buttons centered at y=6
c.icon(8, 6, my_icon);

// Content starts below toolbar
int content_y = 37;

Memory Management

Userspace Heap

Use montauk::malloc, montauk::mfree, and montauk::realloc for dynamic allocation:

#include <montauk/heap.h>

MyState* state = (MyState*)montauk::malloc(sizeof(MyState));
montauk::memset(state, 0, sizeof(MyState));
// ... use state ...
montauk::mfree(state);

// Resize
char* buf = (char*)montauk::malloc(256);
buf = (char*)montauk::realloc(buf, 512);

The allocator uses size-class buckets (32 to 4096 bytes) with an overflow list for larger allocations.

Kernel Page Allocation

montauk::alloc / montauk::free allocate kernel pages. Avoid for temporary buffers; use the heap instead.

Important Notes

  • User stack is 32 KiB (8 pages). Deep call chains (e.g., TrueType rendering) can approach this limit. Avoid large stack allocations.
  • Use inline (not static) for shared globals in headers to avoid per-translation-unit copies and heap corruption.
  • The libc needs -fno-tree-loop-distribute-patterns in CFLAGS to prevent GCC from converting memcpy/memset into calls to themselves.

Networking and HTTPS

MontaukOS provides a shared TLS library (tls/tls.hpp) backed by BearSSL, and the MontaukAI dev environment adds a higher-level HTTP wrapper (http/http.hpp) on top. Build with USE_TLS=1 to link TLS support.

HTTP Wrapper (http/http.hpp)

Header-only library that handles DNS resolution, request building, TLS, response parsing, and cleanup. All functions return an http::Response struct.

Setup

#include <http/http.hpp>

// Load CA certificates once at startup (required for HTTPS)
tls::TrustAnchors tas = tls::load_trust_anchors();

GET

auto resp = http::get("api.example.com", "/v1/data", tas);
if (resp.status == 200) {
    // resp.body is a pointer to the response body
    // resp.body_len is its length
}
http::free_response(&resp);

POST

const char* json = "{\"name\":\"MontaukOS\",\"version\":1}";
auto resp = http::post("api.example.com", "/v1/submit",
                       "application/json",
                       json, montauk::slen(json),
                       tas);
if (resp.status == 201) {
    // Created successfully
}
http::free_response(&resp);

Other Methods (PUT, PATCH, DELETE, ...)

auto resp = http::request("PUT", "api.example.com", "/v1/item/42",
                          "application/json",
                          body, bodyLen, tas);
http::free_response(&resp);

// DELETE with no body
auto resp2 = http::request("DELETE", "api.example.com", "/v1/item/42",
                           nullptr, nullptr, 0, tas);
http::free_response(&resp2);

Plain HTTP (No TLS, Port 80)

auto resp = http::get_plain("example.com", "/");
if (resp.status == 200) {
    // resp.body ...
}
http::free_response(&resp);

Reading Response Headers

char content_type[128];
if (http::get_header(&resp, "Content-Type", content_type, sizeof(content_type))) {
    // content_type is e.g. "application/json; charset=utf-8"
}

Custom Headers

Pass extra headers as a string with \r\n terminators:

auto resp = http::get("api.example.com", "/v1/data", tas,
                      32768,  // response buffer size
                      "Authorization: Bearer tok_abc123\r\n"
                      "Accept: application/json\r\n");
http::free_response(&resp);

Cancellable Requests

For GUI apps that need to stay responsive during network I/O:

static bool g_quit = false;
static bool check_abort() { return g_quit; }

auto resp = http::get("api.example.com", "/v1/slow", tas,
                      32768, nullptr, check_abort);
http::free_response(&resp);

Set g_quit = true from your keyboard handler (e.g., on Escape) to cancel mid-request.

Response Struct Reference

struct http::Response {
    int status;           // HTTP status code (200, 404, ...) or -1 on error
    const char* headers;  // Pointer to header block (within raw buffer)
    int headers_len;
    const char* body;     // Pointer to body (within raw buffer)
    int body_len;
    char* raw;            // Owned buffer — freed by free_response()
    int raw_len;
};

Function Signatures

// HTTPS GET
http::Response http::get(const char* host, const char* path,
                         const tls::TrustAnchors& tas,
                         int resp_buf_size = 32768,
                         const char* extra_headers = nullptr,
                         tls::AbortCheckFn abort_check = nullptr);

// HTTPS POST
http::Response http::post(const char* host, const char* path,
                          const char* content_type,
                          const char* body, int body_len,
                          const tls::TrustAnchors& tas,
                          int resp_buf_size = 32768,
                          const char* extra_headers = nullptr,
                          tls::AbortCheckFn abort_check = nullptr);

// HTTPS with any method
http::Response http::request(const char* method,
                             const char* host, const char* path,
                             const char* content_type,
                             const char* body, int body_len,
                             const tls::TrustAnchors& tas,
                             int resp_buf_size = 32768,
                             const char* extra_headers = nullptr,
                             tls::AbortCheckFn abort_check = nullptr);

// Plain HTTP GET (port 80, no TLS)
http::Response http::get_plain(const char* host, const char* path,
                               int resp_buf_size = 32768,
                               const char* extra_headers = nullptr);

// Parse raw HTTP response buffer (used internally, available if needed)
int http::parse_response(char* buf, int len, http::Response* out);

// Case-insensitive header lookup
bool http::get_header(const http::Response* resp, const char* name,
                      char* out_val, int max_len);

// Free the response's raw buffer
void http::free_response(http::Response* resp);

Low-Level TLS API (tls/tls.hpp)

If you need more control than http:: provides (e.g., streaming responses, custom BearSSL setup), use the TLS layer directly:

#include <tls/tls.hpp>

tls::TrustAnchors tas = tls::load_trust_anchors();

// Build raw HTTP request yourself
char req[512];
// ... "GET /stream HTTP/1.1\r\nHost: ...\r\n\r\n" ...

char buf[65536];
int n = tls::https_fetch("example.com", ip, 443, req, reqLen, tas, buf, sizeof(buf));

See syscalls.md for the full tls:: API reference.

Plain TCP/UDP

For non-TLS networking without the HTTP wrapper, use the raw socket syscalls directly (see syscalls.md):

int sock = montauk::socket(montauk::abi::SOCK_TCP);  // or SOCK_UDP
montauk::connect(sock, ip, port);
montauk::send(sock, data, len);
int n = montauk::recv(sock, buf, maxLen);
montauk::closesocket(sock);

App Manifests

External standalone apps can be discovered by the desktop through TOML manifest files placed in 0:/apps/:

[app]
id = "my-app"
name = "My App"
binary = "myapp.elf"
icon = "myapp_icon.svg"

[menu]
category = "Applications"
visible = true

[desktop]
section = "apps"
admin_only = false

[launch]
pass_home_dir = true

Categories: Applications, Internet, System, Games

The desktop scans 0:/apps/ at startup and builds one catalog used by the application menu, launcher search, and the Apps and Settings virtual folders. app.id must be unique; when omitted, the containing directory name is used. The binary field names an executable in that app's bundle.

desktop.section accepts apps (the default), settings, or hidden. Section placement is independent of menu.visible: a Settings item can be hidden from the application menu while remaining directly searchable in the launcher. Set desktop.admin_only to hide the item from non-administrator users. launch.pass_home_dir controls whether the current user's home path is passed as the executable's first argument.


Build System

Independent Development (MontaukAI)

The MontaukAI/ directory provides a self-contained build environment. Edit the top of Makefile to configure your app:

APP_NAME := myapp
SRCS     := src/main.cpp src/stb_truetype_impl.cpp src/cxxrt.cpp src/network.cpp

Build with optional feature flags:

make                           # GUI-only app
make USE_TLS=1                 # With HTTPS/TLS (links libtls + libbearssl)
make USE_JPEG=1                # With JPEG decoding (links libjpeg)
make USE_TLS=1 USE_JPEG=1     # Both
make install                   # Copy ELF to MontaukOS ramdisk

The sysroot contains all headers and pre-built libraries:

sysroot/
├── include/
│   ├── montauk/     syscall.h, heap.h, string.h, config.h, toml.h, user.h
│   ├── gui/         gui.hpp, canvas.hpp, truetype.hpp, widgets.hpp, svg.hpp, ...
│   ├── tls/         tls.hpp (HTTPS/TLS)
│   ├── Api/         Syscall.hpp (low-level syscall numbers)
│   ├── libc/        stdio.h, stdlib.h, string.h, ... (freestanding libc)
│   ├── bearssl*.h   BearSSL headers (for USE_TLS=1)
│   └── (freestanding C/C++ standard headers)
└── lib/
    ├── crt1.o       Startup shim for main(argc, argv) ports
    ├── crti.o       CRT init prologue placeholder
    ├── crtn.o       CRT init epilogue placeholder
    ├── liblibc.a    C library (always linked)
    ├── libtls.a     TLS helper library
    ├── libbearssl.a BearSSL crypto
    └── libjpeg.a    JPEG decoding (stb_image)

Key build details:

  • Toolchain: x86_64-elf-g++ cross-compiler (falls back to system g++)
  • Standard: C++20 (-std=gnu++20), freestanding, no exceptions/RTTI
  • SSE: Enabled (-msse -msse2) for floating-point / TrueType rendering
  • Entry point: extern "C" void _start() by default, or main(argc, argv) with USE_CRT=1
  • Load address: 0x400000 (set in link.ld)
  • Runtime support: src/cxxrt.cpp provides operator new/delete via montauk::malloc/mfree
  • TrueType support: src/stb_truetype_impl.cpp provides the stb_truetype implementation

USE_CRT=1 is primarily for plain C ports and other code that already expects main(argc, argv). The shared CRT does not run C++ global constructors or destructors yet, so the default template still uses _start().

In-Tree Development

Standalone apps can also live in MontaukOS/programs/src/<appname>/ with their own Makefile. Each app's Makefile sets SRCS, CXXFLAGS, LDFLAGS, and links against libraries in programs/lib/.

Desktop-integrated apps are compiled as part of the desktop binary — add your .cpp file to the desktop's source list in programs/src/desktop/Makefile.