diff --git a/GNUmakefile b/GNUmakefile index f59b26b..90c658f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -176,7 +176,14 @@ kernel: kernel-deps .PHONY: ramdisk -ramdisk: programs +ramdisk: limine/limine kernel programs + mkdir -p programs/bin/boot/limine + cp -v limine/BOOTX64.EFI programs/bin/boot/limine/ + cp -v limine.conf programs/bin/boot/limine/ + cp -v kernel/bin-$(ARCH)/kernel programs/bin/boot/ + rm -f programs/bin/boot/ramdisk.tar + ./scripts/mkramdisk.sh programs/bin ramdisk.tar + cp -v ramdisk.tar programs/bin/boot/ ./scripts/mkramdisk.sh programs/bin ramdisk.tar $(IMAGE_NAME).iso: limine/limine kernel ramdisk diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index d3eda2f..2fde3b3 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -76,4 +76,12 @@ namespace Montauk { static int Sys_FDelete(const char* path) { return Fs::Vfs::VfsDelete(path); } + + static int Sys_FMkdir(const char* path) { + return Fs::Vfs::VfsMkdir(path); + } + + static int Sys_DriveList(int* outDrives, int maxEntries) { + return Fs::Vfs::VfsDriveList(outDrives, maxEntries); + } }; \ No newline at end of file diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index beea054..84d10d6 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -157,6 +157,10 @@ namespace Montauk { return (int64_t)Sys_FCreate((const char*)frame->arg1); case SYS_FDELETE: return (int64_t)Sys_FDelete((const char*)frame->arg1); + case SYS_FMKDIR: + return (int64_t)Sys_FMkdir((const char*)frame->arg1); + case SYS_DRIVELIST: + return (int64_t)Sys_DriveList((int*)frame->arg1, (int)frame->arg2); case SYS_TERMSCALE: return Sys_TermScale(frame->arg1, frame->arg2); case SYS_RESOLVE: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index a380bcb..c579c87 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -91,6 +91,8 @@ namespace Montauk { static constexpr uint64_t SYS_FWRITE = 41; static constexpr uint64_t SYS_FCREATE = 42; static constexpr uint64_t SYS_FDELETE = 77; + static constexpr uint64_t SYS_FMKDIR = 78; + static constexpr uint64_t SYS_DRIVELIST = 79; /* Graphics.hpp */ static constexpr uint64_t SYS_TERMSCALE = 43; diff --git a/kernel/src/Drivers/Storage/Gpt.cpp b/kernel/src/Drivers/Storage/Gpt.cpp index a5d1c6a..5089414 100644 --- a/kernel/src/Drivers/Storage/Gpt.cpp +++ b/kernel/src/Drivers/Storage/Gpt.cpp @@ -483,6 +483,16 @@ namespace Drivers::Storage::Gpt { if (!dev) return -1; if (dev->SectorCount < 68) return -1; // minimum for GPT + // Remove stale in-memory partitions for this device + int dst = 0; + for (int src = 0; src < g_partitionCount; src++) { + if (g_partitions[src].BlockDevIndex != blockDevIndex) { + if (dst != src) g_partitions[dst] = g_partitions[src]; + dst++; + } + } + g_partitionCount = dst; + // Write protective MBR ProtectiveMbr mbr; memset(&mbr, 0, sizeof(mbr)); diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp index 7ea75f2..93b127d 100644 --- a/kernel/src/Fs/Fat32.cpp +++ b/kernel/src/Fs/Fat32.cpp @@ -50,6 +50,9 @@ namespace Fs::Fat32 { // Location of the 32-byte SFN directory entry on disk uint64_t sfnPartSector; // partition-relative sector uint32_t sfnOffInSector; // byte offset within that sector + // Cached position for sequential access (avoids O(n²) chain walks) + uint32_t cachedClusterIdx; // cluster index in chain + uint32_t cachedCluster; // cluster number at that index }; struct Fat32Instance { @@ -805,6 +808,8 @@ namespace Fs::Fat32 { self.files[i].isDirectory = (entry.attributes & ATTR_DIRECTORY) != 0; self.files[i].sfnPartSector = entry.sfnPartSector; self.files[i].sfnOffInSector = entry.sfnOffInSector; + self.files[i].cachedClusterIdx = 0; + self.files[i].cachedCluster = entry.firstCluster; return i; } } @@ -831,15 +836,23 @@ namespace Fs::Fat32 { uint32_t startClusterIdx = (uint32_t)(offset / clusterSize); uint32_t clusterOff = (uint32_t)(offset % clusterSize); - // Walk cluster chain to starting cluster - uint32_t cluster = file.firstCluster; - for (uint32_t i = 0; i < startClusterIdx; i++) { + // Walk cluster chain to starting cluster (use cache if possible) + uint32_t cluster; + uint32_t startFrom = 0; + if (file.cachedCluster >= 2 && file.cachedClusterIdx <= startClusterIdx) { + cluster = file.cachedCluster; + startFrom = file.cachedClusterIdx; + } else { + cluster = file.firstCluster; + } + for (uint32_t i = startFrom; i < startClusterIdx; i++) { cluster = GetNextCluster(self, cluster); if (IsEndOfChain(cluster)) return 0; } // Read data cluster by cluster uint64_t bytesRead = 0; + uint32_t lastCluster = cluster; while (bytesRead < size && !IsEndOfChain(cluster)) { if (!ReadCluster(self, cluster)) break; @@ -851,9 +864,17 @@ namespace Fs::Fat32 { bytesRead += toRead; clusterOff = 0; // subsequent clusters start from offset 0 + lastCluster = cluster; cluster = GetNextCluster(self, cluster); } + // Update cache + if (bytesRead > 0 && lastCluster >= 2) { + uint32_t endClusterIdx = (uint32_t)((offset + bytesRead - 1) / clusterSize); + file.cachedClusterIdx = endClusterIdx; + file.cachedCluster = lastCluster; + } + return (int)bytesRead; } @@ -928,9 +949,18 @@ namespace Fs::Fat32 { uint32_t clusterIdx = (uint32_t)(offset / clusterSize); uint32_t clusterOff = (uint32_t)(offset % clusterSize); - uint32_t cluster = file.firstCluster; + // Use cached position if we can skip ahead + uint32_t cluster; uint32_t prevCluster = 0; - for (uint32_t i = 0; i < clusterIdx; i++) { + uint32_t startIdx = 0; + if (file.cachedCluster >= 2 && file.cachedClusterIdx <= clusterIdx) { + cluster = file.cachedCluster; + startIdx = file.cachedClusterIdx; + } else { + cluster = file.firstCluster; + } + + for (uint32_t i = startIdx; i < clusterIdx; i++) { prevCluster = cluster; uint32_t next = GetNextCluster(self, cluster); if (IsEndOfChain(next)) { @@ -977,6 +1007,13 @@ namespace Fs::Fat32 { } done: + // Update cached position for sequential access + if (bytesWritten > 0 && prevCluster >= 2) { + uint32_t endClusterIdx = (uint32_t)((offset + bytesWritten - 1) / clusterSize); + file.cachedClusterIdx = endClusterIdx; + file.cachedCluster = prevCluster; + } + // Update file size if we wrote past the end uint64_t endPos = offset + bytesWritten; if (endPos > file.fileSize) { @@ -1038,6 +1075,8 @@ namespace Fs::Fat32 { self.files[i].isDirectory = false; self.files[i].sfnPartSector = existing.sfnPartSector; self.files[i].sfnOffInSector = existing.sfnOffInSector; + self.files[i].cachedClusterIdx = 0; + self.files[i].cachedCluster = 0; return i; } } @@ -1124,6 +1163,8 @@ namespace Fs::Fat32 { self.files[i].isDirectory = false; self.files[i].sfnPartSector = sfnSector; self.files[i].sfnOffInSector = sfnOff; + self.files[i].cachedClusterIdx = 0; + self.files[i].cachedCluster = 0; return i; } } @@ -1258,6 +1299,136 @@ namespace Fs::Fat32 { return 0; } + // ========================================================================= + // Mkdir — create a directory + // ========================================================================= + + static int MkdirImpl(int inst, const char* path) { + if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + auto& self = g_instances[inst]; + + // Split path into parent directory and new dir name + char parentPath[MaxNameLen]; + char dirName[MaxNameLen]; + SplitPath(path, parentPath, MaxNameLen, dirName, MaxNameLen); + + if (dirName[0] == '\0') return -1; + + // Traverse to parent directory + ParsedEntry parentEntry; + if (!TraversePath(inst, parentPath, &parentEntry)) return -1; + if (!(parentEntry.attributes & ATTR_DIRECTORY)) return -1; + + uint32_t parentCluster = parentEntry.firstCluster; + + // If directory already exists, return success + ParsedEntry existing; + if (FindInDirectory(inst, parentCluster, dirName, &existing)) { + if (existing.attributes & ATTR_DIRECTORY) return 0; + return -1; // exists as a file + } + + // Allocate a cluster for the new directory + uint32_t newCluster = AllocateCluster(self); + if (newCluster == 0) return -1; + + // Initialize the new directory cluster with . and .. entries + memset(self.clusterBuf, 0, self.clusterSize); + + // "." entry — points to itself + uint8_t* dot = self.clusterBuf; + memset(dot, ' ', 11); + dot[0] = '.'; + dot[11] = ATTR_DIRECTORY; + uint16_t clHi = (uint16_t)(newCluster >> 16); + uint16_t clLo = (uint16_t)(newCluster & 0xFFFF); + memcpy(dot + 20, &clHi, 2); + memcpy(dot + 26, &clLo, 2); + + // ".." entry — points to parent + uint8_t* dotdot = self.clusterBuf + 32; + memset(dotdot, ' ', 11); + dotdot[0] = '.'; dotdot[1] = '.'; + dotdot[11] = ATTR_DIRECTORY; + uint32_t parentCl = (parentCluster == self.rootCluster) ? 0 : parentCluster; + clHi = (uint16_t)(parentCl >> 16); + clLo = (uint16_t)(parentCl & 0xFFFF); + memcpy(dotdot + 20, &clHi, 2); + memcpy(dotdot + 26, &clLo, 2); + + if (!WriteClusterData(self, newCluster, self.clusterBuf)) return -1; + + // Generate 8.3 short name for the directory + char shortName[11]; + GenerateShortName(dirName, shortName); + MakeShortNameUnique(inst, parentCluster, shortName); + + // Build LFN entries if needed + uint8_t lfnEntries[20 * 32]; + int lfnCount = 0; + bool needsLfn = NeedsLfn(dirName, shortName); + + if (needsLfn) { + uint8_t checksum = LfnChecksum(shortName); + lfnCount = BuildLfnEntries(dirName, checksum, lfnEntries); + } + + int totalSlots = lfnCount + 1; + + // Find free directory slots in parent + DirSlotPos pos = FindFreeDirSlots(inst, parentCluster, totalSlots); + if (!pos.found) return -1; + + // Build all entries (LFN + SFN) + uint8_t allEntries[21 * 32]; + if (lfnCount > 0) { + memcpy(allEntries, lfnEntries, lfnCount * 32); + } + + // Build the SFN entry with ATTR_DIRECTORY + uint8_t* sfn = allEntries + lfnCount * 32; + memset(sfn, 0, 32); + memcpy(sfn, shortName, 11); + sfn[11] = ATTR_DIRECTORY; + clHi = (uint16_t)(newCluster >> 16); + clLo = (uint16_t)(newCluster & 0xFFFF); + memcpy(sfn + 20, &clHi, 2); + memcpy(sfn + 26, &clLo, 2); + // Directory size field is 0 per FAT spec + + // Write entries to disk + uint64_t curSector = pos.partSector; + uint32_t curOff = pos.offsetInSec; + + uint8_t sectorBuf[512]; + if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; + + int bytesTotal = totalSlots * 32; + int written = 0; + + while (written < bytesTotal) { + int space = (int)self.bytesPerSector - (int)curOff; + int chunk = bytesTotal - written; + if (chunk > space) chunk = space; + + memcpy(sectorBuf + curOff, allEntries + written, chunk); + written += chunk; + + if (written < bytesTotal || chunk == space) { + if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; + if (written < bytesTotal) { + curSector++; + curOff = 0; + if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; + } + } else { + if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; + } + } + + return 0; + } + // ========================================================================= // Template thunks — generate unique function pointers per instance // ========================================================================= @@ -1271,6 +1442,7 @@ namespace Fs::Fat32 { static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); } static int Create(const char* p) { return CreateImpl(N, p); } static int Delete(const char* p) { return DeleteImpl(N, p); } + static int Mkdir(const char* p) { return MkdirImpl(N, p); } }; template @@ -1284,6 +1456,7 @@ namespace Fs::Fat32 { Thunks::Write, Thunks::Create, Thunks::Delete, + Thunks::Mkdir, }; } diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index 1c74161..8948689 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -176,6 +176,27 @@ namespace Fs::Vfs { return driveTable[drive]->Delete(localPath); } + int VfsMkdir(const char* path) { + int drive; + const char* localPath; + + if (!ParsePath(path, drive, localPath)) return -1; + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1; + if (driveTable[drive]->Mkdir == nullptr) return -1; + + return driveTable[drive]->Mkdir(localPath); + } + + int VfsDriveList(int* outDrives, int maxEntries) { + int count = 0; + for (int i = 0; i < MaxDrives && count < maxEntries; i++) { + if (driveTable[i] != nullptr) { + outDrives[count++] = i; + } + } + return count; + } + int VfsReadDir(const char* path, const char** outNames, int maxEntries) { int drive; const char* localPath; diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index e83cc02..b2d0cc7 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -22,6 +22,7 @@ namespace Fs::Vfs { int (*Write)(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); int (*Create)(const char* path); int (*Delete)(const char* path); + int (*Mkdir)(const char* path); }; void Initialize(); @@ -35,5 +36,9 @@ namespace Fs::Vfs { uint64_t VfsGetSize(int handle); void VfsClose(int handle); int VfsReadDir(const char* path, const char** outNames, int maxEntries); + int VfsMkdir(const char* path); + + // Returns number of registered drives, fills outDrives[] with their indices + int VfsDriveList(int* outDrives, int maxEntries); } diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index f4edaee..0c5a127 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -164,6 +164,7 @@ extern "C" void kmain() { Efi::Init(ST, efi_memmap_request.response); // Initialize ramdisk from Limine modules + bool hasRamdisk = false; if (module_request.response != nullptr && module_request.response->module_count > 0) { Kt::KernelLogStream(OK, "Modules") << "Found " << (uint64_t)module_request.response->module_count << " module(s)"; for (uint64_t i = 0; i < module_request.response->module_count; i++) { @@ -177,30 +178,35 @@ extern "C" void kmain() { modString[6] == 'k' && modString[7] == '\0') { Kt::KernelLogStream(OK, "Modules") << "Ramdisk module at " << kcp::hex << (uint64_t)mod->address << kcp::dec << ", size=" << mod->size; Fs::Ramdisk::Initialize(mod->address, mod->size); + hasRamdisk = true; } } } else { Kt::KernelLogStream(WARNING, "Modules") << "No modules loaded (ramdisk unavailable)"; } - // Initialize VFS and register ramdisk as drive 0 + // Initialize VFS and register ramdisk as drive 0 only if present Fs::Vfs::Initialize(); - static Fs::Vfs::FsDriver ramdiskDriver = { - Fs::Ramdisk::Open, - Fs::Ramdisk::Read, - Fs::Ramdisk::GetSize, - Fs::Ramdisk::Close, - Fs::Ramdisk::ReadDir, - Fs::Ramdisk::Write, - Fs::Ramdisk::Create, - nullptr - }; - Fs::Vfs::RegisterDrive(0, &ramdiskDriver); + if (hasRamdisk) { + static Fs::Vfs::FsDriver ramdiskDriver = { + Fs::Ramdisk::Open, + Fs::Ramdisk::Read, + Fs::Ramdisk::GetSize, + Fs::Ramdisk::Close, + Fs::Ramdisk::ReadDir, + Fs::Ramdisk::Write, + Fs::Ramdisk::Create, + nullptr, + nullptr + }; + Fs::Vfs::RegisterDrive(0, &ramdiskDriver); + } - // Register filesystem probes and auto-mount partitions + // Register filesystem probes and auto-mount partitions. + // When no ramdisk, disk partitions start at drive 0 so init.elf is found there. Fs::Fat32::RegisterProbe(); - Fs::FsProbe::MountPartitions(); + Fs::FsProbe::MountPartitions(hasRamdisk ? 1 : 0); Hal::LoadTSS(); Montauk::InitializeSyscalls(); diff --git a/kernel/src/Net/NetConfig.cpp b/kernel/src/Net/NetConfig.cpp index f166ebf..c8896cd 100644 --- a/kernel/src/Net/NetConfig.cpp +++ b/kernel/src/Net/NetConfig.cpp @@ -8,11 +8,11 @@ namespace Net { - // QEMU user-mode networking defaults - static uint32_t g_ipAddress = Ipv4Addr(10, 0, 68, 99); - static uint32_t g_subnetMask = Ipv4Addr(255, 255, 255, 0); - static uint32_t g_gateway = Ipv4Addr(10, 0, 68, 1); - static uint32_t g_dnsServer = Ipv4Addr(10, 0, 68, 1); + // Unconfigured until usermode DHCP sets them + static uint32_t g_ipAddress = 0; + static uint32_t g_subnetMask = 0; + static uint32_t g_gateway = 0; + static uint32_t g_dnsServer = 0; uint32_t GetIpAddress() { return g_ipAddress; } void SetIpAddress(uint32_t ip) { g_ipAddress = ip; } diff --git a/programs/GNUmakefile b/programs/GNUmakefile index ed75019..a33c8c0 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,15 +59,12 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer desktop SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) -# Build targets: system programs go to bin/os/, games are handled separately. +# Build targets: system programs go to bin/os/, apps go to bin/apps//. TARGETS := $(addprefix $(BINDIR)/os/,$(addsuffix .elf,$(SYSTEM_PROGRAMS))) -# Game data files to copy into bin/games/. -GAME_DATA := $(BINDIR)/games/doom1.wad - # Man pages source directory. MANDIR := man MANSRC := $(wildcard $(MANDIR)/*.*) @@ -84,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Home directory placeholder. HOMEKEEP := $(BINDIR)/home/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer desktop icons fonts bearssl libc tls libjpeg +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer desktop icons fonts bearssl libc tls libjpeg install-apps -all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer doom desktop icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -151,6 +148,10 @@ disks: libc devexplorer: libc $(MAKE) -C src/devexplorer +# Build installer standalone GUI tool (depends on libc). +installer: libc + $(MAKE) -C src/installer + # Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper). desktop: libc libjpeg $(MAKE) -C src/desktop @@ -167,10 +168,9 @@ fonts: doom: $(MAKE) -C src/doom -# Copy doom1.wad from data/games/ into bin/games/. -$(BINDIR)/games/doom1.wad: data/games/doom1.wad - mkdir -p $(BINDIR)/games - cp $< $@ +# Install app bundles (manifests, icons, data files) into bin/apps//. +install-apps: doom spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer + ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. $(BINDIR)/man/%: $(MANDIR)/% @@ -215,3 +215,4 @@ clean: $(MAKE) -C src/pdfviewer clean $(MAKE) -C src/disks clean $(MAKE) -C src/devexplorer clean + $(MAKE) -C src/installer clean diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 16b6d55..ed96908 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -55,6 +55,8 @@ namespace Montauk { static constexpr uint64_t SYS_FWRITE = 41; static constexpr uint64_t SYS_FCREATE = 42; static constexpr uint64_t SYS_FDELETE = 77; + static constexpr uint64_t SYS_FMKDIR = 78; + static constexpr uint64_t SYS_DRIVELIST = 79; static constexpr uint64_t SYS_TERMSCALE = 43; static constexpr uint64_t SYS_RESOLVE = 44; static constexpr uint64_t SYS_GETRANDOM = 45; diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index 73ae850..9388647 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -17,6 +17,16 @@ namespace gui { static constexpr int MAX_WINDOWS = 8; static constexpr int PANEL_HEIGHT = 32; +static constexpr int MAX_EXTERNAL_APPS = 16; + +// External app discovered from 0:/apps/ manifest +struct ExternalApp { + char name[48]; + char binary_path[128]; + char category[24]; + SvgIcon icon; + bool menu_visible; +}; struct DesktopSettings { // Background @@ -84,14 +94,12 @@ struct DesktopState { SvgIcon icon_reboot; SvgIcon icon_shutdown; - SvgIcon icon_weather; - - SvgIcon icon_doom; SvgIcon icon_procmgr; SvgIcon icon_mandelbrot; - SvgIcon icon_devexplorer; - SvgIcon icon_spreadsheet; - SvgIcon icon_disks; + + // External apps discovered from 0:/apps/ manifests + ExternalApp external_apps[MAX_EXTERNAL_APPS]; + int external_app_count; bool ctx_menu_open; int ctx_menu_x, ctx_menu_y; diff --git a/programs/include/montauk/config.h b/programs/include/montauk/config.h new file mode 100644 index 0000000..180924b --- /dev/null +++ b/programs/include/montauk/config.h @@ -0,0 +1,331 @@ +/* + * config.h + * Config file manager for MontaukOS programs + * Loads, modifies, and saves TOML config files from 0:/config/ + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace montauk { +namespace config { + + static constexpr const char* CONFIG_DIR = "0:/config"; + + // ---- Serializer (Doc -> TOML text) ---- + + namespace detail { + + struct Writer { + char* buf; + int len; + int cap; + + void init() { + cap = 1024; + buf = (char*)montauk::malloc(cap); + len = 0; + buf[0] = '\0'; + } + + void destroy() { + if (buf) montauk::mfree(buf); + buf = nullptr; + len = 0; + cap = 0; + } + + void grow(int need) { + if (len + need < cap) return; + int newCap = cap; + while (newCap <= len + need) newCap *= 2; + char* nb = (char*)montauk::malloc(newCap); + montauk::memcpy(nb, buf, len); + montauk::mfree(buf); + buf = nb; + cap = newCap; + } + + void put(char c) { + grow(2); + buf[len++] = c; + buf[len] = '\0'; + } + + void puts(const char* s) { + int n = montauk::slen(s); + grow(n + 1); + montauk::memcpy(buf + len, s, n); + len += n; + buf[len] = '\0'; + } + + void put_int(int64_t v) { + if (v < 0) { put('-'); v = -v; } + if (v == 0) { put('0'); return; } + char tmp[24]; + int n = 0; + while (v > 0) { tmp[n++] = '0' + (v % 10); v /= 10; } + for (int i = n - 1; i >= 0; i--) put(tmp[i]); + } + + // Write a TOML-escaped string value + void put_string(const char* s) { + put('"'); + while (*s) { + switch (*s) { + case '"': puts("\\\""); break; + case '\\': puts("\\\\"); break; + case '\n': puts("\\n"); break; + case '\t': puts("\\t"); break; + case '\r': puts("\\r"); break; + default: put(*s); break; + } + s++; + } + put('"'); + } + + void write_value(toml::Value* v) { + switch (v->type) { + case toml::Type::String: + put_string(v->str); + break; + case toml::Type::Int: + put_int(v->ival); + break; + case toml::Type::Bool: + puts(v->bval ? "true" : "false"); + break; + case toml::Type::Array: { + put('['); + for (int i = 0; i < v->array.count; i++) { + if (i > 0) puts(", "); + write_value(v->array.items[i]); + } + put(']'); + break; + } + case toml::Type::Table: + // Inline tables are not re-serialized here; + // their entries are flattened in the doc + break; + } + } + }; + + // Extract the table prefix from a dotted key (everything before last dot) + // Returns length of prefix, or 0 if no dot + inline int key_table(const char* key, char* out, int outSz) { + int lastDot = -1; + int n = montauk::slen(key); + for (int i = 0; i < n; i++) { + if (key[i] == '.') lastDot = i; + } + if (lastDot <= 0) { out[0] = '\0'; return 0; } + int cp = lastDot < outSz - 1 ? lastDot : outSz - 1; + montauk::memcpy(out, key, cp); + out[cp] = '\0'; + return cp; + } + + // Extract the bare key (after last dot) + inline const char* key_bare(const char* key) { + const char* last = key; + while (*key) { + if (*key == '.') last = key + 1; + key++; + } + return last; + } + + } // namespace detail + + // Serialize a Doc back to TOML text. Caller must montauk::mfree() the result. + inline char* serialize(toml::Doc* doc) { + detail::Writer w; + w.init(); + + char currentTable[256] = {}; + + for (int i = 0; i < doc->entries.count; i++) { + auto* v = doc->entries.items[i]; + if (!v->key) continue; + + // Skip Table entries that only serve as section markers — + // we emit [table] headers based on the keys of actual values + if (v->type == toml::Type::Table && v->array.count == 0) + continue; + + // Determine table prefix for this key + char table[256]; + detail::key_table(v->key, table, sizeof(table)); + + // Emit [table] header if the section changed + if (!montauk::streq(table, currentTable)) { + montauk::strcpy(currentTable, table); + if (table[0]) { + if (w.len > 0) w.put('\n'); + w.put('['); + w.puts(table); + w.puts("]\n"); + } + } + + // Emit key = value + w.puts(detail::key_bare(v->key)); + w.puts(" = "); + w.write_value(v); + w.put('\n'); + } + + return w.buf; + } + + // ---- File operations ---- + + // Ensure the config directory exists + inline void ensure_dir() { + montauk::fmkdir(CONFIG_DIR); + } + + // Build full path: "0:/config/.toml" + inline void build_path(char* out, int outSz, const char* name) { + int p = 0; + const char* dir = CONFIG_DIR; + while (*dir && p < outSz - 2) out[p++] = *dir++; + out[p++] = '/'; + while (*name && p < outSz - 6) out[p++] = *name++; + // Append ".toml" + const char* ext = ".toml"; + while (*ext && p < outSz - 1) out[p++] = *ext++; + out[p] = '\0'; + } + + // Load a config file by name (without extension). + // Returns an initialized Doc (empty if file doesn't exist). + inline toml::Doc load(const char* name) { + char path[128]; + build_path(path, sizeof(path), name); + + int handle = montauk::open(path); + if (handle < 0) { + toml::Doc doc; + doc.init(); + return doc; + } + + uint64_t size = montauk::getsize(handle); + if (size == 0) { + montauk::close(handle); + toml::Doc doc; + doc.init(); + return doc; + } + + char* text = (char*)montauk::malloc(size + 1); + montauk::read(handle, (uint8_t*)text, 0, size); + montauk::close(handle); + text[size] = '\0'; + + toml::Doc doc = toml::parse(text); + montauk::mfree(text); + return doc; + } + + // Save a Doc to disk as a TOML file. + // Creates the file if it doesn't exist. + // Returns 0 on success, negative on error. + inline int save(const char* name, toml::Doc* doc) { + ensure_dir(); + + char path[128]; + build_path(path, sizeof(path), name); + + char* text = serialize(doc); + int textLen = montauk::slen(text); + + // Try to open existing file, create if not found + int handle = montauk::open(path); + if (handle < 0) { + handle = montauk::fcreate(path); + if (handle < 0) { + montauk::mfree(text); + return -1; + } + } + + int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen); + montauk::close(handle); + montauk::mfree(text); + return ret < 0 ? ret : 0; + } + + // Delete a config file. Returns 0 on success. + inline int remove(const char* name) { + char path[128]; + build_path(path, sizeof(path), name); + return montauk::fdelete(path); + } + + // ---- In-place modification helpers ---- + + // Set or overwrite a string value in the doc. + inline void set_string(toml::Doc* doc, const char* key, const char* val) { + auto* existing = doc->get(key); + if (existing) { + if (existing->type == toml::Type::String && existing->str) + montauk::mfree(existing->str); + existing->type = toml::Type::String; + existing->str = toml::Value::dup(val); + } else { + doc->entries.push(toml::Value::make_string(key, val, montauk::slen(val))); + } + } + + // Set or overwrite an integer value in the doc. + inline void set_int(toml::Doc* doc, const char* key, int64_t val) { + auto* existing = doc->get(key); + if (existing) { + if (existing->type == toml::Type::String && existing->str) + montauk::mfree(existing->str); + existing->type = toml::Type::Int; + existing->ival = val; + } else { + doc->entries.push(toml::Value::make_int(key, val)); + } + } + + // Set or overwrite a boolean value in the doc. + inline void set_bool(toml::Doc* doc, const char* key, bool val) { + auto* existing = doc->get(key); + if (existing) { + if (existing->type == toml::Type::String && existing->str) + montauk::mfree(existing->str); + existing->type = toml::Type::Bool; + existing->bval = val; + } else { + doc->entries.push(toml::Value::make_bool(key, val)); + } + } + + // Remove a key from the doc. + inline bool unset(toml::Doc* doc, const char* key) { + for (int i = 0; i < doc->entries.count; i++) { + auto* v = doc->entries.items[i]; + if (v->key && montauk::streq(v->key, key)) { + v->destroy(); + // Shift remaining entries down + for (int j = i; j < doc->entries.count - 1; j++) + doc->entries.items[j] = doc->entries.items[j + 1]; + doc->entries.count--; + return true; + } + } + return false; + } + +} // namespace config +} // namespace montauk diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index b7d2f4a..bd61000 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -146,6 +146,12 @@ namespace montauk { inline int fdelete(const char* path) { return (int)syscall1(Montauk::SYS_FDELETE, (uint64_t)path); } + inline int fmkdir(const char* path) { + return (int)syscall1(Montauk::SYS_FMKDIR, (uint64_t)path); + } + inline int drivelist(int* outDrives, int max) { + return (int)syscall2(Montauk::SYS_DRIVELIST, (uint64_t)outDrives, (uint64_t)max); + } // Memory inline void* alloc(uint64_t size) { return (void*)syscall1(Montauk::SYS_ALLOC, size); } diff --git a/programs/include/montauk/toml.h b/programs/include/montauk/toml.h new file mode 100644 index 0000000..db59843 --- /dev/null +++ b/programs/include/montauk/toml.h @@ -0,0 +1,573 @@ +/* + * toml.h + * TOML config file parser + * Supports: strings, integers, booleans, tables, arrays + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include +#include + +namespace montauk { +namespace toml { + + enum class Type { String, Int, Bool, Array, Table }; + + struct Value; + + // Dynamic array of Value pointers (for arrays and table entries) + struct ValueList { + Value** items; + int count; + int cap; + + void init() { items = nullptr; count = 0; cap = 0; } + + void push(Value* v) { + if (count >= cap) { + int newCap = cap ? cap * 2 : 8; + auto** buf = (Value**)montauk::malloc(newCap * sizeof(Value*)); + if (items) { + montauk::memcpy(buf, items, count * sizeof(Value*)); + montauk::mfree(items); + } + items = buf; + cap = newCap; + } + items[count++] = v; + } + + void destroy(); // forward — defined after Value + }; + + struct Value { + Type type; + char* key; // owned, dotted path (e.g. "server.port"); null for array elements + + union { + char* str; // Type::String — owned + int64_t ival; // Type::Int + bool bval; // Type::Bool + }; + ValueList array; // Type::Array elements or Type::Table entries + + static Value* make_string(const char* k, const char* s, int slen) { + auto* v = (Value*)montauk::malloc(sizeof(Value)); + v->type = Type::String; + v->key = dup(k); + v->str = dupn(s, slen); + v->array.init(); + return v; + } + + static Value* make_int(const char* k, int64_t n) { + auto* v = (Value*)montauk::malloc(sizeof(Value)); + v->type = Type::Int; + v->key = dup(k); + v->ival = n; + v->array.init(); + return v; + } + + static Value* make_bool(const char* k, bool b) { + auto* v = (Value*)montauk::malloc(sizeof(Value)); + v->type = Type::Bool; + v->key = dup(k); + v->bval = b; + v->array.init(); + return v; + } + + static Value* make_array(const char* k) { + auto* v = (Value*)montauk::malloc(sizeof(Value)); + v->type = Type::Array; + v->key = dup(k); + v->ival = 0; + v->array.init(); + return v; + } + + static Value* make_table(const char* k) { + auto* v = (Value*)montauk::malloc(sizeof(Value)); + v->type = Type::Table; + v->key = dup(k); + v->ival = 0; + v->array.init(); + return v; + } + + void destroy() { + if (key) montauk::mfree(key); + if (type == Type::String && str) montauk::mfree(str); + array.destroy(); + montauk::mfree(this); + } + + // helpers + static char* dup(const char* s) { + if (!s) return nullptr; + int n = montauk::slen(s); + return dupn(s, n); + } + static char* dupn(const char* s, int n) { + char* d = (char*)montauk::malloc(n + 1); + montauk::memcpy(d, s, n); + d[n] = '\0'; + return d; + } + }; + + inline void ValueList::destroy() { + for (int i = 0; i < count; i++) items[i]->destroy(); + if (items) montauk::mfree(items); + items = nullptr; + count = 0; + cap = 0; + } + + // ---- Document ---- + + struct Doc { + ValueList entries; // flat list of all top-level and nested values + + void init() { entries.init(); } + + void destroy() { + entries.destroy(); + } + + // Lookup by dotted key path (e.g. "server.port") + Value* get(const char* key) const { + for (int i = 0; i < entries.count; i++) { + if (entries.items[i]->key && montauk::streq(entries.items[i]->key, key)) + return entries.items[i]; + } + return nullptr; + } + + // Typed accessors with defaults + const char* get_string(const char* key, const char* def = "") const { + auto* v = get(key); + return (v && v->type == Type::String) ? v->str : def; + } + + int64_t get_int(const char* key, int64_t def = 0) const { + auto* v = get(key); + return (v && v->type == Type::Int) ? v->ival : def; + } + + bool get_bool(const char* key, bool def = false) const { + auto* v = get(key); + return (v && v->type == Type::Bool) ? v->bval : def; + } + + Value* get_array(const char* key) const { + auto* v = get(key); + return (v && v->type == Type::Array) ? v : nullptr; + } + + Value* get_table(const char* key) const { + auto* v = get(key); + return (v && v->type == Type::Table) ? v : nullptr; + } + }; + + // ---- Parser ---- + + namespace detail { + + struct Parser { + const char* src; + int pos; + int len; + char table_prefix[256]; // current [table] prefix + + char peek() const { return pos < len ? src[pos] : '\0'; } + char next() { return pos < len ? src[pos++] : '\0'; } + bool at_end() const { return pos >= len; } + + void skip_ws() { + while (pos < len && (src[pos] == ' ' || src[pos] == '\t')) pos++; + } + + void skip_line() { + while (pos < len && src[pos] != '\n') pos++; + if (pos < len) pos++; // consume newline + } + + void skip_ws_and_newlines() { + while (pos < len) { + char c = src[pos]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + pos++; + else if (c == '#') + skip_line(); + else + break; + } + } + + // Build dotted key: prefix.key + void build_key(char* out, int outSz, const char* key, int keyLen) { + int p = 0; + if (table_prefix[0]) { + int plen = montauk::slen(table_prefix); + for (int i = 0; i < plen && p < outSz - 2; i++) out[p++] = table_prefix[i]; + out[p++] = '.'; + } + for (int i = 0; i < keyLen && p < outSz - 1; i++) out[p++] = key[i]; + out[p] = '\0'; + } + + // Parse a bare key or quoted key, returns length + int parse_key(char* buf, int bufSz) { + skip_ws(); + int n = 0; + if (peek() == '"') { + next(); // consume opening quote + while (!at_end() && peek() != '"' && n < bufSz - 1) + buf[n++] = next(); + if (peek() == '"') next(); + } else { + while (!at_end()) { + char c = peek(); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') { + buf[n++] = next(); + if (n >= bufSz - 1) break; + } else { + break; + } + } + } + buf[n] = '\0'; + return n; + } + + // Parse a dotted key (e.g. server.host), returns full length written to buf + int parse_dotted_key(char* buf, int bufSz) { + int total = 0; + // Parse first segment + char seg[128]; + int segLen = parse_key(seg, sizeof(seg)); + for (int i = 0; i < segLen && total < bufSz - 1; i++) + buf[total++] = seg[i]; + + // Parse additional dotted segments + while (peek() == '.') { + next(); // consume dot + if (total < bufSz - 1) buf[total++] = '.'; + segLen = parse_key(seg, sizeof(seg)); + for (int i = 0; i < segLen && total < bufSz - 1; i++) + buf[total++] = seg[i]; + } + buf[total] = '\0'; + return total; + } + + int64_t parse_integer() { + bool neg = false; + if (peek() == '-') { neg = true; next(); } + else if (peek() == '+') { next(); } + + // Hex, octal, binary + if (peek() == '0' && pos + 1 < len) { + char c2 = src[pos + 1]; + if (c2 == 'x' || c2 == 'X') { + pos += 2; + int64_t val = 0; + while (!at_end()) { + char c = peek(); + if (c == '_') { next(); continue; } + int d = -1; + if (c >= '0' && c <= '9') d = c - '0'; + else if (c >= 'a' && c <= 'f') d = 10 + c - 'a'; + else if (c >= 'A' && c <= 'F') d = 10 + c - 'A'; + if (d < 0) break; + val = val * 16 + d; + next(); + } + return neg ? -val : val; + } + if (c2 == 'o') { + pos += 2; + int64_t val = 0; + while (!at_end()) { + char c = peek(); + if (c == '_') { next(); continue; } + if (c < '0' || c > '7') break; + val = val * 8 + (c - '0'); + next(); + } + return neg ? -val : val; + } + if (c2 == 'b') { + pos += 2; + int64_t val = 0; + while (!at_end()) { + char c = peek(); + if (c == '_') { next(); continue; } + if (c != '0' && c != '1') break; + val = val * 2 + (c - '0'); + next(); + } + return neg ? -val : val; + } + } + + int64_t val = 0; + while (!at_end()) { + char c = peek(); + if (c == '_') { next(); continue; } + if (c < '0' || c > '9') break; + val = val * 10 + (c - '0'); + next(); + } + return neg ? -val : val; + } + + // Parse string value (opening quote already detected but not consumed) + // Returns heap-allocated string + char* parse_string_value(int* outLen) { + char quote = next(); // consume opening " or ' + bool literal = (quote == '\''); + + // Multi-line? (""" or ''') + bool multi = false; + if (pos + 1 < len && src[pos] == quote && src[pos + 1] == quote) { + pos += 2; + multi = true; + // Skip first newline after opening delimiter + if (peek() == '\r') next(); + if (peek() == '\n') next(); + } + + // Collect into temp buffer + char buf[4096]; + int n = 0; + + while (!at_end() && n < (int)sizeof(buf) - 1) { + if (multi) { + // Check for closing triple-quote + if (pos + 2 < len && src[pos] == quote && + src[pos+1] == quote && src[pos+2] == quote) { + pos += 3; + break; + } + } else { + if (peek() == quote) { next(); break; } + if (peek() == '\n') break; // unterminated single-line + } + + if (!literal && peek() == '\\') { + next(); // consume backslash + char esc = next(); + switch (esc) { + case 'n': buf[n++] = '\n'; break; + case 't': buf[n++] = '\t'; break; + case 'r': buf[n++] = '\r'; break; + case '\\': buf[n++] = '\\'; break; + case '"': buf[n++] = '"'; break; + case '\r': + if (peek() == '\n') next(); + skip_ws(); + break; + case '\n': + skip_ws(); + break; + default: buf[n++] = esc; break; + } + } else { + buf[n++] = next(); + } + } + + *outLen = n; + return Value::dupn(buf, n); + } + + Value* parse_value(const char* fullKey) { + skip_ws(); + char c = peek(); + + // String + if (c == '"' || c == '\'') { + int slen = 0; + char* s = parse_string_value(&slen); + auto* v = Value::make_string(fullKey, s, slen); + montauk::mfree(s); + return v; + } + + // Boolean + if (montauk::starts_with(src + pos, "true")) { + pos += 4; + return Value::make_bool(fullKey, true); + } + if (montauk::starts_with(src + pos, "false")) { + pos += 5; + return Value::make_bool(fullKey, false); + } + + // Array + if (c == '[') { + next(); // consume [ + auto* arr = Value::make_array(fullKey); + skip_ws_and_newlines(); + while (!at_end() && peek() != ']') { + auto* elem = parse_value(nullptr); + if (elem) arr->array.push(elem); + skip_ws_and_newlines(); + if (peek() == ',') next(); + skip_ws_and_newlines(); + } + if (peek() == ']') next(); + return arr; + } + + // Inline table + if (c == '{') { + next(); // consume { + auto* tbl = Value::make_table(fullKey); + skip_ws_and_newlines(); + while (!at_end() && peek() != '}') { + char key[128]; + int keyLen = parse_dotted_key(key, sizeof(key)); + if (keyLen == 0) { skip_line(); continue; } + skip_ws(); + if (peek() == '=') next(); + skip_ws(); + + // Build full dotted key for inline table entry + char entryKey[256]; + int p = 0; + if (fullKey) { + int flen = montauk::slen(fullKey); + for (int i = 0; i < flen && p < 254; i++) entryKey[p++] = fullKey[i]; + entryKey[p++] = '.'; + } + for (int i = 0; i < keyLen && p < 255; i++) entryKey[p++] = key[i]; + entryKey[p] = '\0'; + + auto* val = parse_value(entryKey); + if (val) tbl->array.push(val); + skip_ws(); + if (peek() == ',') next(); + skip_ws(); + } + if (peek() == '}') next(); + return tbl; + } + + // Integer (includes negative) + if ((c >= '0' && c <= '9') || c == '+' || c == '-') { + int64_t n = parse_integer(); + return Value::make_int(fullKey, n); + } + + // Unknown — skip the rest of the line + skip_line(); + return nullptr; + } + + void parse(Doc* doc) { + table_prefix[0] = '\0'; + + while (!at_end()) { + skip_ws_and_newlines(); + if (at_end()) break; + + char c = peek(); + + // Table header: [name] or [name.sub] + if (c == '[') { + next(); + bool array_table = false; + if (peek() == '[') { next(); array_table = true; } + + skip_ws(); + int plen = parse_dotted_key(table_prefix, sizeof(table_prefix)); + skip_ws(); + if (peek() == ']') next(); + if (array_table && peek() == ']') next(); + + // Register the table itself + auto* tbl = Value::make_table(table_prefix); + doc->entries.push(tbl); + + skip_line(); + continue; + } + + // Key = value + char key[128]; + int keyLen = parse_dotted_key(key, sizeof(key)); + if (keyLen == 0) { skip_line(); continue; } + + skip_ws(); + if (peek() != '=') { skip_line(); continue; } + next(); // consume = + skip_ws(); + + char fullKey[256]; + build_key(fullKey, sizeof(fullKey), key, keyLen); + + Value* val = parse_value(fullKey); + if (val) { + doc->entries.push(val); + + // For inline tables, also flatten entries into doc + if (val->type == Type::Table) { + for (int i = 0; i < val->array.count; i++) { + // Clone entry into doc's flat list so dotted lookup works + auto* e = val->array.items[i]; + Value* clone = nullptr; + if (e->type == Type::String) + clone = Value::make_string(e->key, e->str, montauk::slen(e->str)); + else if (e->type == Type::Int) + clone = Value::make_int(e->key, e->ival); + else if (e->type == Type::Bool) + clone = Value::make_bool(e->key, e->bval); + if (clone) doc->entries.push(clone); + } + } + } + + // Consume rest of line (trailing comment, etc.) + skip_ws(); + if (peek() == '#') skip_line(); + else if (peek() == '\r' || peek() == '\n') { + if (peek() == '\r') next(); + if (peek() == '\n') next(); + } + } + } + }; + + } // namespace detail + + // ---- Public API ---- + + // Parse a TOML string into a Doc. Caller must call doc.destroy() when done. + inline Doc parse(const char* text) { + Doc doc; + doc.init(); + + detail::Parser p; + p.src = text; + p.pos = 0; + p.len = montauk::slen(text); + p.table_prefix[0] = '\0'; + p.parse(&doc); + + return doc; + } + + // Parse from a file descriptor (reads entire file into buffer first). + // Requires montauk::syscall read/fstat or similar — omitted here for portability. + // Users can read a file into a buffer and call parse() directly. + +} // namespace toml +} // namespace montauk diff --git a/programs/lib/libjpeg/libjpeg.a b/programs/lib/libjpeg/libjpeg.a index 9d9d5aa..38bca66 100644 Binary files a/programs/lib/libjpeg/libjpeg.a and b/programs/lib/libjpeg/libjpeg.a differ diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a index 5e03410..737b973 100644 Binary files a/programs/lib/tls/libtls.a and b/programs/lib/tls/libtls.a differ diff --git a/programs/lib/tls/obj/tls.o b/programs/lib/tls/obj/tls.o index 8cd7af3..04412cc 100644 Binary files a/programs/lib/tls/obj/tls.o and b/programs/lib/tls/obj/tls.o differ diff --git a/programs/src/desktop/apps/app_devexplorer.cpp b/programs/src/desktop/apps/app_devexplorer.cpp deleted file mode 100644 index 7b4d9ce..0000000 --- a/programs/src/desktop/apps/app_devexplorer.cpp +++ /dev/null @@ -1,12 +0,0 @@ -/* - * app_devexplorer.cpp - * MontaukOS Desktop - Device Explorer launcher (spawns standalone devexplorer.elf) - * Copyright (c) 2026 Daniel Hammer -*/ - -#include "apps_common.hpp" - -void open_devexplorer(DesktopState* ds) { - (void)ds; - montauk::spawn("0:/os/devexplorer.elf"); -} diff --git a/programs/src/desktop/apps/app_disks.cpp b/programs/src/desktop/apps/app_disks.cpp deleted file mode 100644 index e0cb797..0000000 --- a/programs/src/desktop/apps/app_disks.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* - * app_disks.cpp - * Copyright (c) 2026 Daniel Hammer -*/ - -#include "apps_common.hpp" - -void open_disks(DesktopState* ds) { - (void)ds; - montauk::spawn("0:/os/disks.elf"); -} diff --git a/programs/src/desktop/apps/app_doom.cpp b/programs/src/desktop/apps/app_doom.cpp deleted file mode 100644 index 3f028ac..0000000 --- a/programs/src/desktop/apps/app_doom.cpp +++ /dev/null @@ -1,12 +0,0 @@ -/* - * app_doom.cpp - * MontaukOS Desktop - DOOM launcher (spawns standalone doom.elf) - * Copyright (c) 2026 Daniel Hammer -*/ - -#include "apps_common.hpp" - -void open_doom(DesktopState* ds) { - (void)ds; - montauk::spawn("0:/games/doom.elf"); -} diff --git a/programs/src/desktop/apps/app_filemanager.cpp b/programs/src/desktop/apps/app_filemanager.cpp index b24494c..f589329 100644 --- a/programs/src/desktop/apps/app_filemanager.cpp +++ b/programs/src/desktop/apps/app_filemanager.cpp @@ -92,7 +92,12 @@ static void filemanager_read_drives(FileManagerState* fm) { fm->at_drives_root = true; fm->current_path[0] = '\0'; - for (int d = 0; d < FM_MAX_DRIVES; d++) { + int drives[FM_MAX_DRIVES]; + int driveCount = montauk::drivelist(drives, FM_MAX_DRIVES); + + for (int di = 0; di < driveCount; di++) { + int d = drives[di]; + int i = fm->entry_count; char probe[8]; if (d < 10) { probe[0] = '0' + d; @@ -106,21 +111,16 @@ static void filemanager_read_drives(FileManagerState* fm) { probe[3] = '/'; probe[4] = '\0'; } - const char* tmp[1]; - int r = montauk::readdir(probe, tmp, 1); - if (r >= 0) { - int i = fm->entry_count; - char label[64]; - montauk::strcpy(label, "Drive "); - str_append(label, probe, 64); - montauk::strncpy(fm->entry_names[i], label, 63); - fm->entry_types[i] = 3; // drive - fm->entry_sizes[i] = 0; - fm->is_dir[i] = true; - fm->drive_indices[i] = d; - fm->entry_count++; - if (fm->entry_count >= 64) break; - } + char label[64]; + montauk::strcpy(label, "Drive "); + str_append(label, probe, 64); + montauk::strncpy(fm->entry_names[i], label, 63); + fm->entry_types[i] = 3; // drive + fm->entry_sizes[i] = 0; + fm->is_dir[i] = true; + fm->drive_indices[i] = d; + fm->entry_count++; + if (fm->entry_count >= 64) break; } fm->selected = -1; @@ -688,13 +688,13 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { } str_append(fullpath, fm->entry_names[clicked_idx], 512); if (is_image_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/imageviewer.elf", fullpath); + montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath); } else if (is_font_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/fontpreview.elf", fullpath); + montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath); } else if (is_pdf_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/pdfviewer.elf", fullpath); + montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath); } else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/spreadsheet.elf", fullpath); + montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } @@ -744,13 +744,13 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { } str_append(fullpath, fm->entry_names[clicked_idx], 512); if (is_image_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/imageviewer.elf", fullpath); + montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath); } else if (is_font_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/fontpreview.elf", fullpath); + montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath); } else if (is_pdf_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/pdfviewer.elf", fullpath); + montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath); } else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) { - montauk::spawn("0:/os/spreadsheet.elf", fullpath); + montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } diff --git a/programs/src/desktop/apps/app_spreadsheet.cpp b/programs/src/desktop/apps/app_spreadsheet.cpp deleted file mode 100644 index 1100b7e..0000000 --- a/programs/src/desktop/apps/app_spreadsheet.cpp +++ /dev/null @@ -1,12 +0,0 @@ -/* - * app_spreadsheet.cpp - * MontaukOS Desktop - Spreadsheet launcher (spawns standalone spreadsheet.elf) - * Copyright (c) 2026 Daniel Hammer -*/ - -#include "apps_common.hpp" - -void open_spreadsheet(DesktopState* ds) { - (void)ds; - montauk::spawn("0:/os/spreadsheet.elf"); -} diff --git a/programs/src/desktop/apps/app_terminal.cpp b/programs/src/desktop/apps/app_terminal.cpp index 7e4c074..03ef628 100644 --- a/programs/src/desktop/apps/app_terminal.cpp +++ b/programs/src/desktop/apps/app_terminal.cpp @@ -17,12 +17,7 @@ static constexpr int TERM_TAB_GAP = 4; static constexpr int TERM_PLUS_W = 28; static constexpr int TERM_PLUS_PAD = 8; -// Compute the x-offset for left-aligned tabs within bar_w -static int term_tabs_origin(int bar_w, int tab_count) { - (void)bar_w; - (void)tab_count; - return 8; -} +static constexpr int TERM_TAB_PAD = 8; struct TermTabState { TerminalState* tabs[TERM_MAX_TABS]; @@ -150,7 +145,7 @@ static void terminal_on_draw(Window* win, Framebuffer& fb) { c.fill_rect(0, 0, cr.w, TERM_TAB_BAR_H, bar_bg); int fh = system_font_height(); - int tab_x = term_tabs_origin(cr.w, tts->tab_count); + int tab_x = TERM_TAB_PAD; for (int i = 0; i < tts->tab_count; i++) { bool active = (i == tts->active_tab); @@ -241,7 +236,7 @@ static void terminal_on_mouse(Window* win, MouseEvent& ev) { if (ly >= TERM_TAB_BAR_H) return; // click in terminal area, ignore - int tab_x = term_tabs_origin(cr.w, tts->tab_count); + int tab_x = TERM_TAB_PAD; for (int i = 0; i < tts->tab_count; i++) { if (lx >= tab_x && lx < tab_x + TERM_TAB_W) { // Check if click is on the close "x" (last 24px of tab) diff --git a/programs/src/desktop/apps/app_weather.cpp b/programs/src/desktop/apps/app_weather.cpp deleted file mode 100644 index ba54893..0000000 --- a/programs/src/desktop/apps/app_weather.cpp +++ /dev/null @@ -1,13 +0,0 @@ -/* - * app_weather.cpp - * MontaukOS Desktop - Weather app launcher - * Spawns weather.elf as a standalone Window Server process - * Copyright (c) 2026 Daniel Hammer - */ - -#include "apps_common.hpp" - -void open_weather(DesktopState* ds) { - (void)ds; - montauk::spawn("0:/os/weather.elf"); -} diff --git a/programs/src/desktop/apps/app_wiki.cpp b/programs/src/desktop/apps/app_wiki.cpp deleted file mode 100644 index c93f4a0..0000000 --- a/programs/src/desktop/apps/app_wiki.cpp +++ /dev/null @@ -1,13 +0,0 @@ -/* - * app_wiki.cpp - * MontaukOS Desktop - Wikipedia launcher - * Spawns wikipedia.elf as a standalone Window Server process - * Copyright (c) 2026 Daniel Hammer -*/ - -#include "apps_common.hpp" - -void open_wiki(DesktopState* ds) { - (void)ds; - montauk::spawn("0:/os/wikipedia.elf"); -} diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index ee2e0ab..a46199c 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -156,7 +156,7 @@ inline void format_size(char* buf, int size) { } // ============================================================================ -// Forward declarations for app launchers +// Forward declarations for app launchers (embedded apps only) // ============================================================================ void open_terminal(DesktopState* ds); @@ -166,16 +166,10 @@ void open_calculator(DesktopState* ds); void open_texteditor(DesktopState* ds); void open_texteditor_with_file(DesktopState* ds, const char* path); void open_klog(DesktopState* ds); -void open_wiki(DesktopState* ds); -void open_weather(DesktopState* ds); void open_procmgr(DesktopState* ds); void open_mandelbrot(DesktopState* ds); -void open_devexplorer(DesktopState* ds); void open_settings(DesktopState* ds); -void open_doom(DesktopState* ds); void open_reboot_dialog(DesktopState* ds); void open_wordprocessor(DesktopState* ds); -void open_spreadsheet(DesktopState* ds); void open_shutdown_dialog(DesktopState* ds); -void open_disks(DesktopState* ds); void desktop_poll_external_windows(DesktopState* ds); diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index c640aab..c17e133 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -8,9 +8,10 @@ #include "apps/apps_common.hpp" #include "wallpaper.hpp" +#include // ============================================================================ -// App Menu Data +// App Menu Data (dynamic — built at startup from embedded + external apps) // ============================================================================ static constexpr int MENU_W = 220; @@ -20,36 +21,16 @@ static constexpr int MENU_DIV_H = 10; struct MenuRow { bool is_category; - const char* label; // "" for divider-only rows - int app_id; // -1 for category headers / dividers + char label[48]; + int app_id; // embedded dispatch ID, or -1 for categories/dividers + bool external; // true = spawn binary_path + char binary_path[128]; // full VFS path for external apps + SvgIcon* icon; // loaded menu icon (null for categories/dividers) }; -static constexpr int MENU_ROW_COUNT = 23; -static const MenuRow menu_rows[MENU_ROW_COUNT] = { - { true, "Applications", -1 }, // cat 0 - { false, "Terminal", 0 }, - { false, "Files", 1 }, - { false, "Text Editor", 4 }, - { false, "Word Processor", 15 }, - { false, "Spreadsheet", 16 }, - { false, "Calculator", 3 }, - { true, "Internet", -1 }, // cat 1 - { false, "Wikipedia", 9 }, - { false, "Weather", 13 }, - { true, "System", -1 }, // cat 2 - { false, "System Info", 2 }, - { false, "Kernel Log", 5 }, - { false, "Processes", 6 }, - { false, "Devices", 8 }, - { false, "Disks", 17 }, - { true, "Games", -1 }, // cat 3 - { false, "Mandelbrot", 7 }, - { false, "DOOM", 10 }, - { true, "", -1 }, // divider (always visible) - { false, "Settings", 11 }, - { false, "Reboot", 12 }, - { false, "Shutdown", 14 }, -}; +static constexpr int MAX_MENU_ROWS = 48; +inline MenuRow menu_rows[MAX_MENU_ROWS]; +inline int menu_row_count = 0; // Collapsible category state (categories 0-3 are toggleable; divider category always expanded) static constexpr int MENU_NUM_CATS = 5; @@ -77,12 +58,52 @@ inline int menu_row_height(const MenuRow& row) { inline int menu_total_height() { int h = 10; // top + bottom padding - for (int i = 0; i < MENU_ROW_COUNT; i++) + for (int i = 0; i < menu_row_count; i++) if (menu_row_visible(i)) h += menu_row_height(menu_rows[i]); return h; } +// ============================================================================ +// Menu Builder Helpers +// ============================================================================ + +inline int menu_add_category(const char* label) { + if (menu_row_count >= MAX_MENU_ROWS) return -1; + MenuRow& r = menu_rows[menu_row_count++]; + r.is_category = true; + montauk::strncpy(r.label, label, sizeof(r.label)); + r.app_id = -1; + r.external = false; + r.binary_path[0] = '\0'; + r.icon = nullptr; + return menu_row_count - 1; +} + +inline int menu_add_embedded(const char* label, int app_id, SvgIcon* icon) { + if (menu_row_count >= MAX_MENU_ROWS) return -1; + MenuRow& r = menu_rows[menu_row_count++]; + r.is_category = false; + montauk::strncpy(r.label, label, sizeof(r.label)); + r.app_id = app_id; + r.external = false; + r.binary_path[0] = '\0'; + r.icon = icon; + return menu_row_count - 1; +} + +inline int menu_add_external(const char* label, const char* binary, SvgIcon* icon) { + if (menu_row_count >= MAX_MENU_ROWS) return -1; + MenuRow& r = menu_rows[menu_row_count++]; + r.is_category = false; + montauk::strncpy(r.label, label, sizeof(r.label)); + r.app_id = -1; + r.external = true; + montauk::strncpy(r.binary_path, binary, sizeof(r.binary_path)); + r.icon = icon; + return menu_row_count - 1; +} + // ============================================================================ // Forward Declarations (internal functions) // ============================================================================ @@ -95,6 +116,10 @@ gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge); void desktop_draw_app_menu(gui::DesktopState* ds); void desktop_draw_net_popup(gui::DesktopState* ds); +// main.cpp +void desktop_scan_apps(gui::DesktopState* ds); +void desktop_build_menu(gui::DesktopState* ds); + // Month names (shared by panel clock) inline const char* month_names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index 652a080..7be5f53 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -202,7 +202,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { // Walk visible rows to find which one was clicked int iy = menu_y + 5; int cur_cat = -1; - for (int i = 0; i < MENU_ROW_COUNT; i++) { + for (int i = 0; i < menu_row_count; i++) { const MenuRow& row = menu_rows[i]; if (row.is_category) cur_cat++; if (!menu_row_visible(i)) continue; @@ -212,25 +212,25 @@ void gui::desktop_handle_mouse(DesktopState* ds) { // Toggle category expand/collapse menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat]; } else if (!row.is_category) { - switch (row.app_id) { - case 0: open_terminal(ds); break; - case 1: open_filemanager(ds); break; - case 2: open_sysinfo(ds); break; - case 3: open_calculator(ds); break; - case 4: open_texteditor(ds); break; - case 5: open_klog(ds); break; - case 6: open_procmgr(ds); break; - case 7: open_mandelbrot(ds); break; - case 8: open_devexplorer(ds); break; - case 9: open_wiki(ds); break; - case 10: open_doom(ds); break; - case 11: open_settings(ds); break; - case 12: open_reboot_dialog(ds); break; - case 13: open_weather(ds); break; - case 14: open_shutdown_dialog(ds); break; - case 15: open_wordprocessor(ds); break; - case 16: open_spreadsheet(ds); break; - case 17: open_disks(ds); break; + if (row.external) { + // Launch external app from manifest + montauk::spawn(row.binary_path); + } else { + // Dispatch embedded app + switch (row.app_id) { + case 0: open_terminal(ds); break; + case 1: open_filemanager(ds); break; + case 2: open_sysinfo(ds); break; + case 3: open_calculator(ds); break; + case 4: open_texteditor(ds); break; + case 5: open_klog(ds); break; + case 6: open_procmgr(ds); break; + case 7: open_mandelbrot(ds); break; + case 11: open_settings(ds); break; + case 12: open_reboot_dialog(ds); break; + case 14: open_shutdown_dialog(ds); break; + case 15: open_wordprocessor(ds); break; + } } ds->app_menu_open = false; } @@ -534,10 +534,6 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key open_klog(ds); return; } - if (key.ascii == 'd' || key.ascii == 'D') { - open_doom(ds); - return; - } } // Dispatch to focused window diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 18ab9a9..6ee27ec 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -6,6 +6,173 @@ #include "desktop_internal.hpp" +// ============================================================================ +// App Manifest Scanning +// ============================================================================ + +// Extract the basename from a readdir entry. +// readdir returns full paths from drive root (e.g. "apps/doom/") — +// strip the directory prefix and any trailing slash to get just "doom". +static void extract_basename(char* out, int outSz, const char* entry) { + // Strip trailing slash + int len = montauk::slen(entry); + while (len > 0 && entry[len - 1] == '/') len--; + + // Find last slash before that + int last_slash = -1; + for (int i = 0; i < len; i++) { + if (entry[i] == '/') last_slash = i; + } + + const char* base = (last_slash >= 0) ? entry + last_slash + 1 : entry; + int base_len = len - (last_slash >= 0 ? last_slash + 1 : 0); + if (base_len >= outSz) base_len = outSz - 1; + montauk::memcpy(out, base, base_len); + out[base_len] = '\0'; +} + +void desktop_scan_apps(DesktopState* ds) { + ds->external_app_count = 0; + + // Ensure the apps directory exists + montauk::fmkdir("0:/apps"); + + const char* entries[32]; + int count = montauk::readdir("0:/apps", entries, 32); + + for (int i = 0; i < count && ds->external_app_count < MAX_EXTERNAL_APPS; i++) { + // readdir returns paths like "apps/doom/" — extract just "doom" + char dirname[64]; + extract_basename(dirname, sizeof(dirname), entries[i]); + if (dirname[0] == '\0') continue; + + // Try to open the manifest in this app directory + char manifest_path[128]; + snprintf(manifest_path, sizeof(manifest_path), "0:/apps/%s/manifest.toml", dirname); + + int fh = montauk::open(manifest_path); + if (fh < 0) continue; + + uint64_t sz = montauk::getsize(fh); + if (sz == 0 || sz > 4096) { montauk::close(fh); continue; } + + char* text = (char*)montauk::malloc(sz + 1); + montauk::read(fh, (uint8_t*)text, 0, sz); + montauk::close(fh); + text[sz] = '\0'; + + auto doc = montauk::toml::parse(text); + montauk::mfree(text); + + ExternalApp* app = &ds->external_apps[ds->external_app_count]; + + // Read manifest fields + const char* name = doc.get_string("app.name", "Unknown"); + const char* binary = doc.get_string("app.binary", ""); + const char* icon_file = doc.get_string("app.icon", ""); + const char* category = doc.get_string("menu.category", "Applications"); + bool visible = doc.get_bool("menu.visible", true); + + montauk::strncpy(app->name, name, sizeof(app->name)); + montauk::strncpy(app->category, category, sizeof(app->category)); + app->menu_visible = visible; + + // Build full binary path: 0:/apps// + snprintf(app->binary_path, sizeof(app->binary_path), + "0:/apps/%s/%s", dirname, binary); + + // Load icon from app directory + app->icon = {}; + if (icon_file[0]) { + char icon_path[128]; + snprintf(icon_path, sizeof(icon_path), + "0:/apps/%s/%s", dirname, icon_file); + Color defColor = colors::ICON_COLOR; + app->icon = svg_load(icon_path, 20, 20, defColor); + } + + doc.destroy(); + ds->external_app_count++; + } +} + +// ============================================================================ +// Menu Builder +// ============================================================================ + +// Category definitions and their ordering +static const char* CATEGORY_NAMES[] = { "Applications", "Internet", "System", "Games" }; +static constexpr int NUM_CATEGORIES = 4; + +// Embedded app definitions: { display_name, app_id, category_index } +struct EmbeddedAppDef { + const char* name; + int app_id; + int category; // index into CATEGORY_NAMES +}; + +static const EmbeddedAppDef embedded_apps[] = { + { "Terminal", 0, 0 }, + { "Files", 1, 0 }, + { "Text Editor", 4, 0 }, + { "Word Processor", 15, 0 }, + { "Calculator", 3, 0 }, + { "System Info", 2, 2 }, + { "Kernel Log", 5, 2 }, + { "Processes", 6, 2 }, + { "Mandelbrot", 7, 3 }, +}; + +static constexpr int NUM_EMBEDDED = sizeof(embedded_apps) / sizeof(embedded_apps[0]); + +// Resolve embedded app_id to an icon pointer in DesktopState +static SvgIcon* icon_for_embedded(DesktopState* ds, int app_id) { + switch (app_id) { + case 0: return &ds->icon_terminal; + case 1: return &ds->icon_filemanager; + case 2: return &ds->icon_sysinfo; + case 3: return &ds->icon_calculator; + case 4: return &ds->icon_texteditor; + case 5: return &ds->icon_terminal; // Kernel Log uses terminal icon + case 6: return &ds->icon_procmgr; + case 7: return &ds->icon_mandelbrot; + case 15: return &ds->icon_texteditor; // Word Processor uses text editor icon + default: return nullptr; + } +} + +void desktop_build_menu(DesktopState* ds) { + menu_row_count = 0; + + // Build each category + for (int cat = 0; cat < NUM_CATEGORIES; cat++) { + menu_add_category(CATEGORY_NAMES[cat]); + + // Add embedded apps in this category + for (int e = 0; e < NUM_EMBEDDED; e++) { + if (embedded_apps[e].category == cat) { + menu_add_embedded(embedded_apps[e].name, embedded_apps[e].app_id, + icon_for_embedded(ds, embedded_apps[e].app_id)); + } + } + + // Add external apps in this category + for (int x = 0; x < ds->external_app_count; x++) { + ExternalApp* app = &ds->external_apps[x]; + if (!app->menu_visible) continue; + if (montauk::streq(app->category, CATEGORY_NAMES[cat])) { + menu_add_external(app->name, app->binary_path, &app->icon); + } + } + } + + // Divider + always-visible entries + menu_add_category(""); // divider (cat 4, always expanded) + menu_add_embedded("Settings", 11, &ds->icon_settings); + menu_add_embedded("Reboot", 12, &ds->icon_reboot); + menu_add_embedded("Shutdown", 14, &ds->icon_shutdown); +} + // ============================================================================ // Initialization // ============================================================================ @@ -58,14 +225,12 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor); ds->icon_shutdown = svg_load("0:/icons/system-shutdown.svg", 20, 20, defColor); - ds->icon_weather = svg_load("0:/icons/weather-widget.svg", 20, 20, defColor); + ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor); + ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor); - ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor); - ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor); - ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor); - ds->icon_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor); - ds->icon_disks = svg_load("0:/icons/gparted.svg", 20, 20, defColor); // gparted icon, i.e. disk/partition management - ds->icon_spreadsheet = svg_load("0:/icons/spreadsheet.svg", 20, 20, defColor); + // Scan 0:/apps/ for external app manifests and build the menu + desktop_scan_apps(ds); + desktop_build_menu(ds); // Settings defaults ds->settings.bg_gradient = true; @@ -236,6 +401,10 @@ void gui::desktop_run(DesktopState* ds) { // Handle mouse events desktop_handle_mouse(ds); + // Re-poll external windows so that any killed during mouse/key + // handling are removed before we touch their pixel buffers. + desktop_poll_external_windows(ds); + // Compose and present desktop_compose(ds); ds->fb.flip(); diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp index 13d53b3..434f751 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -151,34 +151,12 @@ void desktop_draw_app_menu(DesktopState* ds) { fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG); draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); - // Icon lookup by app_id - SvgIcon* icons[18] = { - &ds->icon_terminal, // 0 - &ds->icon_filemanager, // 1 - &ds->icon_sysinfo, // 2 - &ds->icon_calculator, // 3 - &ds->icon_texteditor, // 4 - &ds->icon_terminal, // 5 (Kernel Log) - &ds->icon_procmgr, // 6 - &ds->icon_mandelbrot, // 7 - &ds->icon_devexplorer, // 8 - &ds->icon_wikipedia, // 9 - &ds->icon_doom, // 10 - &ds->icon_settings, // 11 - &ds->icon_reboot, // 12 - &ds->icon_weather, // 13 - &ds->icon_shutdown, // 14 - &ds->icon_texteditor, // 15 - &ds->icon_spreadsheet, // 16 - &ds->icon_disks, // 17 (Disks) - }; - int mx = ds->mouse.x; int my = ds->mouse.y; int iy = menu_y + 5; int cur_cat = -1; - for (int i = 0; i < MENU_ROW_COUNT; i++) { + for (int i = 0; i < menu_row_count; i++) { const MenuRow& row = menu_rows[i]; if (row.is_category) cur_cat++; @@ -239,11 +217,8 @@ void desktop_draw_app_menu(DesktopState* ds) { // Icon int icon_x = item_rect.x + 8; int icon_y = item_rect.y + (row_h - 20) / 2; - if (row.app_id >= 0 && row.app_id < 18) { - SvgIcon* icon = icons[row.app_id]; - if (icon && icon->pixels) { - fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); - } + if (row.icon && row.icon->pixels) { + fb.blit_alpha(icon_x, icon_y, row.icon->width, row.icon->height, row.icon->pixels); } // Label diff --git a/programs/src/devexplorer/Makefile b/programs/src/devexplorer/Makefile index a575cad..0c29ee7 100644 --- a/programs/src/devexplorer/Makefile +++ b/programs/src/devexplorer/Makefile @@ -68,14 +68,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/devexplorer.elf +TARGET := $(BINDIR)/apps/devexplorer/devexplorer.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/devexplorer $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/devexplorer/manifest.toml b/programs/src/devexplorer/manifest.toml new file mode 100644 index 0000000..5f24534 --- /dev/null +++ b/programs/src/devexplorer/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Devices" +binary = "devexplorer.elf" +icon = "hardware.svg" + +[menu] +category = "System" +visible = true diff --git a/programs/src/disks/Makefile b/programs/src/disks/Makefile index cb75625..f985f96 100644 --- a/programs/src/disks/Makefile +++ b/programs/src/disks/Makefile @@ -68,14 +68,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/disks.elf +TARGET := $(BINDIR)/apps/disks/disks.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/disks $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/disks/manifest.toml b/programs/src/disks/manifest.toml new file mode 100644 index 0000000..0a4bc6f --- /dev/null +++ b/programs/src/disks/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Disks" +binary = "disks.elf" +icon = "gparted.svg" + +[menu] +category = "System" +visible = true diff --git a/programs/src/doom/Makefile b/programs/src/doom/Makefile index 5981ac9..1a29d14 100644 --- a/programs/src/doom/Makefile +++ b/programs/src/doom/Makefile @@ -163,14 +163,14 @@ ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS) # ---- Target ---- -TARGET := $(BINDIR)/games/doom.elf +TARGET := $(BINDIR)/apps/doom/doom.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/games + mkdir -p $(BINDIR)/apps/doom $(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@ # DOOM source files (from doomgeneric directory) diff --git a/programs/src/doom/doomgeneric_montauk.c b/programs/src/doom/doomgeneric_montauk.c index 2004602..1a07f75 100644 --- a/programs/src/doom/doomgeneric_montauk.c +++ b/programs/src/doom/doomgeneric_montauk.c @@ -247,7 +247,7 @@ void DG_SetWindowTitle(const char* title) { /* ---- Entry point ---- */ void _start(void) { - char *argv[] = { "doom", "-iwad", "0:/games/doom1.wad", 0 }; + char *argv[] = { "doom", "-iwad", "0:/apps/doom/doom1.wad", 0 }; doomgeneric_Create(3, argv); for (;;) { doomgeneric_Tick(); diff --git a/programs/src/doom/manifest.toml b/programs/src/doom/manifest.toml new file mode 100644 index 0000000..caf541b --- /dev/null +++ b/programs/src/doom/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "DOOM" +binary = "doom.elf" +icon = "doom.svg" + +[menu] +category = "Games" +visible = true diff --git a/programs/src/fontpreview/Makefile b/programs/src/fontpreview/Makefile index 600828a..9650050 100644 --- a/programs/src/fontpreview/Makefile +++ b/programs/src/fontpreview/Makefile @@ -71,14 +71,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/fontpreview.elf +TARGET := $(BINDIR)/apps/fontpreview/fontpreview.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/fontpreview $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/fontpreview/manifest.toml b/programs/src/fontpreview/manifest.toml new file mode 100644 index 0000000..d5112b8 --- /dev/null +++ b/programs/src/fontpreview/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Font Preview" +binary = "fontpreview.elf" +icon = "font-viewer.svg" + +[menu] +category = "Applications" +visible = false diff --git a/programs/src/imageviewer/Makefile b/programs/src/imageviewer/Makefile index 003789f..2477a40 100644 --- a/programs/src/imageviewer/Makefile +++ b/programs/src/imageviewer/Makefile @@ -74,14 +74,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/imageviewer.elf +TARGET := $(BINDIR)/apps/imageviewer/imageviewer.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/imageviewer $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/imageviewer/manifest.toml b/programs/src/imageviewer/manifest.toml new file mode 100644 index 0000000..0566453 --- /dev/null +++ b/programs/src/imageviewer/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Image Viewer" +binary = "imageviewer.elf" +icon = "image-viewer.svg" + +[menu] +category = "Applications" +visible = false diff --git a/programs/src/installer/Makefile b/programs/src/installer/Makefile new file mode 100644 index 0000000..ad24e75 --- /dev/null +++ b/programs/src/installer/Makefile @@ -0,0 +1,86 @@ +# Makefile for installer (standalone Installer app) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Source files ---- + +SRCS := main.cpp render.cpp actions.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/apps/installer/installer.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/apps/installer + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/installer/actions.cpp b/programs/src/installer/actions.cpp new file mode 100644 index 0000000..40bc0e7 --- /dev/null +++ b/programs/src/installer/actions.cpp @@ -0,0 +1,420 @@ +/* + * actions.cpp + * MontaukOS Installer — install operations + * Copyright (c) 2026 Daniel Hammer + */ + +#include "installer.h" + +// ============================================================================ +// EFI System Partition type GUID: C12A7328-F81F-11D2-BA4B-00A0C93EC93B +// ============================================================================ + +static Montauk::PartGuid esp_type_guid() { + Montauk::PartGuid g; + g.Data1 = 0xC12A7328; + g.Data2 = 0xF81F; + g.Data3 = 0x11D2; + g.Data4[0] = 0xBA; g.Data4[1] = 0x4B; + g.Data4[2] = 0x00; g.Data4[3] = 0xA0; + g.Data4[4] = 0xC9; g.Data4[5] = 0x3E; + g.Data4[6] = 0xC9; g.Data4[7] = 0x3B; + return g; +} + +// ============================================================================ +// Refresh disk list +// ============================================================================ + +void installer_refresh_disks() { + auto& st = g_state; + st.disk_count = 0; + for (int port = 0; port < MAX_DISKS; port++) { + Montauk::DiskInfo info; + montauk::memset(&info, 0, sizeof(info)); + int r = montauk::diskinfo(&info, port); + if (r == 0 && info.type != 0) { + st.disks[st.disk_count++] = info; + } + } + if (st.selected_disk < 0 || st.selected_disk >= st.disk_count) + st.selected_disk = st.disk_count > 0 ? 0 : -1; +} + +// ============================================================================ +// String helpers +// ============================================================================ + +static int slen(const char* s) { + int n = 0; + while (s[n]) n++; + return n; +} + +static void path_join(char* out, int outsize, const char* dir, const char* name) { + int i = 0; + for (int j = 0; dir[j] && i < outsize - 2; j++) out[i++] = dir[j]; + if (i > 0 && out[i - 1] != '/') out[i++] = '/'; + for (int j = 0; name[j] && i < outsize - 1; j++) out[i++] = name[j]; + out[i] = '\0'; +} + +// ============================================================================ +// Copy a single file from src_path to dst_path +// ============================================================================ + +static bool copy_file(const char* src_path, const char* dst_path) { + int src = montauk::open(src_path); + if (src < 0) return false; + + uint64_t size = montauk::getsize(src); + + // Create destination (returns handle) + int dst = montauk::fcreate(dst_path); + if (dst < 0) { + montauk::close(src); + return false; + } + + if (size == 0) { + // Empty file — just create it + montauk::close(dst); + montauk::close(src); + return true; + } + + // Use large buffer for faster copies (256 KB) + static constexpr uint64_t CHUNK = 256 * 1024; + uint8_t* buf = (uint8_t*)montauk::malloc(CHUNK); + if (!buf) { + montauk::close(dst); + montauk::close(src); + return false; + } + + // Show progress for large files (> 1 MB) + bool show_progress = (size > 1024 * 1024); + uint64_t last_progress_mb = 0; + + uint64_t offset = 0; + bool ok = true; + while (offset < size) { + uint64_t to_read = size - offset; + if (to_read > CHUNK) to_read = CHUNK; + + int rd = montauk::read(src, buf, offset, to_read); + if (rd <= 0) { ok = false; break; } + + int wr = montauk::fwrite(dst, buf, offset, rd); + if (wr < rd) { ok = false; break; } + + offset += rd; + + if (show_progress) { + uint64_t cur_mb = offset / (1024 * 1024); + if (cur_mb > last_progress_mb) { + last_progress_mb = cur_mb; + uint64_t total_mb = size / (1024 * 1024); + char prog[64]; + snprintf(prog, sizeof(prog), " %lu / %lu MB", + (unsigned long)cur_mb, (unsigned long)total_mb); + set_status(prog); + flush_ui(); + } + } + } + + montauk::mfree(buf); + montauk::close(dst); + montauk::close(src); + return ok; +} + +// ============================================================================ +// Recursively copy directory contents from ramdisk to target drive +// ============================================================================ + +static int g_files_copied; +static int g_dirs_created; + +// Copy all entries from src_dir (on ramdisk) to dst_dir (on target drive). +// src_dir: e.g. "0:/" or "0:/os" +// dst_dir: e.g. "15:/" or "15:/os" +static bool copy_recursive(const char* src_dir, const char* dst_dir) { + const char* names[64]; + int count = montauk::readdir(src_dir, names, 64); + if (count < 0) return true; // not a directory or empty, 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++; + + for (int i = 0; i < count; i++) { + const char* raw_name = names[i]; + + // 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'; + + // Create directory on target + char target_path[256]; + path_join(target_path, sizeof(target_path), dst_dir, dir_name); + + montauk::fmkdir(target_path); + g_dirs_created++; + + char log_msg[64]; + snprintf(log_msg, sizeof(log_msg), " mkdir %s", dir_name); + 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; + } else { + // Skip ramdisk and limine.conf — installed system boots from + // disk and gets a fresh config without the ramdisk module. + if (strcmp(basename, "ramdisk.tar") == 0) continue; + if (strcmp(basename, "limine.conf") == 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); + + char dst_path[256]; + path_join(dst_path, sizeof(dst_path), dst_dir, basename); + + char log_msg[64]; + snprintf(log_msg, sizeof(log_msg), " copy %s", basename); + add_log(log_msg); + flush_ui(); + + if (!copy_file(src_path, dst_path)) + return false; + + g_files_copied++; + } + } + + return true; +} + +// ============================================================================ +// Install MontaukOS to the selected disk +// ============================================================================ + +void do_install() { + auto& st = g_state; + int disk = st.selected_disk; + + if (disk < 0 || disk >= st.disk_count) { + st.step = STEP_ERROR; + add_log("No disk selected"); + flush_ui(); + return; + } + + g_files_copied = 0; + g_dirs_created = 0; + + // Step 1: Initialize GPT + add_log("Initializing GPT..."); + flush_ui(); + int r = montauk::gpt_init(disk); + if (r < 0) { + add_log("ERROR: Failed to initialize GPT"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + add_log(" GPT initialized"); + flush_ui(); + + // Step 2: Create EFI System Partition (entire disk) + add_log("Creating EFI System Partition..."); + flush_ui(); + Montauk::GptAddParams params; + montauk::memset(¶ms, 0, sizeof(params)); + params.blockDev = disk; + params.startLba = 0; + params.endLba = 0; + params.typeGuid = esp_type_guid(); + + const char* pname = "EFI System"; + int i = 0; + for (; pname[i] && i < 71; i++) params.name[i] = pname[i]; + params.name[i] = '\0'; + + r = montauk::gpt_add(¶ms); + if (r < 0) { + add_log("ERROR: Failed to create partition"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + add_log(" Partition created"); + flush_ui(); + + // Step 3: Format as FAT32 + add_log("Formatting as FAT32..."); + flush_ui(); + + Montauk::PartInfo parts[MAX_PARTS]; + int part_count = montauk::partlist(parts, MAX_PARTS); + int esp_index = -1; + for (int p = 0; p < part_count; p++) { + if (parts[p].blockDev == disk) { + esp_index = p; + break; + } + } + + if (esp_index < 0) { + add_log("ERROR: Could not find partition"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + + Montauk::FsFormatParams fmt; + montauk::memset(&fmt, 0, sizeof(fmt)); + fmt.partIndex = esp_index; + fmt.fsType = Montauk::FS_TYPE_FAT32; + + r = montauk::fs_format(&fmt); + if (r < 0) { + add_log("ERROR: FAT32 format failed"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + add_log(" FAT32 formatted"); + flush_ui(); + + // Step 4: Mount the partition + add_log("Mounting partition..."); + flush_ui(); + int drive_num = 15; + r = montauk::fs_mount(esp_index, drive_num); + if (r < 0) { + 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 5: Create EFI boot directory structure + add_log("Creating boot directories..."); + flush_ui(); + + char path_buf[64]; + snprintf(path_buf, sizeof(path_buf), "%d:/EFI", drive_num); + montauk::fmkdir(path_buf); + snprintf(path_buf, sizeof(path_buf), "%d:/EFI/BOOT", drive_num); + montauk::fmkdir(path_buf); + + // Step 6: Copy entire root filesystem + add_log("Copying root filesystem..."); + flush_ui(); + + if (!copy_recursive("0:/", drive_root)) { + add_log("ERROR: File copy failed"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + + char copy_msg[64]; + snprintf(copy_msg, sizeof(copy_msg), " %d files, %d directories copied", + g_files_copied, g_dirs_created); + add_log(copy_msg); + flush_ui(); + + // Step 7: Write limine.conf without ramdisk module + add_log("Writing boot configuration..."); + flush_ui(); + snprintf(path_buf, sizeof(path_buf), "%d:/boot/limine/limine.conf", drive_num); + { + static const char limine_conf[] = + "timeout: 0\n" + "\n" + "/montaukos\n" + " protocol: limine\n" + " path: boot():/boot/kernel\n"; + + int conf_fd = montauk::fcreate(path_buf); + if (conf_fd < 0) { + add_log("ERROR: Failed to write limine.conf"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + montauk::fwrite(conf_fd, (const uint8_t*)limine_conf, 0, sizeof(limine_conf) - 1); + montauk::close(conf_fd); + } + add_log(" limine.conf written"); + flush_ui(); + + // Step 8: Copy BOOTX64.EFI to EFI/BOOT/ + // (The recursive copy already put it at boot/limine/BOOTX64.EFI, + // but UEFI firmware requires it at /EFI/BOOT/BOOTX64.EFI) + add_log("Installing EFI bootloader..."); + flush_ui(); + snprintf(path_buf, sizeof(path_buf), "%d:/EFI/BOOT/BOOTX64.EFI", drive_num); + if (!copy_file("0:/boot/limine/BOOTX64.EFI", path_buf)) { + add_log("ERROR: Failed to install BOOTX64.EFI"); + flush_ui(); + st.step = STEP_ERROR; + return; + } + add_log(" BOOTX64.EFI installed"); + flush_ui(); + + // Done + add_log("Installation complete!"); + st.step = STEP_DONE; + set_status("MontaukOS installed successfully"); + flush_ui(); +} diff --git a/programs/src/installer/installer.h b/programs/src/installer/installer.h new file mode 100644 index 0000000..93f026b --- /dev/null +++ b/programs/src/installer/installer.h @@ -0,0 +1,137 @@ +/* + * installer.h + * Shared header for the MontaukOS Installer + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +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 MAX_DISKS = 8; +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); + +// ============================================================================ +// Install steps +// ============================================================================ + +enum InstallStep { + STEP_SELECT_DISK, + STEP_PARTITION_SCHEME, + STEP_CONFIRM, + STEP_INSTALLING, + STEP_DONE, + STEP_ERROR, +}; + +// ============================================================================ +// Partition schemes +// ============================================================================ + +enum PartScheme { + SCHEME_SINGLE_FAT32 = 0, + SCHEME_COUNT, +}; + +static const char* scheme_names[] = { + "Single FAT32 partition (entire disk)", +}; + +static const char* scheme_descs[] = { + "One partition for boot, OS, and data. Simple and compatible.", +}; + +// ============================================================================ +// State +// ============================================================================ + +static constexpr int LOG_LINES = 16; +static constexpr int LOG_LINE_LEN = 64; + +struct InstallerState { + Montauk::DiskInfo disks[MAX_DISKS]; + int disk_count; + int selected_disk; + int partition_scheme; + + InstallStep step; + char log[LOG_LINES][LOG_LINE_LEN]; + int log_count; + + char status[80]; + uint64_t status_time; +}; + +// ============================================================================ +// Global state (extern — defined in main.cpp) +// ============================================================================ + +extern int g_win_w, g_win_h; +extern InstallerState g_state; +extern TrueTypeFont* g_font; +extern uint32_t* g_pixels; +extern int g_win_id; + +// ============================================================================ +// Function declarations — helpers (main.cpp) +// ============================================================================ + +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); + +// ============================================================================ +// Function declarations — render.cpp +// ============================================================================ + +void render(uint32_t* pixels); + +// ============================================================================ +// Function declarations — actions.cpp +// ============================================================================ + +void installer_refresh_disks(); +void do_install(); diff --git a/programs/src/installer/main.cpp b/programs/src/installer/main.cpp new file mode 100644 index 0000000..0a3a414 --- /dev/null +++ b/programs/src/installer/main.cpp @@ -0,0 +1,271 @@ +/* + * main.cpp + * MontaukOS Installer — entry point, event loop, state + * Copyright (c) 2026 Daniel Hammer + */ + +#include "installer.h" + +// ============================================================================ +// Global state definitions +// ============================================================================ + +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; +int g_win_id = -1; + +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; + } +} + +// ============================================================================ +// Helpers +// ============================================================================ + +void set_status(const char* msg) { + int i = 0; + for (; i < 79 && msg[i]; i++) g_state.status[i] = msg[i]; + g_state.status[i] = '\0'; + g_state.status_time = montauk::get_milliseconds(); +} + +void add_log(const char* msg) { + if (g_state.log_count >= LOG_LINES) { + // Scroll: shift all lines up by one + for (int j = 0; j < LOG_LINES - 1; j++) + montauk::memcpy(g_state.log[j], g_state.log[j + 1], LOG_LINE_LEN); + g_state.log_count = LOG_LINES - 1; + } + int i = 0; + for (; i < LOG_LINE_LEN - 1 && msg[i]; i++) + g_state.log[g_state.log_count][i] = msg[i]; + g_state.log[g_state.log_count][i] = '\0'; + g_state.log_count++; +} + +void flush_ui() { + if (g_pixels && g_win_id >= 0) { + render(g_pixels); + montauk::win_present(g_win_id); + } +} + +void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorSize) { + uint64_t bytes = sectors * sectorSize; + uint64_t gb = bytes / (1024ULL * 1024 * 1024); + 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); + } 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); + } else { + uint64_t mb = bytes / (1024ULL * 1024); + snprintf(buf, bufsize, "%lu MB", (unsigned)mb); + } +} + +// ============================================================================ +// Mouse handling +// ============================================================================ + +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; + + // Common bottom button area + int btn_w = 120, btn_h = 34; + int btn_y = g_win_h - STATUS_H - btn_h - 12; + + if (st.step == STEP_SELECT_DISK) { + // Disk list items + int y = CONTENT_TOP + 40 + fh + 12; + 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) { + 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) { + installer_refresh_disks(); + return true; + } + } else if (st.step == STEP_PARTITION_SCHEME) { + // Scheme radio buttons + int y = CONTENT_TOP + 40 + fh + 12; + 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) { + st.partition_scheme = i; + return true; + } + y += item_h + 4; + } + + // "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; + return true; + } + + // "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; + + // "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 + } + } + + return true; +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + montauk::memset(&g_state, 0, sizeof(g_state)); + g_state.selected_disk = -1; + g_state.partition_scheme = SCHEME_SINGLE_FAT32; + g_state.step = STEP_SELECT_DISK; + + // 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; + } + + apply_scale(montauk::win_getscale()); + + installer_refresh_disks(); + + // Create window + Montauk::WinCreateResult wres; + if (montauk::win_create("Install MontaukOS", INIT_W, INIT_H, &wres) < 0 || wres.id < 0) + montauk::exit(1); + + int win_id = wres.id; + uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + g_pixels = pixels; + g_win_id = win_id; + + render(pixels); + montauk::win_present(win_id); + + bool install_triggered = false; + + while (true) { + Montauk::WinEvent ev; + bool redraw = false; + + int r = montauk::win_poll(win_id, &ev); + if (r < 0) break; + + if (r == 0) { + if (g_state.step == STEP_INSTALLING && !install_triggered) { + install_triggered = true; + render(pixels); + montauk::win_present(win_id); + do_install(); + render(pixels); + montauk::win_present(win_id); + } + montauk::sleep_ms(16); + continue; + } + + if (ev.type == 3) break; + + if (ev.type == 4) { + apply_scale(ev.scale.scale); + redraw = true; + } + + if (ev.type == 2) { + g_win_w = ev.resize.w; + g_win_h = ev.resize.h; + pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h); + redraw = true; + } + + if (ev.type == 0 && ev.key.pressed) { + if (ev.key.scancode == 0x01) break; + redraw = true; + } + + if (ev.type == 1) { + 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; + } + } + + if (redraw) { render(pixels); montauk::win_present(win_id); } + } + + montauk::win_destroy(win_id); + montauk::exit(0); +} diff --git a/programs/src/installer/manifest.toml b/programs/src/installer/manifest.toml new file mode 100644 index 0000000..ad92e9d --- /dev/null +++ b/programs/src/installer/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Installer" +binary = "installer.elf" +icon = "text-x-install.svg" + +[menu] +category = "System" +visible = true diff --git a/programs/src/installer/render.cpp b/programs/src/installer/render.cpp new file mode 100644 index 0000000..11eb3f7 --- /dev/null +++ b/programs/src/installer/render.cpp @@ -0,0 +1,453 @@ +/* + * render.cpp + * MontaukOS Installer — rendering + * Copyright (c) 2026 Daniel Hammer + */ + +#include "installer.h" + +// ============================================================================ +// Pixel 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 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 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 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(); + } +} + +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: disk selection step +// ============================================================================ + +static void render_select_disk(uint32_t* px) { + auto& st = g_state; + int fh = font_h(); + + 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); + } + + 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); + + Color bg = sel + ? Color::from_rgb(0xD8, 0xE8, 0xF8) + : Color::from_rgb(0xF4, 0xF4, 0xF4); + + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, bg); + + if (sel) + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, 4, item_h, 2, ACCENT); + + px_text(px, g_win_w, g_win_h, item_x + 12, y + 6, st.disks[i].model, TEXT_COLOR); + + char info[64], sz[24]; + 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, i); + px_text(px, g_win_w, g_win_h, item_x + 12, y + 6 + fh + 2, info, DIM_TEXT, FONT_SM); + + y += item_h + 4; + } + + // 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) { + 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; + + 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); + Color bg = selected + ? Color::from_rgb(0xD8, 0xE8, 0xF8) + : Color::from_rgb(0xF4, 0xF4, 0xF4); + + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, bg); + + if (selected) + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, 4, item_h, 2, ACCENT); + + // 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; + } + + // 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_confirm(uint32_t* px) { + auto& st = g_state; + int fh = font_h(); + int y = CONTENT_TOP + 24; + + 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; + + 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; + + char desc[128], 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)", + st.selected_disk, 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; + + // 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; + + 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); + + // 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: installing / done / error steps +// ============================================================================ + +static void render_progress(uint32_t* px) { + auto& st = g_state; + int fh = font_h(); + int fh_sm = font_h(FONT_SM); + int y = CONTENT_TOP + 16; + + const char* title; + Color title_color; + if (st.step == STEP_INSTALLING) { + title = "Installing..."; + title_color = TEXT_COLOR; + } else if (st.step == STEP_DONE) { + title = "Installation Complete"; + title_color = SUCCESS_COLOR; + } else { + title = "Installation Failed"; + title_color = DANGER; + } + + px_text(px, g_win_w, g_win_h, (g_win_w - text_w(title)) / 2, 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; + + 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; + 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); + } +} + +// ============================================================================ +// Render: toolbar, step bar, and status bar +// ============================================================================ + +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); + + static const char* step_labels[] = { "Disk", "Partition", "Confirm", "Install" }; + static constexpr int STEP_COUNT = 4; + + // Map current state to step index (0-3) + int cur = 0; + 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) { + 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]) { + uint64_t age = montauk::get_milliseconds() - st.status_time; + Color 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); + } +} + +// ============================================================================ +// Top-level render +// ============================================================================ + +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); + + switch (g_state.step) { + 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_INSTALLING: + case STEP_DONE: + case STEP_ERROR: render_progress(pixels); break; + } + + render_status(pixels); +} diff --git a/programs/src/installer/stb_truetype_impl.cpp b/programs/src/installer/stb_truetype_impl.cpp new file mode 100644 index 0000000..9ce2266 --- /dev/null +++ b/programs/src/installer/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +// Override all stb_truetype dependencies before including the implementation + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/pdfviewer/Makefile b/programs/src/pdfviewer/Makefile index e1e4a3b..c707348 100644 --- a/programs/src/pdfviewer/Makefile +++ b/programs/src/pdfviewer/Makefile @@ -68,14 +68,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/pdfviewer.elf +TARGET := $(BINDIR)/apps/pdfviewer/pdfviewer.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/pdfviewer $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ HEADERS := pdfviewer.h diff --git a/programs/src/pdfviewer/manifest.toml b/programs/src/pdfviewer/manifest.toml new file mode 100644 index 0000000..9751d22 --- /dev/null +++ b/programs/src/pdfviewer/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "PDF Viewer" +binary = "pdfviewer.elf" +icon = "pdf-viewer.svg" + +[menu] +category = "Applications" +visible = false diff --git a/programs/src/spreadsheet/Makefile b/programs/src/spreadsheet/Makefile index cd15794..de3d4bb 100644 --- a/programs/src/spreadsheet/Makefile +++ b/programs/src/spreadsheet/Makefile @@ -68,14 +68,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/spreadsheet.elf +TARGET := $(BINDIR)/apps/spreadsheet/spreadsheet.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/spreadsheet $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/spreadsheet/manifest.toml b/programs/src/spreadsheet/manifest.toml new file mode 100644 index 0000000..97a2e3d --- /dev/null +++ b/programs/src/spreadsheet/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Spreadsheet" +binary = "spreadsheet.elf" +icon = "spreadsheet.svg" + +[menu] +category = "Applications" +visible = true diff --git a/programs/src/weather/Makefile b/programs/src/weather/Makefile index 3883a3a..8275747 100644 --- a/programs/src/weather/Makefile +++ b/programs/src/weather/Makefile @@ -76,14 +76,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/weather.elf +TARGET := $(BINDIR)/apps/weather/weather.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/weather $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/weather/manifest.toml b/programs/src/weather/manifest.toml new file mode 100644 index 0000000..070e825 --- /dev/null +++ b/programs/src/weather/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Weather" +binary = "weather.elf" +icon = "weather-widget.svg" + +[menu] +category = "Internet" +visible = true diff --git a/programs/src/wikipedia/Makefile b/programs/src/wikipedia/Makefile index 4241b94..113c9d5 100644 --- a/programs/src/wikipedia/Makefile +++ b/programs/src/wikipedia/Makefile @@ -76,14 +76,14 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- -TARGET := $(BINDIR)/os/wikipedia.elf +TARGET := $(BINDIR)/apps/wikipedia/wikipedia.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile - mkdir -p $(BINDIR)/os + mkdir -p $(BINDIR)/apps/wikipedia $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ $(OBJDIR)/%.o: %.cpp Makefile diff --git a/programs/src/wikipedia/manifest.toml b/programs/src/wikipedia/manifest.toml new file mode 100644 index 0000000..6dfaae5 --- /dev/null +++ b/programs/src/wikipedia/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Wikipedia" +binary = "wikipedia.elf" +icon = "web-browser.svg" + +[menu] +category = "Internet" +visible = true diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index f301f14..4eafe9b 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -53,13 +53,9 @@ ICONS=( "categories/scalable/help-about.svg" "categories/scalable/system-reboot.svg" "categories/scalable/system-shutdown.svg" - "apps/scalable/doom.svg" "apps/scalable/system-monitor.svg" "apps/scalable/applications-science.svg" - "apps/scalable/hardware.svg" "apps/scalable/unsettings.svg" # settings icon; toolbox in flat remix - # Weather app icon (for desktop app menu launcher) - "apps/scalable/weather-widget.svg" # Weather condition icons (panel, colorful, used by weather.elf) "panel/weather-clear.svg" "panel/weather-clear-night.svg" @@ -84,10 +80,8 @@ ICONS=( "panel/weather-none-available.svg" "apps/symbolic/utilities-system-monitor-symbolic.svg" # system monitor "apps/scalable/utilities-system-monitor.svg" # system monitor - "mimetypes/scalable/spreadsheet.svg" "apps/scalable/drive-harddisk.svg" "actions/16/trash-empty.svg" - "apps/scalable/gparted.svg" ) copied=0 diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh new file mode 100755 index 0000000..37a4a8d --- /dev/null +++ b/scripts/install_apps.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# install_apps.sh - Bundle standalone apps into bin/apps// directories +# Each bundle contains: binary, manifest.toml, icon SVG +# Usage: ./scripts/install_apps.sh (from project root) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +SRC="$PROJECT_ROOT/programs/src" +BIN="$PROJECT_ROOT/programs/bin" +ICON_SRC="$PROJECT_ROOT/programs/gui/icons/Flat-Remix-Blue-Light-darkPanel" + +# App definitions: name|icon_source_path +# The binary is already built by each app's Makefile into bin/apps// +APPS=( + "doom|apps/scalable/doom.svg" + "spreadsheet|mimetypes/scalable/spreadsheet.svg" + "weather|apps/scalable/weather-widget.svg" + "wikipedia|apps/scalable/web-browser.svg" + "imageviewer|apps/scalable/utilities-terminal.svg" + "fontpreview|apps/scalable/utilities-terminal.svg" + "pdfviewer|apps/scalable/utilities-terminal.svg" + "disks|apps/scalable/gparted.svg" + "devexplorer|apps/scalable/hardware.svg" + "installer|mimetypes/scalable/text-x-install.svg" +) + +installed=0 +for entry in "${APPS[@]}"; do + IFS='|' read -r name icon_rel <<< "$entry" + + app_dir="$BIN/apps/$name" + mkdir -p "$app_dir" + + # Copy manifest + manifest="$SRC/$name/manifest.toml" + if [ -f "$manifest" ]; then + cp "$manifest" "$app_dir/manifest.toml" + else + echo "install_apps: warning: $manifest not found" >&2 + fi + + # Copy icon + icon_src="$ICON_SRC/$icon_rel" + # Extract target icon name from manifest + icon_name=$(grep '^icon' "$manifest" 2>/dev/null | sed 's/.*= *"\(.*\)"/\1/' || echo "") + if [ -n "$icon_name" ] && [ -f "$icon_src" ]; then + cp "$icon_src" "$app_dir/$icon_name" + elif [ -n "$icon_name" ]; then + echo "install_apps: warning: icon $icon_src not found for $name" >&2 + fi + + installed=$((installed + 1)) +done + +# Special case: copy doom1.wad alongside doom binary +if [ -f "$PROJECT_ROOT/programs/data/games/doom1.wad" ]; then + cp "$PROJECT_ROOT/programs/data/games/doom1.wad" "$BIN/apps/doom/doom1.wad" +fi + +echo "install_apps: installed $installed app bundles to $BIN/apps/"