From 1edbec3c667fdfb20d79b00d09d3c458c3a7cb09 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 8 Mar 2026 12:06:29 +0100 Subject: [PATCH] feat: MontaukOS installer app, toml config, app directory with manifests, and more --- GNUmakefile | 9 +- kernel/src/Api/Filesystem.hpp | 8 + kernel/src/Api/Syscall.cpp | 4 + kernel/src/Api/Syscall.hpp | 2 + kernel/src/Drivers/Storage/Gpt.cpp | 10 + kernel/src/Fs/Fat32.cpp | 183 +++++- kernel/src/Fs/Vfs.cpp | 21 + kernel/src/Fs/Vfs.hpp | 5 + kernel/src/Main.cpp | 34 +- kernel/src/Net/NetConfig.cpp | 10 +- programs/GNUmakefile | 23 +- programs/include/Api/Syscall.hpp | 2 + programs/include/gui/desktop.hpp | 20 +- programs/include/montauk/config.h | 331 ++++++++++ programs/include/montauk/syscall.h | 6 + programs/include/montauk/toml.h | 573 ++++++++++++++++++ programs/lib/libjpeg/libjpeg.a | Bin 330158 -> 330158 bytes programs/lib/tls/libtls.a | Bin 106098 -> 106682 bytes programs/lib/tls/obj/tls.o | Bin 105704 -> 106288 bytes programs/src/desktop/apps/app_devexplorer.cpp | 12 - programs/src/desktop/apps/app_disks.cpp | 11 - programs/src/desktop/apps/app_doom.cpp | 12 - programs/src/desktop/apps/app_filemanager.cpp | 48 +- programs/src/desktop/apps/app_spreadsheet.cpp | 12 - programs/src/desktop/apps/app_terminal.cpp | 11 +- programs/src/desktop/apps/app_weather.cpp | 13 - programs/src/desktop/apps/app_wiki.cpp | 13 - programs/src/desktop/apps/apps_common.hpp | 8 +- programs/src/desktop/desktop_internal.hpp | 85 ++- programs/src/desktop/input.cpp | 44 +- programs/src/desktop/main.cpp | 183 +++++- programs/src/desktop/panel.cpp | 31 +- programs/src/devexplorer/Makefile | 4 +- programs/src/devexplorer/manifest.toml | 8 + programs/src/disks/Makefile | 4 +- programs/src/disks/manifest.toml | 8 + programs/src/doom/Makefile | 4 +- programs/src/doom/doomgeneric_montauk.c | 2 +- programs/src/doom/manifest.toml | 8 + programs/src/fontpreview/Makefile | 4 +- programs/src/fontpreview/manifest.toml | 8 + programs/src/imageviewer/Makefile | 4 +- programs/src/imageviewer/manifest.toml | 8 + programs/src/installer/Makefile | 86 +++ programs/src/installer/actions.cpp | 420 +++++++++++++ programs/src/installer/installer.h | 137 +++++ programs/src/installer/main.cpp | 271 +++++++++ programs/src/installer/manifest.toml | 8 + programs/src/installer/render.cpp | 453 ++++++++++++++ programs/src/installer/stb_truetype_impl.cpp | 35 ++ programs/src/pdfviewer/Makefile | 4 +- programs/src/pdfviewer/manifest.toml | 8 + programs/src/spreadsheet/Makefile | 4 +- programs/src/spreadsheet/manifest.toml | 8 + programs/src/weather/Makefile | 4 +- programs/src/weather/manifest.toml | 8 + programs/src/wikipedia/Makefile | 4 +- programs/src/wikipedia/manifest.toml | 8 + scripts/copy_icons.sh | 6 - scripts/install_apps.sh | 63 ++ 60 files changed, 3045 insertions(+), 268 deletions(-) create mode 100644 programs/include/montauk/config.h create mode 100644 programs/include/montauk/toml.h delete mode 100644 programs/src/desktop/apps/app_devexplorer.cpp delete mode 100644 programs/src/desktop/apps/app_disks.cpp delete mode 100644 programs/src/desktop/apps/app_doom.cpp delete mode 100644 programs/src/desktop/apps/app_spreadsheet.cpp delete mode 100644 programs/src/desktop/apps/app_weather.cpp delete mode 100644 programs/src/desktop/apps/app_wiki.cpp create mode 100644 programs/src/devexplorer/manifest.toml create mode 100644 programs/src/disks/manifest.toml create mode 100644 programs/src/doom/manifest.toml create mode 100644 programs/src/fontpreview/manifest.toml create mode 100644 programs/src/imageviewer/manifest.toml create mode 100644 programs/src/installer/Makefile create mode 100644 programs/src/installer/actions.cpp create mode 100644 programs/src/installer/installer.h create mode 100644 programs/src/installer/main.cpp create mode 100644 programs/src/installer/manifest.toml create mode 100644 programs/src/installer/render.cpp create mode 100644 programs/src/installer/stb_truetype_impl.cpp create mode 100644 programs/src/pdfviewer/manifest.toml create mode 100644 programs/src/spreadsheet/manifest.toml create mode 100644 programs/src/weather/manifest.toml create mode 100644 programs/src/wikipedia/manifest.toml create mode 100755 scripts/install_apps.sh 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 9d9d5aa54e717157d2c03c83f4c3f63956e943d5..38bca665766d9a5fbbe29f6e86b84688a8ce5e49 100644 GIT binary patch delta 51 wcmZ2CS!CU0kqI(vmS#qVhL#(ZOqig|<`|~-7$!y_2GPtw%(6X(i8Y8F0B_(7)&Kwi delta 51 wcmZ2CS!CU0kqI(v7Dgs!CPo{TOqig|<`|~-7$!y_2GPtw%(6X(i8Y8F0B>Fl(f|Me diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a index 5e034107d5cf28653aa1ce0331dee3d4c7c4edbc..737b973c2aede84d511e382c5d9e847ded93c901 100644 GIT binary patch literal 106682 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 JRU2FW{XfXGUZ(&6 literal 106098 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 BD*pfg diff --git a/programs/lib/tls/obj/tls.o b/programs/lib/tls/obj/tls.o index 8cd7af31c8817171772166680326ebbbb1838de2..04412cc43af47138ce9943b9cb614cbe052766ea 100644 GIT binary patch literal 106288 zcmdSC3w)Ku@jv`L&pyw|IXSr;NJv5;kRbPK!bQS8Aqkv70ti8aw-8POi9(VlCy2FN z6tKjILaUZ4ioL0A)lyp(Q7d=@t!>q|YHe$6wN*r`^?qyle`j`|=j;QA$A1gv|FwP?=Kp8<6;7Yf7>`Ye z)sKrcw#S=SH^=JZjUD4-?Qx^5Vrfm~yvnlDnhLhITP!~i0H};!y1cZe^Z{yOqhI>w z9%?(1L^aVP^MmRMlN$!Co$hQuv|)Hv?BxU0!WETWk41NuoQTdkaU#iBKjc8jZHQj_ zC^PGN!Dm>ba_2^q?OxgSIL+EG4v@5pq7z_;DXHrEw7Tn!bSJ^6>~VX%@epW0^eXp^ z9@)O~#EBDB1+Xarwkh!Vz}4f^2X7++W9!EvecQbNs;9k~zV%K3nEQo0Cl6+!o*yZnEK8mS)l-F~QZ zN^~ddSNafneDsJz6!eL9F{tT$Yz-KLRk@!o(dGEpqTLVDV58el#2uDU)%6iSk6r&S z^xYNhW?`&Gbmz4jK+A)H6oRGP?X|?>NW~ULk5ufSCv%B*-%V{WJNM$cEwiZEh&Obz zz8KQk(VZK26@7TH1l6ir#TIBr5~5uddx{RJKIzvUigs<>)m3qG`aRrvrkTEtmeEWr zJ{)66-}W<7<7Qf^np0rHxxXm>ww zHKN^9_@P#NRre(b8<#{Xr=3XO`UF}qu4s2SdiXDaXm`b~>Yek=s;)8w2fGjOJ<+c6 zKA^9-nScm@+X%=30Ey*&@&OzmU?c#HtGrJkfTIKyAN%m17`msV2@Map8t(r$8mcZJ zKkAYn0(ttjQZi+)gE}^xnlS$tM|awDqK98gigxjYDh7wS|7zA}5)GI}i#RwHF)-S7 zZ3)lO-BEsQUxJ71*Zm+>DOgf<@1$mU6uV1TINDVfIra$V1kOZNC9vp26o8M*wMC|1 zeqbOKxu>Gv+by*&M%iaSx9_oMQzr%RjPU5BMn z-t}yG*E7-PLxZ;;KjZMH*494@()jS}!=i`Z9$|iJzB}sS=z+oDE+t?t7#qa$XhJX- zTthU#o{nyP9TQRU0{*{TidDQRUfT7G0~T7Vc2@s%bs{KeU}<;NrJtM6lukQTlD_Xq zbnC~dT~C&zU;B7;>mjpb+ee*$T3xaItV$Hns=fp+?5`2i5JJ+UdeJ0fq0`O&Vjbuu-ui{MKO2AgAnO8W^mcvHlKk1Te`VK4GzFBAY<-g!c(iNz&0W=d ztGX(7m3LL|Pj`+~@a7raxu>wxFdnbC7lxBdsB z#nKNSwbHj=4oQF_>D%|hhJb?c?WTg*~ z-Ff@JVVjlBaoS0CdMZM^OH?DWA~0Ba-fm>@^Z+bLA_X0<1eZ=oCF-eb}3jkoatx|ct| zF1P*J^u-T%RXlJ!cl9oJtT2jfdm?@DVYGEuJOCGsYTkMBxX2Lb*S&H7°jxM`n$ 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 literal 105704 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 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/"