From 0377be452466268c7f60ff93ddfbdc9b5034ec09 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Fri, 17 Jul 2026 20:52:19 +0200 Subject: [PATCH] feat: redesign installer on MTK toolkit with component selection Rewrite the Installer app on the Montauk Toolkit (Canvas + gui/mtk widgets, theme, hover states, scrollbar) replacing the hand-rolled px_* renderer and bespoke TrueType usage. Add a new "Software" step: an expandable checkbox tree for optional components (Montauk SDK with gcc/g++, binutils, tcc, lua sub-items; Games; Office; Internet apps; Printing; experimental httpd and SDR). Unchecked components' paths are excluded from the install copy, with a longest-path ownership rule so e.g. sdk/tcc installs even when the rest of the SDK is deselected. The update flow refreshes only the components the target already has. Add tri-state checkbox and disclosure-arrow widgets to the MTK toolkit (anti-aliased, supersampled). Merge tcc and lua into 0:/sdk (0:/sdk/tcc, 0:/sdk/lua) and drop 0:/lib: update tcc config.h, lua luaconf.h, both Makefiles, the devkit clean scope, man pages and montaukos.org notices. Make printing optional: init skips printd when 0:/os/printd.elf is absent. Co-Authored-By: Claude Opus 4.8 --- kernel/src/Api/BuildNo.hpp | 2 +- montaukos.org/THIRD-PARTY-NOTICES.txt | 4 +- montaukos.org/docs/man/legal.html | 4 +- programs/GNUmakefile | 15 +- programs/include/gui/mtk/widgets.hpp | 211 ++++++ programs/man/intro.1 | 7 +- programs/man/legal.7 | 4 +- programs/src/init/main.cpp | 14 +- programs/src/installer/Makefile | 2 +- programs/src/installer/actions.cpp | 225 +++--- programs/src/installer/components.cpp | 439 ++++++++++++ programs/src/installer/installer.h | 181 ++++- programs/src/installer/main.cpp | 381 +++++----- programs/src/installer/render.cpp | 955 ++++++++++++-------------- programs/src/lua/Makefile | 2 +- programs/src/lua/src/luaconf.h | 4 +- programs/src/tcc/Makefile | 2 +- programs/src/tcc/config.h | 8 +- 18 files changed, 1647 insertions(+), 813 deletions(-) create mode 100644 programs/src/installer/components.cpp diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 0a5fc31..5781418 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 29 +#define MONTAUK_BUILD_NUMBER 31 diff --git a/montaukos.org/THIRD-PARTY-NOTICES.txt b/montaukos.org/THIRD-PARTY-NOTICES.txt index 9e20f04..fb732d2 100644 --- a/montaukos.org/THIRD-PARTY-NOTICES.txt +++ b/montaukos.org/THIRD-PARTY-NOTICES.txt @@ -252,7 +252,7 @@ exception, is included on the MontaukOS ISO at 0:/os/licenses/AGPL-3.0-C059.txt. 10. Tiny C Compiler (TCC) ================================================================================ Shipped as the bundled C compiler (tcc.elf) with its runtime libraries and -headers on the ISO (0:/lib/tcc/). MontaukOS ships a modified version of TCC +headers on the ISO (0:/sdk/tcc/). MontaukOS ships a modified version of TCC 0.9.28rc, ported to target MontaukOS (added platform startup/compatibility code and a MontaukOS sysroot). @@ -281,7 +281,7 @@ License: GNU Lesser General Public License, version 2.1 (LGPL-2.1) 11. Lua ================================================================================ Shipped as the bundled Lua interpreter (lua.elf), Lua 5.4.8, with its static -library and headers on the ISO (0:/lib/lua/). +library and headers on the ISO (0:/sdk/lua/). License: MIT License diff --git a/montaukos.org/docs/man/legal.html b/montaukos.org/docs/man/legal.html index dd499df..faae91b 100644 --- a/montaukos.org/docs/man/legal.html +++ b/montaukos.org/docs/man/legal.html @@ -153,9 +153,9 @@ * Roboto font, Copyright The Roboto Project Authors - OFL-1.1 * C059 font (URW Base 35), Copyright (C) (URW)++ Design and Development GmbH - AGPLv3 with font-embedding exception - * Tiny C Compiler (tcc.elf, 0:/lib/tcc), Copyright (c) Fabrice Bellard and + * Tiny C Compiler (tcc.elf, 0:/sdk/tcc), Copyright (c) Fabrice Bellard and contributors - LGPL-2.1 - * Lua (lua.elf, 0:/lib/lua), Copyright (C) Lua.org, PUC-Rio - MIT + * Lua (lua.elf, 0:/sdk/lua), Copyright (C) Lua.org, PUC-Rio - MIT * Mozilla CA certificate bundle (0:/os/certs), Mozilla CA Certificate Program - MPL-2.0 * Intel Bluetooth firmware (0:/os/firmware/intel), Copyright (c) Intel diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 2b1bbab..2f9994d 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -295,11 +295,11 @@ gen-syscalls: # check-syscalls guards against shipping a stale C API header into the ramdisk. tcc: libc check-syscalls $(MAKE) -C src/tcc - mkdir -p $(BINDIR)/lib/tcc/include - cp -r src/tcc/tcc_include/* $(BINDIR)/lib/tcc/include/ - cp include/libc/*.h $(BINDIR)/lib/tcc/include/ - mkdir -p $(BINDIR)/lib/tcc/include/sys - cp include/libc/sys/*.h $(BINDIR)/lib/tcc/include/sys/ + mkdir -p $(BINDIR)/sdk/tcc/include + cp -r src/tcc/tcc_include/* $(BINDIR)/sdk/tcc/include/ + cp include/libc/*.h $(BINDIR)/sdk/tcc/include/ + mkdir -p $(BINDIR)/sdk/tcc/include/sys + cp include/libc/sys/*.h $(BINDIR)/sdk/tcc/include/sys/ # Build Lua interpreter and static library (depends on libc). lua: libc @@ -388,7 +388,10 @@ DEVKIT_TOOLS := as ld ar nm objcopy objdump readelf ranlib strip size strings ad .PHONY: devkit devkit: libc ifneq ($(wildcard $(NATIVE_BIN)/as),) - rm -rf $(BINDIR)/sdk + # Clean only the devkit-owned subtrees; sdk/tcc and sdk/lua are + # installed into bin/sdk/ by their own targets. + rm -rf $(BINDIR)/sdk/bin $(BINDIR)/sdk/lib $(BINDIR)/sdk/include \ + $(BINDIR)/sdk/libexec $(BINDIR)/sdk/x86_64-montauk mkdir -p $(BINDIR)/sdk/bin $(BINDIR)/sdk/lib $(BINDIR)/sdk/include for t in $(DEVKIT_TOOLS); do \ cp $(NATIVE_BIN)/$$t $(BINDIR)/sdk/bin/$$t.elf || exit 1; \ diff --git a/programs/include/gui/mtk/widgets.hpp b/programs/include/gui/mtk/widgets.hpp index e4171aa..2115404 100644 --- a/programs/include/gui/mtk/widgets.hpp +++ b/programs/include/gui/mtk/widgets.hpp @@ -942,6 +942,217 @@ inline void draw_radio(Canvas& c, label, theme.text); } +// ============================================================================ +// Checkboxes and disclosure arrows +// ============================================================================ + +enum CheckState : uint8_t { + CHECK_OFF = 0, + CHECK_ON, + CHECK_MIXED, +}; + +inline CheckState check_state(bool checked) { + return checked ? CHECK_ON : CHECK_OFF; +} + +inline Rect checkbox_indicator_rect(const Rect& option, int size = 16) { + return {option.x, option.y + (option.h - size) / 2, size, size}; +} + +// Coverage-based pixel blend used by the anti-aliased checkbox drawing. +// `cov` is 0..255; the destination is treated as opaque. +inline void checkbox_blend_px(Canvas& c, int x, int y, Color color, int cov) { + if (x < 0 || x >= c.w || y < 0 || y >= c.h || cov <= 0) return; + if (cov > 255) cov = 255; + uint32_t dst = c.pixels[y * c.w + x]; + uint32_t dr = (dst >> 16) & 0xFF, dg = (dst >> 8) & 0xFF, db = dst & 0xFF; + uint32_t a = (uint32_t)cov, ia = 255 - a; + uint32_t nr = (color.r * a + dr * ia + 127) / 255; + uint32_t ng = (color.g * a + dg * ia + 127) / 255; + uint32_t nb = (color.b * a + db * ia + 127) / 255; + c.pixels[y * c.w + x] = 0xFF000000u | (nr << 16) | (ng << 8) | nb; +} + +// Point (px, py) inside a rounded rect of size w x h with corner radius r, +// evaluated in the rect's local coordinate space. +inline bool round_rect_contains(double px, double py, double w, double h, double r) { + if (px < 0 || py < 0 || px > w || py > h) return false; + double rx = 0, ry = 0; + bool corner = false; + if (px < r && py < r) { rx = r - px; ry = r - py; corner = true; } + else if (px > w - r && py < r) { rx = px - (w - r); ry = r - py; corner = true; } + else if (px < r && py > h - r) { rx = r - px; ry = py - (h - r); corner = true; } + else if (px > w - r && py > h - r) { rx = px - (w - r); ry = py - (h - r); corner = true; } + if (!corner) return true; + return rx * rx + ry * ry <= r * r; +} + +// Anti-aliased rounded-rect fill via 4x4 supersampling. Kept local to the +// checkbox so the rest of the toolkit's crisp fills are unchanged. +inline void checkbox_fill_round_aa(Canvas& c, const Rect& box, double radius, Color color) { + if (box.w <= 0 || box.h <= 0) return; + if (radius < 0) radius = 0; + const int S = 4; + for (int row = -1; row <= box.h; row++) { + int py = box.y + row; + if (py < 0 || py >= c.h) continue; + for (int col = -1; col <= box.w; col++) { + int px = box.x + col; + if (px < 0 || px >= c.w) continue; + int hits = 0; + for (int sy = 0; sy < S; sy++) + for (int sx = 0; sx < S; sx++) { + double fx = col + (sx + 0.5) / S; + double fy = row + (sy + 0.5) / S; + if (round_rect_contains(fx, fy, box.w, box.h, radius)) hits++; + } + if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S)); + } + } +} + +// Squared distance from a point to a line segment (no sqrt; used for the +// thickness test of the checkmark strokes). +inline double checkbox_seg_dist2(double px, double py, + double ax, double ay, double bx, double by) { + double dx = bx - ax, dy = by - ay; + double len2 = dx * dx + dy * dy; + double t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0.0; + if (t < 0) t = 0; else if (t > 1) t = 1; + double ex = px - (ax + t * dx), ey = py - (ay + t * dy); + return ex * ex + ey * ey; +} + +// Anti-aliased two-stroke checkmark via 4x4 supersampling of a fixed-width +// polyline. +inline void draw_check_mark(Canvas& c, const Rect& box, Color color) { + double ax = box.x + box.w * 0.24, ay = box.y + box.h * 0.50; + double bx = box.x + box.w * 0.42, by = box.y + box.h * 0.68; + double cx = box.x + box.w * 0.74, cy = box.y + box.h * 0.26; + double hw = box.w * 0.085 + 0.35; // half stroke width + double hw2 = hw * hw; + const int S = 4; + for (int py = box.y - 1; py <= box.y + box.h + 1; py++) { + if (py < 0 || py >= c.h) continue; + for (int px = box.x - 1; px <= box.x + box.w + 1; px++) { + if (px < 0 || px >= c.w) continue; + int hits = 0; + for (int sy = 0; sy < S; sy++) + for (int sx = 0; sx < S; sx++) { + double fx = px + (sx + 0.5) / S; + double fy = py + (sy + 0.5) / S; + double d1 = checkbox_seg_dist2(fx, fy, ax, ay, bx, by); + double d2 = checkbox_seg_dist2(fx, fy, bx, by, cx, cy); + if ((d1 < d2 ? d1 : d2) <= hw2) hits++; + } + if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S)); + } + } +} + +inline void draw_checkbox(Canvas& c, + const Rect& option, + const char* label, + CheckState state, + const Theme& theme, + bool enabled = true, + bool hovered = false) { + Rect box = checkbox_indicator_rect(option); + double radius = box.w * 0.22; + + Color fill, border, mark; + if (!enabled) { + fill = theme.disabled_bg; + border = theme.disabled_bg; + mark = theme.disabled_fg; + } else if (state == CHECK_OFF) { + fill = colors::WHITE; + border = hovered ? theme.accent : theme.border; + mark = theme.accent_fg; + } else { + fill = hovered ? theme.accent_hover : theme.accent; + border = fill; + mark = theme.accent_fg; + } + + if (state == CHECK_OFF) { + // Colored ring with a white interior. + checkbox_fill_round_aa(c, box, radius, border); + Rect inner = {box.x + 1, box.y + 1, box.w - 2, box.h - 2}; + checkbox_fill_round_aa(c, inner, radius - 1, fill); + } else { + checkbox_fill_round_aa(c, box, radius, fill); + } + + if (state == CHECK_ON) { + draw_check_mark(c, box, mark); + } else if (state == CHECK_MIXED) { + Rect dash = {box.x + 4, box.y + box.h / 2 - 1, box.w - 8, 2}; + checkbox_fill_round_aa(c, dash, 1, mark); + } + + if (label && label[0]) { + int fh = system_font_height(); + c.text(box.x + box.w + theme.gap_sm, + option.y + (option.h - fh) / 2, + label, enabled ? theme.text : theme.text_muted); + } +} + +inline Rect disclosure_rect(const Rect& option, int size = 16) { + return {option.x, option.y + (option.h - size) / 2, size, size}; +} + +inline void draw_disclosure(Canvas& c, + const Rect& box, + bool expanded, + const Theme& theme, + bool hovered = false) { + Color color = hovered ? theme.text : theme.text_subtle; + double cx = box.x + box.w / 2.0; + double cy = box.y + box.h / 2.0; + double s = box.w * 0.26; + if (s < 3) s = 3; + + // Triangle vertices (equilateral-ish), rotated per state. + double vx[3], vy[3]; + if (expanded) { // pointing down + vx[0] = cx - s; vy[0] = cy - s * 0.6; + vx[1] = cx + s; vy[1] = cy - s * 0.6; + vx[2] = cx; vy[2] = cy + s * 0.75; + } else { // pointing right + vx[0] = cx - s * 0.6; vy[0] = cy - s; + vx[1] = cx - s * 0.6; vy[1] = cy + s; + vx[2] = cx + s * 0.75; vy[2] = cy; + } + + // Anti-aliased fill via 4x4 supersampling of the triangle's half-plane test. + auto edge = [](double ax, double ay, double bx, double by, double px, double py) { + return (px - ax) * (by - ay) - (py - ay) * (bx - ax); + }; + const int S = 4; + for (int py = box.y; py < box.y + box.h; py++) { + if (py < 0 || py >= c.h) continue; + for (int px = box.x; px < box.x + box.w; px++) { + if (px < 0 || px >= c.w) continue; + int hits = 0; + for (int sy = 0; sy < S; sy++) + for (int sx = 0; sx < S; sx++) { + double fx = px + (sx + 0.5) / S; + double fy = py + (sy + 0.5) / S; + double e0 = edge(vx[0], vy[0], vx[1], vy[1], fx, fy); + double e1 = edge(vx[1], vy[1], vx[2], vy[2], fx, fy); + double e2 = edge(vx[2], vy[2], vx[0], vy[0], fx, fy); + bool inside = (e0 >= 0 && e1 >= 0 && e2 >= 0) || + (e0 <= 0 && e1 <= 0 && e2 <= 0); + if (inside) hits++; + } + if (hits) checkbox_blend_px(c, px, py, color, hits * 255 / (S * S)); + } + } +} + inline Rect swatch_rect(int row_x, int row_y, int index, int size = 24, int gap = 6) { return {row_x + index * (size + gap), row_y, size, size}; } diff --git a/programs/man/intro.1 b/programs/man/intro.1 index 95c9867..eb23526 100644 --- a/programs/man/intro.1 +++ b/programs/man/intro.1 @@ -52,13 +52,14 @@ 0:/icons/ Shared icons 0:/man/ Manual pages 0:/www/ Web server content - 0:/lib/ Lua and TinyCC toolchain payloads + 0:/sdk/ Montauk SDK (native gcc/binutils, TinyCC, + Lua, headers and libraries), when installed 0:/boot/ Kernel, bootloader, ramdisk image There is no 0:/games/, 0:/common/, 0:/home/, or 0:/etc/ -- these were used by earlier single-user releases and no longer - exist. Games and other GUI programs (including doom) ship as - bundles under 0:/apps/. + exist. 0:/lib/ was merged into 0:/sdk/. Games and other GUI + programs (including doom) ship as bundles under 0:/apps/. .SH SHELL The interactive shell is the primary way to interact with diff --git a/programs/man/legal.7 b/programs/man/legal.7 index 6dc31e3..92815c9 100644 --- a/programs/man/legal.7 +++ b/programs/man/legal.7 @@ -46,9 +46,9 @@ * Roboto font, Copyright The Roboto Project Authors - OFL-1.1 * C059 font (URW Base 35), Copyright (C) (URW)++ Design and Development GmbH - AGPLv3 with font-embedding exception - * Tiny C Compiler (tcc.elf, 0:/lib/tcc), Copyright (c) Fabrice Bellard and + * Tiny C Compiler (tcc.elf, 0:/sdk/tcc), Copyright (c) Fabrice Bellard and contributors - LGPL-2.1 - * Lua (lua.elf, 0:/lib/lua), Copyright (C) Lua.org, PUC-Rio - MIT + * Lua (lua.elf, 0:/sdk/lua), Copyright (C) Lua.org, PUC-Rio - MIT * Mozilla CA certificate bundle (0:/os/certs), Mozilla CA Certificate Program - MPL-2.0 * Intel Bluetooth firmware (0:/os/firmware/intel), Copyright (c) Intel diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp index 51a378e..74023dd 100644 --- a/programs/src/init/main.cpp +++ b/programs/src/init/main.cpp @@ -124,6 +124,13 @@ static void log_err(const char* msg) { log(LOG_ERR, msg); } // ---- Service runner ---- +static bool service_installed(const char* path) { + int fd = montauk::open(path); + if (fd < 0) return false; + montauk::close(fd); + return true; +} + static bool run_service(const char* path, const char* name, bool wait = true) { char msg[128]; @@ -156,7 +163,12 @@ extern "C" void _start() { // ---- Stage 1: Network configuration (non-blocking) ---- run_service("0:/os/dhcp.elf", "dhcp", false); - run_service("0:/os/printd.elf", "print spooler", false); + + // Printing is an optional install component; skip quietly when absent. + if (service_installed("0:/os/printd.elf")) + run_service("0:/os/printd.elf", "print spooler", false); + else + log_info("Print spooler not installed, skipping"); // ---- Stage 2: Login screen -> desktop (falls back to desktop, then shell) ---- if (!run_service("0:/os/login.elf", "login")) { diff --git a/programs/src/installer/Makefile b/programs/src/installer/Makefile index a679444..3dd00b3 100644 --- a/programs/src/installer/Makefile +++ b/programs/src/installer/Makefile @@ -52,7 +52,7 @@ LDFLAGS := \ # ---- Source files ---- -SRCS := main.cpp render.cpp actions.cpp stb_truetype_impl.cpp +SRCS := main.cpp render.cpp actions.cpp components.cpp stb_truetype_impl.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/installer/actions.cpp b/programs/src/installer/actions.cpp index bc3edcb..c189ee0 100644 --- a/programs/src/installer/actions.cpp +++ b/programs/src/installer/actions.cpp @@ -81,6 +81,96 @@ static void path_join(char* out, int outsize, const char* dir, const char* name) out[i] = '\0'; } +static const char* rel_basename(const char* rel) { + const char* base = rel; + for (int i = 0; rel[i]; i++) + if (rel[i] == '/') base = rel + i + 1; + return base; +} + +// ============================================================================ +// Filesystem probes and heap-backed directory listing +// ============================================================================ + +bool installer_path_exists(const char* abs_path) { + int fd = montauk::open(abs_path); + if (fd >= 0) { + montauk::close(fd); + return true; + } + const char* names[4]; + return montauk::readdir_at(abs_path, names, 4, 0) >= 0; +} + +void installer_free_dir_list(DirList* list) { + if (!list) return; + for (int i = 0; i < list->count; i++) + montauk::mfree(list->entries[i].rel); + montauk::mfree(list->entries); + list->entries = nullptr; + list->count = 0; +} + +// The kernel serves readdir strings from a small ring of scratch pages, so +// nested listings invalidate earlier results. Copy each batch to the heap +// and page with readdir_at so directories of any size enumerate fully. +bool installer_list_dir(const char* abs_dir, DirList* out) { + out->entries = nullptr; + out->count = 0; + + static constexpr int BATCH = 128; + const char* batch[BATCH]; + + int scanned = 0; + int capacity = 0; + + int got = montauk::readdir_at(abs_dir, batch, BATCH, 0); + if (got < 0) return false; + + while (got > 0) { + for (int i = 0; i < got; i++) { + const char* raw = batch[i]; + int len = slen(raw); + + bool is_dir = (len > 0 && raw[len - 1] == '/'); + if (is_dir) len--; + if (len <= 0) continue; + + // Skip "." and ".." + const char* base = raw; + for (int k = 0; k < len; k++) + if (raw[k] == '/') base = raw + k + 1; + int base_len = (int)(len - (base - raw)); + if (base_len == 1 && base[0] == '.') continue; + if (base_len == 2 && base[0] == '.' && base[1] == '.') continue; + + if (out->count >= capacity) { + int new_cap = capacity ? capacity * 2 : 64; + DirEntry* grown = (DirEntry*)montauk::malloc(new_cap * sizeof(DirEntry)); + if (!grown) { installer_free_dir_list(out); return false; } + for (int k = 0; k < out->count; k++) grown[k] = out->entries[k]; + montauk::mfree(out->entries); + out->entries = grown; + capacity = new_cap; + } + + char* rel = (char*)montauk::malloc(len + 1); + if (!rel) { installer_free_dir_list(out); return false; } + montauk::memcpy(rel, raw, len); + rel[len] = '\0'; + + out->entries[out->count].rel = rel; + out->entries[out->count].is_dir = is_dir; + out->count++; + } + + scanned += got; + got = montauk::readdir_at(abs_dir, batch, BATCH, scanned); + } + + return true; +} + // ============================================================================ // Copy a single file from src_path to dst_path // ============================================================================ @@ -159,113 +249,78 @@ static bool copy_file(const char* src_path, const char* dst_path) { static int g_files_copied; static int g_dirs_created; -// Copy all entries from src_dir (on ramdisk) to dst_dir (on target drive). +// Copy all entries from src_dir (on ramdisk) to dst_dir (on target drive), +// honoring the component selection: paths owned by an unchecked component +// are skipped, and directories are still entered when a checked component +// lives deeper inside (e.g. sdk/tcc with the rest of sdk deselected). // If skip_toplevel is non-null, skip that directory name at the top level. static bool copy_recursive(const char* src_dir, const char* dst_dir, const char* skip_toplevel = nullptr) { - const char* names[256]; - int count = montauk::readdir(src_dir, names, 256); - if (count < 0) return true; // not a directory or empty, skip + DirList list; + if (!installer_list_dir(src_dir, &list)) + return true; // not a directory, skip - // Ramdisk readdir returns names relative to the drive root with the - // full internal path (e.g., for readdir("0:/os"), names come back as - // "os/init.elf", "os/shell.elf", etc.). We need to strip the prefix - // that corresponds to the local path portion of src_dir. - // - // Find the local path after the "N:/" prefix. - const char* src_local = src_dir; - for (int k = 0; src_local[k]; k++) { - if (src_local[k] == ':') { - src_local += k + 1; - if (src_local[0] == '/') src_local++; - break; - } - } - int prefix_len = slen(src_local); - // If prefix is non-empty, we also skip the trailing '/' - if (prefix_len > 0 && src_local[prefix_len - 1] != '/') prefix_len++; + bool ok = true; - for (int i = 0; i < count; i++) { - const char* raw_name = names[i]; + for (int i = 0; i < list.count && ok; i++) { + const char* rel = list.entries[i].rel; + const char* base = rel_basename(rel); - // Strip prefix to get basename - const char* basename = raw_name; - if (prefix_len > 0 && slen(raw_name) > prefix_len) { - basename = raw_name + prefix_len; - } - - // Skip "." and ".." - if (basename[0] == '.' && (basename[1] == '\0' || basename[1] == '/')) continue; - if (basename[0] == '.' && basename[1] == '.' && (basename[2] == '\0' || basename[2] == '/')) continue; - - int blen = slen(basename); - - // Check if this is a directory (trailing '/') - bool is_dir = (blen > 0 && basename[blen - 1] == '/'); - - if (is_dir) { - // Strip trailing '/' for the name - char dir_name[256]; - int j = 0; - for (; j < blen - 1 && j < 255; j++) dir_name[j] = basename[j]; - dir_name[j] = '\0'; + char src_path[256]; + snprintf(src_path, sizeof(src_path), "0:/%s", rel); + if (list.entries[i].is_dir) { // Skip the installer app — no need on the installed system - if (strcmp(dir_name, "installer") == 0 && - strcmp(src_local, "apps") == 0) continue; + if (strcmp(rel, "apps/installer") == 0) continue; // Skip requested top-level directory - if (skip_toplevel && strcmp(dir_name, skip_toplevel) == 0) continue; + if (skip_toplevel && strcmp(base, skip_toplevel) == 0) continue; + + bool allowed = component_allows(rel); + if (!allowed && !component_subtree_needed(rel)) continue; - // Create directory on target char target_path[256]; - path_join(target_path, sizeof(target_path), dst_dir, dir_name); + path_join(target_path, sizeof(target_path), dst_dir, base); montauk::fmkdir(target_path); g_dirs_created++; char log_msg[64]; - snprintf(log_msg, sizeof(log_msg), " mkdir %s", dir_name); + snprintf(log_msg, sizeof(log_msg), " mkdir %s", base); add_log(log_msg); flush_ui(); - // Recurse into this directory - char src_subdir[256]; - path_join(src_subdir, sizeof(src_subdir), src_dir, dir_name); - - if (!copy_recursive(src_subdir, target_path)) - return false; + ok = copy_recursive(src_path, target_path); } else { // Skip ramdisk and limine.conf — installed system boots from // disk and gets a fresh config without the ramdisk module. // Skip setup.toml — live/setup environment config that should // not be present on the installed system. - if (strcmp(basename, "ramdisk.tar") == 0) continue; - if (strcmp(basename, "limine.conf") == 0) continue; - if (strcmp(basename, "setup.toml") == 0) continue; + if (strcmp(base, "ramdisk.tar") == 0) continue; + if (strcmp(base, "limine.conf") == 0) continue; + if (strcmp(base, "setup.toml") == 0) continue; - // It's a file — copy it - char src_path[256]; - // Build source path: drive prefix + raw_name (which is the full internal path) - // src_dir starts with "0:/" so we need "0:/" + raw_name - snprintf(src_path, sizeof(src_path), "0:/%s", raw_name); + if (!component_allows(rel)) continue; char dst_path[256]; - path_join(dst_path, sizeof(dst_path), dst_dir, basename); + path_join(dst_path, sizeof(dst_path), dst_dir, base); char log_msg[64]; - snprintf(log_msg, sizeof(log_msg), " copy %s", basename); + snprintf(log_msg, sizeof(log_msg), " copy %s", base); add_log(log_msg); flush_ui(); - if (!copy_file(src_path, dst_path)) - return false; + if (!copy_file(src_path, dst_path)) { + ok = false; + break; + } g_files_copied++; } } - return true; + installer_free_dir_list(&list); + return ok; } // ============================================================================ @@ -412,8 +467,7 @@ static void install_efi_ext2(int disk) { add_log(" Mounted"); flush_ui(); - char efi_root[8], ext2_root[8]; - snprintf(efi_root, sizeof(efi_root), "%d:/", efi_drive); + char ext2_root[8]; snprintf(ext2_root, sizeof(ext2_root), "%d:/", root_drive); // Step 8: Create EFI boot directory structure @@ -705,13 +759,14 @@ void do_update() { add_log("ERROR: Mount failed"); flush_ui(); st.step = STEP_ERROR; return; } - - char drive_root[8]; - snprintf(drive_root, sizeof(drive_root), "%d:/", drive_num); add_log(" Mounted"); flush_ui(); - // Step 2: Update os/ — remove old and copy fresh + // Step 2: Detect which optional components the target has, so the + // update refreshes those without re-adding deselected ones. + components_set_from_target(drive_num); + + // Step 3: Update os/ — overwrite existing and add new add_log("Updating os/..."); flush_ui(); @@ -724,7 +779,7 @@ void do_update() { flush_ui(); st.step = STEP_ERROR; return; } - // Step 3: Update apps/ — copy all, overwriting existing and adding new + // Step 4: Update apps/ — copy all, overwriting existing and adding new add_log("Updating apps/..."); flush_ui(); @@ -736,16 +791,18 @@ void do_update() { flush_ui(); st.step = STEP_ERROR; return; } - // Step 4: Update lib/ — compiler runtime, headers, libc - add_log("Updating lib/..."); - flush_ui(); + // Step 5: Update sdk/ — dev tools, headers, tcc, lua + if (component_allows("sdk") || component_subtree_needed("sdk")) { + add_log("Updating sdk/..."); + flush_ui(); - snprintf(path_buf, sizeof(path_buf), "%d:/lib", drive_num); - montauk::fmkdir(path_buf); + snprintf(path_buf, sizeof(path_buf), "%d:/sdk", drive_num); + montauk::fmkdir(path_buf); - if (!copy_recursive("0:/lib", path_buf)) { - add_log("ERROR: Failed to update lib/"); - flush_ui(); st.step = STEP_ERROR; return; + if (!copy_recursive("0:/sdk", path_buf)) { + add_log("ERROR: Failed to update sdk/"); + flush_ui(); st.step = STEP_ERROR; return; + } } char copy_msg[64]; diff --git a/programs/src/installer/components.cpp b/programs/src/installer/components.cpp new file mode 100644 index 0000000..66e5a4a --- /dev/null +++ b/programs/src/installer/components.cpp @@ -0,0 +1,439 @@ +/* + * components.cpp + * MontaukOS Installer — optional component selection + * Copyright (c) 2026 Daniel Hammer + */ + +#include "installer.h" + +// ============================================================================ +// Component table +// ============================================================================ + +enum ComponentIndex { + CI_SDK = 0, + CI_GCC, + CI_BINUTILS, + CI_TCC, + CI_LUA, + CI_GAMES, + CI_DOOM, + CI_2048, + CI_RPGDEMO, + CI_MANDELBROT, + CI_OFFICE, + CI_WORDPROCESSOR, + CI_SPREADSHEET, + CI_PDFVIEWER, + CI_INTERNET, + CI_WIKIPEDIA, + CI_WEATHER, + CI_IRC, + CI_PRINTING, + CI_HTTPD, + CI_SDR, + CI_COUNT, +}; + +Component g_components[] = { + { "sdk", "Montauk SDK", + "Native development kit under 0:/sdk: headers, libc, libstdc++ and tools", + -1, "sdk/include", {"sdk", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "gcc", "GNU C/C++ compiler (gcc, g++)", + "Native GCC 14 with C and C++ support (needs binutils)", + CI_SDK, "sdk/bin/gcc.elf", + {"sdk/bin/gcc.elf", "sdk/bin/g++.elf", "sdk/bin/cpp.elf", + "sdk/libexec", "sdk/lib/gcc", nullptr}, + true, CI_BINUTILS, + false, false, false, false, 0 }, + + { "binutils", "GNU binutils (as, ld, ar, ...)", + "Assembler, linker and object file tools", + CI_SDK, "sdk/bin/as.elf", + {"sdk/bin/as.elf", "sdk/bin/ld.elf", "sdk/bin/ar.elf", "sdk/bin/nm.elf", + "sdk/bin/objcopy.elf", "sdk/bin/objdump.elf", "sdk/bin/readelf.elf", + "sdk/bin/ranlib.elf", "sdk/bin/strip.elf", "sdk/bin/size.elf", + "sdk/bin/strings.elf", "sdk/bin/addr2line.elf", "sdk/bin/c++filt.elf", + "sdk/bin/elfedit.elf", nullptr}, + true, -1, + false, false, false, false, 0 }, + + { "tcc", "Tiny C Compiler", + "Lightweight single-pass C compiler (tcc) with its own headers", + CI_SDK, "sdk/tcc", {"sdk/tcc", "os/tcc.elf", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "lua", "Lua 5.4", + "Lua interpreter (lua) with static library and headers", + CI_SDK, "sdk/lua", {"sdk/lua", "os/lua.elf", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "games", "Games & demos", + "Games and graphics demos", + -1, nullptr, {nullptr}, false, -1, + false, false, false, false, 0 }, + + { "doom", "DOOM", + "id Software's DOOM on the doomgeneric engine", + CI_GAMES, "apps/doom", {"apps/doom", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "2048", "2048", + "Sliding tile puzzle game", + CI_GAMES, "apps/2048", {"apps/2048", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "rpgdemo", "RPG demo", + "Top-down RPG game engine demo", + CI_GAMES, "apps/rpgdemo", {"apps/rpgdemo", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "mandelbrot", "Mandelbrot", + "Fractal explorer", + CI_GAMES, "apps/mandelbrot", {"apps/mandelbrot", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "office", "Office & productivity", + "Document, spreadsheet and PDF applications", + -1, nullptr, {nullptr}, false, -1, + false, false, false, false, 0 }, + + { "wordprocessor", "Word Processor", + "Rich text documents with printing support", + CI_OFFICE, "apps/wordprocessor", {"apps/wordprocessor", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "spreadsheet", "Spreadsheet", + "Formula-based spreadsheet", + CI_OFFICE, "apps/spreadsheet", {"apps/spreadsheet", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "pdfviewer", "PDF Viewer", + "PDF document viewer", + CI_OFFICE, "apps/pdfviewer", {"apps/pdfviewer", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "internet", "Internet apps", + "Optional network applications", + -1, nullptr, {nullptr}, false, -1, + false, false, false, false, 0 }, + + { "wikipedia", "Wikipedia", + "Wikipedia reader (GUI app and wiki CLI)", + CI_INTERNET, "apps/wikipedia", {"apps/wikipedia", "os/wiki.elf", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "weather", "Weather", + "Weather forecast app", + CI_INTERNET, "apps/weather", {"apps/weather", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "irc", "IRC client", + "Command-line IRC client", + CI_INTERNET, "os/irc.elf", {"os/irc.elf", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "printing", "Printing support", + "Print spooler daemon (printd), printer setup app and print tools", + -1, "os/printd.elf", + {"os/printd.elf", "os/printctl.elf", "os/printersapplet.lib", + "apps/printers", nullptr}, + false, -1, + false, false, false, false, 0 }, + + { "httpd", "HTTP server (experimental)", + "Experimental web server (httpd) with 0:/www content", + -1, "os/httpd.elf", {"os/httpd.elf", "www", nullptr}, false, -1, + false, false, false, false, 0 }, + + { "sdr", "SDR tools (experimental)", + "RTL-SDR software defined radio demo (sdr CLI)", + -1, "os/sdr.elf", {"os/sdr.elf", nullptr}, false, -1, + false, false, false, false, 0 }, +}; + +const int g_component_count = CI_COUNT; + +static bool g_sizes_ready = false; + +// ============================================================================ +// Helpers +// ============================================================================ + +static bool comp_is_group(int idx) { + return g_components[idx].paths[0] == nullptr; +} + +bool component_has_children(int idx) { + for (int i = 0; i < g_component_count; i++) + if (g_components[i].parent == idx) return true; + return false; +} + +// True when rel equals prefix or lies inside the prefix directory. +static bool path_under(const char* prefix, const char* rel) { + int pl = 0; + while (prefix[pl]) { + if (rel[pl] != prefix[pl]) return false; + pl++; + } + return rel[pl] == '\0' || rel[pl] == '/'; +} + +static int str_len(const char* s) { + int n = 0; + while (s[n]) n++; + return n; +} + +// Component owning rel, resolved by the longest matching owned path. This is +// what lets "sdk/tcc" stay installable while the rest of "sdk" is skipped. +static int component_owning(const char* rel) { + int best = -1, best_len = -1; + for (int i = 0; i < g_component_count; i++) { + for (int p = 0; g_components[i].paths[p]; p++) { + const char* path = g_components[i].paths[p]; + if (!path_under(path, rel)) continue; + int len = str_len(path); + if (len > best_len) { best_len = len; best = i; } + } + } + return best; +} + +bool component_allows(const char* rel) { + int owner = component_owning(rel); + return owner < 0 || g_components[owner].checked; +} + +bool component_subtree_needed(const char* rel) { + int rel_len = str_len(rel); + for (int i = 0; i < g_component_count; i++) { + if (!g_components[i].checked) continue; + for (int p = 0; g_components[i].paths[p]; p++) { + const char* path = g_components[i].paths[p]; + if (str_len(path) > rel_len && path_under(rel, path)) return true; + } + } + return false; +} + +// ============================================================================ +// Presence detection and defaults +// ============================================================================ + +static bool rel_exists(const char* rel) { + char abs[160]; + snprintf(abs, sizeof(abs), "0:/%s", rel); + return installer_path_exists(abs); +} + +void components_init() { + for (int i = 0; i < g_component_count; i++) { + Component& c = g_components[i]; + c.expanded = false; + c.bytes = 0; + c.base_present = c.probe ? rel_exists(c.probe) : false; + c.present = c.base_present; + c.checked = c.base_present; + } + + // A parent counts as present when its own payload or any child exists. + for (int i = 0; i < g_component_count; i++) { + int p = g_components[i].parent; + if (p >= 0 && g_components[i].present) + g_components[p].present = true; + } + + g_sizes_ready = false; +} + +// ============================================================================ +// Check state and toggling +// ============================================================================ + +mtk::CheckState component_state(int idx) { + const Component& c = g_components[idx]; + if (!component_has_children(idx)) + return mtk::check_state(c.checked); + + int total = 0, on = 0; + if (c.base_present) { total++; if (c.checked) on++; } + for (int i = 0; i < g_component_count; i++) { + if (g_components[i].parent != idx || !g_components[i].present) continue; + total++; + if (g_components[i].checked) on++; + } + + if (total == 0 || on == 0) return mtk::CHECK_OFF; + if (on == total) return mtk::CHECK_ON; + return mtk::CHECK_MIXED; +} + +void component_toggle(int idx) { + Component& c = g_components[idx]; + if (!c.present) return; + + if (component_has_children(idx)) { + bool on = component_state(idx) != mtk::CHECK_ON; + c.checked = on && c.base_present; + for (int i = 0; i < g_component_count; i++) { + if (g_components[i].parent == idx && g_components[i].present) + g_components[i].checked = on; + } + return; + } + + c.checked = !c.checked; + + if (c.checked) { + if (c.depends_on >= 0 && g_components[c.depends_on].present) + g_components[c.depends_on].checked = true; + if (c.requires_parent && c.parent >= 0 && + g_components[c.parent].base_present) + g_components[c.parent].checked = true; + } else { + // Drop anything that declared a dependency on this component. + for (int i = 0; i < g_component_count; i++) { + if (g_components[i].depends_on == idx) + g_components[i].checked = false; + } + } +} + +int component_visible_rows(int* out, int max) { + int count = 0; + for (int i = 0; i < g_component_count && count < max; i++) { + int p = g_components[i].parent; + if (p >= 0 && !g_components[p].expanded) continue; + out[count++] = i; + } + return count; +} + +// ============================================================================ +// Size measurement +// ============================================================================ + +static uint64_t file_size_abs(const char* abs) { + int fd = montauk::open(abs); + if (fd < 0) return 0; + uint64_t size = montauk::getsize(fd); + montauk::close(fd); + return size; +} + +// Recursive size of rel, counting only entries owned by component `owner` +// (entries claimed by a more specific component are counted there instead). +static uint64_t du_walk(const char* rel, int owner) { + if (component_owning(rel) != owner) return 0; + + char abs[192]; + snprintf(abs, sizeof(abs), "0:/%s", rel); + + DirList list; + if (installer_list_dir(abs, &list)) { + uint64_t sum = 0; + for (int i = 0; i < list.count; i++) + sum += du_walk(list.entries[i].rel, owner); + installer_free_dir_list(&list); + return sum; + } + + return file_size_abs(abs); +} + +void components_compute_sizes() { + if (g_sizes_ready) return; + + for (int i = 0; i < g_component_count; i++) { + Component& c = g_components[i]; + c.bytes = 0; + if (!c.present || comp_is_group(i)) continue; + for (int p = 0; c.paths[p]; p++) + c.bytes += du_walk(c.paths[p], i); + } + + // Pure groups display the sum of their children. + for (int i = 0; i < g_component_count; i++) { + if (!comp_is_group(i)) continue; + for (int j = 0; j < g_component_count; j++) + if (g_components[j].parent == i) + g_components[i].bytes += g_components[j].bytes; + } + + g_sizes_ready = true; +} + +bool components_sizes_ready() { + return g_sizes_ready; +} + +uint64_t components_selected_bytes() { + uint64_t sum = 0; + for (int i = 0; i < g_component_count; i++) { + if (comp_is_group(i)) continue; + if (g_components[i].checked) sum += g_components[i].bytes; + } + return sum; +} + +uint64_t components_unselected_bytes() { + uint64_t sum = 0; + for (int i = 0; i < g_component_count; i++) { + if (comp_is_group(i)) continue; + if (g_components[i].present && !g_components[i].checked) + sum += g_components[i].bytes; + } + return sum; +} + +// ============================================================================ +// Summaries and the update flow +// ============================================================================ + +void components_excluded_summary(char* buf, int bufsize) { + int excluded = 0; + int named = 0; + int pos = 0; + buf[0] = '\0'; + + for (int i = 0; i < g_component_count; i++) { + if (comp_is_group(i)) continue; + if (!g_components[i].present || g_components[i].checked) continue; + excluded++; + if (named < 3) { + pos += snprintf(buf + pos, bufsize - pos, "%s%s", + named ? ", " : "Skipping: ", + g_components[i].label); + named++; + } + } + + if (excluded == 0) { + snprintf(buf, bufsize, "All optional components selected"); + } else if (excluded > named) { + snprintf(buf + pos, bufsize - pos, " (+%d more)", excluded - named); + } +} + +// For the update flow: refresh only the optional components the target +// already has; never re-add ones that were deselected at install time. +void components_set_from_target(int drive) { + for (int i = 0; i < g_component_count; i++) { + Component& c = g_components[i]; + if (!c.probe) continue; + char abs[160]; + snprintf(abs, sizeof(abs), "%d:/%s", drive, c.probe); + c.checked = c.present && installer_path_exists(abs); + + // Migration: pre-sdk installs kept tcc/lua under 0:/lib. + if (!c.checked && c.present && + (strcmp(c.id, "tcc") == 0 || strcmp(c.id, "lua") == 0)) { + snprintf(abs, sizeof(abs), "%d:/lib/%s", drive, c.id); + c.checked = installer_path_exists(abs); + } + } +} diff --git a/programs/src/installer/installer.h b/programs/src/installer/installer.h index acdda21..e408d0d 100644 --- a/programs/src/installer/installer.h +++ b/programs/src/installer/installer.h @@ -10,7 +10,8 @@ #include #include #include -#include +#include +#include extern "C" { #include @@ -23,34 +24,20 @@ using namespace gui; // Constants // ============================================================================ -static constexpr int INIT_W = 520; -static constexpr int INIT_H = 440; -static constexpr int TOOLBAR_H = 36; -static constexpr int STEP_BAR_H = 32; -static constexpr int CONTENT_TOP = TOOLBAR_H + STEP_BAR_H; +static constexpr int INIT_W = 560; +static constexpr int INIT_H = 500; +static constexpr int HEADER_H = 44; +static constexpr int STEP_BAR_H = 30; +static constexpr int CONTENT_TOP = HEADER_H + STEP_BAR_H; +static constexpr int BOTTOM_H = 48; +static constexpr int PAD = 16; +static constexpr int BTN_H = 30; static constexpr int MAX_DISKS = 32; static constexpr int MAX_PARTS = 32; -static constexpr int STATUS_H = 26; -// Font sizes — adjusted at runtime by apply_scale() -extern int FONT_SIZE; -extern int FONT_SM; - -static constexpr int TB_BTN_Y = 5; -static constexpr int TB_BTN_H = 26; -static constexpr int TB_BTN_RAD = 8; - -static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); -static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); -static constexpr Color BORDER_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC); -static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22); -static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66); -static constexpr Color FAINT_TEXT = Color::from_rgb(0x88, 0x88, 0x88); -static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); -static constexpr Color ACCENT = Color::from_rgb(0x33, 0x66, 0xCC); -static constexpr Color DANGER = Color::from_rgb(0xCC, 0x33, 0x33); -static constexpr Color SUCCESS_COLOR = Color::from_rgb(0x33, 0x99, 0x33); -static constexpr Color DISABLED_BG = Color::from_rgb(0xBB, 0xBB, 0xBB); +static constexpr int COMP_ROW_H = 28; +static constexpr int COMP_INDENT = 26; +static constexpr int COMP_TOGGLE_W = 18; // ============================================================================ // Install steps @@ -65,6 +52,7 @@ enum InstallStep { STEP_MODE_SELECT, STEP_SELECT_DISK, STEP_PARTITION_SCHEME, + STEP_COMPONENTS, STEP_CONFIRM, STEP_INSTALLING, STEP_DONE, @@ -95,6 +83,55 @@ static const char* scheme_descs[] = { "One partition for boot, OS, and data. Simple and compatible.", }; +// ============================================================================ +// Optional components +// ============================================================================ + +static constexpr int MAX_COMPONENT_PATHS = 16; + +struct Component { + const char* id; + const char* label; + const char* desc; // shown in the status bar on hover + int parent; // index of parent component, or -1 for top level + // Path that identifies this component in an image / on a target drive. + const char* probe; + // Files or directories (relative to the drive root, no "0:/" prefix) + // owned by this component; nullptr-terminated. Empty for pure groups. + const char* paths[MAX_COMPONENT_PATHS]; + // A child that needs its parent's base payload (e.g. gcc needs the + // SDK sysroot). Checking it re-checks the parent. + bool requires_parent; + // Another component this one cannot work without (e.g. gcc drives the + // binutils assembler/linker), or -1. + int depends_on; + + // Runtime state + bool checked; + bool expanded; // parents only + bool present; // this component (or a child) exists in the image + bool base_present; // own probe exists (parents with own payload) + uint64_t bytes; // exclusive payload size in the source image +}; + +extern Component g_components[]; +extern const int g_component_count; + +// components.cpp +void components_init(); +void components_compute_sizes(); +bool component_has_children(int idx); +mtk::CheckState component_state(int idx); +void component_toggle(int idx); +int component_visible_rows(int* out, int max); +bool component_allows(const char* rel); +bool component_subtree_needed(const char* rel); +void components_set_from_target(int drive); +uint64_t components_selected_bytes(); +uint64_t components_unselected_bytes(); +void components_excluded_summary(char* buf, int bufsize); +bool components_sizes_ready(); + // ============================================================================ // State // ============================================================================ @@ -120,8 +157,12 @@ struct InstallerState { char log[LOG_LINES][LOG_LINE_LEN]; int log_count; - char status[80]; + char status[96]; uint64_t status_time; + + int mouse_x; + int mouse_y; + int comp_scroll; }; // ============================================================================ @@ -130,10 +171,68 @@ struct InstallerState { extern int g_win_w, g_win_h; extern InstallerState g_state; -extern TrueTypeFont* g_font; -extern uint32_t* g_pixels; -extern uint32_t* g_backbuf; -extern int g_win_id; +extern Color g_accent; + +// ============================================================================ +// Shared layout helpers +// ============================================================================ + +inline int content_x() { return PAD; } +inline int content_w() { return g_win_w - PAD * 2; } +inline int content_bottom() { return g_win_h - BOTTOM_H; } + +// Heading block: title line + smaller subtitle line +inline int heading_bottom() { + return CONTENT_TOP + 14 + system_font_height() * 2 + 16; +} + +inline Rect bottom_bar_rect() { + return {0, g_win_h - BOTTOM_H, g_win_w, BOTTOM_H}; +} + +inline Rect primary_btn_rect() { + int y = g_win_h - BOTTOM_H + (BOTTOM_H - BTN_H) / 2; + return {g_win_w - PAD - 108, y, 108, BTN_H}; +} + +inline Rect secondary_btn_rect() { + Rect p = primary_btn_rect(); + return {p.x - 8 - 96, p.y, 96, BTN_H}; +} + +inline Rect tertiary_btn_rect() { + Rect s = secondary_btn_rect(); + return {s.x - 8 - 84, s.y, 84, BTN_H}; +} + +// Mode select cards +inline Rect mode_card_rect(int index) { + int card_h = 56; + int y = heading_bottom() + index * (card_h + 10); + return {content_x(), y, content_w(), card_h}; +} + +// Disk / partition / scheme list rows +inline Rect list_row_rect(int index, int row_h) { + int y = heading_bottom() + index * (row_h + 6); + return {content_x(), y, content_w(), row_h}; +} + +static constexpr int DISK_ROW_H = 48; +static constexpr int SCHEME_ROW_H = 50; +static constexpr int PART_ROW_H = 48; + +// Component tree viewport +inline Rect components_list_rect() { + int top = heading_bottom(); + return {content_x(), top, content_w(), content_bottom() - top - 8}; +} + +inline Rect component_row_rect(int visible_index) { + Rect list = components_list_rect(); + return {list.x, list.y + visible_index * COMP_ROW_H - g_state.comp_scroll, + list.w, COMP_ROW_H}; +} // ============================================================================ // Function declarations — helpers (main.cpp) @@ -143,7 +242,7 @@ void set_status(const char* msg); void add_log(const char* msg); void flush_ui(); void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorSize); -void apply_scale(int scale); +void format_bytes(char* buf, int bufsize, uint64_t bytes); // ============================================================================ // Function declarations — render.cpp @@ -159,3 +258,21 @@ void installer_refresh_disks(); void installer_refresh_parts(); void do_install(); void do_update(); +bool installer_path_exists(const char* abs_path); + +// Heap-backed directory listing. Entries are full paths relative to the +// drive root (e.g. "apps/doom"), with is_dir derived from the trailing '/' +// the VFS appends to directory names. Survives nested listings, unlike the +// kernel's rotating readdir scratch pages. +struct DirEntry { + char* rel; // path relative to drive root, no trailing '/' + bool is_dir; +}; + +struct DirList { + DirEntry* entries; + int count; +}; + +bool installer_list_dir(const char* abs_dir, DirList* out); +void installer_free_dir_list(DirList* list); diff --git a/programs/src/installer/main.cpp b/programs/src/installer/main.cpp index 6d231a5..41b7dd9 100644 --- a/programs/src/installer/main.cpp +++ b/programs/src/installer/main.cpp @@ -5,6 +5,7 @@ */ #include "installer.h" +#include #include // ============================================================================ @@ -14,23 +15,10 @@ int g_win_w = INIT_W; int g_win_h = INIT_H; -int FONT_SIZE = 18; -int FONT_SM = 14; - InstallerState g_state; -TrueTypeFont* g_font = nullptr; -uint32_t* g_pixels = nullptr; -uint32_t* g_backbuf = nullptr; -int g_win_id = -1; -static WsWindow g_window; +Color g_accent = colors::ACCENT; -void apply_scale(int scale) { - switch (scale) { - case 0: FONT_SIZE = 14; FONT_SM = 11; break; - case 2: FONT_SIZE = 22; FONT_SM = 17; break; - default: FONT_SIZE = 18; FONT_SM = 14; break; - } -} +static WsWindow g_window; // ============================================================================ // Helpers @@ -38,7 +26,8 @@ void apply_scale(int scale) { void set_status(const char* msg) { int i = 0; - for (; i < 79 && msg[i]; i++) g_state.status[i] = msg[i]; + for (; i < (int)sizeof(g_state.status) - 1 && msg[i]; i++) + g_state.status[i] = msg[i]; g_state.status[i] = '\0'; g_state.status_time = montauk::get_milliseconds(); } @@ -58,9 +47,8 @@ void add_log(const char* msg) { } void flush_ui() { - if (g_pixels && g_backbuf && g_window.id >= 0) { - render(g_backbuf); - montauk::memcpy(g_pixels, g_backbuf, g_win_w * g_win_h * 4); + if (g_window.pixels && g_window.id >= 0) { + render(g_window.pixels); g_window.present(); } } @@ -71,13 +59,95 @@ void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorS if (gb >= 1024) { uint64_t tb = gb / 1024; uint64_t frac = ((gb % 1024) * 10) / 1024; - snprintf(buf, bufsize, "%lu.%lu TB", (unsigned)tb, (unsigned)frac); + snprintf(buf, bufsize, "%lu.%lu TB", (unsigned long)tb, (unsigned long)frac); } else if (gb > 0) { uint64_t frac = ((bytes % (1024ULL * 1024 * 1024)) * 10) / (1024ULL * 1024 * 1024); - snprintf(buf, bufsize, "%lu.%lu GB", (unsigned)gb, (unsigned)frac); + snprintf(buf, bufsize, "%lu.%lu GB", (unsigned long)gb, (unsigned long)frac); } else { uint64_t mb = bytes / (1024ULL * 1024); - snprintf(buf, bufsize, "%lu MB", (unsigned)mb); + snprintf(buf, bufsize, "%lu MB", (unsigned long)mb); + } +} + +void format_bytes(char* buf, int bufsize, uint64_t bytes) { + if (bytes >= 1024ULL * 1024 * 1024) { + uint64_t gb = bytes / (1024ULL * 1024 * 1024); + uint64_t frac = ((bytes % (1024ULL * 1024 * 1024)) * 10) / (1024ULL * 1024 * 1024); + snprintf(buf, bufsize, "%lu.%lu GB", (unsigned long)gb, (unsigned long)frac); + } else if (bytes >= 1024 * 1024) { + snprintf(buf, bufsize, "%lu MB", (unsigned long)(bytes / (1024 * 1024))); + } else if (bytes >= 1024) { + snprintf(buf, bufsize, "%lu KB", (unsigned long)(bytes / 1024)); + } else { + snprintf(buf, bufsize, "%lu B", (unsigned long)bytes); + } +} + +// ============================================================================ +// Step transitions +// ============================================================================ + +static void clamp_comp_scroll() { + int vis[32]; + int n = component_visible_rows(vis, 32); + Rect list = components_list_rect(); + int max_scroll = n * COMP_ROW_H - list.h; + if (max_scroll < 0) max_scroll = 0; + // Round up to a row boundary so the top visible row stays aligned. + max_scroll = ((max_scroll + COMP_ROW_H - 1) / COMP_ROW_H) * COMP_ROW_H; + if (g_state.comp_scroll > max_scroll) g_state.comp_scroll = max_scroll; + if (g_state.comp_scroll < 0) g_state.comp_scroll = 0; +} + +static void goto_components() { + g_state.step = STEP_COMPONENTS; + g_state.comp_scroll = 0; + if (!components_sizes_ready()) { + set_status("Measuring component sizes..."); + flush_ui(); + components_compute_sizes(); + set_status(""); + } +} + +static void step_advance() { + auto& st = g_state; + switch (st.step) { + case STEP_SELECT_DISK: + if (st.selected_disk >= 0) st.step = STEP_PARTITION_SCHEME; + break; + case STEP_PARTITION_SCHEME: + goto_components(); + break; + case STEP_COMPONENTS: + st.step = STEP_CONFIRM; + break; + case STEP_CONFIRM: + st.step = STEP_INSTALLING; + st.log_count = 0; + break; + case STEP_UPDATE_SELECT_PART: + if (st.selected_part >= 0) st.step = STEP_UPDATE_CONFIRM; + break; + case STEP_UPDATE_CONFIRM: + st.step = STEP_UPDATING; + st.log_count = 0; + break; + default: + break; + } +} + +static void step_back() { + auto& st = g_state; + switch (st.step) { + case STEP_SELECT_DISK: st.step = STEP_MODE_SELECT; break; + case STEP_PARTITION_SCHEME: st.step = STEP_SELECT_DISK; break; + case STEP_COMPONENTS: st.step = STEP_PARTITION_SCHEME; break; + case STEP_CONFIRM: st.step = STEP_COMPONENTS; break; + case STEP_UPDATE_SELECT_PART: st.step = STEP_MODE_SELECT; break; + case STEP_UPDATE_CONFIRM: st.step = STEP_UPDATE_SELECT_PART; break; + default: break; } } @@ -85,166 +155,141 @@ void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorS // Mouse handling // ============================================================================ +// Returns false to signal quit. static bool handle_click(int mx, int my) { auto& st = g_state; - int fh = g_font ? g_font->get_cache(FONT_SIZE)->ascent - g_font->get_cache(FONT_SIZE)->descent : 16; - int fh_sm = g_font ? g_font->get_cache(FONT_SM)->ascent - g_font->get_cache(FONT_SM)->descent : 12; - // Common bottom button area - int btn_w = 120, btn_h = 34; - int btn_y = g_win_h - STATUS_H - btn_h - 12; + Rect primary = primary_btn_rect(); + Rect secondary = secondary_btn_rect(); + Rect tertiary = tertiary_btn_rect(); - if (st.step == STEP_MODE_SELECT) { - // Two mode cards - int y = CONTENT_TOP + 16 + fh + 4 + fh_sm + 16; - int card_h = 52, card_x = 16, card_w = g_win_w - 32; - - // Install card - if (mx >= card_x && mx < card_x + card_w && my >= y && my < y + card_h) { + switch (st.step) { + case STEP_MODE_SELECT: + if (mode_card_rect(0).contains(mx, my)) { st.mode = MODE_INSTALL; installer_refresh_disks(); st.step = STEP_SELECT_DISK; - return true; - } - y += card_h + 8; - - // Update card - if (mx >= card_x && mx < card_x + card_w && my >= y && my < y + card_h) { + } else if (mode_card_rect(1).contains(mx, my)) { st.mode = MODE_UPDATE; installer_refresh_parts(); st.step = STEP_UPDATE_SELECT_PART; - return true; } - } else if (st.step == STEP_SELECT_DISK) { - // Disk list items - int y = CONTENT_TOP + 40 + fh + 12; + break; + + case STEP_SELECT_DISK: for (int i = 0; i < st.disk_count; i++) { - int item_h = 48; - if (mx >= 16 && mx < g_win_w - 16 && my >= y && my < y + item_h) { + if (list_row_rect(i, DISK_ROW_H).contains(mx, my)) { st.selected_disk = i; return true; } - y += item_h + 4; } - - // "Next" button - int next_x = g_win_w - btn_w - 16; - if (st.selected_disk >= 0 && - mx >= next_x && mx < next_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_PARTITION_SCHEME; - return true; - } - - // "Refresh" button - int ref_w = 80; - int ref_x = next_x - ref_w - 8; - if (mx >= ref_x && mx < ref_x + ref_w && - my >= btn_y && my < btn_y + btn_h) { + if (primary.contains(mx, my)) step_advance(); + else if (secondary.contains(mx, my)) step_back(); + else if (tertiary.contains(mx, my)) { installer_refresh_disks(); - return true; + set_status("Disk list refreshed"); } - } else if (st.step == STEP_PARTITION_SCHEME) { - // Scheme radio buttons - int y = CONTENT_TOP + 40 + fh + 12; + break; + + case STEP_PARTITION_SCHEME: for (int i = 0; i < SCHEME_COUNT; i++) { - int item_h = 52; - if (mx >= 16 && mx < g_win_w - 16 && my >= y && my < y + item_h) { + if (list_row_rect(i, SCHEME_ROW_H).contains(mx, my)) { st.partition_scheme = i; return true; } - y += item_h + 4; } + if (primary.contains(mx, my)) step_advance(); + else if (secondary.contains(mx, my)) step_back(); + break; - // "Next" button - int next_x = g_win_w - btn_w - 16; - if (mx >= next_x && mx < next_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_CONFIRM; + case STEP_COMPONENTS: { + int vis[32]; + int n = component_visible_rows(vis, 32); + Rect list = components_list_rect(); + for (int k = 0; k < n; k++) { + Rect row = component_row_rect(k); + if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue; + if (!row.contains(mx, my)) continue; + + int idx = vis[k]; + bool child = g_components[idx].parent >= 0; + int x = row.x + 4 + (child ? COMP_INDENT : 0); + + if (component_has_children(idx) && mx < x + COMP_TOGGLE_W + 4) { + g_components[idx].expanded = !g_components[idx].expanded; + clamp_comp_scroll(); + } else { + component_toggle(idx); + } return true; } + if (primary.contains(mx, my)) step_advance(); + else if (secondary.contains(mx, my)) step_back(); + break; + } - // "Back" button - int back_x = next_x - btn_w - 8; - if (mx >= back_x && mx < back_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_SELECT_DISK; - return true; - } - } else if (st.step == STEP_CONFIRM) { - int center_x = g_win_w / 2; - int gap = 16; + case STEP_CONFIRM: + case STEP_UPDATE_CONFIRM: + if (primary.contains(mx, my)) step_advance(); + else if (secondary.contains(mx, my)) step_back(); + break; - // "Install" button - int conf_x = center_x - btn_w - gap / 2; - if (mx >= conf_x && mx < conf_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_INSTALLING; - st.log_count = 0; - return true; - } - - // "Back" button - int canc_x = center_x + gap / 2; - if (mx >= canc_x && mx < canc_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_PARTITION_SCHEME; - return true; - } - } else if (st.step == STEP_DONE || st.step == STEP_ERROR) { - int close_x = (g_win_w - 100) / 2; - if (mx >= close_x && mx < close_x + 100 && - my >= btn_y && my < btn_y + btn_h) { - return false; // signal quit - } - } else if (st.step == STEP_UPDATE_SELECT_PART) { - // Partition list items - int y = CONTENT_TOP + 16 + fh + 4 + fh_sm + 12; + case STEP_UPDATE_SELECT_PART: for (int i = 0; i < st.part_count; i++) { - int item_h = 48; - if (mx >= 16 && mx < g_win_w - 16 && my >= y && my < y + item_h) { + if (list_row_rect(i, PART_ROW_H).contains(mx, my)) { st.selected_part = i; return true; } - y += item_h + 4; } + if (primary.contains(mx, my)) step_advance(); + else if (secondary.contains(mx, my)) step_back(); + break; - // "Next" button - int next_x = g_win_w - btn_w - 16; - if (st.selected_part >= 0 && - mx >= next_x && mx < next_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_UPDATE_CONFIRM; - return true; - } + case STEP_DONE: + case STEP_ERROR: + if (primary.contains(mx, my)) return false; + break; - // "Back" button - int back_x = next_x - btn_w - 8; - if (mx >= back_x && mx < back_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_MODE_SELECT; - return true; - } - } else if (st.step == STEP_UPDATE_CONFIRM) { - int center_x = g_win_w / 2; - int gap = 16; + default: + break; + } - // "Update" button - int upd_x = center_x - btn_w - gap / 2; - if (mx >= upd_x && mx < upd_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_UPDATING; - st.log_count = 0; - return true; - } + return true; +} - // "Back" button - int back_x = center_x + gap / 2; - if (mx >= back_x && mx < back_x + btn_w && - my >= btn_y && my < btn_y + btn_h) { - st.step = STEP_UPDATE_SELECT_PART; +// ============================================================================ +// Key handling — returns false to signal quit +// ============================================================================ + +static bool handle_key(const montauk::abi::KeyEvent& key) { + auto& st = g_state; + + if (key.scancode == 0x01) { // Escape + if (st.step == STEP_INSTALLING || st.step == STEP_UPDATING) return true; - } + return false; + } + + if (key.ascii == '\n' || key.ascii == '\r') { + if (st.step == STEP_DONE || st.step == STEP_ERROR) return false; + step_advance(); + return true; + } + + if (key.scancode == 0x48) { // Up + if (st.step == STEP_SELECT_DISK && st.selected_disk > 0) + st.selected_disk--; + else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part > 0) + st.selected_part--; + else if (st.step == STEP_PARTITION_SCHEME && st.partition_scheme > 0) + st.partition_scheme--; + } else if (key.scancode == 0x50) { // Down + if (st.step == STEP_SELECT_DISK && st.selected_disk < st.disk_count - 1) + st.selected_disk++; + else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part < st.part_count - 1) + st.selected_part++; + else if (st.step == STEP_PARTITION_SCHEME && st.partition_scheme < SCHEME_COUNT - 1) + st.partition_scheme++; } return true; @@ -260,31 +305,21 @@ extern "C" void _start() { g_state.selected_part = -1; g_state.partition_scheme = SCHEME_EFI_EXT2; g_state.step = STEP_MODE_SELECT; + g_state.mouse_x = -1; + g_state.mouse_y = -1; - // Load font - { - TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); - if (f) { - montauk::memset(f, 0, sizeof(TrueTypeFont)); - if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; } - } - g_font = f; - } + if (!fonts::init()) + montauk::exit(1); - apply_scale(montauk::win_getscale()); + g_accent = mtk::load_system_accent(); + components_init(); installer_refresh_disks(); if (!g_window.create("Install MontaukOS", INIT_W, INIT_H)) montauk::exit(1); - g_pixels = g_window.pixels; - g_win_id = g_window.id; - g_backbuf = (uint32_t*)montauk::malloc(g_win_w * g_win_h * 4); - - render(g_backbuf); - montauk::memcpy(g_pixels, g_backbuf, g_win_w * g_win_h * 4); - g_window.present(); + flush_ui(); bool install_triggered = false; @@ -314,32 +349,40 @@ extern "C" void _start() { if (ev.type == 3) break; - if (ev.type == 4) { - apply_scale(g_window.scale_factor); - redraw = true; - } + if (ev.type == 4) redraw = true; if (ev.type == 2) { g_win_w = g_window.width; g_win_h = g_window.height; - g_pixels = g_window.pixels; - montauk::mfree(g_backbuf); - g_backbuf = (uint32_t*)montauk::malloc(g_win_w * g_win_h * 4); + if (g_state.step == STEP_COMPONENTS) clamp_comp_scroll(); redraw = true; } if (ev.type == 0 && ev.key.pressed) { - if (ev.key.scancode == 0x01) break; + if (!handle_key(ev.key)) break; redraw = true; } if (ev.type == 1) { + bool moved = (ev.mouse.x != g_state.mouse_x || + ev.mouse.y != g_state.mouse_y); + g_state.mouse_x = ev.mouse.x; + g_state.mouse_y = ev.mouse.y; + + if (ev.mouse.scroll != 0 && g_state.step == STEP_COMPONENTS) { + g_state.comp_scroll -= ev.mouse.scroll * COMP_ROW_H; + clamp_comp_scroll(); + redraw = true; + } + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (clicked) { if (!handle_click(ev.mouse.x, ev.mouse.y)) break; redraw = true; } + + redraw |= moved; } if (redraw) flush_ui(); diff --git a/programs/src/installer/render.cpp b/programs/src/installer/render.cpp index 616af01..503fa6d 100644 --- a/programs/src/installer/render.cpp +++ b/programs/src/installer/render.cpp @@ -1,591 +1,536 @@ /* * render.cpp - * MontaukOS Installer — rendering + * MontaukOS Installer — MTK-based rendering * Copyright (c) 2026 Daniel Hammer */ #include "installer.h" +#include // ============================================================================ -// Pixel helpers +// Theme and small helpers // ============================================================================ -static void px_fill(uint32_t* px, int bw, int bh, - int x, int y, int w, int h, Color c) { - uint32_t v = c.to_pixel(); - int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; - int x1 = x + w > bw ? bw : x + w; - int y1 = y + h > bh ? bh : y + h; - for (int row = y0; row < y1; row++) - for (int col = x0; col < x1; col++) - px[row * bw + col] = v; +static mtk::Theme installer_theme() { + mtk::Theme theme = mtk::make_theme(g_accent); + theme.disabled_bg = Color::from_rgb(0xE4, 0xE4, 0xE4); + theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A); + return theme; } -static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) { - if (y < 0 || y >= bh) return; - uint32_t v = c.to_pixel(); - int x0 = x < 0 ? 0 : x; - int x1 = x + w > bw ? bw : x + w; - for (int col = x0; col < x1; col++) - px[y * bw + col] = v; +static bool mouse_in_rect(const Rect& rect) { + return rect.contains(g_state.mouse_x, g_state.mouse_y); } -static void px_fill_rounded(uint32_t* px, int bw, int bh, - int x, int y, int w, int h, int r, Color c) { - uint32_t v = c.to_pixel(); - for (int row = 0; row < h; row++) { - int dy = y + row; - if (dy < 0 || dy >= bh) continue; - for (int col = 0; col < w; col++) { - int dx = x + col; - if (dx < 0 || dx >= bw) continue; - bool skip = false; - int cx, cy; - if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; } - else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; } - else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; } - else if (col >= w - r && row >= h - r) { cx = col - (w - r); cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; } - if (!skip) px[dy * bw + dx] = v; +static mtk::WidgetState btn_state(const Rect& rect, bool enabled, bool active = false) { + return mtk::widget_state(active, mouse_in_rect(rect), enabled); +} + +static void draw_text_fit(Canvas& c, int x, int y, + const char* text, int max_w, Color color) { + if (!text || !text[0] || max_w <= 4) return; + if (text_width(text) <= max_w) { + c.text(x, y, text, color); + return; + } + + const char* ellipsis = "..."; + int ell_w = text_width(ellipsis); + if (ell_w >= max_w) return; + + char buf[96]; + int out = 0; + while (text[out] && out < (int)sizeof(buf) - 4) { + buf[out] = text[out]; + buf[out + 1] = '\0'; + if (text_width(buf) + ell_w > max_w) + break; + out++; + } + + if (out <= 0) return; + buf[out] = '.'; + buf[out + 1] = '.'; + buf[out + 2] = '.'; + buf[out + 3] = '\0'; + c.text(x, y, buf, color); +} + +static void draw_centered_text(Canvas& c, int y, const char* text, Color color) { + c.text((g_win_w - text_width(text)) / 2, y, text, color); +} + +static void draw_heading(Canvas& c, const mtk::Theme& theme, + const char* title, const char* subtitle) { + int fh = system_font_height(); + int y = CONTENT_TOP + 14; + c.text(content_x(), y, title, theme.text); + if (subtitle && subtitle[0]) + c.text(content_x(), y + fh + 2, subtitle, theme.text_subtle); +} + +// Radio-style two-line list row shared by disk/scheme/partition steps +static void draw_option_row(Canvas& c, const mtk::Theme& theme, + const Rect& row, bool selected, + const char* title, const char* info) { + Color fill = selected ? theme.accent_soft + : mouse_in_rect(row) ? mtk::mix(theme.surface_alt, theme.accent, 14) + : theme.surface_alt; + Color border = selected ? theme.accent : theme.border; + mtk::draw_rounded_frame(c, row, theme.radius_md, fill, border); + + Rect bubble = {row.x + 14, row.y, 14, row.h}; + mtk::draw_radio(c, bubble, "", selected, theme); + + int fh = system_font_height(); + int tx = row.x + 40; + int tw = row.w - 40 - 12; + if (info && info[0]) { + draw_text_fit(c, tx, row.y + (row.h - fh * 2 - 4) / 2, title, tw, theme.text); + draw_text_fit(c, tx, row.y + (row.h - fh * 2 - 4) / 2 + fh + 4, info, tw, + theme.text_subtle); + } else { + draw_text_fit(c, tx, row.y + (row.h - fh) / 2, title, tw, theme.text); + } +} + +// ============================================================================ +// Header and step bar +// ============================================================================ + +static void render_header(Canvas& c, const mtk::Theme& theme) { + c.fill_rect(0, 0, g_win_w, HEADER_H, theme.surface); + mtk::draw_separator(c, 0, HEADER_H - 1, g_win_w, theme); + + int fh = system_font_height(); + c.text(PAD, (HEADER_H - fh) / 2, "MontaukOS Installer", theme.text); + + const char* tag = "Setup"; + c.text(g_win_w - PAD - text_width(tag), (HEADER_H - fh) / 2, tag, + theme.text_muted); +} + +static void render_step_bar(Canvas& c, const mtk::Theme& theme) { + int bar_y = HEADER_H; + c.fill_rect(0, bar_y, g_win_w, STEP_BAR_H, theme.surface_alt); + mtk::draw_separator(c, 0, bar_y + STEP_BAR_H - 1, g_win_w, theme); + + if (g_state.step == STEP_MODE_SELECT) return; + + static const char* install_labels[] = { "Disk", "Partition", "Software", "Confirm", "Install" }; + static const char* update_labels[] = { "Partition", "Confirm", "Update" }; + + const char** labels; + int count; + int cur = 0; + + if (g_state.mode == MODE_UPDATE) { + labels = update_labels; + count = 3; + if (g_state.step == STEP_UPDATE_CONFIRM) cur = 1; + else if (g_state.step == STEP_UPDATING || + g_state.step == STEP_DONE || + g_state.step == STEP_ERROR) cur = 2; + } else { + labels = install_labels; + count = 5; + if (g_state.step == STEP_PARTITION_SCHEME) cur = 1; + else if (g_state.step == STEP_COMPONENTS) cur = 2; + else if (g_state.step == STEP_CONFIRM) cur = 3; + else if (g_state.step >= STEP_INSTALLING) cur = 4; + } + + int fh = system_font_height(); + int arrow_w = text_width(">"); + + int total_w = 0; + int label_ws[8]; + for (int i = 0; i < count; i++) { + label_ws[i] = text_width(labels[i]); + total_w += label_ws[i]; + if (i < count - 1) total_w += arrow_w + 16; + } + + int x = (g_win_w - total_w) / 2; + int ty = bar_y + (STEP_BAR_H - fh) / 2; + + for (int i = 0; i < count; i++) { + Color col; + if (i == cur) col = theme.accent; + else if (i < cur) col = mtk::mix(theme.text_muted, theme.accent, 96); + else col = theme.text_muted; + + c.text(x, ty, labels[i], col); + x += label_ws[i] + 8; + + if (i < count - 1) { + c.text(x, ty, ">", theme.border); + x += arrow_w + 8; } } } -static void px_rect(uint32_t* px, int bw, int bh, - int x, int y, int w, int h, Color c) { - px_hline(px, bw, bh, x, y, w, c); - px_hline(px, bw, bh, x, y + h - 1, w, c); - for (int row = y; row < y + h; row++) { - if (row < 0 || row >= bh) continue; - if (x >= 0 && x < bw) px[row * bw + x] = c.to_pixel(); - int rx = x + w - 1; - if (rx >= 0 && rx < bw) px[row * bw + rx] = c.to_pixel(); +// ============================================================================ +// Steps +// ============================================================================ + +static void render_mode_select(Canvas& c, const mtk::Theme& theme) { + draw_heading(c, theme, "Welcome to MontaukOS", + "Choose what you would like to do."); + + static const char* titles[] = { "Install", "Update" }; + static const char* descs[] = { + "Erase a disk and install MontaukOS.", + "Update OS, apps and SDK on an existing installation.", + }; + + int fh = system_font_height(); + for (int i = 0; i < 2; i++) { + Rect card = mode_card_rect(i); + bool hov = mouse_in_rect(card); + Color fill = hov ? mtk::mix(theme.surface_alt, theme.accent, 14) + : theme.surface_alt; + mtk::draw_rounded_frame(c, card, theme.radius_md, fill, + hov ? theme.accent : theme.border); + c.text(card.x + 16, card.y + 9, titles[i], theme.text); + c.text(card.x + 16, card.y + 9 + fh + 4, descs[i], theme.text_subtle); + + const char* chevron = ">"; + c.text(card.x + card.w - 16 - text_width(chevron), + card.y + (card.h - fh) / 2, chevron, + hov ? theme.accent : theme.text_muted); } } -static void px_text(uint32_t* px, int bw, int bh, - int x, int y, const char* text, Color c, int size = FONT_SIZE) { - if (g_font) - g_font->draw_to_buffer(px, bw, bh, x, y, text, c, size); -} - -static int text_w(const char* text, int size = FONT_SIZE) { - return g_font ? g_font->measure_text(text, size) : 0; -} - -static int font_h(int size = FONT_SIZE) { - if (!g_font) return 16; - auto* cache = g_font->get_cache(size); - return cache->ascent - cache->descent; -} - -static void px_stroke_rounded(uint32_t* px, int bw, int bh, - int x, int y, int w, int h, int r, Color c) { - uint32_t v = c.to_pixel(); - for (int row = 0; row < h; row++) { - int dy = y + row; - if (dy < 0 || dy >= bh) continue; - for (int col = 0; col < w; col++) { - int dx = x + col; - if (dx < 0 || dx >= bw) continue; - // Check if pixel is on the edge (within 1px of border) - bool on_edge = (row == 0 || row == h - 1 || col == 0 || col == w - 1); - // For corner regions, check the rounded boundary - bool in_shape = true; - int cx, cy; - if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; in_shape = cx*cx + cy*cy < r*r; } - else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; in_shape = cx*cx + cy*cy < r*r; } - else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); in_shape = cx*cx + cy*cy < r*r; } - else if (col >= w - r && row >= h - r) { cx = col - (w - r); cy = row - (h - r); in_shape = cx*cx + cy*cy < r*r; } - if (!in_shape) continue; - // Check if the pixel one step inward is still in shape - bool inner = true; - if (on_edge) { inner = false; } - else { - // Check neighbors — if any neighbor is outside, we're on the edge - int offsets[][2] = {{-1,0},{1,0},{0,-1},{0,1}}; - for (auto& o : offsets) { - int nc = col + o[0], nr = row + o[1]; - if (nc < 0 || nc >= w || nr < 0 || nr >= h) { inner = false; break; } - bool nb_in = true; - if (nc < r && nr < r) { int a = r-nc-1, b = r-nr-1; nb_in = a*a+b*b < r*r; } - else if (nc >= w-r && nr < r) { int a = nc-(w-r), b = r-nr-1; nb_in = a*a+b*b < r*r; } - else if (nc < r && nr >= h-r) { int a = r-nc-1, b = nr-(h-r); nb_in = a*a+b*b < r*r; } - else if (nc >= w-r && nr >= h-r) { int a = nc-(w-r), b = nr-(h-r); nb_in = a*a+b*b < r*r; } - if (!nb_in) { inner = false; break; } - } - } - if (!inner) px[dy * bw + dx] = v; - } - } -} - -static void px_button(uint32_t* px, int bw, int bh, - int x, int y, int w, int h, - const char* label, Color bg, Color fg, int r) { - px_fill_rounded(px, bw, bh, x, y, w, h, r, bg); - int tw = text_w(label); - int fh = font_h(); - px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg); -} - -static void px_button_outline(uint32_t* px, int bw, int bh, - int x, int y, int w, int h, - const char* label, Color border, Color fg, int r) { - px_stroke_rounded(px, bw, bh, x, y, w, h, r, border); - int tw = text_w(label); - int fh = font_h(); - px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg); -} - -// ============================================================================ -// Render: mode selection step -// ============================================================================ - -static void render_mode_select(uint32_t* px) { - int fh = font_h(); - int y = CONTENT_TOP + 16; - - px_text(px, g_win_w, g_win_h, 16, y, "Welcome to MontaukOS Installer", TEXT_COLOR); - y += fh + 4; - px_text(px, g_win_w, g_win_h, 16, y, "Choose an action below.", DIM_TEXT, FONT_SM); - y += font_h(FONT_SM) + 16; - - // Install card - int card_h = 52, card_x = 16, card_w = g_win_w - 32; - px_fill_rounded(px, g_win_w, g_win_h, card_x, y, card_w, card_h, 6, - Color::from_rgb(0xF4, 0xF4, 0xF4)); - px_text(px, g_win_w, g_win_h, card_x + 16, y + 8, "Install", TEXT_COLOR); - px_text(px, g_win_w, g_win_h, card_x + 16, y + 8 + fh + 2, - "Erase a disk and install MontaukOS.", DIM_TEXT, FONT_SM); - y += card_h + 8; - - // Update card - px_fill_rounded(px, g_win_w, g_win_h, card_x, y, card_w, card_h, 6, - Color::from_rgb(0xF4, 0xF4, 0xF4)); - px_text(px, g_win_w, g_win_h, card_x + 16, y + 8, "Update", TEXT_COLOR); - px_text(px, g_win_w, g_win_h, card_x + 16, y + 8 + fh + 2, - "Update OS and apps on an existing partition.", DIM_TEXT, FONT_SM); -} - -// ============================================================================ -// Render: disk selection step -// ============================================================================ - -static void render_select_disk(uint32_t* px) { +static void render_select_disk(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - int fh = font_h(); + draw_heading(c, theme, "Select a target disk", + "MontaukOS will be installed on this disk."); - int y = CONTENT_TOP + 16; - px_text(px, g_win_w, g_win_h, 16, y, "Select a target disk", TEXT_COLOR); - y += fh + 4; - px_text(px, g_win_w, g_win_h, 16, y, "MontaukOS will be installed to this disk.", DIM_TEXT, FONT_SM); - y += font_h(FONT_SM) + 12; - - if (st.disk_count == 0) { - px_text(px, g_win_w, g_win_h, 16, y, "No disks detected", FAINT_TEXT); - } + if (st.disk_count == 0) + c.text(content_x(), heading_bottom() + 8, "No disks detected", + theme.text_muted); for (int i = 0; i < st.disk_count; i++) { - int item_h = 48; - int item_x = 16; - int item_w = g_win_w - 32; - bool sel = (i == st.selected_disk); - - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, - Color::from_rgb(0xF4, 0xF4, 0xF4)); - - // Radio indicator - int rx = item_x + 14; - int ry = y + item_h / 2; - px_fill_rounded(px, g_win_w, g_win_h, rx - 6, ry - 6, 12, 12, 6, DIM_TEXT); - px_fill_rounded(px, g_win_w, g_win_h, rx - 5, ry - 5, 10, 10, 5, BG_COLOR); - if (sel) - px_fill_rounded(px, g_win_w, g_win_h, rx - 3, ry - 3, 6, 6, 3, ACCENT); - - px_text(px, g_win_w, g_win_h, item_x + 32, y + 6, st.disks[i].model, TEXT_COLOR); + Rect row = list_row_rect(i, DISK_ROW_H); + if (row.y + row.h > content_bottom()) break; char info[64], sz[24]; - format_disk_size(sz, sizeof(sz), st.disks[i].sectorCount, st.disks[i].sectorSizeLog); + format_disk_size(sz, sizeof(sz), st.disks[i].sectorCount, + st.disks[i].sectorSizeLog); const char* dtype = st.disks[i].rpm == 1 ? "SSD" : "HDD"; snprintf(info, sizeof(info), "%s %s (Disk %d)", sz, dtype, (int)st.disks[i].port); - px_text(px, g_win_w, g_win_h, item_x + 32, y + 6 + fh + 2, info, DIM_TEXT, FONT_SM); - y += item_h + 4; + draw_option_row(c, theme, row, i == st.selected_disk, + st.disks[i].model, info); } - - // Buttons - int btn_w = 120, btn_h = 34; - int btn_y = g_win_h - STATUS_H - btn_h - 12; - int next_x = g_win_w - btn_w - 16; - - Color next_bg = (st.selected_disk >= 0) ? ACCENT : DISABLED_BG; - px_button(px, g_win_w, g_win_h, next_x, btn_y, btn_w, btn_h, - "Next", next_bg, WHITE, TB_BTN_RAD); - - int ref_w = 80; - int ref_x = next_x - ref_w - 8; - px_button_outline(px, g_win_w, g_win_h, ref_x, btn_y, ref_w, btn_h, - "Refresh", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); } -// ============================================================================ -// Render: partition scheme step -// ============================================================================ - -static void render_partition_scheme(uint32_t* px) { +static void render_partition_scheme(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - int fh = font_h(); - int fh_sm = font_h(FONT_SM); - - int y = CONTENT_TOP + 16; - px_text(px, g_win_w, g_win_h, 16, y, "Partition scheme", TEXT_COLOR); - y += fh + 4; - px_text(px, g_win_w, g_win_h, 16, y, "How should the disk be partitioned?", DIM_TEXT, FONT_SM); - y += fh_sm + 12; + draw_heading(c, theme, "Partition scheme", + "How should the disk be partitioned?"); for (int i = 0; i < SCHEME_COUNT; i++) { - int item_h = 52; - int item_x = 16; - int item_w = g_win_w - 32; - - bool selected = (i == st.partition_scheme); - - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, - Color::from_rgb(0xF4, 0xF4, 0xF4)); - - // Radio indicator - int rx = item_x + 14; - int ry = y + item_h / 2; - px_fill_rounded(px, g_win_w, g_win_h, rx - 6, ry - 6, 12, 12, 6, DIM_TEXT); - px_fill_rounded(px, g_win_w, g_win_h, rx - 5, ry - 5, 10, 10, 5, BG_COLOR); - if (selected) - px_fill_rounded(px, g_win_w, g_win_h, rx - 3, ry - 3, 6, 6, 3, ACCENT); - - px_text(px, g_win_w, g_win_h, item_x + 32, y + 8, scheme_names[i], TEXT_COLOR); - px_text(px, g_win_w, g_win_h, item_x + 32, y + 8 + fh + 2, scheme_descs[i], DIM_TEXT, FONT_SM); - - y += item_h + 4; + Rect row = list_row_rect(i, SCHEME_ROW_H); + draw_option_row(c, theme, row, i == st.partition_scheme, + scheme_names[i], scheme_descs[i]); } - - // Buttons - int btn_w = 120, btn_h = 34; - int btn_y = g_win_h - STATUS_H - btn_h - 12; - int next_x = g_win_w - btn_w - 16; - - px_button(px, g_win_w, g_win_h, next_x, btn_y, btn_w, btn_h, - "Next", ACCENT, WHITE, TB_BTN_RAD); - - int back_x = next_x - btn_w - 8; - px_button_outline(px, g_win_w, g_win_h, back_x, btn_y, btn_w, btn_h, - "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); } -// ============================================================================ -// Render: confirmation step -// ============================================================================ +static void render_components(Canvas& c, const mtk::Theme& theme) { + draw_heading(c, theme, "Choose software", + "Optional components. The base system is always installed."); -static void render_confirm(uint32_t* px) { + Rect list = components_list_rect(); + + int vis[32]; + int n = component_visible_rows(vis, 32); + int fh = system_font_height(); + + for (int k = 0; k < n; k++) { + Rect row = component_row_rect(k); + if (row.y + row.h <= list.y) continue; + if (row.y >= list.y + list.h) break; + + int idx = vis[k]; + const Component& comp = g_components[idx]; + bool child = comp.parent >= 0; + bool hov = mouse_in_rect(row); + + if (hov && comp.present) + c.fill_rect(row.x, row.y, row.w, row.h, + mtk::accent_hover_tint(theme.accent)); + + int x = row.x + 4 + (child ? COMP_INDENT : 0); + + if (component_has_children(idx)) { + Rect tri = mtk::disclosure_rect({x, row.y, COMP_TOGGLE_W, row.h}); + mtk::draw_disclosure(c, tri, comp.expanded, theme, hov); + } + + Rect opt = {x + COMP_TOGGLE_W + 4, row.y, + row.w - (x - row.x) - COMP_TOGGLE_W - 4, row.h}; + mtk::draw_checkbox(c, opt, comp.label, component_state(idx), theme, + comp.present, hov && comp.present); + + // Right-aligned size / availability column + char right[24]; + right[0] = '\0'; + if (!comp.present) + snprintf(right, sizeof(right), "not in image"); + else if (components_sizes_ready() && comp.bytes > 0) + format_bytes(right, sizeof(right), comp.bytes); + + if (right[0]) { + int rw = text_width(right); + c.text(row.x + row.w - rw - 18, row.y + (row.h - fh) / 2, right, + theme.text_muted); + } + } + + // Scrollbar + int content_extent = n * COMP_ROW_H; + if (mtk::scrollbar_needed(content_extent, list.h)) { + Rect track = mtk::scrollbar_track_rect(list); + Rect thumb = mtk::scrollbar_thumb_rect(track, content_extent, list.h, + g_state.comp_scroll); + mtk::draw_scrollbar(c, track, thumb, mouse_in_rect(track), false, theme); + } +} + +static void render_confirm(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - int fh = font_h(); - int y = CONTENT_TOP + 24; + draw_heading(c, theme, "Confirm installation", + "Review your choices before continuing."); - const char* title = "Confirm Installation"; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(title)) / 2, y, title, TEXT_COLOR); - y += fh + 16; + int fh = system_font_height(); + int line_h = fh + 8; - const char* warn1 = "This will ERASE ALL DATA on:"; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(warn1)) / 2, y, warn1, DANGER); - y += fh + 8; + Rect panel = {content_x(), heading_bottom(), content_w(), line_h * 3 + 20}; + mtk::draw_rounded_frame(c, panel, theme.radius_md, theme.surface_alt, + theme.border); - char desc[128], sz[24]; + char disk_desc[96], sz[24]; format_disk_size(sz, sizeof(sz), st.disks[st.selected_disk].sectorCount, st.disks[st.selected_disk].sectorSizeLog); - snprintf(desc, sizeof(desc), "Disk %d: %s (%s)", - (int)st.disks[st.selected_disk].port, - st.disks[st.selected_disk].model, sz); - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(desc)) / 2, y, desc, TEXT_COLOR); - y += fh + 16; + snprintf(disk_desc, sizeof(disk_desc), "%s (%s, Disk %d)", + st.disks[st.selected_disk].model, sz, + (int)st.disks[st.selected_disk].port); - // Scheme summary - const char* scheme_label = scheme_names[st.partition_scheme]; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(scheme_label, FONT_SM)) / 2, y, - scheme_label, DIM_TEXT, FONT_SM); - y += font_h(FONT_SM) + 4; + char software[96]; + components_excluded_summary(software, sizeof(software)); - const char* warn3 = "The entire root filesystem will be installed."; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(warn3, FONT_SM)) / 2, y, warn3, DIM_TEXT, FONT_SM); + const char* keys[3] = { "Disk", "Scheme", "Software" }; + const char* vals[3] = { disk_desc, scheme_names[st.partition_scheme], software }; - // Buttons - int btn_w = 120, btn_h = 34; - int center_x = g_win_w / 2; - int btn_y = g_win_h - STATUS_H - btn_h - 12; - int gap = 16; - - px_button(px, g_win_w, g_win_h, center_x - btn_w - gap / 2, btn_y, btn_w, btn_h, - "Install", DANGER, WHITE, TB_BTN_RAD); - px_button_outline(px, g_win_w, g_win_h, center_x + gap / 2, btn_y, btn_w, btn_h, - "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); -} - -// ============================================================================ -// Render: update partition selection step -// ============================================================================ - -static void render_update_select_part(uint32_t* px) { - auto& st = g_state; - int fh = font_h(); - int fh_sm = font_h(FONT_SM); - - int y = CONTENT_TOP + 16; - px_text(px, g_win_w, g_win_h, 16, y, "Select a root partition", TEXT_COLOR); - y += fh + 4; - px_text(px, g_win_w, g_win_h, 16, y, - "OS and apps will be updated on this partition.", DIM_TEXT, FONT_SM); - y += fh_sm + 12; - - if (st.part_count == 0) { - px_text(px, g_win_w, g_win_h, 16, y, "No partitions detected", FAINT_TEXT); + int y = panel.y + 10; + for (int i = 0; i < 3; i++) { + c.text(panel.x + 14, y, keys[i], theme.text_muted); + draw_text_fit(c, panel.x + 100, y, vals[i], panel.w - 114, theme.text); + y += line_h; } + Rect warn = {content_x(), panel.y + panel.h + 12, content_w(), fh * 2 + 18}; + mtk::draw_rounded_frame(c, warn, theme.radius_md, theme.danger_soft, + theme.danger_soft); + draw_centered_text(c, warn.y + 9, "This will ERASE ALL DATA on the selected disk.", + theme.danger); + draw_centered_text(c, warn.y + 9 + fh + 4, + "This cannot be undone.", theme.danger); +} + +static void render_update_select_part(Canvas& c, const mtk::Theme& theme) { + auto& st = g_state; + draw_heading(c, theme, "Select a root partition", + "OS, apps and SDK will be updated on this partition."); + + if (st.part_count == 0) + c.text(content_x(), heading_bottom() + 8, "No partitions detected", + theme.text_muted); + for (int i = 0; i < st.part_count; i++) { - int item_h = 48; - int item_x = 16; - int item_w = g_win_w - 32; - bool sel = (i == st.selected_part); + Rect row = list_row_rect(i, PART_ROW_H); + if (row.y + row.h > content_bottom()) break; - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, - Color::from_rgb(0xF4, 0xF4, 0xF4)); - - // Radio indicator - int rx = item_x + 14; - int ry = y + item_h / 2; - px_fill_rounded(px, g_win_w, g_win_h, rx - 6, ry - 6, 12, 12, 6, DIM_TEXT); - px_fill_rounded(px, g_win_w, g_win_h, rx - 5, ry - 5, 10, 10, 5, BG_COLOR); - if (sel) - px_fill_rounded(px, g_win_w, g_win_h, rx - 3, ry - 3, 6, 6, 3, ACCENT); - - // Partition name const char* pname = st.parts[i].name[0] ? st.parts[i].name : "Unnamed"; - px_text(px, g_win_w, g_win_h, item_x + 32, y + 6, pname, TEXT_COLOR); - - // Info line: type + size char info[64], sz[24]; format_disk_size(sz, sizeof(sz), st.parts[i].sectorCount, 512); snprintf(info, sizeof(info), "%s %s (Disk %d)", sz, st.parts[i].typeName, st.parts[i].blockDev); - px_text(px, g_win_w, g_win_h, item_x + 32, y + 6 + fh + 2, info, DIM_TEXT, FONT_SM); - y += item_h + 4; + draw_option_row(c, theme, row, i == st.selected_part, pname, info); } - - // Buttons - int btn_w = 120, btn_h = 34; - int btn_y = g_win_h - STATUS_H - btn_h - 12; - int next_x = g_win_w - btn_w - 16; - - Color next_bg = (st.selected_part >= 0) ? ACCENT : DISABLED_BG; - px_button(px, g_win_w, g_win_h, next_x, btn_y, btn_w, btn_h, - "Next", next_bg, WHITE, TB_BTN_RAD); - - int back_x = next_x - btn_w - 8; - px_button_outline(px, g_win_w, g_win_h, back_x, btn_y, btn_w, btn_h, - "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); } -// ============================================================================ -// Render: update confirmation step -// ============================================================================ - -static void render_update_confirm(uint32_t* px) { +static void render_update_confirm(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - int fh = font_h(); - int y = CONTENT_TOP + 24; - - const char* title = "Confirm Update"; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(title)) / 2, y, title, TEXT_COLOR); - y += fh + 16; - - const char* warn1 = "This will overwrite os/ and apps/ on:"; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(warn1)) / 2, y, warn1, ACCENT); - y += fh + 8; + draw_heading(c, theme, "Confirm update", + "Review your choices before continuing."); + int fh = system_font_height(); char desc[128], sz[24]; format_disk_size(sz, sizeof(sz), st.parts[st.selected_part].sectorCount, 512); const char* pname = st.parts[st.selected_part].name[0] ? st.parts[st.selected_part].name : "Unnamed"; snprintf(desc, sizeof(desc), "%s (%s, Disk %d)", pname, sz, st.parts[st.selected_part].blockDev); - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(desc)) / 2, y, desc, TEXT_COLOR); - y += fh + 16; - const char* info = "Existing user data will not be affected."; - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(info, FONT_SM)) / 2, y, info, DIM_TEXT, FONT_SM); + Rect panel = {content_x(), heading_bottom(), content_w(), (fh + 8) * 2 + 20}; + mtk::draw_rounded_frame(c, panel, theme.radius_md, theme.surface_alt, + theme.border); + c.text(panel.x + 14, panel.y + 10, "Partition", theme.text_muted); + draw_text_fit(c, panel.x + 100, panel.y + 10, desc, panel.w - 114, theme.text); + c.text(panel.x + 14, panel.y + 10 + fh + 8, "Updates", theme.text_muted); + c.text(panel.x + 100, panel.y + 10 + fh + 8, "os/, apps/, sdk/", theme.text); - // Buttons - int btn_w = 120, btn_h = 34; - int center_x = g_win_w / 2; - int btn_y = g_win_h - STATUS_H - btn_h - 12; - int gap = 16; - - px_button(px, g_win_w, g_win_h, center_x - btn_w - gap / 2, btn_y, btn_w, btn_h, - "Update", ACCENT, WHITE, TB_BTN_RAD); - px_button_outline(px, g_win_w, g_win_h, center_x + gap / 2, btn_y, btn_w, btn_h, - "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); + Rect note = {content_x(), panel.y + panel.h + 12, content_w(), fh + 18}; + mtk::draw_rounded_frame(c, note, theme.radius_md, theme.accent_soft, + theme.accent_soft); + draw_centered_text(c, note.y + 9, + "Existing user data will not be affected.", theme.text); } -// ============================================================================ -// Render: installing / done / error steps -// ============================================================================ - -static void render_progress(uint32_t* px) { +static void render_progress(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - int fh = font_h(); - int fh_sm = font_h(FONT_SM); - int y = CONTENT_TOP + 16; + int fh = system_font_height(); const char* title; Color title_color; if (st.step == STEP_INSTALLING || st.step == STEP_UPDATING) { title = (st.mode == MODE_UPDATE) ? "Updating..." : "Installing..."; - title_color = TEXT_COLOR; + title_color = theme.text; } else if (st.step == STEP_DONE) { - title = (st.mode == MODE_UPDATE) ? "Update Complete" : "Installation Complete"; - title_color = SUCCESS_COLOR; + title = (st.mode == MODE_UPDATE) ? "Update complete" : "Installation complete"; + title_color = Color::from_rgb(0x33, 0x99, 0x33); } else { - title = (st.mode == MODE_UPDATE) ? "Update Failed" : "Installation Failed"; - title_color = DANGER; + title = (st.mode == MODE_UPDATE) ? "Update failed" : "Installation failed"; + title_color = theme.danger; } - px_text(px, g_win_w, g_win_h, (g_win_w - text_w(title)) / 2, y, title, title_color); + int y = CONTENT_TOP + 14; + draw_centered_text(c, y, title, title_color); y += fh + 12; - // Log area - int log_x = 16; - int log_w = g_win_w - 32; - int log_h = st.log_count * (fh_sm + 2) + 16; - int max_log_h = g_win_h - y - STATUS_H - 60; - if (log_h < 60) log_h = 60; - if (log_h > max_log_h) log_h = max_log_h; + Rect log = {content_x(), y, content_w(), content_bottom() - y - 8}; + if (log.h < 40) log.h = 40; + mtk::draw_rounded_frame(c, log, theme.radius_md, theme.surface_alt, + theme.border); - px_fill_rounded(px, g_win_w, g_win_h, log_x, y, log_w, log_h, 4, - Color::from_rgb(0xF4, 0xF4, 0xF4)); - - int ly = y + 8; + int ly = log.y + 8; for (int i = 0; i < st.log_count; i++) { - if (ly + fh_sm > y + log_h - 4) break; - px_text(px, g_win_w, g_win_h, log_x + 8, ly, st.log[i], TEXT_COLOR, FONT_SM); - ly += fh_sm + 2; - } - - if (st.step == STEP_DONE || st.step == STEP_ERROR) { - int btn_w = 100, btn_h = 34; - int btn_x = (g_win_w - btn_w) / 2; - int btn_y = g_win_h - STATUS_H - btn_h - 12; - if (st.step == STEP_DONE) - px_button(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h, - "Close", ACCENT, WHITE, TB_BTN_RAD); - else - px_button_outline(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h, - "Close", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); + if (ly + fh > log.y + log.h - 6) break; + draw_text_fit(c, log.x + 10, ly, st.log[i], log.w - 20, theme.text); + ly += fh + 3; } } // ============================================================================ -// Render: toolbar, step bar, and status bar +// Bottom bar: status text + step buttons // ============================================================================ -static void render_toolbar(uint32_t* px) { - px_fill(px, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG); - px_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR); - - const char* title = "MontaukOS Installer"; - int fh = font_h(); - px_text(px, g_win_w, g_win_h, 12, (TOOLBAR_H - fh) / 2, title, TEXT_COLOR); -} - -static void render_step_bar(uint32_t* px) { - int bar_y = TOOLBAR_H; - px_fill(px, g_win_w, g_win_h, 0, bar_y, g_win_w, STEP_BAR_H, - Color::from_rgb(0xFA, 0xFA, 0xFA)); - px_hline(px, g_win_w, g_win_h, 0, bar_y + STEP_BAR_H - 1, g_win_w, BORDER_COLOR); - - // No step bar for mode selection - if (g_state.step == STEP_MODE_SELECT) return; - - static const char* install_labels[] = { "Disk", "Partition", "Confirm", "Install" }; - static const char* update_labels[] = { "Partition", "Confirm", "Update" }; - - const char** step_labels; - int STEP_COUNT; - int cur = 0; - - if (g_state.mode == MODE_UPDATE) { - step_labels = update_labels; - STEP_COUNT = 3; - if (g_state.step == STEP_UPDATE_CONFIRM) cur = 1; - else if (g_state.step >= STEP_UPDATING) cur = 2; - else if (g_state.step == STEP_DONE || g_state.step == STEP_ERROR) cur = 2; - } else { - step_labels = install_labels; - STEP_COUNT = 4; - if (g_state.step == STEP_PARTITION_SCHEME) cur = 1; - else if (g_state.step == STEP_CONFIRM) cur = 2; - else if (g_state.step >= STEP_INSTALLING) cur = 3; - } - - int fh = font_h(); - - // Measure total width to center the bar - int total_w = 0; - int label_ws[STEP_COUNT]; - int arrow_w = text_w(">"); - for (int i = 0; i < STEP_COUNT; i++) { - label_ws[i] = text_w(step_labels[i]); - total_w += label_ws[i]; - if (i < STEP_COUNT - 1) total_w += arrow_w + 16; // padding around arrow - } - - int x = (g_win_w - total_w) / 2; - int ty = bar_y + (STEP_BAR_H - fh) / 2; - - for (int i = 0; i < STEP_COUNT; i++) { - Color col; - if (i == cur) col = ACCENT; - else if (i < cur) col = Color::from_rgb(0x88, 0xAA, 0xDD); - else col = Color::from_rgb(0xAA, 0xAA, 0xAA); - - px_text(px, g_win_w, g_win_h, x, ty, step_labels[i], col); - x += label_ws[i]; - - if (i < STEP_COUNT - 1) { - x += 8; - px_text(px, g_win_w, g_win_h, x, ty, ">", Color::from_rgb(0xCC, 0xCC, 0xCC)); - x += arrow_w + 8; - } - } -} - -static void render_status(uint32_t* px) { +static void render_bottom(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - int fh = font_h(); - int sy = g_win_h - STATUS_H; - px_fill(px, g_win_w, g_win_h, 0, sy, g_win_w, STATUS_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); - px_hline(px, g_win_w, g_win_h, 0, sy, g_win_w, BORDER_COLOR); - if (st.status[0]) { - Color sc; - if (st.step == STEP_DONE) - sc = TEXT_COLOR; - else if (st.step == STEP_ERROR) - sc = DANGER; - else { - uint64_t age = montauk::get_milliseconds() - st.status_time; - sc = (age < 5000) - ? Color::from_rgb(0x33, 0x33, 0x33) - : Color::from_rgb(0xAA, 0xAA, 0xAA); - } - px_text(px, g_win_w, g_win_h, 8, sy + (STATUS_H - fh) / 2, st.status, sc); + int fh = system_font_height(); + + Rect bar = bottom_bar_rect(); + c.fill_rect(bar.x, bar.y, bar.w, bar.h, theme.surface); + mtk::draw_separator(c, bar.x, bar.y, bar.w, theme); + + Rect primary = primary_btn_rect(); + Rect secondary = secondary_btn_rect(); + Rect tertiary = tertiary_btn_rect(); + int text_right = g_win_w - PAD; + + switch (st.step) { + case STEP_SELECT_DISK: + mtk::draw_button(c, primary, "Next", mtk::BUTTON_PRIMARY, + btn_state(primary, st.selected_disk >= 0), theme); + mtk::draw_button(c, secondary, "Back", mtk::BUTTON_SECONDARY, + btn_state(secondary, true), theme); + mtk::draw_button(c, tertiary, "Refresh", mtk::BUTTON_SECONDARY, + btn_state(tertiary, true), theme); + text_right = tertiary.x - 12; + break; + case STEP_PARTITION_SCHEME: + case STEP_COMPONENTS: + mtk::draw_button(c, primary, "Next", mtk::BUTTON_PRIMARY, + btn_state(primary, true), theme); + mtk::draw_button(c, secondary, "Back", mtk::BUTTON_SECONDARY, + btn_state(secondary, true), theme); + text_right = secondary.x - 12; + break; + case STEP_CONFIRM: + mtk::draw_button(c, primary, "Install", mtk::BUTTON_DANGER, + btn_state(primary, true), theme); + mtk::draw_button(c, secondary, "Back", mtk::BUTTON_SECONDARY, + btn_state(secondary, true), theme); + text_right = secondary.x - 12; + break; + case STEP_UPDATE_SELECT_PART: + mtk::draw_button(c, primary, "Next", mtk::BUTTON_PRIMARY, + btn_state(primary, st.selected_part >= 0), theme); + mtk::draw_button(c, secondary, "Back", mtk::BUTTON_SECONDARY, + btn_state(secondary, true), theme); + text_right = secondary.x - 12; + break; + case STEP_UPDATE_CONFIRM: + mtk::draw_button(c, primary, "Update", mtk::BUTTON_PRIMARY, + btn_state(primary, true), theme); + mtk::draw_button(c, secondary, "Back", mtk::BUTTON_SECONDARY, + btn_state(secondary, true), theme); + text_right = secondary.x - 12; + break; + case STEP_DONE: + case STEP_ERROR: + mtk::draw_button(c, primary, "Close", + st.step == STEP_DONE ? mtk::BUTTON_PRIMARY + : mtk::BUTTON_SECONDARY, + btn_state(primary, true), theme); + text_right = primary.x - 12; + break; + default: + break; } + + // Left side: hovered component description, selection summary, or status + char left[128]; + left[0] = '\0'; + + if (st.step == STEP_COMPONENTS) { + int vis[32]; + int n = component_visible_rows(vis, 32); + Rect list = components_list_rect(); + for (int k = 0; k < n; k++) { + Rect row = component_row_rect(k); + if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue; + if (mouse_in_rect(row) && g_components[vis[k]].desc) { + snprintf(left, sizeof(left), "%s", g_components[vis[k]].desc); + break; + } + } + if (!left[0] && components_sizes_ready()) { + char sel[24]; + format_bytes(sel, sizeof(sel), components_selected_bytes()); + snprintf(left, sizeof(left), "Optional software selected: %s", sel); + } + } else if (st.status[0]) { + Color sc = theme.text; + if (st.step == STEP_ERROR) { + sc = theme.danger; + } else if (st.step != STEP_DONE) { + uint64_t age = montauk::get_milliseconds() - st.status_time; + sc = (age < 5000) ? theme.text : theme.text_muted; + } + draw_text_fit(c, PAD, bar.y + (bar.h - fh) / 2, st.status, + text_right - PAD, sc); + return; + } + + if (left[0]) + draw_text_fit(c, PAD, bar.y + (bar.h - fh) / 2, left, + text_right - PAD, theme.text_subtle); } // ============================================================================ @@ -593,22 +538,28 @@ static void render_status(uint32_t* px) { // ============================================================================ void render(uint32_t* pixels) { - px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR); - render_toolbar(pixels); - render_step_bar(pixels); + if (!pixels) return; + + Canvas c(pixels, g_win_w, g_win_h); + mtk::Theme theme = installer_theme(); + c.fill(theme.window_bg); + + render_header(c, theme); + render_step_bar(c, theme); switch (g_state.step) { - case STEP_MODE_SELECT: render_mode_select(pixels); break; - case STEP_SELECT_DISK: render_select_disk(pixels); break; - case STEP_PARTITION_SCHEME: render_partition_scheme(pixels); break; - case STEP_CONFIRM: render_confirm(pixels); break; - case STEP_UPDATE_SELECT_PART: render_update_select_part(pixels); break; - case STEP_UPDATE_CONFIRM: render_update_confirm(pixels); break; + case STEP_MODE_SELECT: render_mode_select(c, theme); break; + case STEP_SELECT_DISK: render_select_disk(c, theme); break; + case STEP_PARTITION_SCHEME: render_partition_scheme(c, theme); break; + case STEP_COMPONENTS: render_components(c, theme); break; + case STEP_CONFIRM: render_confirm(c, theme); break; + case STEP_UPDATE_SELECT_PART: render_update_select_part(c, theme); break; + case STEP_UPDATE_CONFIRM: render_update_confirm(c, theme); break; case STEP_INSTALLING: case STEP_UPDATING: case STEP_DONE: - case STEP_ERROR: render_progress(pixels); break; + case STEP_ERROR: render_progress(c, theme); break; } - render_status(pixels); + render_bottom(c, theme); } diff --git a/programs/src/lua/Makefile b/programs/src/lua/Makefile index 5ae753d..ca89025 100644 --- a/programs/src/lua/Makefile +++ b/programs/src/lua/Makefile @@ -14,7 +14,7 @@ SRCDIR := src OBJDIR := obj LIBC_DIR := ../../lib/libc BINDIR := ../../bin/os -LIBDIR := ../../bin/lib/lua +LIBDIR := ../../bin/sdk/lua CFLAGS := \ -std=gnu11 \ diff --git a/programs/src/lua/src/luaconf.h b/programs/src/lua/src/luaconf.h index eddc4a4..e52211c 100644 --- a/programs/src/lua/src/luaconf.h +++ b/programs/src/lua/src/luaconf.h @@ -223,8 +223,8 @@ #elif defined(__MONTAUKOS__) /* }{ */ -#define LUA_LDIR "0:/lib/lua/" -#define LUA_CDIR "0:/lib/lua/" +#define LUA_LDIR "0:/sdk/lua/" +#define LUA_CDIR "0:/sdk/lua/" #if !defined(LUA_PATH_DEFAULT) #define LUA_PATH_DEFAULT \ diff --git a/programs/src/tcc/Makefile b/programs/src/tcc/Makefile index e68cae0..017da8e 100644 --- a/programs/src/tcc/Makefile +++ b/programs/src/tcc/Makefile @@ -89,7 +89,7 @@ TARGET := $(BINDIR)/tcc.elf # ---- CRT objects (staged from shared libc) ---- SHARED_CRT_DIR := $(LIBDIR)/libc -CRT_DIR := ../../bin/lib/tcc/lib +CRT_DIR := ../../bin/sdk/tcc/lib CRT_OBJS := $(CRT_DIR)/crt1.o $(CRT_DIR)/crti.o $(CRT_DIR)/crtn.o # ---- Rules ---- diff --git a/programs/src/tcc/config.h b/programs/src/tcc/config.h index 30ced40..065ef2e 100644 --- a/programs/src/tcc/config.h +++ b/programs/src/tcc/config.h @@ -16,10 +16,10 @@ #undef CONFIG_TCC_BCHECK /* Paths on MontaukOS ramdisk */ -#define CONFIG_TCCDIR "0:/lib/tcc" -#define CONFIG_TCC_SYSINCLUDEPATHS "0:/lib/tcc/include" -#define CONFIG_TCC_LIBPATHS "0:/lib/tcc/lib" -#define CONFIG_TCC_CRTPREFIX "0:/lib/tcc/lib" +#define CONFIG_TCCDIR "0:/sdk/tcc" +#define CONFIG_TCC_SYSINCLUDEPATHS "0:/sdk/tcc/include" +#define CONFIG_TCC_LIBPATHS "0:/sdk/tcc/lib" +#define CONFIG_TCC_CRTPREFIX "0:/sdk/tcc/lib" #define CONFIG_TCC_ELFINTERP "" /* We are not native -- no -run support */