feat: MontaukOS installer app, toml config, app directory with manifests, and more

This commit is contained in:
2026-03-08 12:06:29 +01:00
parent 4d0177d55e
commit 1edbec3c66
60 changed files with 3045 additions and 268 deletions
+8 -1
View File
@@ -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
+8
View File
@@ -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);
}
};
+4
View File
@@ -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:
+2
View File
@@ -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;
+10
View File
@@ -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));
+178 -5
View File
@@ -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<int N>
@@ -1284,6 +1456,7 @@ namespace Fs::Fat32 {
Thunks<N>::Write,
Thunks<N>::Create,
Thunks<N>::Delete,
Thunks<N>::Mkdir,
};
}
+21
View File
@@ -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;
+5
View File
@@ -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);
}
+20 -14
View File
@@ -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();
+5 -5
View File
@@ -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; }
+12 -11
View File
@@ -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/<name>/.
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/<name>/.
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
+2
View File
@@ -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;
+14 -6
View File
@@ -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;
+331
View File
@@ -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 <montauk/toml.h>
#include <montauk/syscall.h>
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/<name>.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
+6
View File
@@ -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); }
+573
View File
@@ -0,0 +1,573 @@
/*
* toml.h
* TOML config file parser
* Supports: strings, integers, booleans, tables, arrays
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/heap.h>
#include <montauk/string.h>
#include <cstdint>
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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");
}
-11
View File
@@ -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");
}
-12
View File
@@ -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");
}
+24 -24
View File
@@ -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);
}
@@ -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");
}
+3 -8
View File
@@ -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)
-13
View File
@@ -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");
}
-13
View File
@@ -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");
}
+1 -7
View File
@@ -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);
+55 -30
View File
@@ -8,9 +8,10 @@
#include "apps/apps_common.hpp"
#include "wallpaper.hpp"
#include <montauk/toml.h>
// ============================================================================
// 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",
+20 -24
View File
@@ -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
+176 -7
View File
@@ -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/<dir>/<binary>
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();
+3 -28
View File
@@ -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
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Devices"
binary = "devexplorer.elf"
icon = "hardware.svg"
[menu]
category = "System"
visible = true
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Disks"
binary = "disks.elf"
icon = "gparted.svg"
[menu]
category = "System"
visible = true
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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();
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "DOOM"
binary = "doom.elf"
icon = "doom.svg"
[menu]
category = "Games"
visible = true
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Font Preview"
binary = "fontpreview.elf"
icon = "font-viewer.svg"
[menu]
category = "Applications"
visible = false
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Image Viewer"
binary = "imageviewer.elf"
icon = "image-viewer.svg"
[menu]
category = "Applications"
visible = false
+86
View File
@@ -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)
+420
View File
@@ -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(&params, 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(&params);
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();
}
+137
View File
@@ -0,0 +1,137 @@
/*
* installer.h
* Shared header for the MontaukOS Installer
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
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();
+271
View File
@@ -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);
}
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Installer"
binary = "installer.elf"
icon = "text-x-install.svg"
[menu]
category = "System"
visible = true
+453
View File
@@ -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);
}
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
// 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 <gui/stb_truetype.h>
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "PDF Viewer"
binary = "pdfviewer.elf"
icon = "pdf-viewer.svg"
[menu]
category = "Applications"
visible = false
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Spreadsheet"
binary = "spreadsheet.elf"
icon = "spreadsheet.svg"
[menu]
category = "Applications"
visible = true
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Weather"
binary = "weather.elf"
icon = "weather-widget.svg"
[menu]
category = "Internet"
visible = true
+2 -2
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Wikipedia"
binary = "wikipedia.elf"
icon = "web-browser.svg"
[menu]
category = "Internet"
visible = true
-6
View File
@@ -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
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# install_apps.sh - Bundle standalone apps into bin/apps/<name>/ 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/<name>/
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/"