diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index db0bd11..d3eda2f 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -72,4 +72,8 @@ namespace Montauk { static int Sys_FCreate(const char* path) { return Fs::Vfs::VfsCreate(path); } + + static int Sys_FDelete(const char* path) { + return Fs::Vfs::VfsDelete(path); + } }; \ No newline at end of file diff --git a/kernel/src/Api/Storage.hpp b/kernel/src/Api/Storage.hpp new file mode 100644 index 0000000..af0fd31 --- /dev/null +++ b/kernel/src/Api/Storage.hpp @@ -0,0 +1,123 @@ +/* + * Storage.hpp + * Storage, GPT, format, and mount syscall implementations + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include +#include +#include +#include + +#include "Syscall.hpp" + +namespace Montauk { + + static void dl_strcpy_s(char* dst, const char* src, int max) { + int i = 0; + for (; i < max - 1 && src[i]; i++) dst[i] = src[i]; + dst[i] = '\0'; + } + + // Fill a user buffer with partition info. Returns the number of entries written. + static int Sys_PartList(PartInfo* buf, int maxCount) { + if (buf == nullptr || maxCount <= 0) return 0; + + int total = Drivers::Storage::Gpt::GetPartitionCount(); + int count = total < maxCount ? total : maxCount; + + for (int i = 0; i < count; i++) { + auto* p = Drivers::Storage::Gpt::GetPartition(i); + if (!p) break; + + buf[i].blockDev = p->BlockDevIndex; + buf[i]._pad0 = 0; + buf[i].startLba = p->StartLba; + buf[i].endLba = p->EndLba; + buf[i].sectorCount = p->SectorCount; + + // Copy GUIDs (layout-compatible) + __builtin_memcpy(&buf[i].typeGuid, &p->TypeGuid, sizeof(PartGuid)); + __builtin_memcpy(&buf[i].uniqueGuid, &p->UniqueGuid, sizeof(PartGuid)); + + buf[i].attributes = p->Attributes; + dl_strcpy_s(buf[i].name, p->Name, 72); + dl_strcpy_s(buf[i].typeName, + Drivers::Storage::Gpt::GetTypeName(p->TypeGuid), 24); + } + + return count; + } + + // Read sectors from a block device. Returns bytes read, or -1 on error. + static int64_t Sys_DiskRead(int blockDev, uint64_t lba, uint32_t count, void* buffer) { + if (buffer == nullptr || count == 0) return -1; + if (count > 128) return -1; // same limit as AHCI + + auto* dev = Drivers::Storage::GetBlockDevice(blockDev); + if (!dev) return -1; + + if (lba + count > dev->SectorCount) return -1; + + if (!dev->ReadSectors(dev->Ctx, lba, count, buffer)) return -1; + + return (int64_t)(count * dev->SectorSize); + } + + // Write sectors to a block device. Returns bytes written, or -1 on error. + static int64_t Sys_DiskWrite(int blockDev, uint64_t lba, uint32_t count, const void* buffer) { + if (buffer == nullptr || count == 0) return -1; + if (count > 128) return -1; + + auto* dev = Drivers::Storage::GetBlockDevice(blockDev); + if (!dev) return -1; + + if (lba + count > dev->SectorCount) return -1; + + if (!dev->WriteSectors(dev->Ctx, lba, count, buffer)) return -1; + + return (int64_t)(count * dev->SectorSize); + } + + // Initialize a new GPT on a block device. Returns 0 on success, -1 on error. + static int64_t Sys_GptInit(int blockDev) { + return (int64_t)Drivers::Storage::Gpt::InitializeGpt(blockDev); + } + + // Add a partition to an existing GPT. Returns partition index on success, -1 on error. + static int64_t Sys_GptAdd(const GptAddParams* params) { + if (params == nullptr) return -1; + + Drivers::Storage::Gpt::Guid typeGuid; + __builtin_memcpy(&typeGuid, ¶ms->typeGuid, sizeof(typeGuid)); + + return (int64_t)Drivers::Storage::Gpt::AddPartition( + params->blockDev, params->startLba, params->endLba, + typeGuid, params->name); + } + + // Mount a partition as a VFS drive. Returns 0 on success, -1 on error. + static int64_t Sys_FsMount(int partIndex, int driveNum) { + return (int64_t)Fs::FsProbe::MountPartition(partIndex, driveNum); + } + + // Format a partition with a filesystem. Returns 0 on success, -1 on error. + static int64_t Sys_FsFormat(const FsFormatParams* params) { + if (params == nullptr) return -1; + + auto* part = Drivers::Storage::Gpt::GetPartition(params->partIndex); + if (!part) return -1; + + switch (params->fsType) { + case FS_TYPE_FAT32: + return (int64_t)Fs::Fat32::Format( + part->BlockDevIndex, part->StartLba, part->SectorCount, + params->label[0] ? params->label : nullptr); + default: + return -1; // unknown filesystem type + } + } + +}; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 5e5c015..beea054 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -27,7 +27,8 @@ #include "IoRedir.hpp" // SYS_SPAWN_REDIR, SYS_CHILDIO_READ, SYS_CHILDIO_WRITE, SYS_CHILDIO_WRITEKEY, SYS_CHILDIO_SETTERMSZ #include "Random.hpp" // SYS_GETRANDOM #include "MemInfo.hpp" // SYS_MEMSTATS -#include "Device.hpp" // SYS_DEVLIST +#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO +#include "Storage.hpp" // SYS_PARTLIST, SYS_DISKREAD, SYS_DISKWRITE #include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETSCALE, SYS_WINGETSCALE // Assembly entry point @@ -154,6 +155,8 @@ namespace Montauk { frame->arg3, frame->arg4); case SYS_FCREATE: return (int64_t)Sys_FCreate((const char*)frame->arg1); + case SYS_FDELETE: + return (int64_t)Sys_FDelete((const char*)frame->arg1); case SYS_TERMSCALE: return Sys_TermScale(frame->arg1, frame->arg2); case SYS_RESOLVE: @@ -220,6 +223,22 @@ namespace Montauk { case SYS_MEMSTATS: Sys_MemStats((MemStats*)frame->arg1); return 0; + case SYS_PARTLIST: + return (int64_t)Sys_PartList((PartInfo*)frame->arg1, (int)frame->arg2); + case SYS_DISKREAD: + return (int64_t)Sys_DiskRead((int)frame->arg1, frame->arg2, + (uint32_t)frame->arg3, (void*)frame->arg4); + case SYS_DISKWRITE: + return (int64_t)Sys_DiskWrite((int)frame->arg1, frame->arg2, + (uint32_t)frame->arg3, (const void*)frame->arg4); + case SYS_GPTINIT: + return (int64_t)Sys_GptInit((int)frame->arg1); + case SYS_GPTADD: + return (int64_t)Sys_GptAdd((const GptAddParams*)frame->arg1); + case SYS_FSMOUNT: + return (int64_t)Sys_FsMount((int)frame->arg1, (int)frame->arg2); + case SYS_FSFORMAT: + return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1); default: return -1; } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 2687ec5..a380bcb 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -90,6 +90,7 @@ namespace Montauk { /* Filesystem.hpp */ static constexpr uint64_t SYS_FWRITE = 41; static constexpr uint64_t SYS_FCREATE = 42; + static constexpr uint64_t SYS_FDELETE = 77; /* Graphics.hpp */ static constexpr uint64_t SYS_TERMSCALE = 43; @@ -137,6 +138,15 @@ namespace Montauk { /* MemInfo.hpp */ static constexpr uint64_t SYS_MEMSTATS = 67; + /* Storage.hpp */ + static constexpr uint64_t SYS_PARTLIST = 70; + static constexpr uint64_t SYS_DISKREAD = 71; + static constexpr uint64_t SYS_DISKWRITE = 72; + static constexpr uint64_t SYS_GPTINIT = 73; + static constexpr uint64_t SYS_GPTADD = 74; + static constexpr uint64_t SYS_FSMOUNT = 75; + static constexpr uint64_t SYS_FSFORMAT = 76; + static constexpr int SOCK_TCP = 1; static constexpr int SOCK_UDP = 2; @@ -254,6 +264,44 @@ namespace Montauk { char _pad2[1]; }; + struct PartGuid { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; + }; + + struct PartInfo { + int32_t blockDev; // block device index + uint32_t _pad0; + uint64_t startLba; + uint64_t endLba; + uint64_t sectorCount; + PartGuid typeGuid; + PartGuid uniqueGuid; + uint64_t attributes; + char name[72]; // ASCII partition name + char typeName[24]; // human-readable type name + }; + + struct GptAddParams { + int32_t blockDev; + uint32_t _pad0; + uint64_t startLba; // 0 = auto (fill largest free region) + uint64_t endLba; // 0 = auto + PartGuid typeGuid; + char name[72]; + }; + + // Filesystem type IDs for SYS_FSFORMAT + static constexpr int FS_TYPE_FAT32 = 1; + + struct FsFormatParams { + int32_t partIndex; // global partition index + int32_t fsType; // FS_TYPE_FAT32, etc. + char label[32]; // volume label + }; + struct ProcInfo { int32_t pid; int32_t parentPid; diff --git a/kernel/src/Drivers/Init.cpp b/kernel/src/Drivers/Init.cpp index 1c170b9..a66acef 100644 --- a/kernel/src/Drivers/Init.cpp +++ b/kernel/src/Drivers/Init.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -157,9 +158,9 @@ namespace Drivers { } void InitializeStorage() { - // AHCI driver registered SATA devices during ProbeNormal(). - // Nothing else to do here for now — the VFS registration - // is handled in Main.cpp after this call. + // AHCI driver registered SATA devices as block devices during + // ProbeNormal(). Now probe all block devices for GPT partitions. + Storage::Gpt::ProbeAll(); } } diff --git a/kernel/src/Drivers/Storage/Ahci.cpp b/kernel/src/Drivers/Storage/Ahci.cpp index 126c583..f705512 100644 --- a/kernel/src/Drivers/Storage/Ahci.cpp +++ b/kernel/src/Drivers/Storage/Ahci.cpp @@ -5,6 +5,7 @@ */ #include "Ahci.hpp" +#include "BlockDevice.hpp" #include #include #include @@ -164,13 +165,23 @@ namespace Drivers::Storage::Ahci { return PortType::None; } + // Wait for device to become ready (BSY clear) so the signature is valid. + // After HBA/port reset, the device sends a D2H Register FIS with its + // signature once it finishes COMRESET. Until that FIS arrives PORT_SIG + // may contain stale data. + for (int i = 0; i < 1000000; i++) { + uint32_t tfd = ReadPortReg(port, PORT_TFD); + if (!(tfd & PORT_TFD_BSY)) break; + asm volatile("" ::: "memory"); + } + uint32_t sig = ReadPortReg(port, PORT_SIG); switch (sig) { case SIG_ATA: return PortType::Sata; case SIG_ATAPI: return PortType::Satapi; case SIG_SEMB: return PortType::Semb; case SIG_PM: return PortType::PortMultiplier; - default: return PortType::Sata; // Default to SATA for unknown signatures + default: return PortType::None; } } @@ -725,6 +736,20 @@ namespace Drivers::Storage::Ahci { if (IdentifyDevice(i)) { g_ports[i].Active = true; g_activePortCount++; + + // Register as a block device + Storage::BlockDevice bdev = {}; + bdev.ReadSectors = [](void* ctx, uint64_t lba, uint32_t count, void* buffer) -> bool { + return ReadSectors((int)(uintptr_t)ctx, lba, count, buffer); + }; + bdev.WriteSectors = [](void* ctx, uint64_t lba, uint32_t count, const void* buffer) -> bool { + return WriteSectors((int)(uintptr_t)ctx, lba, count, buffer); + }; + bdev.Ctx = (void*)(uintptr_t)i; + bdev.SectorCount = g_ports[i].SectorCount; + bdev.SectorSize = g_ports[i].SectorSizeLog; + memcpy(bdev.Model, g_ports[i].Model, 41); + Storage::RegisterBlockDevice(bdev); } } } diff --git a/kernel/src/Drivers/Storage/BlockDevice.cpp b/kernel/src/Drivers/Storage/BlockDevice.cpp new file mode 100644 index 0000000..0bd83be --- /dev/null +++ b/kernel/src/Drivers/Storage/BlockDevice.cpp @@ -0,0 +1,29 @@ +/* + * BlockDevice.cpp + * Driver-agnostic block device registry + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "BlockDevice.hpp" + +namespace Drivers::Storage { + + static BlockDevice g_devices[MaxBlockDevices] = {}; + static int g_deviceCount = 0; + + int RegisterBlockDevice(const BlockDevice& dev) { + if (g_deviceCount >= MaxBlockDevices) return -1; + g_devices[g_deviceCount] = dev; + return g_deviceCount++; + } + + const BlockDevice* GetBlockDevice(int index) { + if (index < 0 || index >= g_deviceCount) return nullptr; + return &g_devices[index]; + } + + int GetBlockDeviceCount() { + return g_deviceCount; + } + +}; diff --git a/kernel/src/Drivers/Storage/BlockDevice.hpp b/kernel/src/Drivers/Storage/BlockDevice.hpp new file mode 100644 index 0000000..b8ef8d8 --- /dev/null +++ b/kernel/src/Drivers/Storage/BlockDevice.hpp @@ -0,0 +1,32 @@ +/* + * BlockDevice.hpp + * Driver-agnostic block device registry + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Drivers::Storage { + + static constexpr int MaxBlockDevices = 32; + + struct BlockDevice { + bool (*ReadSectors)(void* ctx, uint64_t lba, uint32_t count, void* buffer); + bool (*WriteSectors)(void* ctx, uint64_t lba, uint32_t count, const void* buffer); + void* Ctx; + uint64_t SectorCount; + uint16_t SectorSize; + char Model[41]; + }; + + // Register a block device. Returns the assigned index, or -1 on failure. + int RegisterBlockDevice(const BlockDevice& dev); + + // Get a registered block device by index. Returns nullptr if invalid. + const BlockDevice* GetBlockDevice(int index); + + // Get the number of registered block devices. + int GetBlockDeviceCount(); + +}; diff --git a/kernel/src/Drivers/Storage/Gpt.cpp b/kernel/src/Drivers/Storage/Gpt.cpp new file mode 100644 index 0000000..a5d1c6a --- /dev/null +++ b/kernel/src/Drivers/Storage/Gpt.cpp @@ -0,0 +1,710 @@ +/* + * Gpt.cpp + * GUID Partition Table (GPT) parser + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Gpt.hpp" +#include "BlockDevice.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Storage::Gpt { + + // ------------------------------------------------------------------------- + // State + // ------------------------------------------------------------------------- + + static PartitionInfo g_partitions[MaxPartitions] = {}; + static int g_partitionCount = 0; + + // ------------------------------------------------------------------------- + // CRC32 (ISO 3309 / UEFI spec, polynomial 0xEDB88320) + // ------------------------------------------------------------------------- + + static uint32_t Crc32(const void* data, uint32_t length) { + const uint8_t* buf = (const uint8_t*)data; + uint32_t crc = 0xFFFFFFFF; + + for (uint32_t i = 0; i < length; i++) { + crc ^= buf[i]; + for (int bit = 0; bit < 8; bit++) { + if (crc & 1) + crc = (crc >> 1) ^ 0xEDB88320; + else + crc >>= 1; + } + } + + return ~crc; + } + + // ------------------------------------------------------------------------- + // GUID helpers + // ------------------------------------------------------------------------- + + static bool GuidIsZero(const Guid& g) { + return g.Data1 == 0 && g.Data2 == 0 && g.Data3 == 0 && + g.Data4[0] == 0 && g.Data4[1] == 0 && g.Data4[2] == 0 && + g.Data4[3] == 0 && g.Data4[4] == 0 && g.Data4[5] == 0 && + g.Data4[6] == 0 && g.Data4[7] == 0; + } + + static bool GuidEquals(const Guid& a, const Guid& b) { + return a.Data1 == b.Data1 && a.Data2 == b.Data2 && a.Data3 == b.Data3 && + a.Data4[0] == b.Data4[0] && a.Data4[1] == b.Data4[1] && + a.Data4[2] == b.Data4[2] && a.Data4[3] == b.Data4[3] && + a.Data4[4] == b.Data4[4] && a.Data4[5] == b.Data4[5] && + a.Data4[6] == b.Data4[6] && a.Data4[7] == b.Data4[7]; + } + + // ------------------------------------------------------------------------- + // UTF-16LE to ASCII narrowing + // ------------------------------------------------------------------------- + + static void Utf16ToAscii(const uint16_t* src, int srcLen, char* dst, int dstMax) { + int j = 0; + for (int i = 0; i < srcLen && j < dstMax - 1; i++) { + uint16_t c = src[i]; + if (c == 0) break; + dst[j++] = (c < 128) ? (char)c : '?'; + } + dst[j] = '\0'; + } + + // ------------------------------------------------------------------------- + // Validate protective MBR + // ------------------------------------------------------------------------- + + static bool ValidateProtectiveMbr(const uint8_t* sector0) { + const ProtectiveMbr* mbr = (const ProtectiveMbr*)sector0; + + if (mbr->Signature != 0xAA55) return false; + + // At least one partition entry should have type 0xEE (GPT protective) + for (int i = 0; i < 4; i++) { + if (mbr->Partitions[i].Type == 0xEE) return true; + } + + return false; + } + + // ------------------------------------------------------------------------- + // Validate GPT header + // ------------------------------------------------------------------------- + + static bool ValidateHeader(GptHeader* hdr, uint64_t expectedLba) { + if (hdr->Signature != GPT_HEADER_SIGNATURE) { + KernelLogStream(ERROR, "GPT") << "Invalid signature"; + return false; + } + + if (hdr->Revision < GPT_HEADER_REVISION) { + KernelLogStream(ERROR, "GPT") << "Unsupported revision"; + return false; + } + + if (hdr->HeaderSize < 92 || hdr->HeaderSize > 512) { + KernelLogStream(ERROR, "GPT") << "Invalid header size: " << (uint64_t)hdr->HeaderSize; + return false; + } + + if (hdr->MyLba != expectedLba) { + KernelLogStream(ERROR, "GPT") << "MyLBA mismatch: expected " + << expectedLba << ", got " << hdr->MyLba; + return false; + } + + // Verify header CRC32 + uint32_t savedCrc = hdr->HeaderCrc32; + hdr->HeaderCrc32 = 0; + uint32_t computed = Crc32(hdr, hdr->HeaderSize); + hdr->HeaderCrc32 = savedCrc; + + if (computed != savedCrc) { + KernelLogStream(ERROR, "GPT") << "Header CRC32 mismatch: expected " + << base::hex << (uint64_t)savedCrc << ", computed " << (uint64_t)computed; + return false; + } + + if (hdr->SizeOfPartitionEntry < 128) { + KernelLogStream(ERROR, "GPT") << "Partition entry size too small: " + << (uint64_t)hdr->SizeOfPartitionEntry; + return false; + } + + return true; + } + + // ------------------------------------------------------------------------- + // Validate partition entry array CRC32 + // ------------------------------------------------------------------------- + + static bool ValidatePartitionArrayCrc(const BlockDevice* dev, const GptHeader* hdr) { + uint32_t totalBytes = hdr->NumberOfPartitionEntries * hdr->SizeOfPartitionEntry; + uint32_t totalSectors = (totalBytes + 511) / 512; + + // Read all partition entry sectors into a temporary buffer + // Max 128 entries * 128 bytes = 16384 bytes = 32 sectors = 4 pages + int pagesNeeded = (totalBytes + 0xFFF) / 0x1000; + if (pagesNeeded > 8) { + KernelLogStream(ERROR, "GPT") << "Partition array too large"; + return false; + } + + void* buf; + if (pagesNeeded == 1) { + buf = Memory::g_pfa->AllocateZeroed(); + } else { + buf = Memory::g_pfa->ReallocConsecutive(nullptr, pagesNeeded); + memset(buf, 0, pagesNeeded * 0x1000); + } + + // Read in chunks of 128 sectors max + uint8_t* dst = (uint8_t*)buf; + uint64_t lba = hdr->PartitionEntryLba; + uint32_t remaining = totalSectors; + + while (remaining > 0) { + uint32_t chunk = remaining > 128 ? 128 : remaining; + if (!dev->ReadSectors(dev->Ctx, lba, chunk, dst)) { + KernelLogStream(ERROR, "GPT") << "Failed to read partition entries at LBA " << lba; + Memory::g_pfa->Free(buf, pagesNeeded); + return false; + } + dst += chunk * 512; + lba += chunk; + remaining -= chunk; + } + + uint32_t computed = Crc32(buf, totalBytes); + Memory::g_pfa->Free(buf, pagesNeeded); + + if (computed != hdr->PartitionEntryArrayCrc32) { + KernelLogStream(ERROR, "GPT") << "Partition array CRC32 mismatch: expected " + << base::hex << (uint64_t)hdr->PartitionEntryArrayCrc32 + << ", computed " << (uint64_t)computed; + return false; + } + + return true; + } + + // ------------------------------------------------------------------------- + // Parse partition entries + // ------------------------------------------------------------------------- + + static int ParsePartitions(const BlockDevice* dev, const GptHeader* hdr, int blockDevIndex) { + uint32_t totalBytes = hdr->NumberOfPartitionEntries * hdr->SizeOfPartitionEntry; + uint32_t totalSectors = (totalBytes + 511) / 512; + + int pagesNeeded = (totalBytes + 0xFFF) / 0x1000; + if (pagesNeeded > 8) return 0; + + void* buf; + if (pagesNeeded == 1) { + buf = Memory::g_pfa->AllocateZeroed(); + } else { + buf = Memory::g_pfa->ReallocConsecutive(nullptr, pagesNeeded); + memset(buf, 0, pagesNeeded * 0x1000); + } + + uint8_t* dst = (uint8_t*)buf; + uint64_t lba = hdr->PartitionEntryLba; + uint32_t remaining = totalSectors; + + while (remaining > 0) { + uint32_t chunk = remaining > 128 ? 128 : remaining; + if (!dev->ReadSectors(dev->Ctx, lba, chunk, dst)) { + Memory::g_pfa->Free(buf, pagesNeeded); + return 0; + } + dst += chunk * 512; + lba += chunk; + remaining -= chunk; + } + + int found = 0; + + for (uint32_t i = 0; i < hdr->NumberOfPartitionEntries && g_partitionCount < MaxPartitions; i++) { + const GptPartitionEntry* entry = (const GptPartitionEntry*) + ((uint8_t*)buf + i * hdr->SizeOfPartitionEntry); + + if (GuidIsZero(entry->TypeGuid)) continue; + if (entry->StartingLba == 0 || entry->EndingLba == 0) continue; + if (entry->StartingLba > entry->EndingLba) continue; + + PartitionInfo& part = g_partitions[g_partitionCount]; + part.BlockDevIndex = blockDevIndex; + part.StartLba = entry->StartingLba; + part.EndLba = entry->EndingLba; + part.SectorCount = entry->EndingLba - entry->StartingLba + 1; + part.TypeGuid = entry->TypeGuid; + part.UniqueGuid = entry->UniqueGuid; + part.Attributes = entry->Attributes; + uint16_t nameCopy[36]; + memcpy(nameCopy, entry->Name, sizeof(nameCopy)); + Utf16ToAscii(nameCopy, 36, part.Name, 72); + + g_partitionCount++; + found++; + } + + Memory::g_pfa->Free(buf, pagesNeeded); + return found; + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + const char* GetTypeName(const Guid& typeGuid) { + if (GuidEquals(typeGuid, GUID_EFI_SYSTEM)) return "EFI System"; + if (GuidEquals(typeGuid, GUID_BASIC_DATA)) return "Basic Data"; + if (GuidEquals(typeGuid, GUID_LINUX_FS)) return "Linux Filesystem"; + if (GuidEquals(typeGuid, GUID_LINUX_SWAP)) return "Linux Swap"; + return "Unknown"; + } + + int ProbeDevice(int blockDevIndex) { + const BlockDevice* dev = GetBlockDevice(blockDevIndex); + if (!dev) return 0; + + // Need at least 34 sectors (MBR + GPT header + 32 sectors of entries) + if (dev->SectorCount < 34) return 0; + + // Read LBA 0 (protective MBR) and LBA 1 (GPT header) — 2 sectors + uint8_t sectorBuf[1024]; + + if (!dev->ReadSectors(dev->Ctx, 0, 2, sectorBuf)) { + return 0; + } + + // Validate protective MBR + if (!ValidateProtectiveMbr(sectorBuf)) { + return 0; + } + + // Validate primary GPT header (LBA 1) + GptHeader* hdr = (GptHeader*)(sectorBuf + 512); + + if (!ValidateHeader(hdr, 1)) { + KernelLogStream(WARNING, "GPT") << "Primary header invalid on device " << blockDevIndex + << ", trying backup..."; + + // Try backup header at last LBA + uint64_t lastLba = dev->SectorCount - 1; + if (!dev->ReadSectors(dev->Ctx, lastLba, 1, sectorBuf + 512)) { + KernelLogStream(ERROR, "GPT") << "Failed to read backup header"; + return 0; + } + + if (!ValidateHeader(hdr, lastLba)) { + KernelLogStream(ERROR, "GPT") << "Backup header also invalid"; + return 0; + } + + KernelLogStream(OK, "GPT") << "Using backup GPT header"; + } + + // Validate partition entry array CRC + if (!ValidatePartitionArrayCrc(dev, hdr)) { + return 0; + } + + KernelLogStream(OK, "GPT") << "Valid GPT on device " << blockDevIndex + << " (" << dev->Model << "): " + << (uint64_t)hdr->NumberOfPartitionEntries << " entry slots, " + << (uint64_t)hdr->SizeOfPartitionEntry << " bytes each"; + + // Parse partition entries + int found = ParsePartitions(dev, hdr, blockDevIndex); + + // Log discovered partitions + for (int i = g_partitionCount - found; i < g_partitionCount; i++) { + const PartitionInfo& p = g_partitions[i]; + uint64_t sizeMB = (p.SectorCount * dev->SectorSize) / (1024 * 1024); + uint64_t sizeGB = sizeMB / 1024; + + if (sizeGB > 0) { + KernelLogStream(OK, "GPT") << " Partition " << (i - (g_partitionCount - found)) + << ": " << (p.Name[0] ? p.Name : "(unnamed)") + << " [" << GetTypeName(p.TypeGuid) << "] " + << sizeGB << " GiB" + << " (LBA " << p.StartLba << "-" << p.EndLba << ")"; + } else { + KernelLogStream(OK, "GPT") << " Partition " << (i - (g_partitionCount - found)) + << ": " << (p.Name[0] ? p.Name : "(unnamed)") + << " [" << GetTypeName(p.TypeGuid) << "] " + << sizeMB << " MiB" + << " (LBA " << p.StartLba << "-" << p.EndLba << ")"; + } + } + + return found; + } + + void ProbeAll() { + int devCount = GetBlockDeviceCount(); + if (devCount == 0) return; + + KernelLogStream(INFO, "GPT") << "Probing " << devCount << " block device(s) for GPT..."; + + int totalPartitions = 0; + for (int i = 0; i < devCount; i++) { + totalPartitions += ProbeDevice(i); + } + + if (totalPartitions > 0) { + KernelLogStream(OK, "GPT") << "Found " << totalPartitions << " partition(s) total"; + } else { + KernelLogStream(INFO, "GPT") << "No GPT partitions found"; + } + } + + int GetPartitionCount() { + return g_partitionCount; + } + + const PartitionInfo* GetPartition(int index) { + if (index < 0 || index >= g_partitionCount) return nullptr; + return &g_partitions[index]; + } + + // ------------------------------------------------------------------------- + // ASCII to UTF-16LE for partition names + // ------------------------------------------------------------------------- + + static void AsciiToUtf16(const char* src, uint16_t* dst, int maxChars) { + int i = 0; + for (; i < maxChars - 1 && src[i]; i++) { + dst[i] = (uint16_t)(uint8_t)src[i]; + } + for (; i < maxChars; i++) { + dst[i] = 0; + } + } + + // ------------------------------------------------------------------------- + // Simple GUID generation from RDTSC + mixing + // ------------------------------------------------------------------------- + + static uint64_t SimpleRand64() { + uint32_t lo, hi; + asm volatile ("rdtsc" : "=a"(lo), "=d"(hi)); + uint64_t val = ((uint64_t)hi << 32) | lo; + // xorshift64 + val ^= val << 13; + val ^= val >> 7; + val ^= val << 17; + return val; + } + + static Guid GenerateGuid() { + uint64_t a = SimpleRand64(); + uint64_t b = SimpleRand64(); + Guid g; + memcpy(&g, &a, 8); + memcpy(((uint8_t*)&g) + 8, &b, 8); + // Set version 4 (random) and variant 1 + g.Data3 = (g.Data3 & 0x0FFF) | 0x4000; + g.Data4[0] = (g.Data4[0] & 0x3F) | 0x80; + return g; + } + + // ------------------------------------------------------------------------- + // Write helpers + // ------------------------------------------------------------------------- + + static bool WriteSector(const BlockDevice* dev, uint64_t lba, const void* buf) { + return dev->WriteSectors(dev->Ctx, lba, 1, buf); + } + + // Rebuild and write the partition entry array + both GPT headers. + // entries is the full 128-entry array in memory. + static bool WriteGptStructures(const BlockDevice* dev, GptHeader* primary, + uint8_t* entryArray, uint32_t entryArrayBytes) { + uint32_t entryArraySectors = (entryArrayBytes + 511) / 512; + + // Compute partition entry array CRC + uint32_t entryCrc = Crc32(entryArray, entryArrayBytes); + primary->PartitionEntryArrayCrc32 = entryCrc; + + // Write primary entry array (starts at LBA 2) + for (uint32_t s = 0; s < entryArraySectors; s++) { + if (!dev->WriteSectors(dev->Ctx, primary->PartitionEntryLba + s, 1, + entryArray + s * 512)) + return false; + } + + // Write primary header (LBA 1) + primary->HeaderCrc32 = 0; + primary->HeaderCrc32 = Crc32(primary, primary->HeaderSize); + + uint8_t hdrSector[512]; + memset(hdrSector, 0, 512); + memcpy(hdrSector, primary, primary->HeaderSize); + if (!WriteSector(dev, 1, hdrSector)) return false; + + // Build and write backup header at last LBA + GptHeader backup = *primary; + backup.MyLba = primary->AlternateLba; + backup.AlternateLba = primary->MyLba; + // Backup partition entries are right before the backup header + backup.PartitionEntryLba = backup.MyLba - entryArraySectors; + + // Write backup entry array + for (uint32_t s = 0; s < entryArraySectors; s++) { + if (!dev->WriteSectors(dev->Ctx, backup.PartitionEntryLba + s, 1, + entryArray + s * 512)) + return false; + } + + // Write backup header CRC + backup.HeaderCrc32 = 0; + backup.HeaderCrc32 = Crc32(&backup, backup.HeaderSize); + memset(hdrSector, 0, 512); + memcpy(hdrSector, &backup, backup.HeaderSize); + if (!WriteSector(dev, backup.MyLba, hdrSector)) return false; + + return true; + } + + // ------------------------------------------------------------------------- + // InitializeGpt + // ------------------------------------------------------------------------- + + int InitializeGpt(int blockDevIndex) { + const BlockDevice* dev = GetBlockDevice(blockDevIndex); + if (!dev) return -1; + if (dev->SectorCount < 68) return -1; // minimum for GPT + + // Write protective MBR + ProtectiveMbr mbr; + memset(&mbr, 0, sizeof(mbr)); + mbr.Signature = 0xAA55; + mbr.Partitions[0].Status = 0x00; + mbr.Partitions[0].Type = 0xEE; + mbr.Partitions[0].LbaFirst = 1; + uint64_t mbrSectors = dev->SectorCount - 1; + mbr.Partitions[0].SectorCount = (mbrSectors > 0xFFFFFFFF) ? 0xFFFFFFFF : (uint32_t)mbrSectors; + + if (!WriteSector(dev, 0, &mbr)) { + KernelLogStream(ERROR, "GPT") << "Failed to write protective MBR"; + return -1; + } + + // Build primary GPT header + uint32_t numEntries = 128; + uint32_t entrySize = 128; + uint32_t entryArrayBytes = numEntries * entrySize; + uint32_t entryArraySectors = (entryArrayBytes + 511) / 512; // 32 sectors + + GptHeader hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.Signature = GPT_HEADER_SIGNATURE; + hdr.Revision = GPT_HEADER_REVISION; + hdr.HeaderSize = 92; + hdr.MyLba = 1; + hdr.AlternateLba = dev->SectorCount - 1; + hdr.FirstUsableLba = 2 + entryArraySectors; // after primary entries + hdr.LastUsableLba = dev->SectorCount - 2 - entryArraySectors; // before backup entries + hdr.DiskGuid = GenerateGuid(); + hdr.PartitionEntryLba = 2; + hdr.NumberOfPartitionEntries = numEntries; + hdr.SizeOfPartitionEntry = entrySize; + + // Empty partition entry array + uint8_t* entryArray = (uint8_t*)Memory::g_pfa->ReallocConsecutive( + nullptr, (entryArrayBytes + 0xFFF) / 0x1000); + memset(entryArray, 0, entryArrayBytes); + + bool ok = WriteGptStructures(dev, &hdr, entryArray, entryArrayBytes); + Memory::g_pfa->Free(entryArray, (entryArrayBytes + 0xFFF) / 0x1000); + + if (!ok) { + KernelLogStream(ERROR, "GPT") << "Failed to write GPT structures"; + return -1; + } + + KernelLogStream(OK, "GPT") << "Initialized GPT on device " << blockDevIndex + << " (usable LBA " << hdr.FirstUsableLba << "-" << hdr.LastUsableLba << ")"; + + return 0; + } + + // ------------------------------------------------------------------------- + // AddPartition + // ------------------------------------------------------------------------- + + int AddPartition(int blockDevIndex, uint64_t startLba, uint64_t endLba, + const Guid& typeGuid, const char* name) { + const BlockDevice* dev = GetBlockDevice(blockDevIndex); + if (!dev) return -1; + + // Read existing primary GPT header + uint8_t hdrBuf[512]; + if (!dev->ReadSectors(dev->Ctx, 1, 1, hdrBuf)) return -1; + + GptHeader* hdr = (GptHeader*)hdrBuf; + if (!ValidateHeader(hdr, 1)) { + KernelLogStream(ERROR, "GPT") << "No valid GPT on device " << blockDevIndex; + return -1; + } + + // Read partition entry array + uint32_t entryArrayBytes = hdr->NumberOfPartitionEntries * hdr->SizeOfPartitionEntry; + uint32_t pages = (entryArrayBytes + 0xFFF) / 0x1000; + uint8_t* entryArray = (uint8_t*)Memory::g_pfa->ReallocConsecutive(nullptr, pages); + memset(entryArray, 0, pages * 0x1000); + + uint32_t entryArraySectors = (entryArrayBytes + 511) / 512; + for (uint32_t s = 0; s < entryArraySectors; s++) { + if (!dev->ReadSectors(dev->Ctx, hdr->PartitionEntryLba + s, 1, entryArray + s * 512)) { + Memory::g_pfa->Free(entryArray, pages); + return -1; + } + } + + // Find a free entry slot + int freeSlot = -1; + for (uint32_t i = 0; i < hdr->NumberOfPartitionEntries; i++) { + GptPartitionEntry* e = (GptPartitionEntry*)(entryArray + i * hdr->SizeOfPartitionEntry); + if (GuidIsZero(e->TypeGuid)) { + freeSlot = (int)i; + break; + } + } + + if (freeSlot < 0) { + KernelLogStream(ERROR, "GPT") << "No free partition entry slots"; + Memory::g_pfa->Free(entryArray, pages); + return -1; + } + + // Auto-fill: find largest free region if startLba/endLba are both 0 + if (startLba == 0 && endLba == 0) { + // Collect used ranges + struct Range { uint64_t start; uint64_t end; }; + Range used[MaxPartitions]; + int usedCount = 0; + + for (uint32_t i = 0; i < hdr->NumberOfPartitionEntries && usedCount < MaxPartitions; i++) { + GptPartitionEntry* e = (GptPartitionEntry*)(entryArray + i * hdr->SizeOfPartitionEntry); + if (!GuidIsZero(e->TypeGuid)) { + used[usedCount].start = e->StartingLba; + used[usedCount].end = e->EndingLba; + usedCount++; + } + } + + // Simple bubble sort by start LBA + for (int i = 0; i < usedCount - 1; i++) { + for (int j = i + 1; j < usedCount; j++) { + if (used[j].start < used[i].start) { + Range tmp = used[i]; used[i] = used[j]; used[j] = tmp; + } + } + } + + // Find largest gap + uint64_t bestStart = 0, bestEnd = 0, bestSize = 0; + + // Gap before first partition + uint64_t gapStart = hdr->FirstUsableLba; + uint64_t gapEnd = (usedCount > 0) ? used[0].start - 1 : hdr->LastUsableLba; + if (gapEnd >= gapStart && gapEnd - gapStart + 1 > bestSize) { + bestStart = gapStart; bestEnd = gapEnd; + bestSize = gapEnd - gapStart + 1; + } + + // Gaps between partitions + for (int i = 0; i < usedCount - 1; i++) { + gapStart = used[i].end + 1; + gapEnd = used[i + 1].start - 1; + if (gapEnd >= gapStart && gapEnd - gapStart + 1 > bestSize) { + bestStart = gapStart; bestEnd = gapEnd; + bestSize = gapEnd - gapStart + 1; + } + } + + // Gap after last partition + if (usedCount > 0) { + gapStart = used[usedCount - 1].end + 1; + gapEnd = hdr->LastUsableLba; + if (gapEnd >= gapStart && gapEnd - gapStart + 1 > bestSize) { + bestStart = gapStart; bestEnd = gapEnd; + bestSize = gapEnd - gapStart + 1; + } + } + + if (bestSize == 0) { + KernelLogStream(ERROR, "GPT") << "No free space for new partition"; + Memory::g_pfa->Free(entryArray, pages); + return -1; + } + + startLba = bestStart; + endLba = bestEnd; + } + + // Validate range + if (startLba < hdr->FirstUsableLba || endLba > hdr->LastUsableLba || startLba > endLba) { + KernelLogStream(ERROR, "GPT") << "Invalid partition range"; + Memory::g_pfa->Free(entryArray, pages); + return -1; + } + + // Fill the entry + GptPartitionEntry* newEntry = (GptPartitionEntry*)(entryArray + freeSlot * hdr->SizeOfPartitionEntry); + newEntry->TypeGuid = typeGuid; + newEntry->UniqueGuid = GenerateGuid(); + newEntry->StartingLba = startLba; + newEntry->EndingLba = endLba; + newEntry->Attributes = 0; + AsciiToUtf16(name ? name : "", newEntry->Name, 36); + + // Write updated GPT structures + bool ok = WriteGptStructures(dev, hdr, entryArray, entryArrayBytes); + Memory::g_pfa->Free(entryArray, pages); + + if (!ok) { + KernelLogStream(ERROR, "GPT") << "Failed to write updated GPT"; + return -1; + } + + // Add to in-memory partition table + if (g_partitionCount < MaxPartitions) { + PartitionInfo& part = g_partitions[g_partitionCount]; + part.BlockDevIndex = blockDevIndex; + part.StartLba = startLba; + part.EndLba = endLba; + part.SectorCount = endLba - startLba + 1; + part.TypeGuid = typeGuid; + part.UniqueGuid = newEntry->UniqueGuid; + part.Attributes = 0; + if (name) { + int i = 0; + for (; i < 71 && name[i]; i++) part.Name[i] = name[i]; + part.Name[i] = '\0'; + } else { + part.Name[0] = '\0'; + } + + int idx = g_partitionCount++; + + KernelLogStream(OK, "GPT") << "Added partition " << freeSlot + << " [" << GetTypeName(typeGuid) << "] LBA " + << startLba << "-" << endLba; + + return idx; + } + + return -1; + } + +}; diff --git a/kernel/src/Drivers/Storage/Gpt.hpp b/kernel/src/Drivers/Storage/Gpt.hpp new file mode 100644 index 0000000..2e2d547 --- /dev/null +++ b/kernel/src/Drivers/Storage/Gpt.hpp @@ -0,0 +1,147 @@ +/* + * Gpt.hpp + * GUID Partition Table (GPT) parser + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Drivers::Storage::Gpt { + + // ========================================================================= + // On-disk structures (UEFI Specification §5) + // ========================================================================= + + struct Guid { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; + } __attribute__((packed)); + + // Protective MBR partition entry (at LBA 0) + struct MbrPartitionEntry { + uint8_t Status; + uint8_t ChsFirst[3]; + uint8_t Type; + uint8_t ChsLast[3]; + uint32_t LbaFirst; + uint32_t SectorCount; + } __attribute__((packed)); + + struct ProtectiveMbr { + uint8_t Bootstrap[446]; + MbrPartitionEntry Partitions[4]; + uint16_t Signature; // 0xAA55 + } __attribute__((packed)); + + static_assert(sizeof(ProtectiveMbr) == 512); + + // GPT Header (at LBA 1, backup at last LBA) + static constexpr uint64_t GPT_HEADER_SIGNATURE = 0x5452415020494645ULL; // "EFI PART" + static constexpr uint32_t GPT_HEADER_REVISION = 0x00010000; + + struct GptHeader { + uint64_t Signature; + uint32_t Revision; + uint32_t HeaderSize; + uint32_t HeaderCrc32; + uint32_t Reserved; + uint64_t MyLba; + uint64_t AlternateLba; + uint64_t FirstUsableLba; + uint64_t LastUsableLba; + Guid DiskGuid; + uint64_t PartitionEntryLba; + uint32_t NumberOfPartitionEntries; + uint32_t SizeOfPartitionEntry; + uint32_t PartitionEntryArrayCrc32; + } __attribute__((packed)); + + static_assert(sizeof(GptHeader) == 92); + + // GPT Partition Entry (typically 128 bytes each) + struct GptPartitionEntry { + Guid TypeGuid; + Guid UniqueGuid; + uint64_t StartingLba; + uint64_t EndingLba; + uint64_t Attributes; + uint16_t Name[36]; // UTF-16LE, 36 code units = 72 bytes + } __attribute__((packed)); + + static_assert(sizeof(GptPartitionEntry) == 128); + + // Partition attribute bits + static constexpr uint64_t ATTR_PLATFORM_REQUIRED = (1ULL << 0); + static constexpr uint64_t ATTR_EFI_IGNORE = (1ULL << 1); + static constexpr uint64_t ATTR_LEGACY_BIOS_BOOT = (1ULL << 2); + + // Well-known partition type GUIDs + static constexpr Guid GUID_UNUSED = {0, 0, 0, {0,0,0,0,0,0,0,0}}; + // EFI System Partition: C12A7328-F81F-11D2-BA4B-00A0C93EC93B + static constexpr Guid GUID_EFI_SYSTEM = {0xC12A7328, 0xF81F, 0x11D2, {0xBA,0x4B,0x00,0xA0,0xC9,0x3E,0xC9,0x3B}}; + // Microsoft Basic Data: EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 + static constexpr Guid GUID_BASIC_DATA = {0xEBD0A0A2, 0xB9E5, 0x4433, {0x87,0xC0,0x68,0xB6,0xB7,0x26,0x99,0xC7}}; + // Linux Filesystem: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 + static constexpr Guid GUID_LINUX_FS = {0x0FC63DAF, 0x8483, 0x4772, {0x8E,0x79,0x3D,0x69,0xD8,0x47,0x7D,0xE4}}; + // Linux Swap: 0657FD6D-A4AB-43C4-84E5-0933C84B4F4F + static constexpr Guid GUID_LINUX_SWAP = {0x0657FD6D, 0xA4AB, 0x43C4, {0x84,0xE5,0x09,0x33,0xC8,0x4B,0x4F,0x4F}}; + + // ========================================================================= + // Parsed partition info (kernel-side) + // ========================================================================= + + static constexpr int MaxPartitions = 128; + + struct PartitionInfo { + int BlockDevIndex; // which block device this partition lives on + uint64_t StartLba; + uint64_t EndLba; + uint64_t SectorCount; + Guid TypeGuid; + Guid UniqueGuid; + uint64_t Attributes; + char Name[72]; // ASCII-narrowed from UTF-16LE + }; + + // ========================================================================= + // Public API + // ========================================================================= + + // Probe a block device for a GPT. Returns the number of partitions found, + // or 0 if no valid GPT was detected. + int ProbeDevice(int blockDevIndex); + + // Probe all registered block devices for GPT. + void ProbeAll(); + + // Get total number of discovered partitions (across all devices). + int GetPartitionCount(); + + // Get partition info by global index. + const PartitionInfo* GetPartition(int index); + + // Look up the human-readable name for a partition type GUID. + const char* GetTypeName(const Guid& typeGuid); + + // ========================================================================= + // Write support — create GPT and add partitions + // ========================================================================= + + // Initialize a fresh GPT on a block device (destroys all existing data). + // Creates protective MBR, primary + backup GPT headers, empty partition array. + // Returns 0 on success, -1 on error. + int InitializeGpt(int blockDevIndex); + + // Add a partition to an existing GPT. + // startLba/endLba define the range (inclusive). Pass 0/0 to auto-fill + // the largest contiguous free region. + // typeGuid selects the partition type. + // name is an ASCII label (up to 36 chars). + // Returns the global partition index on success, -1 on error. + int AddPartition(int blockDevIndex, uint64_t startLba, uint64_t endLba, + const Guid& typeGuid, const char* name); + +}; diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp new file mode 100644 index 0000000..7ea75f2 --- /dev/null +++ b/kernel/src/Fs/Fat32.cpp @@ -0,0 +1,1618 @@ +/* + * Fat32.cpp + * FAT32 filesystem driver + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Fat32.hpp" +#include "FsProbe.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Fs::Fat32 { + + // ========================================================================= + // Constants + // ========================================================================= + + static constexpr int MaxInstances = 8; + static constexpr int MaxFilesPerInstance = 16; + static constexpr int MaxDirEntries = 128; + static constexpr int MaxNameLen = 256; + + // Directory entry attributes + static constexpr uint8_t ATTR_READ_ONLY = 0x01; + static constexpr uint8_t ATTR_HIDDEN = 0x02; + static constexpr uint8_t ATTR_SYSTEM = 0x04; + static constexpr uint8_t ATTR_VOLUME_ID = 0x08; + static constexpr uint8_t ATTR_DIRECTORY = 0x10; + static constexpr uint8_t ATTR_ARCHIVE = 0x20; + static constexpr uint8_t ATTR_LFN = 0x0F; + + // Cluster chain sentinels + static constexpr uint32_t CLUSTER_FREE = 0x00000000; + static constexpr uint32_t CLUSTER_BAD = 0x0FFFFFF7; + static constexpr uint32_t CLUSTER_END_MIN = 0x0FFFFFF8; + + // ========================================================================= + // Types + // ========================================================================= + + struct Fat32File { + bool inUse; + uint32_t firstCluster; + uint32_t fileSize; + bool isDirectory; + // Location of the 32-byte SFN directory entry on disk + uint64_t sfnPartSector; // partition-relative sector + uint32_t sfnOffInSector; // byte offset within that sector + }; + + struct Fat32Instance { + bool active; + int blockDevIndex; + uint64_t partStartLba; + + // BPB fields + uint16_t bytesPerSector; + uint8_t sectorsPerCluster; + uint16_t reservedSectors; + uint8_t numFats; + uint32_t fatSize32; + uint32_t rootCluster; + uint32_t totalSectors; + char volumeLabel[12]; + + // Computed + uint32_t clusterSize; // bytesPerSector * sectorsPerCluster + uint32_t dataStartSector; // partition-relative sector of first data cluster + uint32_t clusterCount; + + // Cluster read buffer (page-allocated) + uint8_t* clusterBuf; + int clusterBufPages; + + // Open file handles + Fat32File files[MaxFilesPerInstance]; + + // ReadDir name cache + char dirNames[MaxDirEntries][MaxNameLen]; + int dirNameCount; + }; + + struct ParsedEntry { + char name[MaxNameLen]; + uint32_t firstCluster; + uint32_t fileSize; + uint8_t attributes; + // Location of the SFN entry on disk (for write support) + uint64_t sfnPartSector; + uint32_t sfnOffInSector; + }; + + // ========================================================================= + // Instance table + // ========================================================================= + + static Fat32Instance g_instances[MaxInstances] = {}; + static int g_instanceCount = 0; + + // ========================================================================= + // Low-level helpers + // ========================================================================= + + static bool ReadPartSectors(const Fat32Instance& inst, uint64_t partSector, + uint32_t count, void* buf) { + auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex); + if (!dev) return false; + return dev->ReadSectors(dev->Ctx, inst.partStartLba + partSector, count, buf); + } + + static uint64_t ClusterToPartSector(const Fat32Instance& inst, uint32_t cluster) { + return (uint64_t)inst.dataStartSector + + (uint64_t)(cluster - 2) * inst.sectorsPerCluster; + } + + static bool ReadCluster(Fat32Instance& inst, uint32_t cluster) { + if (cluster < 2) return false; + return ReadPartSectors(inst, ClusterToPartSector(inst, cluster), + inst.sectorsPerCluster, inst.clusterBuf); + } + + static uint32_t GetNextCluster(const Fat32Instance& inst, uint32_t cluster) { + uint32_t fatOffset = cluster * 4; + uint32_t fatSector = fatOffset / inst.bytesPerSector; + uint32_t entryOffset = fatOffset % inst.bytesPerSector; + + uint8_t sectorBuf[512]; + if (!ReadPartSectors(inst, inst.reservedSectors + fatSector, 1, sectorBuf)) { + return CLUSTER_END_MIN; // treat read error as end of chain + } + + uint32_t entry; + memcpy(&entry, sectorBuf + entryOffset, 4); + return entry & 0x0FFFFFFF; + } + + static bool IsEndOfChain(uint32_t cluster) { + return cluster >= CLUSTER_END_MIN || cluster == CLUSTER_BAD || cluster < 2; + } + + // ========================================================================= + // Write helpers + // ========================================================================= + + static bool WritePartSectors(const Fat32Instance& inst, uint64_t partSector, + uint32_t count, const void* buf) { + auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex); + if (!dev) return false; + return dev->WriteSectors(dev->Ctx, inst.partStartLba + partSector, count, buf); + } + + static bool WriteClusterData(Fat32Instance& inst, uint32_t cluster, const void* data) { + if (cluster < 2) return false; + return WritePartSectors(inst, ClusterToPartSector(inst, cluster), + inst.sectorsPerCluster, data); + } + + static bool WriteFatEntry(Fat32Instance& inst, uint32_t cluster, uint32_t value) { + uint32_t fatOffset = cluster * 4; + uint32_t fatSector = fatOffset / inst.bytesPerSector; + uint32_t entryOffset = fatOffset % inst.bytesPerSector; + + uint8_t sectorBuf[512]; + + for (int f = 0; f < inst.numFats; f++) { + uint64_t fatStart = inst.reservedSectors + (uint64_t)f * inst.fatSize32; + + if (!ReadPartSectors(inst, fatStart + fatSector, 1, sectorBuf)) return false; + + // Preserve upper 4 bits of the existing entry + uint32_t existing; + memcpy(&existing, sectorBuf + entryOffset, 4); + uint32_t merged = (existing & 0xF0000000) | (value & 0x0FFFFFFF); + memcpy(sectorBuf + entryOffset, &merged, 4); + + if (!WritePartSectors(inst, fatStart + fatSector, 1, sectorBuf)) return false; + } + + return true; + } + + static uint32_t AllocateCluster(Fat32Instance& inst) { + uint8_t sectorBuf[512]; + uint32_t entriesPerSector = inst.bytesPerSector / 4; + + for (uint32_t s = 0; s < inst.fatSize32; s++) { + if (!ReadPartSectors(inst, inst.reservedSectors + s, 1, sectorBuf)) continue; + + for (uint32_t e = 0; e < entriesPerSector; e++) { + uint32_t cluster = s * entriesPerSector + e; + if (cluster < 2) continue; + if (cluster >= inst.clusterCount + 2) return 0; + + uint32_t entry; + memcpy(&entry, sectorBuf + e * 4, 4); + if ((entry & 0x0FFFFFFF) == CLUSTER_FREE) { + if (!WriteFatEntry(inst, cluster, 0x0FFFFFFF)) return 0; + return cluster; + } + } + } + + return 0; + } + + // Update the file size field in a file's SFN directory entry on disk + static bool UpdateDirEntrySize(Fat32Instance& inst, const Fat32File& file) { + uint8_t sectorBuf[512]; + if (!ReadPartSectors(inst, file.sfnPartSector, 1, sectorBuf)) return false; + memcpy(sectorBuf + file.sfnOffInSector + 28, &file.fileSize, 4); + return WritePartSectors(inst, file.sfnPartSector, 1, sectorBuf); + } + + // Update both first-cluster and file-size in a file's SFN directory entry + static bool UpdateDirEntry(Fat32Instance& inst, const Fat32File& file) { + uint8_t sectorBuf[512]; + if (!ReadPartSectors(inst, file.sfnPartSector, 1, sectorBuf)) return false; + uint8_t* e = sectorBuf + file.sfnOffInSector; + uint16_t clHi = (uint16_t)(file.firstCluster >> 16); + uint16_t clLo = (uint16_t)(file.firstCluster & 0xFFFF); + memcpy(e + 20, &clHi, 2); + memcpy(e + 26, &clLo, 2); + memcpy(e + 28, &file.fileSize, 4); + return WritePartSectors(inst, file.sfnPartSector, 1, sectorBuf); + } + + // ========================================================================= + // Short name generation and LFN helpers for Create + // ========================================================================= + + static void GenerateShortName(const char* longName, char* shortName) { + memset(shortName, ' ', 11); + + // Find last dot + int lastDot = -1; + int nameLen = 0; + for (int i = 0; longName[i]; i++) { + nameLen = i + 1; + if (longName[i] == '.') lastDot = i; + } + + // Base name (up to 8 chars, uppercase, skip invalid chars) + int baseEnd = (lastDot >= 0) ? lastDot : nameLen; + int j = 0; + for (int i = 0; i < baseEnd && j < 8; i++) { + char c = longName[i]; + if (c == ' ' || c == '.') continue; + if (c >= 'a' && c <= 'z') c -= 32; + shortName[j++] = c; + } + + // Extension (up to 3 chars, uppercase) + if (lastDot >= 0) { + j = 0; + for (int i = lastDot + 1; longName[i] && j < 3; i++) { + char c = longName[i]; + if (c >= 'a' && c <= 'z') c -= 32; + shortName[8 + j++] = c; + } + } + } + + // Check if a short name already exists in a directory + static bool ShortNameExists(int inst, uint32_t dirCluster, const char* shortName) { + auto& self = g_instances[inst]; + uint32_t cluster = dirCluster; + + while (!IsEndOfChain(cluster)) { + if (!ReadCluster(self, cluster)) return false; + + int perCluster = (int)(self.clusterSize / 32); + for (int i = 0; i < perCluster; i++) { + uint8_t* e = self.clusterBuf + i * 32; + if (e[0] == 0x00) return false; + if (e[0] == 0xE5) continue; + if (e[11] == ATTR_LFN) continue; + + if (memcmp(e, shortName, 11) == 0) return true; + } + cluster = GetNextCluster(self, cluster); + } + return false; + } + + // Make the short name unique by appending ~N + static void MakeShortNameUnique(int inst, uint32_t dirCluster, char* shortName) { + if (!ShortNameExists(inst, dirCluster, shortName)) return; + + // Find base length (non-space chars in name portion) + int baseLen = 8; + while (baseLen > 0 && shortName[baseLen - 1] == ' ') baseLen--; + + for (int n = 1; n <= 999; n++) { + // Build suffix like "~1", "~23" + char suffix[8]; + suffix[0] = '~'; + int sLen = 1; + int tmp = n; + char digits[4]; + int dLen = 0; + while (tmp > 0) { digits[dLen++] = '0' + (tmp % 10); tmp /= 10; } + for (int d = dLen - 1; d >= 0; d--) suffix[sLen++] = digits[d]; + + int maxBase = 8 - sLen; + if (baseLen < maxBase) maxBase = baseLen; + + char trial[11]; + memcpy(trial, shortName, 11); + // Overwrite end of base with suffix + for (int i = 0; i < sLen; i++) trial[maxBase + i] = suffix[i]; + // Pad remainder with spaces + for (int i = maxBase + sLen; i < 8; i++) trial[i] = ' '; + + if (!ShortNameExists(inst, dirCluster, trial)) { + memcpy(shortName, trial, 11); + return; + } + } + } + + static uint8_t LfnChecksum(const char* shortName) { + uint8_t sum = 0; + for (int i = 0; i < 11; i++) { + sum = ((sum & 1) ? 0x80 : 0) + (sum >> 1) + (uint8_t)shortName[i]; + } + return sum; + } + + // Build LFN directory entries in disk order (highest seq first). + // Returns the number of LFN entries. + static int BuildLfnEntries(const char* longName, uint8_t checksum, + uint8_t* outEntries) { + // Convert to UTF-16 + uint16_t utf16[MaxNameLen]; + int len = 0; + while (longName[len] && len < MaxNameLen - 1) { + utf16[len] = (uint8_t)longName[len]; + len++; + } + utf16[len] = 0; + + int totalChars = len + 1; // include null terminator + int numEntries = (totalChars + 12) / 13; + + // Pad remaining with 0xFFFF + for (int i = totalChars; i < numEntries * 13; i++) { + utf16[i] = 0xFFFF; + } + + // Build in disk order: entry N (0x40|N), then N-1, ..., 1 + for (int e = 0; e < numEntries; e++) { + int seqNum = numEntries - e; // disk order: highest first + uint8_t* ent = outEntries + e * 32; + memset(ent, 0, 32); + + ent[0] = (uint8_t)seqNum; + if (e == 0) ent[0] |= 0x40; // first physical = last logical + + ent[11] = ATTR_LFN; + ent[13] = checksum; + + int base = (seqNum - 1) * 13; + // Chars 1-5 at offset 1 + for (int i = 0; i < 5; i++) memcpy(ent + 1 + i * 2, &utf16[base + i], 2); + // Chars 6-11 at offset 14 + for (int i = 0; i < 6; i++) memcpy(ent + 14 + i * 2, &utf16[base + 5 + i], 2); + // Chars 12-13 at offset 28 + for (int i = 0; i < 2; i++) memcpy(ent + 28 + i * 2, &utf16[base + 11 + i], 2); + } + + return numEntries; + } + + // Forward declaration (defined below in case-insensitive comparison section) + static bool StrEqualNoCase(const char* a, const char* b); + + // Check if a short name is needed (i.e. long name differs from 8.3) + static bool NeedsLfn(const char* longName, const char* shortName) { + // If name contains lowercase, spaces, or is longer than 8.3, need LFN + char reconstructed[13]; + int pos = 0; + for (int i = 0; i < 8 && shortName[i] != ' '; i++) + reconstructed[pos++] = shortName[i]; + if (shortName[8] != ' ') { + reconstructed[pos++] = '.'; + for (int i = 8; i < 11 && shortName[i] != ' '; i++) + reconstructed[pos++] = shortName[i]; + } + reconstructed[pos] = '\0'; + + // Compare case-insensitively + if (!StrEqualNoCase(longName, reconstructed)) return true; + + // Check if long name has lowercase (short names are uppercase) + for (int i = 0; longName[i]; i++) { + if (longName[i] >= 'a' && longName[i] <= 'z') return true; + } + return false; + } + + // ========================================================================= + // Directory entry allocation for Create + // ========================================================================= + + // Find `slotsNeeded` consecutive free (0x00 or 0xE5) 32-byte entries in a + // directory. If there isn't room, extends the directory by allocating a new + // cluster. Returns the partition-relative sector and byte offset of the + // first free slot, plus the cluster it's in. + struct DirSlotPos { + uint64_t partSector; // partition-relative sector of the first slot + uint32_t offsetInSec; // byte offset within that sector + bool found; + }; + + static DirSlotPos FindFreeDirSlots(int inst, uint32_t dirFirstCluster, + int slotsNeeded) { + auto& self = g_instances[inst]; + uint32_t cluster = dirFirstCluster; + uint32_t prevCluster = 0; + + while (!IsEndOfChain(cluster)) { + if (!ReadCluster(self, cluster)) return {0, 0, false}; + + int perCluster = (int)(self.clusterSize / 32); + int consecutive = 0; + int startIdx = 0; + + for (int i = 0; i < perCluster; i++) { + uint8_t* e = self.clusterBuf + i * 32; + if (e[0] == 0x00 || e[0] == 0xE5) { + if (consecutive == 0) startIdx = i; + consecutive++; + if (consecutive >= slotsNeeded) { + // Found enough slots + uint64_t clusterPartSec = ClusterToPartSector(self, cluster); + uint32_t byteOff = startIdx * 32; + uint64_t sector = clusterPartSec + (byteOff / self.bytesPerSector); + uint32_t offInSec = byteOff % self.bytesPerSector; + return {sector, offInSec, true}; + } + } else { + consecutive = 0; + } + } + + prevCluster = cluster; + cluster = GetNextCluster(self, cluster); + } + + // No room — extend directory with a new cluster + uint32_t newCluster = AllocateCluster(self); + if (newCluster == 0) return {0, 0, false}; + + // Link previous last cluster to new one + if (prevCluster != 0) { + WriteFatEntry(self, prevCluster, newCluster); + } + + // Zero the new cluster + memset(self.clusterBuf, 0, self.clusterSize); + WriteClusterData(self, newCluster, self.clusterBuf); + + uint64_t sec = ClusterToPartSector(self, newCluster); + return {sec, 0, true}; + } + + // Split a path into parent directory path and filename. + // E.g. "foo/bar/baz.txt" → parentPath="foo/bar", name="baz.txt" + // For "baz.txt" → parentPath="", name="baz.txt" + static void SplitPath(const char* path, char* parentPath, int parentMax, + char* fileName, int nameMax) { + // Skip leading slashes + while (*path == '/') path++; + + int len = 0; + while (path[len]) len++; + + // Find last slash + int lastSlash = -1; + for (int i = len - 1; i >= 0; i--) { + if (path[i] == '/') { lastSlash = i; break; } + } + + if (lastSlash < 0) { + parentPath[0] = '\0'; + int j = 0; + while (j < nameMax - 1 && path[j]) { fileName[j] = path[j]; j++; } + fileName[j] = '\0'; + } else { + int j = 0; + for (int i = 0; i < lastSlash && j < parentMax - 1; i++) { + parentPath[j++] = path[i]; + } + parentPath[j] = '\0'; + + j = 0; + for (int i = lastSlash + 1; i < len && j < nameMax - 1; i++) { + fileName[j++] = path[i]; + } + fileName[j] = '\0'; + } + } + + // ========================================================================= + // Short name parsing + // ========================================================================= + + static void ParseShortName(const uint8_t* entry, char* out) { + int pos = 0; + uint8_t ntRes = entry[12]; // NTRes case flags + + // Base name (8 bytes, trim trailing spaces) + for (int i = 0; i < 8 && entry[i] != ' '; i++) { + char c = (char)entry[i]; + if (ntRes & 0x08) { // lowercase name + if (c >= 'A' && c <= 'Z') c += 32; + } + out[pos++] = c; + } + + // Extension (3 bytes) + if (entry[8] != ' ') { + out[pos++] = '.'; + for (int i = 8; i < 11 && entry[i] != ' '; i++) { + char c = (char)entry[i]; + if (ntRes & 0x10) { // lowercase extension + if (c >= 'A' && c <= 'Z') c += 32; + } + out[pos++] = c; + } + } + + out[pos] = '\0'; + } + + // ========================================================================= + // LFN support + // ========================================================================= + + static void ExtractLfnChars(const uint8_t* entry, uint16_t* out) { + // Chars 1-5 at offset 1 + for (int i = 0; i < 5; i++) memcpy(&out[i], entry + 1 + i * 2, 2); + // Chars 6-11 at offset 14 + for (int i = 0; i < 6; i++) memcpy(&out[5 + i], entry + 14 + i * 2, 2); + // Chars 12-13 at offset 28 + for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2); + } + + static void Utf16ToAscii(const uint16_t* src, int maxLen, char* dst) { + int j = 0; + for (int i = 0; i < maxLen && src[i] != 0 && j < MaxNameLen - 1; i++) { + dst[j++] = (src[i] < 128) ? (char)src[i] : '?'; + } + dst[j] = '\0'; + } + + // ========================================================================= + // Directory reading + // ========================================================================= + + static int ReadDirectory(int inst, uint32_t dirCluster, + ParsedEntry* entries, int maxEntries) { + auto& self = g_instances[inst]; + uint16_t lfnBuf[MaxNameLen]; + bool hasLfn = false; + int count = 0; + + uint32_t cluster = dirCluster; + while (!IsEndOfChain(cluster) && count < maxEntries) { + if (!ReadCluster(self, cluster)) break; + + int perCluster = (int)(self.clusterSize / 32); + for (int i = 0; i < perCluster && count < maxEntries; i++) { + uint8_t* e = self.clusterBuf + i * 32; + + if (e[0] == 0x00) return count; // end of directory + if (e[0] == 0xE5) { hasLfn = false; continue; } // deleted + + uint8_t attr = e[11]; + + if (attr == ATTR_LFN) { + uint8_t seq = e[0]; + int seqNum = seq & 0x1F; + + if (seq & 0x40) { + // Last logical (first physical) LFN fragment + for (int k = 0; k < MaxNameLen; k++) lfnBuf[k] = 0; + hasLfn = true; + } + + if (hasLfn && seqNum >= 1 && seqNum <= 20) { + uint16_t chars[13]; + ExtractLfnChars(e, chars); + int offset = (seqNum - 1) * 13; + for (int k = 0; k < 13 && offset + k < MaxNameLen; k++) { + lfnBuf[offset + k] = chars[k]; + } + } + continue; + } + + // Skip volume label entries + if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; } + + ParsedEntry& pe = entries[count]; + + uint16_t clusterHi, clusterLo; + memcpy(&clusterHi, e + 20, 2); + memcpy(&clusterLo, e + 26, 2); + pe.firstCluster = ((uint32_t)clusterHi << 16) | (uint32_t)clusterLo; + memcpy(&pe.fileSize, e + 28, 4); + pe.attributes = attr; + + if (hasLfn) { + Utf16ToAscii(lfnBuf, MaxNameLen, pe.name); + } else { + ParseShortName(e, pe.name); + } + + hasLfn = false; + + // Skip '.' and '..' entries + if (pe.name[0] == '.' && + (pe.name[1] == '\0' || (pe.name[1] == '.' && pe.name[2] == '\0'))) { + continue; + } + + count++; + } + + cluster = GetNextCluster(self, cluster); + } + + return count; + } + + // ========================================================================= + // Case-insensitive string comparison + // ========================================================================= + + static char ToLower(char c) { + return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; + } + + static bool StrEqualNoCase(const char* a, const char* b) { + while (*a && *b) { + if (ToLower(*a) != ToLower(*b)) return false; + a++; b++; + } + return *a == *b; + } + + // ========================================================================= + // Path traversal + // ========================================================================= + + // Find a single entry by name in a directory. + // Returns true and fills 'out' on success. + static bool FindInDirectory(int inst, uint32_t dirCluster, + const char* name, ParsedEntry* out) { + uint32_t cluster = dirCluster; + auto& self = g_instances[inst]; + uint16_t lfnBuf[MaxNameLen]; + bool hasLfn = false; + + while (!IsEndOfChain(cluster)) { + if (!ReadCluster(self, cluster)) return false; + + int perCluster = (int)(self.clusterSize / 32); + for (int i = 0; i < perCluster; i++) { + uint8_t* e = self.clusterBuf + i * 32; + + if (e[0] == 0x00) return false; + if (e[0] == 0xE5) { hasLfn = false; continue; } + + uint8_t attr = e[11]; + + if (attr == ATTR_LFN) { + uint8_t seq = e[0]; + int seqNum = seq & 0x1F; + if (seq & 0x40) { + for (int k = 0; k < MaxNameLen; k++) lfnBuf[k] = 0; + hasLfn = true; + } + if (hasLfn && seqNum >= 1 && seqNum <= 20) { + uint16_t chars[13]; + ExtractLfnChars(e, chars); + int offset = (seqNum - 1) * 13; + for (int k = 0; k < 13 && offset + k < MaxNameLen; k++) { + lfnBuf[offset + k] = chars[k]; + } + } + continue; + } + + if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; } + + char entryName[MaxNameLen]; + if (hasLfn) { + Utf16ToAscii(lfnBuf, MaxNameLen, entryName); + } else { + ParseShortName(e, entryName); + } + hasLfn = false; + + if (StrEqualNoCase(entryName, name)) { + uint16_t clHi, clLo; + memcpy(&clHi, e + 20, 2); + memcpy(&clLo, e + 26, 2); + out->firstCluster = ((uint32_t)clHi << 16) | (uint32_t)clLo; + memcpy(&out->fileSize, e + 28, 4); + out->attributes = attr; + int j = 0; + while (entryName[j] && j < MaxNameLen - 1) { + out->name[j] = entryName[j]; j++; + } + out->name[j] = '\0'; + // Record SFN entry disk location + uint32_t byteOff = i * 32; + uint64_t clusterPartSec = ClusterToPartSector(self, cluster); + out->sfnPartSector = clusterPartSec + (byteOff / self.bytesPerSector); + out->sfnOffInSector = byteOff % self.bytesPerSector; + return true; + } + } + + cluster = GetNextCluster(self, cluster); + } + + return false; + } + + // Traverse a full path from root. Returns true and fills 'out'. + // For "/" or "" returns the root directory pseudo-entry. + static bool TraversePath(int inst, const char* path, ParsedEntry* out) { + auto& self = g_instances[inst]; + + // Skip leading '/' + while (*path == '/') path++; + + // Empty path = root directory + if (*path == '\0') { + out->firstCluster = self.rootCluster; + out->fileSize = 0; + out->attributes = ATTR_DIRECTORY; + out->name[0] = '/'; + out->name[1] = '\0'; + return true; + } + + uint32_t currentCluster = self.rootCluster; + + while (*path) { + // Extract next path component + char component[MaxNameLen]; + int len = 0; + while (*path && *path != '/' && len < MaxNameLen - 1) { + component[len++] = *path++; + } + component[len] = '\0'; + + // Skip trailing slashes + while (*path == '/') path++; + + ParsedEntry found; + if (!FindInDirectory(inst, currentCluster, component, &found)) { + return false; + } + + if (*path == '\0') { + // This is the final component + *out = found; + return true; + } + + // Must be a directory to continue traversal + if (!(found.attributes & ATTR_DIRECTORY)) return false; + currentCluster = found.firstCluster; + } + + return false; + } + + // ========================================================================= + // FsDriver implementation functions + // ========================================================================= + + static int OpenImpl(int inst, const char* path) { + if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + + ParsedEntry entry; + if (!TraversePath(inst, path, &entry)) return -1; + + // Find a free file handle + auto& self = g_instances[inst]; + for (int i = 0; i < MaxFilesPerInstance; i++) { + if (!self.files[i].inUse) { + self.files[i].inUse = true; + self.files[i].firstCluster = entry.firstCluster; + self.files[i].fileSize = entry.fileSize; + self.files[i].isDirectory = (entry.attributes & ATTR_DIRECTORY) != 0; + self.files[i].sfnPartSector = entry.sfnPartSector; + self.files[i].sfnOffInSector = entry.sfnOffInSector; + return i; + } + } + + return -1; // no free handle + } + + static int ReadImpl(int inst, int handle, uint8_t* buffer, + uint64_t offset, uint64_t size) { + if (inst < 0 || inst >= g_instanceCount) return -1; + auto& self = g_instances[inst]; + if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; + + auto& file = self.files[handle]; + + // Directories don't have a meaningful fileSize for reading + if (file.isDirectory) return -1; + + if (offset >= file.fileSize) return 0; + if (offset + size > file.fileSize) size = file.fileSize - offset; + if (size == 0) return 0; + + uint32_t clusterSize = self.clusterSize; + 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++) { + cluster = GetNextCluster(self, cluster); + if (IsEndOfChain(cluster)) return 0; + } + + // Read data cluster by cluster + uint64_t bytesRead = 0; + while (bytesRead < size && !IsEndOfChain(cluster)) { + if (!ReadCluster(self, cluster)) break; + + uint32_t available = clusterSize - clusterOff; + uint64_t toRead = size - bytesRead; + if (toRead > available) toRead = available; + + memcpy(buffer + bytesRead, self.clusterBuf + clusterOff, toRead); + bytesRead += toRead; + clusterOff = 0; // subsequent clusters start from offset 0 + + cluster = GetNextCluster(self, cluster); + } + + return (int)bytesRead; + } + + static uint64_t GetSizeImpl(int inst, int handle) { + if (inst < 0 || inst >= g_instanceCount) return 0; + auto& self = g_instances[inst]; + if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return 0; + return self.files[handle].fileSize; + } + + static void CloseImpl(int inst, int handle) { + if (inst < 0 || inst >= g_instanceCount) return; + auto& self = g_instances[inst]; + if (handle < 0 || handle >= MaxFilesPerInstance) return; + self.files[handle].inUse = false; + } + + static int ReadDirImpl(int inst, const char* path, + const char** outNames, int maxEntries) { + if (inst < 0 || inst >= g_instanceCount) return -1; + auto& self = g_instances[inst]; + + ParsedEntry dirEntry; + if (!TraversePath(inst, path, &dirEntry)) return -1; + if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1; + + ParsedEntry entries[MaxDirEntries]; + int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries; + int count = ReadDirectory(inst, dirEntry.firstCluster, entries, limit); + + // Copy names into persistent cache + self.dirNameCount = count; + for (int i = 0; i < count; i++) { + int j = 0; + while (entries[i].name[j] && j < MaxNameLen - 1) { + self.dirNames[i][j] = entries[i].name[j]; + j++; + } + self.dirNames[i][j] = '\0'; + outNames[i] = self.dirNames[i]; + } + + return count; + } + + static int WriteImpl(int inst, int handle, const uint8_t* buffer, + uint64_t offset, uint64_t size) { + if (inst < 0 || inst >= g_instanceCount) return -1; + auto& self = g_instances[inst]; + if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; + + auto& file = self.files[handle]; + if (file.isDirectory) return -1; + if (size == 0) return 0; + + uint32_t clusterSize = self.clusterSize; + + // If file has no clusters yet, allocate the first one + if (file.firstCluster < 2) { + uint32_t cl = AllocateCluster(self); + if (cl == 0) return -1; + file.firstCluster = cl; + // Zero the new cluster + memset(self.clusterBuf, 0, clusterSize); + WriteClusterData(self, cl, self.clusterBuf); + UpdateDirEntry(self, file); + } + + uint64_t bytesWritten = 0; + + // Walk cluster chain to the starting cluster for this offset + uint32_t clusterIdx = (uint32_t)(offset / clusterSize); + uint32_t clusterOff = (uint32_t)(offset % clusterSize); + + uint32_t cluster = file.firstCluster; + uint32_t prevCluster = 0; + for (uint32_t i = 0; i < clusterIdx; i++) { + prevCluster = cluster; + uint32_t next = GetNextCluster(self, cluster); + if (IsEndOfChain(next)) { + // Need to extend the chain + next = AllocateCluster(self); + if (next == 0) goto done; + WriteFatEntry(self, cluster, next); + memset(self.clusterBuf, 0, clusterSize); + WriteClusterData(self, next, self.clusterBuf); + } + cluster = next; + } + + // Write data cluster by cluster + while (bytesWritten < size) { + if (IsEndOfChain(cluster) || cluster < 2) { + // Allocate a new cluster and link it + uint32_t newCl = AllocateCluster(self); + if (newCl == 0) goto done; + if (prevCluster != 0) { + WriteFatEntry(self, prevCluster, newCl); + } + memset(self.clusterBuf, 0, clusterSize); + WriteClusterData(self, newCl, self.clusterBuf); + cluster = newCl; + } + + // Read existing cluster data (for partial writes) + if (!ReadCluster(self, cluster)) goto done; + + uint32_t available = clusterSize - clusterOff; + uint64_t toWrite = size - bytesWritten; + if (toWrite > available) toWrite = available; + + memcpy(self.clusterBuf + clusterOff, buffer + bytesWritten, toWrite); + + if (!WriteClusterData(self, cluster, self.clusterBuf)) goto done; + + bytesWritten += toWrite; + clusterOff = 0; + + prevCluster = cluster; + cluster = GetNextCluster(self, cluster); + } + + done: + // Update file size if we wrote past the end + uint64_t endPos = offset + bytesWritten; + if (endPos > file.fileSize) { + file.fileSize = (uint32_t)endPos; + UpdateDirEntrySize(self, file); + } + + return (int)bytesWritten; + } + + static int CreateImpl(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 filename + char parentPath[MaxNameLen]; + char fileName[MaxNameLen]; + SplitPath(path, parentPath, MaxNameLen, fileName, MaxNameLen); + + if (fileName[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 file already exists, truncate it (free its cluster chain, reset size) + ParsedEntry existing; + if (FindInDirectory(inst, parentCluster, fileName, &existing)) { + if (existing.attributes & ATTR_DIRECTORY) return -1; // can't truncate a dir + + // Free the cluster chain + uint32_t cl = existing.firstCluster; + while (!IsEndOfChain(cl) && cl >= 2) { + uint32_t next = GetNextCluster(self, cl); + WriteFatEntry(self, cl, CLUSTER_FREE); + cl = next; + } + + // Update directory entry: zero size, zero first cluster + uint8_t sectorBuf[512]; + if (!ReadPartSectors(self, existing.sfnPartSector, 1, sectorBuf)) return -1; + uint8_t* e = sectorBuf + existing.sfnOffInSector; + uint32_t zero32 = 0; + uint16_t zero16 = 0; + memcpy(e + 20, &zero16, 2); // cluster high + memcpy(e + 26, &zero16, 2); // cluster low + memcpy(e + 28, &zero32, 4); // file size + if (!WritePartSectors(self, existing.sfnPartSector, 1, sectorBuf)) return -1; + + // Open a handle to it + for (int i = 0; i < MaxFilesPerInstance; i++) { + if (!self.files[i].inUse) { + self.files[i].inUse = true; + self.files[i].firstCluster = 0; + self.files[i].fileSize = 0; + self.files[i].isDirectory = false; + self.files[i].sfnPartSector = existing.sfnPartSector; + self.files[i].sfnOffInSector = existing.sfnOffInSector; + return i; + } + } + return -1; + } + + // Generate 8.3 short name + char shortName[11]; + GenerateShortName(fileName, shortName); + MakeShortNameUnique(inst, parentCluster, shortName); + + // Build LFN entries if needed + uint8_t lfnEntries[20 * 32]; // max 20 LFN entries + int lfnCount = 0; + bool needsLfn = NeedsLfn(fileName, shortName); + + if (needsLfn) { + uint8_t checksum = LfnChecksum(shortName); + lfnCount = BuildLfnEntries(fileName, checksum, lfnEntries); + } + + int totalSlots = lfnCount + 1; // LFN entries + SFN entry + + // Find free directory slots + DirSlotPos pos = FindFreeDirSlots(inst, parentCluster, totalSlots); + if (!pos.found) return -1; + + // Write the entries into the directory sector by sector + // Build all entries (LFN + SFN) contiguously + uint8_t allEntries[21 * 32]; // max 21 entries total + if (lfnCount > 0) { + memcpy(allEntries, lfnEntries, lfnCount * 32); + } + + // Build the SFN entry + uint8_t* sfn = allEntries + lfnCount * 32; + memset(sfn, 0, 32); + memcpy(sfn, shortName, 11); + sfn[11] = ATTR_ARCHIVE; + // firstCluster = 0, fileSize = 0 (file starts empty) + + // Write entries to disk, handling sector boundaries + 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) { + // Need to flush this sector and move to next + if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; + if (written < bytesTotal) { + curSector++; + curOff = 0; + if (!ReadPartSectors(self, curSector, 1, sectorBuf)) return -1; + } + } else { + // Last write didn't fill the sector + if (!WritePartSectors(self, curSector, 1, sectorBuf)) return -1; + } + } + + // Compute the SFN entry's disk location + uint64_t sfnSector = pos.partSector + (pos.offsetInSec + lfnCount * 32) / self.bytesPerSector; + uint32_t sfnOff = (pos.offsetInSec + lfnCount * 32) % self.bytesPerSector; + + // Open a handle + for (int i = 0; i < MaxFilesPerInstance; i++) { + if (!self.files[i].inUse) { + self.files[i].inUse = true; + self.files[i].firstCluster = 0; + self.files[i].fileSize = 0; + self.files[i].isDirectory = false; + self.files[i].sfnPartSector = sfnSector; + self.files[i].sfnOffInSector = sfnOff; + return i; + } + } + + return -1; + } + + static int DeleteImpl(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 filename + char parentPath[MaxNameLen]; + char fileName[MaxNameLen]; + SplitPath(path, parentPath, MaxNameLen, fileName, MaxNameLen); + + if (fileName[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; + + // Find the file + ParsedEntry existing; + if (!FindInDirectory(inst, parentCluster, fileName, &existing)) return -1; + if (existing.attributes & ATTR_DIRECTORY) return -1; // don't delete directories + + // Free the cluster chain + uint32_t cl = existing.firstCluster; + while (!IsEndOfChain(cl) && cl >= 2) { + uint32_t next = GetNextCluster(self, cl); + WriteFatEntry(self, cl, CLUSTER_FREE); + cl = next; + } + + // Mark directory entries as deleted (0xE5) + // Walk the directory again to find and mark LFN + SFN entries + uint32_t cluster = parentCluster; + bool inLfnRun = false; + + while (!IsEndOfChain(cluster)) { + if (!ReadCluster(self, cluster)) return -1; + + int perCluster = (int)(self.clusterSize / 32); + bool modified = false; + + for (int i = 0; i < perCluster; i++) { + uint8_t* e = self.clusterBuf + i * 32; + + if (e[0] == 0x00) goto write_back; + if (e[0] == 0xE5) { inLfnRun = false; continue; } + + uint8_t attr = e[11]; + + if (attr == ATTR_LFN) { + uint8_t seq = e[0]; + if (seq & 0x40) { + // Start of a new LFN run — check if it belongs to our file + // by peeking ahead to the SFN entry + inLfnRun = false; // will set to true if we find the match + + // Count how many LFN entries in this run + int lfnCount = seq & 0x1F; + + // Check if the SFN entry after the LFN sequence matches + int sfnIdx = i + lfnCount; + // The SFN might be in this cluster or a later one — only + // handle the simple same-cluster case here. + if (sfnIdx < perCluster) { + uint8_t* sfnE = self.clusterBuf + sfnIdx * 32; + uint64_t clusterPartSec = ClusterToPartSector(self, cluster); + uint32_t byteOff = sfnIdx * 32; + uint64_t sfnSec = clusterPartSec + (byteOff / self.bytesPerSector); + uint32_t sfnOff = byteOff % self.bytesPerSector; + + if (sfnSec == existing.sfnPartSector && + sfnOff == existing.sfnOffInSector) { + inLfnRun = true; + e[0] = 0xE5; + modified = true; + } + } + } else if (inLfnRun) { + e[0] = 0xE5; + modified = true; + } + continue; + } + + if (inLfnRun || (!inLfnRun && attr != ATTR_LFN)) { + // Check if this is the SFN entry we're looking for + uint64_t clusterPartSec = ClusterToPartSector(self, cluster); + uint32_t byteOff = i * 32; + uint64_t entrySec = clusterPartSec + (byteOff / self.bytesPerSector); + uint32_t entryOff = byteOff % self.bytesPerSector; + + if (entrySec == existing.sfnPartSector && + entryOff == existing.sfnOffInSector) { + e[0] = 0xE5; + modified = true; + inLfnRun = false; + goto write_back; + } + } + + inLfnRun = false; + } + + write_back: + if (modified) { + WriteClusterData(self, cluster, self.clusterBuf); + } + + // Check if we already marked the SFN entry + // (the goto write_back above handles early exit) + { + // Re-check if the SFN was in this cluster + uint64_t clusterPartSec = ClusterToPartSector(self, cluster); + uint64_t clusterEndSec = clusterPartSec + self.sectorsPerCluster; + if (existing.sfnPartSector >= clusterPartSec && + existing.sfnPartSector < clusterEndSec) { + return 0; // done + } + } + + cluster = GetNextCluster(self, cluster); + } + + return 0; + } + + // ========================================================================= + // Template thunks — generate unique function pointers per instance + // ========================================================================= + + template struct Thunks { + static int Open(const char* p) { return OpenImpl(N, p); } + static int Read(int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(N, h, b, o, s); } + static uint64_t GetSize(int h) { return GetSizeImpl(N, h); } + static void Close(int h) { CloseImpl(N, h); } + static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); } + 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); } + }; + + template + static Vfs::FsDriver MakeDriver() { + return { + Thunks::Open, + Thunks::Read, + Thunks::GetSize, + Thunks::Close, + Thunks::ReadDir, + Thunks::Write, + Thunks::Create, + Thunks::Delete, + }; + } + + static Vfs::FsDriver g_drivers[] = { + MakeDriver<0>(), MakeDriver<1>(), MakeDriver<2>(), MakeDriver<3>(), + MakeDriver<4>(), MakeDriver<5>(), MakeDriver<6>(), MakeDriver<7>(), + }; + + // ========================================================================= + // BPB validation and mount + // ========================================================================= + + Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) { + if (g_instanceCount >= MaxInstances) return nullptr; + + auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex); + if (!dev) return nullptr; + + // Read the first sector of the partition (BPB / VBR) + uint8_t bpb[512]; + if (!dev->ReadSectors(dev->Ctx, startLba, 1, bpb)) return nullptr; + + // Validate boot signature + if (bpb[510] != 0x55 || bpb[511] != 0xAA) return nullptr; + + // Parse BPB fields + uint16_t bytesPerSector; + memcpy(&bytesPerSector, bpb + 11, 2); + if (bytesPerSector != 512 && bytesPerSector != 1024 && + bytesPerSector != 2048 && bytesPerSector != 4096) return nullptr; + + uint8_t sectorsPerCluster = bpb[13]; + if (sectorsPerCluster == 0 || + (sectorsPerCluster & (sectorsPerCluster - 1)) != 0) return nullptr; // must be power of 2 + + uint16_t reservedSectors; + memcpy(&reservedSectors, bpb + 14, 2); + if (reservedSectors == 0) return nullptr; + + uint8_t numFats = bpb[16]; + if (numFats == 0) return nullptr; + + // FAT32-specific checks + uint16_t rootEntryCount; + memcpy(&rootEntryCount, bpb + 17, 2); + if (rootEntryCount != 0) return nullptr; // must be 0 for FAT32 + + uint16_t fatSize16; + memcpy(&fatSize16, bpb + 22, 2); + if (fatSize16 != 0) return nullptr; // must be 0 for FAT32 + + uint32_t fatSize32; + memcpy(&fatSize32, bpb + 36, 4); + if (fatSize32 == 0) return nullptr; + + uint32_t rootCluster; + memcpy(&rootCluster, bpb + 44, 4); + if (rootCluster < 2) return nullptr; + + uint16_t totalSectors16; + memcpy(&totalSectors16, bpb + 19, 2); + uint32_t totalSectors32; + memcpy(&totalSectors32, bpb + 32, 4); + uint32_t totalSectors = (totalSectors16 != 0) ? totalSectors16 : totalSectors32; + if (totalSectors == 0) return nullptr; + + // Compute geometry + uint32_t dataStartSector = reservedSectors + numFats * fatSize32; + uint32_t dataSectors = totalSectors - dataStartSector; + uint32_t clusterCount = dataSectors / sectorsPerCluster; + + // FAT32 requires >= 65525 clusters + if (clusterCount < 65525) return nullptr; + + // Check FS type string for extra confidence (offset 82, "FAT32 ") + // Not strictly required by spec, but good sanity check + bool hasFat32Str = (bpb[82] == 'F' && bpb[83] == 'A' && bpb[84] == 'T' && + bpb[85] == '3' && bpb[86] == '2'); + + // At least one of: valid cluster count or FS type string + if (!hasFat32Str && clusterCount < 65525) return nullptr; + + // Success — initialize instance + int idx = g_instanceCount; + auto& inst = g_instances[idx]; + + inst.active = true; + inst.blockDevIndex = blockDevIndex; + inst.partStartLba = startLba; + inst.bytesPerSector = bytesPerSector; + inst.sectorsPerCluster = sectorsPerCluster; + inst.reservedSectors = reservedSectors; + inst.numFats = numFats; + inst.fatSize32 = fatSize32; + inst.rootCluster = rootCluster; + inst.totalSectors = totalSectors; + inst.clusterSize = (uint32_t)bytesPerSector * sectorsPerCluster; + inst.dataStartSector = dataStartSector; + inst.clusterCount = clusterCount; + + // Volume label (offset 71, 11 bytes) + memcpy(inst.volumeLabel, bpb + 71, 11); + inst.volumeLabel[11] = '\0'; + // Trim trailing spaces + for (int i = 10; i >= 0; i--) { + if (inst.volumeLabel[i] == ' ') inst.volumeLabel[i] = '\0'; + else break; + } + + // Allocate cluster buffer + inst.clusterBufPages = ((int)inst.clusterSize + 0xFFF) / 0x1000; + if (inst.clusterBufPages == 1) { + inst.clusterBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed(); + } else { + inst.clusterBuf = (uint8_t*)Memory::g_pfa->ReallocConsecutive( + nullptr, inst.clusterBufPages); + } + + // Clear file handles + for (int i = 0; i < MaxFilesPerInstance; i++) { + inst.files[i].inUse = false; + } + + g_instanceCount++; + + KernelLogStream(OK, "FAT32") << "Mounted volume \"" + << inst.volumeLabel << "\" (" << clusterCount << " clusters, " + << (uint64_t)inst.clusterSize << " bytes/cluster)"; + + return &g_drivers[idx]; + } + + void RegisterProbe() { + FsProbe::Register(Mount); + } + + // ========================================================================= + // FAT32 Format + // ========================================================================= + + int Format(int blockDevIndex, uint64_t startLba, uint64_t sectorCount, + const char* volumeLabel) { + auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex); + if (!dev) return -1; + + // FAT32 requires at least 65525 clusters. With 8 sectors/cluster (4K), + // minimum is ~32MB. Reject anything too small. + if (sectorCount < 2048) { + KernelLogStream(ERROR, "FAT32") << "Partition too small to format as FAT32"; + return -1; + } + + // Geometry parameters + uint16_t bytesPerSector = 512; + uint8_t sectorsPerCluster; + + // Choose cluster size based on volume size + uint64_t sizeMB = (sectorCount * bytesPerSector) / (1024 * 1024); + if (sizeMB < 256) sectorsPerCluster = 1; // 512B clusters + else if (sizeMB < 8192) sectorsPerCluster = 8; // 4K clusters + else if (sizeMB < 16384) sectorsPerCluster = 16; // 8K clusters + else if (sizeMB < 32768) sectorsPerCluster = 32; // 16K clusters + else sectorsPerCluster = 64; // 32K clusters + + uint16_t reservedSectors = 32; + uint8_t numFats = 2; + + // Compute FAT size using the standard formula: + // fatSize = ceil((totalSectors - reservedSectors) / (spc * 128 + numFats)) + // where 128 = bytesPerSector / 4 (FAT32 entries per sector) + uint32_t totalSectors = (uint32_t)sectorCount; + uint32_t denom = (uint32_t)sectorsPerCluster * 128 + numFats; + uint32_t numer = totalSectors - reservedSectors; + uint32_t fatSize = (numer + denom - 1) / denom; + + uint32_t dataStart = reservedSectors + numFats * fatSize; + uint32_t dataSectors = totalSectors - dataStart; + uint32_t clusterCount = dataSectors / sectorsPerCluster; + + if (clusterCount < 65525) { + KernelLogStream(ERROR, "FAT32") << "Cluster count " << (uint64_t)clusterCount + << " too low for FAT32 (need >= 65525)"; + return -1; + } + + uint32_t rootCluster = 2; // first data cluster + + // --- Build boot sector (BPB) --- + uint8_t bpb[512]; + memset(bpb, 0, 512); + + // Jump instruction + bpb[0] = 0xEB; bpb[1] = 0x58; bpb[2] = 0x90; + // OEM name + memcpy(bpb + 3, "MNTK ", 8); + + // BPB fields + memcpy(bpb + 11, &bytesPerSector, 2); + bpb[13] = sectorsPerCluster; + memcpy(bpb + 14, &reservedSectors, 2); + bpb[16] = numFats; + // rootEntryCount = 0 (FAT32) + // totalSectors16 = 0 (FAT32) + bpb[21] = 0xF8; // media type: hard disk + // fatSize16 = 0 (FAT32) + + // Sectors per track / heads (for CHS, not critical) + uint16_t spt = 63; memcpy(bpb + 24, &spt, 2); + uint16_t heads = 255; memcpy(bpb + 26, &heads, 2); + + // Hidden sectors (LBA of partition start) + uint32_t hidden = (uint32_t)startLba; + memcpy(bpb + 28, &hidden, 4); + + // Total sectors 32 + memcpy(bpb + 32, &totalSectors, 4); + + // FAT32-specific fields + memcpy(bpb + 36, &fatSize, 4); + // Ext flags = 0, FS version = 0 + memcpy(bpb + 44, &rootCluster, 4); + uint16_t fsInfoSector = 1; memcpy(bpb + 48, &fsInfoSector, 2); + uint16_t backupBootSector = 6; memcpy(bpb + 50, &backupBootSector, 2); + + // Extended boot record + bpb[64] = 0x80; // drive number + bpb[66] = 0x29; // extended boot signature + // Volume serial (from RDTSC) + uint32_t lo, hi; + asm volatile ("rdtsc" : "=a"(lo), "=d"(hi)); + uint32_t serial = lo ^ hi; + memcpy(bpb + 67, &serial, 4); + + // Volume label (11 bytes, space-padded) + char label[11]; + memset(label, ' ', 11); + if (volumeLabel) { + for (int i = 0; i < 11 && volumeLabel[i]; i++) { + char ch = volumeLabel[i]; + if (ch >= 'a' && ch <= 'z') ch -= 32; // uppercase + label[i] = ch; + } + } else { + memcpy(label, "NO NAME ", 11); + } + memcpy(bpb + 71, label, 11); + + // FS type + memcpy(bpb + 82, "FAT32 ", 8); + + // Boot signature + bpb[510] = 0x55; + bpb[511] = 0xAA; + + // --- Write boot sector --- + if (!dev->WriteSectors(dev->Ctx, startLba, 1, bpb)) { + KernelLogStream(ERROR, "FAT32") << "Failed to write boot sector"; + return -1; + } + + // --- Write backup boot sector at sector 6 --- + if (!dev->WriteSectors(dev->Ctx, startLba + backupBootSector, 1, bpb)) { + KernelLogStream(ERROR, "FAT32") << "Failed to write backup boot sector"; + return -1; + } + + // --- Build and write FSInfo sector --- + uint8_t fsinfo[512]; + memset(fsinfo, 0, 512); + uint32_t fsInfoSig1 = 0x41615252; memcpy(fsinfo + 0, &fsInfoSig1, 4); + uint32_t fsInfoSig2 = 0x61417272; memcpy(fsinfo + 484, &fsInfoSig2, 4); + uint32_t freeCount = clusterCount - 1; // minus root dir cluster + memcpy(fsinfo + 488, &freeCount, 4); + uint32_t nextFree = 3; // next free cluster after root + memcpy(fsinfo + 492, &nextFree, 4); + uint32_t fsInfoSig3 = 0xAA550000; memcpy(fsinfo + 508, &fsInfoSig3, 4); + + if (!dev->WriteSectors(dev->Ctx, startLba + 1, 1, fsinfo)) { + KernelLogStream(ERROR, "FAT32") << "Failed to write FSInfo"; + return -1; + } + // Backup FSInfo at sector 7 + if (!dev->WriteSectors(dev->Ctx, startLba + 7, 1, fsinfo)) { + KernelLogStream(ERROR, "FAT32") << "Failed to write backup FSInfo"; + return -1; + } + + // --- Zero out remaining reserved sectors --- + uint8_t zeroBuf[512]; + memset(zeroBuf, 0, 512); + for (uint16_t s = 2; s < reservedSectors; s++) { + if (s == backupBootSector || s == 7) continue; // already written + dev->WriteSectors(dev->Ctx, startLba + s, 1, zeroBuf); + } + + // --- Write FAT tables --- + // First sector of each FAT: media byte + EOC for clusters 0,1 + EOC for root dir (cluster 2) + uint8_t fatFirstSector[512]; + memset(fatFirstSector, 0, 512); + uint32_t fat0 = 0x0FFFFFF8; memcpy(fatFirstSector + 0, &fat0, 4); // cluster 0: media + uint32_t fat1 = 0x0FFFFFFF; memcpy(fatFirstSector + 4, &fat1, 4); // cluster 1: EOC + uint32_t fat2 = 0x0FFFFFFF; memcpy(fatFirstSector + 8, &fat2, 4); // cluster 2: root dir EOC + + for (int f = 0; f < numFats; f++) { + uint64_t fatStart = startLba + reservedSectors + (uint64_t)f * fatSize; + + // Write first sector with media byte + root cluster entry + if (!dev->WriteSectors(dev->Ctx, fatStart, 1, fatFirstSector)) { + KernelLogStream(ERROR, "FAT32") << "Failed to write FAT " << f; + return -1; + } + + // Zero remaining FAT sectors + for (uint32_t s = 1; s < fatSize; s++) { + dev->WriteSectors(dev->Ctx, fatStart + s, 1, zeroBuf); + } + } + + // --- Zero root directory cluster --- + uint64_t rootSector = startLba + dataStart; + for (uint8_t s = 0; s < sectorsPerCluster; s++) { + dev->WriteSectors(dev->Ctx, rootSector + s, 1, zeroBuf); + } + + KernelLogStream(OK, "FAT32") << "Formatted: " << (uint64_t)clusterCount + << " clusters, " << (uint64_t)sectorsPerCluster << " sec/cluster, FAT=" + << (uint64_t)fatSize << " sectors"; + + return 0; + } + +}; diff --git a/kernel/src/Fs/Fat32.hpp b/kernel/src/Fs/Fat32.hpp new file mode 100644 index 0000000..51b592b --- /dev/null +++ b/kernel/src/Fs/Fat32.hpp @@ -0,0 +1,29 @@ +/* + * Fat32.hpp + * FAT32 filesystem driver + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Fs::Fat32 { + + // Try to mount a FAT32 filesystem at the given partition range. + // Returns a FsDriver* on success, nullptr if not a valid FAT32 volume. + Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount); + + // Register Fat32::Mount as a filesystem probe with FsProbe. + void RegisterProbe(); + + // Format a partition as FAT32. + // blockDevIndex: which block device the partition is on + // startLba: first LBA of the partition + // sectorCount: number of sectors in the partition + // volumeLabel: up to 11-char volume label (null-terminated) + // Returns 0 on success, -1 on error. + int Format(int blockDevIndex, uint64_t startLba, uint64_t sectorCount, + const char* volumeLabel); + +}; diff --git a/kernel/src/Fs/FsProbe.cpp b/kernel/src/Fs/FsProbe.cpp new file mode 100644 index 0000000..be8250f --- /dev/null +++ b/kernel/src/Fs/FsProbe.cpp @@ -0,0 +1,70 @@ +/* + * FsProbe.cpp + * Filesystem probe registry + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "FsProbe.hpp" +#include +#include + +namespace Fs::FsProbe { + + static ProbeFn g_probes[MaxProbes] = {}; + static int g_probeCount = 0; + + void Register(ProbeFn fn) { + if (g_probeCount < MaxProbes && fn) { + g_probes[g_probeCount++] = fn; + } + } + + void MountPartitions(int firstDrive) { + int partCount = Drivers::Storage::Gpt::GetPartitionCount(); + if (partCount == 0 || g_probeCount == 0) return; + + int driveNum = firstDrive; + + for (int i = 0; i < partCount && driveNum < Vfs::MaxDrives; i++) { + auto* part = Drivers::Storage::Gpt::GetPartition(i); + if (!part) continue; + + for (int p = 0; p < g_probeCount; p++) { + Vfs::FsDriver* driver = g_probes[p]( + part->BlockDevIndex, part->StartLba, part->SectorCount); + + if (driver) { + Vfs::RegisterDrive(driveNum, driver); + Kt::KernelLogStream(Kt::OK, "FsProbe") << "Mounted partition " + << i << " as drive " << driveNum; + driveNum++; + break; + } + } + } + } + + int MountPartition(int partIndex, int driveNum) { + if (driveNum < 0 || driveNum >= Vfs::MaxDrives) return -1; + if (g_probeCount == 0) return -1; + + auto* part = Drivers::Storage::Gpt::GetPartition(partIndex); + if (!part) return -1; + + for (int p = 0; p < g_probeCount; p++) { + Vfs::FsDriver* driver = g_probes[p]( + part->BlockDevIndex, part->StartLba, part->SectorCount); + + if (driver) { + Vfs::RegisterDrive(driveNum, driver); + Kt::KernelLogStream(Kt::OK, "FsProbe") << "Mounted partition " + << partIndex << " as drive " << driveNum; + return 0; + } + } + + Kt::KernelLogStream(Kt::WARNING, "FsProbe") << "No filesystem recognized on partition " << partIndex; + return -1; + } + +}; diff --git a/kernel/src/Fs/FsProbe.hpp b/kernel/src/Fs/FsProbe.hpp new file mode 100644 index 0000000..9fc70d5 --- /dev/null +++ b/kernel/src/Fs/FsProbe.hpp @@ -0,0 +1,29 @@ +/* + * FsProbe.hpp + * Filesystem probe registry + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Fs::FsProbe { + + static constexpr int MaxProbes = 8; + + // A probe function attempts to mount a filesystem on the given partition. + // Returns a FsDriver* on success, nullptr if the filesystem is not recognized. + using ProbeFn = Vfs::FsDriver* (*)(int blockDevIndex, uint64_t startLba, uint64_t sectorCount); + + // Register a filesystem probe function (called once per FS driver at init). + void Register(ProbeFn fn); + + // Probe all discovered GPT partitions and auto-mount recognized filesystems. + // Assigns VFS drive numbers starting from firstDrive. + void MountPartitions(int firstDrive = 1); + + // Try to mount a single partition (by global partition index) as the given VFS drive. + // Returns 0 on success, -1 if no probe recognized the filesystem. + int MountPartition(int partIndex, int driveNum); + +}; diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index 1f19e85..1c74161 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -163,6 +163,19 @@ namespace Fs::Vfs { return globalHandle; } + int VfsDelete(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]->Delete == nullptr) return -1; + + return driveTable[drive]->Delete(localPath); + } + int VfsReadDir(const char* path, const char** outNames, int maxEntries) { int drive; const char* localPath; diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index 1c60f80..e83cc02 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -21,6 +21,7 @@ namespace Fs::Vfs { int (*ReadDir)(const char* path, const char** outNames, int maxEntries); int (*Write)(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); int (*Create)(const char* path); + int (*Delete)(const char* path); }; void Initialize(); @@ -30,6 +31,7 @@ namespace Fs::Vfs { int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); int VfsCreate(const char* path); + int VfsDelete(const char* path); uint64_t VfsGetSize(int handle); void VfsClose(int handle); int VfsReadDir(const char* path, const char** outNames, int maxEntries); diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index c7a677e..f4edaee 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -33,6 +33,8 @@ #include #include #include +#include +#include #include #include using namespace Kt; @@ -191,10 +193,15 @@ extern "C" void kmain() { Fs::Ramdisk::Close, Fs::Ramdisk::ReadDir, Fs::Ramdisk::Write, - Fs::Ramdisk::Create + Fs::Ramdisk::Create, + nullptr }; Fs::Vfs::RegisterDrive(0, &ramdiskDriver); + // Register filesystem probes and auto-mount partitions + Fs::Fat32::RegisterProbe(); + Fs::FsProbe::MountPartitions(); + Hal::LoadTSS(); Montauk::InitializeSyscalls(); diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 420134e..ed75019 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer desktop SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, games are handled separately. @@ -84,9 +84,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 desktop icons fonts bearssl libc tls libjpeg +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer desktop icons fonts bearssl libc tls libjpeg -all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer 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 doom desktop icons fonts $(GAME_DATA) $(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) @@ -143,6 +143,14 @@ spreadsheet: libc pdfviewer: libc $(MAKE) -C src/pdfviewer +# Build disks standalone GUI tool (depends on libc). +disks: libc + $(MAKE) -C src/disks + +# Build devexplorer standalone GUI tool (depends on libc). +devexplorer: libc + $(MAKE) -C src/devexplorer + # Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper). desktop: libc libjpeg $(MAKE) -C src/desktop @@ -205,3 +213,5 @@ clean: $(MAKE) -C src/fontpreview clean $(MAKE) -C src/spreadsheet clean $(MAKE) -C src/pdfviewer clean + $(MAKE) -C src/disks clean + $(MAKE) -C src/devexplorer clean diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 62539aa..16b6d55 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -54,6 +54,7 @@ namespace Montauk { static constexpr uint64_t SYS_RECVFROM = 40; 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_TERMSCALE = 43; static constexpr uint64_t SYS_RESOLVE = 44; static constexpr uint64_t SYS_GETRANDOM = 45; @@ -88,6 +89,15 @@ namespace Montauk { // Kernel introspection syscalls static constexpr uint64_t SYS_MEMSTATS = 67; + // Storage / partition syscalls + static constexpr uint64_t SYS_PARTLIST = 70; + static constexpr uint64_t SYS_DISKREAD = 71; + static constexpr uint64_t SYS_DISKWRITE = 72; + static constexpr uint64_t SYS_GPTINIT = 73; + static constexpr uint64_t SYS_GPTADD = 74; + static constexpr uint64_t SYS_FSMOUNT = 75; + static constexpr uint64_t SYS_FSFORMAT = 76; + static constexpr int SOCK_TCP = 1; static constexpr int SOCK_UDP = 2; @@ -198,6 +208,44 @@ namespace Montauk { char _pad2[1]; }; + struct PartGuid { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; + }; + + struct PartInfo { + int32_t blockDev; // block device index + uint32_t _pad0; + uint64_t startLba; + uint64_t endLba; + uint64_t sectorCount; + PartGuid typeGuid; + PartGuid uniqueGuid; + uint64_t attributes; + char name[72]; // ASCII partition name + char typeName[24]; // human-readable type name + }; + + struct GptAddParams { + int32_t blockDev; + uint32_t _pad0; + uint64_t startLba; // 0 = auto (fill largest free region) + uint64_t endLba; // 0 = auto + PartGuid typeGuid; + char name[72]; + }; + + // Filesystem type IDs for SYS_FSFORMAT + static constexpr int FS_TYPE_FAT32 = 1; + + struct FsFormatParams { + int32_t partIndex; // global partition index + int32_t fsType; // FS_TYPE_FAT32, etc. + char label[32]; // volume label + }; + struct ProcInfo { int32_t pid; int32_t parentPid; diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index b742bef..73ae850 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -76,6 +76,9 @@ struct DesktopState { SvgIcon icon_folder_lg; SvgIcon icon_file_lg; SvgIcon icon_exec_lg; + SvgIcon icon_drive; + SvgIcon icon_drive_lg; + SvgIcon icon_delete; SvgIcon icon_settings; SvgIcon icon_reboot; @@ -88,6 +91,7 @@ struct DesktopState { SvgIcon icon_mandelbrot; SvgIcon icon_devexplorer; SvgIcon icon_spreadsheet; + SvgIcon icon_disks; bool ctx_menu_open; int ctx_menu_x, ctx_menu_y; diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp index a52176a..5b821cf 100644 --- a/programs/include/gui/svg.hpp +++ b/programs/include/gui/svg.hpp @@ -161,6 +161,23 @@ inline int svg_parse_fixed(const char* s, fixed_t* out) { val += (int32_t)(((int64_t)frac << 16) / frac_div); } + // Scientific notation (e.g. 2e-3, 7e-3) + if (*p == 'e' || *p == 'E') { + ++p; + bool exp_neg = false; + if (*p == '-') { exp_neg = true; ++p; } + else if (*p == '+') { ++p; } + int exp_val = 0; + while (*p >= '0' && *p <= '9') { + exp_val = exp_val * 10 + (*p - '0'); + ++p; + } + for (int i = 0; i < exp_val; i++) { + if (exp_neg) val /= 10; + else val *= 10; + } + } + if (neg) val = -val; *out = val; return (int)(p - s); @@ -1207,6 +1224,12 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Heap-allocated path data buffer (avoids 8 KiB on the stack) char* d_buf = (char*)montauk::malloc(SVG_MAX_PATH_LEN); + // Group fill inheritance stack: track so child elements inherit + static constexpr int MAX_GROUP_DEPTH = 8; + struct GroupFill { Color color; bool has_fill; }; + GroupFill g_stack[MAX_GROUP_DEPTH]; + int g_depth = 0; + // Scan for blocks (they define reusable items, not rendered directly) const char* p = svg_data; @@ -1230,6 +1253,42 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t continue; } + // Track closing tags to pop the group fill stack + if (remaining > 3 && svg_strncmp(p, "", 4)) { + if (g_depth > 0) g_depth--; + p += 4; + continue; + } + + // Track opening tags for fill inheritance + if (remaining > 2 && svg_strncmp(p, "')) { + const char* g_start = p; + const char* g_end = p; + while (g_end < end && *g_end != '>') ++g_end; + if (g_end < end) ++g_end; + int g_len = (int)(g_end - g_start); + + if (g_depth < MAX_GROUP_DEPTH) { + g_stack[g_depth].has_fill = false; + Color gc = fill_color; + char fbuf[64]; + int fLen = svg_get_attr(g_start, g_len, " fill", fbuf, sizeof(fbuf)); + if (fLen > 0) { + int fr = svg_resolve_fill_value(fbuf, &gc, &grads); + if (fr == 1) { + g_stack[g_depth].color = gc; + g_stack[g_depth].has_fill = true; + } else if (fr == -1) { + // fill="none" on group — children inherit none + g_stack[g_depth].has_fill = false; + } + } + g_depth++; + } + p = g_end; + continue; + } + // Check for 5 && svg_strncmp(p, " Color elem_color = fill_color; + // Apply inherited group fill as default + for (int gi = g_depth - 1; gi >= 0; gi--) { + if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } // fill="none" @@ -1278,6 +1341,9 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; + for (int gi = g_depth - 1; gi >= 0; gi--) { + if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } @@ -1321,6 +1387,9 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; + for (int gi = g_depth - 1; gi >= 0; gi--) { + if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; } + } int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 97b19ac..b7d2f4a 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -143,6 +143,9 @@ namespace montauk { inline int fcreate(const char* path) { return (int)syscall1(Montauk::SYS_FCREATE, (uint64_t)path); } + inline int fdelete(const char* path) { + return (int)syscall1(Montauk::SYS_FDELETE, (uint64_t)path); + } // Memory inline void* alloc(uint64_t size) { return (void*)syscall1(Montauk::SYS_ALLOC, size); } @@ -298,6 +301,35 @@ namespace montauk { return (int)syscall2(Montauk::SYS_DISKINFO, (uint64_t)buf, (uint64_t)port); } + // Partition table + inline int partlist(Montauk::PartInfo* buf, int max) { + return (int)syscall2(Montauk::SYS_PARTLIST, (uint64_t)buf, (uint64_t)max); + } + + // Raw block device I/O (driver-agnostic) + inline int64_t disk_read(int blockDev, uint64_t lba, uint32_t sectorCount, void* buf) { + return syscall4(Montauk::SYS_DISKREAD, (uint64_t)blockDev, lba, + (uint64_t)sectorCount, (uint64_t)buf); + } + inline int64_t disk_write(int blockDev, uint64_t lba, uint32_t sectorCount, const void* buf) { + return syscall4(Montauk::SYS_DISKWRITE, (uint64_t)blockDev, lba, + (uint64_t)sectorCount, (uint64_t)buf); + } + + // GPT management + inline int gpt_init(int blockDev) { + return (int)syscall1(Montauk::SYS_GPTINIT, (uint64_t)blockDev); + } + inline int gpt_add(const Montauk::GptAddParams* params) { + return (int)syscall1(Montauk::SYS_GPTADD, (uint64_t)params); + } + inline int fs_mount(int partIndex, int driveNum) { + return (int)syscall2(Montauk::SYS_FSMOUNT, (uint64_t)partIndex, (uint64_t)driveNum); + } + inline int fs_format(const Montauk::FsFormatParams* params) { + return (int)syscall1(Montauk::SYS_FSFORMAT, (uint64_t)params); + } + // Kernel introspection inline void memstats(Montauk::MemStats* out) { syscall1(Montauk::SYS_MEMSTATS, (uint64_t)out); } diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 4c261d3..044c053 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -46,6 +46,7 @@ CXXFLAGS := \ -msse2 \ -mno-red-zone \ -mcmodel=small \ + -MMD -MP \ -I $(PROG_INC) \ -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include @@ -94,5 +95,7 @@ $(OBJDIR)/%.o: apps/%.cpp Makefile mkdir -p $(OBJDIR) $(CXX) $(CXXFLAGS) -c $< -o $@ +-include $(OBJS:.o=.d) + clean: rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/desktop/apps/app_devexplorer.cpp b/programs/src/desktop/apps/app_devexplorer.cpp index d6b8f56..7b4d9ce 100644 --- a/programs/src/desktop/apps/app_devexplorer.cpp +++ b/programs/src/desktop/apps/app_devexplorer.cpp @@ -1,807 +1,12 @@ /* * app_devexplorer.cpp - * MontaukOS Desktop - Device Explorer + * MontaukOS Desktop - Device Explorer launcher (spawns standalone devexplorer.elf) * Copyright (c) 2026 Daniel Hammer */ #include "apps_common.hpp" -// ============================================================================ -// Device Explorer state -// ============================================================================ - -static constexpr int DE_TOOLBAR_H = 36; -static constexpr int DE_CAT_H = 28; -static constexpr int DE_ITEM_H = 24; -static constexpr int DE_MAX_DEVS = 64; -static constexpr int DE_POLL_MS = 2000; -static constexpr int DE_INDENT = 28; - -// Category names matching DevInfo.category values -static const char* category_names[] = { - "CPU", // 0 - "Interrupt", // 1 - "Timer", // 2 - "Input", // 3 - "USB", // 4 - "Network", // 5 - "Display", // 6 - "Storage", // 7 - "PCI", // 8 -}; -static constexpr int NUM_CATEGORIES = 9; - -static Color category_colors[] = { - Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue - Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple - Color::from_rgb(0x22, 0x88, 0x22), // Timer - green - Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber - Color::from_rgb(0x00, 0x88, 0x88), // USB - teal - Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange - Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo - Color::from_rgb(0x99, 0x55, 0x00), // Storage - brown - Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray -}; - -struct DevExplorerState { - DesktopState* desktop; - Montauk::DevInfo devs[DE_MAX_DEVS]; - int dev_count; - bool collapsed[NUM_CATEGORIES]; // per-category collapse state - int selected_row; // index into visible display rows (-1 = none) - int scroll_y; // scroll offset in display rows - uint64_t last_poll_ms; - - // Double-click tracking - int last_click_row; - uint64_t last_click_ms; -}; - -// ============================================================================ -// Display row model -// ============================================================================ - -enum RowType { ROW_CATEGORY, ROW_DEVICE }; - -struct DisplayRow { - RowType type; - int category; // category index (0..7) - int dev_index; // index into devs[] (only valid for ROW_DEVICE) -}; - -static constexpr int MAX_DISPLAY_ROWS = DE_MAX_DEVS + NUM_CATEGORIES; - -static int build_display_rows(DevExplorerState* de, DisplayRow* rows) { - int count = 0; - for (int cat = 0; cat < NUM_CATEGORIES; cat++) { - // Count devices in this category - int cat_count = 0; - for (int d = 0; d < de->dev_count; d++) { - if (de->devs[d].category == cat) cat_count++; - } - if (cat_count == 0) continue; - - // Emit category header - rows[count].type = ROW_CATEGORY; - rows[count].category = cat; - rows[count].dev_index = -1; - count++; - - // Emit device rows if expanded - if (!de->collapsed[cat]) { - for (int d = 0; d < de->dev_count; d++) { - if (de->devs[d].category == cat) { - rows[count].type = ROW_DEVICE; - rows[count].category = cat; - rows[count].dev_index = d; - count++; - } - } - } - } - return count; -} - -// ============================================================================ -// Triangle drawing helpers -// ============================================================================ - -static void draw_triangle_right(Canvas& c, int x, int y, int size, Color col) { - // Right-pointing filled triangle (▶) - int half = size / 2; - for (int row = 0; row < size; row++) { - int dist = (row <= half) ? row : (size - 1 - row); - for (int col_px = 0; col_px <= dist; col_px++) { - c.put_pixel(x + col_px, y + row, col); - } - } -} - -static void draw_triangle_down(Canvas& c, int x, int y, int size, Color col) { - // Down-pointing filled triangle (▼) - int half = size / 2; - for (int row = 0; row < half + 1; row++) { - int w = size - row * 2; - for (int col_px = 0; col_px < w; col_px++) { - c.put_pixel(x + row + col_px, y + row, col); - } - } -} - -// ============================================================================ -// Callbacks -// ============================================================================ - -static void devexplorer_on_poll(Window* win) { - DevExplorerState* de = (DevExplorerState*)win->app_data; - if (!de) return; - - uint64_t now = montauk::get_milliseconds(); - if (now - de->last_poll_ms < DE_POLL_MS) return; - de->last_poll_ms = now; - - de->dev_count = montauk::devlist(de->devs, DE_MAX_DEVS); -} - -static void devexplorer_on_draw(Window* win, Framebuffer& fb) { - DevExplorerState* de = (DevExplorerState*)win->app_data; - if (!de) return; - - Canvas c(win); - c.fill(colors::WINDOW_BG); - - int fh = system_font_height(); - - // --- Toolbar --- - c.fill_rect(0, 0, c.w, DE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); - c.hline(0, DE_TOOLBAR_H - 1, c.w, colors::BORDER); - - // "Refresh" button - int btn_w = 80; - int btn_h = 26; - int btn_x = 8; - int btn_y = (DE_TOOLBAR_H - btn_h) / 2; - c.button(btn_x, btn_y, btn_w, btn_h, "Refresh", - Color::from_rgb(0x33, 0x66, 0xCC), colors::WHITE, 4); - - // Device count on right - char count_str[24]; - snprintf(count_str, sizeof(count_str), "%d devices", de->dev_count); - int cw = text_width(count_str); - c.text(c.w - cw - 12, (DE_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR); - - // --- Build display rows --- - DisplayRow rows[MAX_DISPLAY_ROWS]; - int row_count = build_display_rows(de, rows); - - // --- Compute visible area --- - int list_y = DE_TOOLBAR_H; - int list_h = c.h - list_y; - if (list_h < 1) return; - - // Clamp scroll - // Calculate total content height - int total_h = 0; - for (int i = 0; i < row_count; i++) { - total_h += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - } - int max_scroll_px = total_h - list_h; - if (max_scroll_px < 0) max_scroll_px = 0; - - // Convert scroll_y (in rows) to pixel offset for simplicity, - // but keep the row-based model: scroll_y = row offset - // We'll compute cumulative pixel offsets - int scroll_px = 0; - for (int i = 0; i < de->scroll_y && i < row_count; i++) { - scroll_px += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - } - if (scroll_px > max_scroll_px) { - scroll_px = max_scroll_px; - // Recalculate scroll_y to match - de->scroll_y = 0; - int acc = 0; - for (int i = 0; i < row_count; i++) { - int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - if (acc + rh > max_scroll_px) break; - acc += rh; - de->scroll_y = i + 1; - } - } - if (de->scroll_y < 0) de->scroll_y = 0; - - // Clamp selected_row - if (de->selected_row >= row_count) de->selected_row = row_count - 1; - - // --- Draw rows --- - int draw_y = list_y - scroll_px; - // Advance to first visible area by recomputing from scroll offset - draw_y = list_y; - int first_visible = de->scroll_y; - - // Compute starting y by accumulating heights of skipped rows - // Actually, let's just iterate all rows and skip those above the viewport - draw_y = list_y; - for (int i = 0; i < first_visible && i < row_count; i++) { - draw_y -= (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - } - - // Draw from first_visible onward - int cur_y = list_y; - for (int i = first_visible; i < row_count; i++) { - int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - if (cur_y >= c.h) break; // Past bottom of window - - if (rows[i].type == ROW_CATEGORY) { - // Category header - int cat = rows[i].category; - c.fill_rect(0, cur_y, c.w, DE_CAT_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); - - // Highlight if selected - if (i == de->selected_row) { - c.fill_rect(0, cur_y, c.w, DE_CAT_H, colors::MENU_HOVER); - } - - // Expand/collapse triangle - int tri_x = 10; - int tri_y = cur_y + (DE_CAT_H - 8) / 2; - Color tri_color = Color::from_rgb(0x55, 0x55, 0x55); - if (de->collapsed[cat]) { - draw_triangle_right(c, tri_x, tri_y, 8, tri_color); - } else { - draw_triangle_down(c, tri_x, tri_y, 8, tri_color); - } - - // Colored dot - int dot_x = 24; - int dot_y = cur_y + (DE_CAT_H - 8) / 2; - Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES) - ? category_colors[cat] : colors::TEXT_COLOR; - c.fill_rounded_rect(dot_x, dot_y, 8, 8, 4, cat_col); - - // Category name - const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES) - ? category_names[cat] : "?"; - int text_y = cur_y + (DE_CAT_H - fh) / 2; - c.text(36, text_y, cat_name, Color::from_rgb(0x33, 0x33, 0x33)); - - // Device count in parentheses on right - int cat_count = 0; - for (int d = 0; d < de->dev_count; d++) { - if (de->devs[d].category == cat) cat_count++; - } - char cnt_buf[16]; - snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count); - int name_w = text_width(cat_name); - c.text(36 + name_w + 8, text_y, cnt_buf, - Color::from_rgb(0x88, 0x88, 0x88)); - - // Bottom border - c.hline(0, cur_y + DE_CAT_H - 1, c.w, Color::from_rgb(0xE0, 0xE0, 0xE0)); - } else { - // Device item row - int di = rows[i].dev_index; - - // Selected row highlight - if (i == de->selected_row) { - c.fill_rect(0, cur_y, c.w, DE_ITEM_H, colors::MENU_HOVER); - } - - int text_y = cur_y + (DE_ITEM_H - fh) / 2; - - // Device name (indented) - c.text(DE_INDENT + 10, text_y, de->devs[di].name, colors::TEXT_COLOR); - - // Detail string (right column, gray) - int detail_x = c.w / 2 + 20; - c.text(detail_x, text_y, de->devs[di].detail, - Color::from_rgb(0x66, 0x66, 0x66)); - } - - cur_y += row_h; - } - - // --- Scrollbar --- - if (total_h > list_h) { - int sb_x = c.w - 6; - int thumb_h = (list_h * list_h) / total_h; - if (thumb_h < 20) thumb_h = 20; - int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px; - c.fill_rect(sb_x, list_y, 4, list_h, Color::from_rgb(0xE0, 0xE0, 0xE0)); - c.fill_rect(sb_x, thumb_y, 4, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA)); - } -} - -// ============================================================================ -// Disk Detail Window -// ============================================================================ - -static constexpr int DD_TAB_BAR_H = 32; -static constexpr int DD_TAB_COUNT = 2; -static const char* dd_tab_labels[DD_TAB_COUNT] = { "General", "Features" }; - -struct DiskDetailState { - DesktopState* desktop; - Montauk::DiskInfo info; - int active_tab; -}; - -// Helper: format uint64 to string (snprintf %d can't handle 64-bit) -static void fmt_u64(char* buf, int bufsize, uint64_t v) { - if (v == 0) { buf[0] = '0'; buf[1] = '\0'; return; } - char tmp[24]; int i = 0; - while (v > 0) { tmp[i++] = '0' + (int)(v % 10); v /= 10; } - int j = 0; - while (i > 0 && j < bufsize - 1) buf[j++] = tmp[--i]; - buf[j] = '\0'; -} - -// Draw a two-column table row -static void dd_table_row(Canvas& c, int x, int y, int col1_w, int row_h, - const char* key, const char* value, Color key_col, Color val_col) { - int fh = system_font_height(); - int ty = y + (row_h - fh) / 2; - c.text(x + 8, ty, key, key_col); - c.text(x + col1_w + 8, ty, value, val_col); -} - -// Draw a feature row with colored status indicator -static void dd_feature_row(Canvas& c, int x, int y, int w, int row_h, - const char* label, bool supported, const char* extra) { - int fh = system_font_height(); - int ty = y + (row_h - fh) / 2; - - // Status dot - int dot_y = y + (row_h - 8) / 2; - Color dot_col = supported ? Color::from_rgb(0x22, 0x88, 0x22) - : Color::from_rgb(0xBB, 0xBB, 0xBB); - c.fill_rounded_rect(x + 10, dot_y, 8, 8, 4, dot_col); - - // Label - Color text_col = supported ? colors::TEXT_COLOR : Color::from_rgb(0xAA, 0xAA, 0xAA); - c.text(x + 26, ty, label, text_col); - - // Extra info on right - if (extra && extra[0]) { - int ew = text_width(extra); - c.text(x + w - ew - 12, ty, extra, Color::from_rgb(0x66, 0x66, 0x66)); - } -} - -static void diskdetail_draw_general(Canvas& c, DiskDetailState* dd) { - char line[128]; - int fh = system_font_height(); - int x = 0; - int y = 12; - int w = c.w; - int col1_w = 110; - int row_h = fh + 8; - Color key_col = Color::from_rgb(0x66, 0x66, 0x66); - Color val_col = colors::TEXT_COLOR; - Color hdr_col = colors::ACCENT; - Color border = Color::from_rgb(0xE0, 0xE0, 0xE0); - - // --- Identification section --- - c.text(x + 8, y, "Identification", hdr_col); - y += fh + 8; - c.hline(x + 8, y, w - 16, border); - y += 1; - - // Table rows with alternating backgrounds - auto row_bg = [&](int ry) { - static int rowIdx = 0; - if (rowIdx++ % 2 == 0) - c.fill_rect(x + 8, ry, w - 16, row_h, Color::from_rgb(0xF7, 0xF7, 0xF7)); - }; - - int rowIdx = 0; - auto table_row = [&](const char* key, const char* val) { - if (rowIdx++ % 2 == 0) - c.fill_rect(x + 8, y, w - 16, row_h, Color::from_rgb(0xF7, 0xF7, 0xF7)); - dd_table_row(c, x, y, col1_w, row_h, key, val, key_col, val_col); - y += row_h; - }; - - table_row("Model", dd->info.model); - table_row("Serial", dd->info.serial); - table_row("Firmware", dd->info.firmware); - - const char* typeStr = "Unknown"; - if (dd->info.type == 1) typeStr = "SATA"; - else if (dd->info.type == 2) typeStr = "SATAPI"; - table_row("Type", typeStr); - - snprintf(line, sizeof(line), "%d", (int)dd->info.port); - table_row("AHCI Port", line); - - c.hline(x + 8, y, w - 16, border); - y += 12; - - // --- Capacity section --- - c.text(x + 8, y, "Capacity", hdr_col); - y += fh + 8; - c.hline(x + 8, y, w - 16, border); - y += 1; - - rowIdx = 0; - uint64_t totalBytes = dd->info.sectorCount * (uint64_t)dd->info.sectorSizeLog; - uint64_t totalMB = totalBytes / (1024 * 1024); - uint64_t totalGB = totalMB / 1024; - if (totalGB > 0) { - int fracGB = (int)((totalMB % 1024) * 10 / 1024); - snprintf(line, sizeof(line), "%d.%d GiB", (int)totalGB, fracGB); - } else { - snprintf(line, sizeof(line), "%d MiB", (int)totalMB); - } - table_row("Size", line); - - char secbuf[24]; - fmt_u64(secbuf, sizeof(secbuf), dd->info.sectorCount); - table_row("Sectors", secbuf); - - snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizeLog); - table_row("Logical", line); - - snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizePhys); - table_row("Physical", line); - - c.hline(x + 8, y, w - 16, border); - y += 12; - - // --- Interface section --- - c.text(x + 8, y, "Interface", hdr_col); - y += fh + 8; - c.hline(x + 8, y, w - 16, border); - y += 1; - - rowIdx = 0; - const char* sataSpeed = "Unknown"; - if (dd->info.sataGen == 1) sataSpeed = "SATA I (1.5 Gb/s)"; - else if (dd->info.sataGen == 2) sataSpeed = "SATA II (3.0 Gb/s)"; - else if (dd->info.sataGen == 3) sataSpeed = "SATA III (6.0 Gb/s)"; - table_row("Link Speed", sataSpeed); - - if (dd->info.rpm == 0) - table_row("Media", "Not reported"); - else if (dd->info.rpm == 1) - table_row("Media", "Solid State (SSD)"); - else { - snprintf(line, sizeof(line), "%d RPM", (int)dd->info.rpm); - table_row("Media", line); - } - - c.hline(x + 8, y, w - 16, border); -} - -static void diskdetail_draw_features(Canvas& c, DiskDetailState* dd) { - char line[64]; - int fh = system_font_height(); - int x = 0; - int y = 12; - int w = c.w; - int row_h = fh + 10; - Color hdr_col = colors::ACCENT; - Color border = Color::from_rgb(0xE0, 0xE0, 0xE0); - - c.text(x + 8, y, "Supported Features", hdr_col); - y += fh + 8; - c.hline(x + 8, y, w - 16, border); - y += 4; - - dd_feature_row(c, x, y, w, row_h, "48-bit LBA", dd->info.supportsLba48, nullptr); - y += row_h; - - if (dd->info.supportsNcq) { - snprintf(line, sizeof(line), "Depth: %d", (int)dd->info.ncqDepth); - dd_feature_row(c, x, y, w, row_h, "Native Command Queuing (NCQ)", true, line); - } else { - dd_feature_row(c, x, y, w, row_h, "Native Command Queuing (NCQ)", false, nullptr); - } - y += row_h; - - dd_feature_row(c, x, y, w, row_h, "TRIM (Data Set Management)", dd->info.supportsTrim, nullptr); - y += row_h; - - dd_feature_row(c, x, y, w, row_h, "S.M.A.R.T.", dd->info.supportsSmart, nullptr); - y += row_h; - - dd_feature_row(c, x, y, w, row_h, "Write Cache", dd->info.supportsWriteCache, nullptr); - y += row_h; - - dd_feature_row(c, x, y, w, row_h, "Read Look-Ahead", dd->info.supportsReadAhead, nullptr); - y += row_h; - - y += 4; - c.hline(x + 8, y, w - 16, border); - y += 12; - - // Legend - Color legend_col = Color::from_rgb(0x88, 0x88, 0x88); - int ly = y + (fh - 8) / 2; - c.fill_rounded_rect(x + 10, ly, 8, 8, 4, Color::from_rgb(0x22, 0x88, 0x22)); - c.text(x + 26, y, "Supported", legend_col); - - c.fill_rounded_rect(x + 120, ly, 8, 8, 4, Color::from_rgb(0xBB, 0xBB, 0xBB)); - c.text(x + 136, y, "Not supported", legend_col); -} - -static void diskdetail_on_draw(Window* win, Framebuffer& fb) { - DiskDetailState* dd = (DiskDetailState*)win->app_data; - if (!dd) return; - - Canvas c(win); - c.fill(colors::WINDOW_BG); - - int sfh = system_font_height(); - Color accent = colors::ACCENT; - - // --- Tab bar --- - c.fill_rect(0, 0, c.w, DD_TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); - c.hline(0, DD_TAB_BAR_H - 1, c.w, colors::BORDER); - - int tab_w = c.w / DD_TAB_COUNT; - for (int i = 0; i < DD_TAB_COUNT; i++) { - int tx = i * tab_w; - bool active = (i == dd->active_tab); - - if (active) { - c.fill_rect(tx, 0, tab_w, DD_TAB_BAR_H, colors::WINDOW_BG); - c.fill_rect(tx + 4, DD_TAB_BAR_H - 3, tab_w - 8, 3, accent); - } - - int tw = text_width(dd_tab_labels[i]); - Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66); - c.text(tx + (tab_w - tw) / 2, (DD_TAB_BAR_H - sfh) / 2, dd_tab_labels[i], tc); - } - - // --- Tab content --- - Canvas content(win->content + DD_TAB_BAR_H * win->content_w, - win->content_w, win->content_h - DD_TAB_BAR_H); - - switch (dd->active_tab) { - case 0: diskdetail_draw_general(content, dd); break; - case 1: diskdetail_draw_features(content, dd); break; - } -} - -static void diskdetail_on_mouse(Window* win, MouseEvent& ev) { - DiskDetailState* dd = (DiskDetailState*)win->app_data; - if (!dd || !ev.left_pressed()) return; - - Rect cr = win->content_rect(); - int mx = ev.x - cr.x; - int my = ev.y - cr.y; - - // Tab bar click - if (my >= 0 && my < DD_TAB_BAR_H) { - int tab_w = win->content_w / DD_TAB_COUNT; - int tab = mx / tab_w; - if (tab >= 0 && tab < DD_TAB_COUNT) { - dd->active_tab = tab; - } - } -} - -static void diskdetail_on_close(Window* win) { - if (win->app_data) { - montauk::mfree(win->app_data); - win->app_data = nullptr; - } -} - -static void open_disk_detail(DesktopState* ds, int port, const char* model) { - char title[64]; - int i = 0; - const char* prefix = "Disk: "; - while (prefix[i]) { title[i] = prefix[i]; i++; } - int j = 0; - while (model[j] && i < 62) { title[i++] = model[j++]; } - title[i] = '\0'; - - int idx = desktop_create_window(ds, title, 180, 90, 440, 420); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - DiskDetailState* dd = (DiskDetailState*)montauk::malloc(sizeof(DiskDetailState)); - montauk::memset(dd, 0, sizeof(DiskDetailState)); - - dd->desktop = ds; - dd->active_tab = 0; - montauk::diskinfo(&dd->info, port); - - win->app_data = dd; - win->on_draw = diskdetail_on_draw; - win->on_mouse = diskdetail_on_mouse; - win->on_close = diskdetail_on_close; -} - -// ============================================================================ -// Callbacks -// ============================================================================ - -static void devexplorer_on_mouse(Window* win, MouseEvent& ev) { - DevExplorerState* de = (DevExplorerState*)win->app_data; - if (!de) return; - - Rect cr = win->content_rect(); - int lx = ev.x - cr.x; - int ly = ev.y - cr.y; - - // Scroll - if (ev.scroll != 0) { - de->scroll_y += ev.scroll; - if (de->scroll_y < 0) de->scroll_y = 0; - return; - } - - if (ev.left_pressed()) { - // Check "Refresh" button click - int btn_w = 80; - int btn_h = 26; - int btn_x = 8; - int btn_y = (DE_TOOLBAR_H - btn_h) / 2; - Rect btn_rect = {btn_x, btn_y, btn_w, btn_h}; - if (btn_rect.contains(lx, ly)) { - de->last_poll_ms = 0; // force refresh - return; - } - - // Check row click in list area - int list_y = DE_TOOLBAR_H; - if (ly >= list_y) { - // Build display rows to map click to row - DisplayRow rows[MAX_DISPLAY_ROWS]; - int row_count = build_display_rows(de, rows); - - // Walk rows from scroll offset, accumulating y - uint64_t now = montauk::get_milliseconds(); - int cur_y = list_y; - for (int i = de->scroll_y; i < row_count; i++) { - int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - if (ly >= cur_y && ly < cur_y + row_h) { - // Hit this row - if (rows[i].type == ROW_CATEGORY) { - // Toggle collapse - int cat = rows[i].category; - de->collapsed[cat] = !de->collapsed[cat]; - de->selected_row = -1; - de->last_click_row = -1; - } else { - // Check for double-click on storage device - int di = rows[i].dev_index; - bool is_double = (de->last_click_row == i) - && (now - de->last_click_ms < 400); - - if (is_double && de->devs[di].category == 7) { - // Storage device — open detail window - int port = (int)de->devs[di]._pad[0]; - open_disk_detail(de->desktop, port, de->devs[di].name); - de->last_click_row = -1; - } else { - de->selected_row = i; - de->last_click_row = i; - de->last_click_ms = now; - } - } - return; - } - cur_y += row_h; - if (cur_y >= win->content_h) break; - } - // Clicked below all rows - de->selected_row = -1; - de->last_click_row = -1; - } - } -} - -static void devexplorer_on_key(Window* win, const Montauk::KeyEvent& key) { - DevExplorerState* de = (DevExplorerState*)win->app_data; - if (!de || !key.pressed) return; - - DisplayRow rows[MAX_DISPLAY_ROWS]; - int row_count = build_display_rows(de, rows); - if (row_count == 0) return; - - if (key.scancode == 0x48) { // Up arrow - if (de->selected_row <= 0) { - de->selected_row = 0; - } else { - de->selected_row--; - } - // Scroll to keep selection visible - if (de->selected_row < de->scroll_y) { - de->scroll_y = de->selected_row; - } - } else if (key.scancode == 0x50) { // Down arrow - if (de->selected_row < row_count - 1) { - de->selected_row++; - } - // Scroll to keep selection visible — estimate visible rows - int list_h = win->content_h - DE_TOOLBAR_H; - int cur_h = 0; - int last_visible = de->scroll_y; - for (int i = de->scroll_y; i < row_count; i++) { - int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H; - if (cur_h + rh > list_h) break; - cur_h += rh; - last_visible = i; - } - if (de->selected_row > last_visible) { - de->scroll_y += (de->selected_row - last_visible); - } - } else if (key.scancode == 0x4B) { // Left arrow — collapse - if (de->selected_row >= 0 && de->selected_row < row_count) { - int cat = rows[de->selected_row].category; - if (!de->collapsed[cat]) { - de->collapsed[cat] = true; - // If we were on a device row, move selection to the category header - if (rows[de->selected_row].type == ROW_DEVICE) { - // Find the category header row for this category - for (int i = de->selected_row - 1; i >= 0; i--) { - if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) { - de->selected_row = i; - break; - } - } - // After collapsing, rebuild and clamp - int new_count = build_display_rows(de, rows); - if (de->selected_row >= new_count) - de->selected_row = new_count - 1; - } - } - } - } else if (key.scancode == 0x4D) { // Right arrow — expand - if (de->selected_row >= 0 && de->selected_row < row_count) { - int cat = rows[de->selected_row].category; - de->collapsed[cat] = false; - } - } else if (key.scancode == 0x1C) { // Enter — toggle on category row - if (de->selected_row >= 0 && de->selected_row < row_count) { - if (rows[de->selected_row].type == ROW_CATEGORY) { - int cat = rows[de->selected_row].category; - de->collapsed[cat] = !de->collapsed[cat]; - } - } - } -} - -static void devexplorer_on_close(Window* win) { - if (win->app_data) { - montauk::mfree(win->app_data); - win->app_data = nullptr; - } -} - -// ============================================================================ -// Device Explorer launcher -// ============================================================================ - void open_devexplorer(DesktopState* ds) { - int idx = desktop_create_window(ds, "Devices", 140, 70, 640, 460); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - - DevExplorerState* de = (DevExplorerState*)montauk::malloc(sizeof(DevExplorerState)); - montauk::memset(de, 0, sizeof(DevExplorerState)); - de->desktop = ds; - de->selected_row = -1; - de->scroll_y = 0; - de->last_poll_ms = 0; - de->last_click_row = -1; - de->last_click_ms = 0; - - // All categories start expanded - for (int i = 0; i < NUM_CATEGORIES; i++) - de->collapsed[i] = false; - - // Initial poll - de->dev_count = montauk::devlist(de->devs, DE_MAX_DEVS); - - win->app_data = de; - win->on_draw = devexplorer_on_draw; - win->on_mouse = devexplorer_on_mouse; - win->on_key = devexplorer_on_key; - win->on_close = devexplorer_on_close; - win->on_poll = devexplorer_on_poll; + (void)ds; + montauk::spawn("0:/os/devexplorer.elf"); } diff --git a/programs/src/desktop/apps/app_disks.cpp b/programs/src/desktop/apps/app_disks.cpp new file mode 100644 index 0000000..e0cb797 --- /dev/null +++ b/programs/src/desktop/apps/app_disks.cpp @@ -0,0 +1,11 @@ +/* + * app_disks.cpp + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "apps_common.hpp" + +void open_disks(DesktopState* ds) { + (void)ds; + montauk::spawn("0:/os/disks.elf"); +} diff --git a/programs/src/desktop/apps/app_filemanager.cpp b/programs/src/desktop/apps/app_filemanager.cpp index 507577d..b24494c 100644 --- a/programs/src/desktop/apps/app_filemanager.cpp +++ b/programs/src/desktop/apps/app_filemanager.cpp @@ -10,13 +10,15 @@ // File Manager state // ============================================================================ +static constexpr int FM_MAX_DRIVES = 16; + struct FileManagerState { char current_path[256]; char history[16][256]; int history_pos; int history_count; char entry_names[64][64]; - int entry_types[64]; // 0=file, 1=directory, 2=executable + int entry_types[64]; // 0=file, 1=directory, 2=executable, 3=drive int entry_sizes[64]; int entry_count; int selected; @@ -27,6 +29,8 @@ struct FileManagerState { Scrollbar scrollbar; DesktopState* desktop; bool grid_view; + bool at_drives_root; + int drive_indices[FM_MAX_DRIVES]; // which drive number each entry maps to }; static constexpr int FM_TOOLBAR_H = 32; @@ -83,7 +87,51 @@ static int detect_file_type(const char* name, bool is_dir) { // Directory reading with sorting and file sizes // ============================================================================ +static void filemanager_read_drives(FileManagerState* fm) { + fm->entry_count = 0; + fm->at_drives_root = true; + fm->current_path[0] = '\0'; + + for (int d = 0; d < FM_MAX_DRIVES; d++) { + char probe[8]; + if (d < 10) { + probe[0] = '0' + d; + probe[1] = ':'; + probe[2] = '/'; + probe[3] = '\0'; + } else { + probe[0] = '1'; + probe[1] = '0' + (d - 10); + probe[2] = ':'; + 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; + } + } + + fm->selected = -1; + fm->scroll_offset = 0; + fm->scrollbar.scroll_offset = 0; + fm->last_click_item = -1; + fm->last_click_time = 0; +} + static void filemanager_read_dir(FileManagerState* fm) { + fm->at_drives_root = false; const char* names[64]; fm->entry_count = montauk::readdir(fm->current_path, names, 64); if (fm->entry_count < 0) fm->entry_count = 0; @@ -218,8 +266,25 @@ static void filemanager_navigate(FileManagerState* fm, const char* name) { } static void filemanager_go_up(FileManagerState* fm) { + if (fm->at_drives_root) return; + int len = montauk::slen(fm->current_path); - if (len <= 3) return; // "0:/" is root + + // At drive root (e.g. "0:/" or "10:/") — go to drives view + bool is_drive_root = false; + if (len >= 3 && fm->current_path[len - 1] == '/') { + // Check if path is just "N:/" or "NN:/" + int colon = -1; + for (int i = 0; i < len; i++) { + if (fm->current_path[i] == ':') { colon = i; break; } + } + if (colon >= 0 && colon + 2 == len) is_drive_root = true; + } + if (is_drive_root) { + filemanager_read_drives(fm); + filemanager_push_history(fm); + return; + } if (len > 0 && fm->current_path[len - 1] == '/') { fm->current_path[len - 1] = '\0'; @@ -237,24 +302,30 @@ static void filemanager_go_up(FileManagerState* fm) { filemanager_read_dir(fm); } +static void filemanager_navigate_to_history(FileManagerState* fm) { + montauk::strcpy(fm->current_path, fm->history[fm->history_pos]); + if (fm->current_path[0] == '\0') { + filemanager_read_drives(fm); + } else { + filemanager_read_dir(fm); + } +} + static void filemanager_go_back(FileManagerState* fm) { if (fm->history_pos <= 0) return; fm->history_pos--; - montauk::strcpy(fm->current_path, fm->history[fm->history_pos]); - filemanager_read_dir(fm); + filemanager_navigate_to_history(fm); } static void filemanager_go_forward(FileManagerState* fm) { if (fm->history_pos >= fm->history_count - 1) return; fm->history_pos++; - montauk::strcpy(fm->current_path, fm->history[fm->history_pos]); - filemanager_read_dir(fm); + filemanager_navigate_to_history(fm); } static void filemanager_go_home(FileManagerState* fm) { - montauk::strcpy(fm->current_path, "0:/"); + filemanager_read_drives(fm); filemanager_push_history(fm); - filemanager_read_dir(fm); } // ============================================================================ @@ -302,13 +373,27 @@ static void filemanager_draw_header(Canvas& c, FileManagerState* fm, } } + // Delete button (6th toolbar button) — only active when a file is selected + { + int bx = 148, by = 4; + bool has_sel = fm->selected >= 0 && fm->selected < fm->entry_count + && !fm->at_drives_root && !fm->is_dir[fm->selected]; + Color del_bg = has_sel ? btn_bg : Color::from_rgb(0xF0, 0xF0, 0xF0); + c.fill_rect(bx, by, 24, 24, del_bg); + if (ds && ds->icon_delete.pixels) { + int ix = bx + (24 - ds->icon_delete.width) / 2; + int iy = by + (24 - ds->icon_delete.height) / 2; + c.icon(ix, iy, ds->icon_delete); + } + } + // Toolbar separator c.hline(0, FM_TOOLBAR_H - 1, c.w, colors::BORDER); // ---- Path bar ---- int pathbar_y = FM_TOOLBAR_H; c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); - c.text(8, pathbar_y + 4, fm->current_path, colors::TEXT_COLOR); + c.text(8, pathbar_y + 4, fm->at_drives_root ? "Drives" : fm->current_path, colors::TEXT_COLOR); // Path bar separator c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER); @@ -364,7 +449,9 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { // Large icon centered horizontally int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2; int icon_y = cell_y + FM_GRID_PAD; - if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) { + if (ds && fm->entry_types[i] == 3 && ds->icon_drive_lg.pixels) { + c.icon(icon_x, icon_y, ds->icon_drive_lg); + } else if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) { c.icon(icon_x, icon_y, ds->icon_folder_lg); } else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) { c.icon(icon_x, icon_y, ds->icon_exec_lg); @@ -451,7 +538,9 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { // Icon int ico_x = 8; int ico_y = iy + (FM_ITEM_H - 16) / 2; - if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) { + if (ds && fm->entry_types[i] == 3 && ds->icon_drive.pixels) { + c.icon(ico_x, ico_y, ds->icon_drive); + } else if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) { c.icon(ico_x, ico_y, ds->icon_folder); } else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) { c.icon(ico_x, ico_y, ds->icon_exec); @@ -484,7 +573,8 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { // Type if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) { const char* type_str = "File"; - if (fm->entry_types[i] == 1) type_str = "Dir"; + if (fm->entry_types[i] == 3) type_str = "Drive"; + else if (fm->entry_types[i] == 1) type_str = "Dir"; else if (fm->entry_types[i] == 2) type_str = "Exec"; c.text(type_col_x, ty, type_str, dim); } @@ -543,6 +633,20 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { fm->grid_view = !fm->grid_view; fm->scrollbar.scroll_offset = 0; } + else if (local_x >= 148 && local_x < 172) { + // Delete selected file + if (fm->selected >= 0 && fm->selected < fm->entry_count + && !fm->at_drives_root && !fm->is_dir[fm->selected]) { + char fullpath[512]; + montauk::strcpy(fullpath, fm->current_path); + int plen = montauk::slen(fullpath); + if (plen > 0 && fullpath[plen - 1] != '/') + str_append(fullpath, "/", 512); + str_append(fullpath, fm->entry_names[fm->selected], 512); + montauk::fdelete(fullpath); + filemanager_read_dir(fm); + } + } return; } @@ -561,7 +665,19 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { if (fm->last_click_item == clicked_idx && (now - fm->last_click_time) < 400) { - if (fm->is_dir[clicked_idx]) { + if (fm->at_drives_root && fm->entry_types[clicked_idx] == 3) { + // Navigate into drive + int d = fm->drive_indices[clicked_idx]; + char dpath[8]; + if (d < 10) { + dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0'; + } else { + dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0'; + } + montauk::strcpy(fm->current_path, dpath); + filemanager_push_history(fm); + filemanager_read_dir(fm); + } else if (fm->is_dir[clicked_idx]) { filemanager_navigate(fm, fm->entry_names[clicked_idx]); } else { char fullpath[512]; @@ -605,7 +721,18 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { // Double-click detection if (fm->last_click_item == clicked_idx && (now - fm->last_click_time) < 400) { - if (fm->is_dir[clicked_idx]) { + if (fm->at_drives_root && fm->entry_types[clicked_idx] == 3) { + int d = fm->drive_indices[clicked_idx]; + char dpath[8]; + if (d < 10) { + dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0'; + } else { + dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0'; + } + montauk::strcpy(fm->current_path, dpath); + filemanager_push_history(fm); + filemanager_read_dir(fm); + } else if (fm->is_dir[clicked_idx]) { filemanager_navigate(fm, fm->entry_names[clicked_idx]); } else { // Open file in appropriate viewer @@ -693,10 +820,34 @@ static void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) { if (fm->selected < fm->entry_count - 1) fm->selected++; } else if (key.ascii == '\n' || key.ascii == '\r') { if (fm->selected >= 0 && fm->selected < fm->entry_count) { - if (fm->is_dir[fm->selected]) { + if (fm->at_drives_root && fm->entry_types[fm->selected] == 3) { + int d = fm->drive_indices[fm->selected]; + char dpath[8]; + if (d < 10) { + dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0'; + } else { + dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0'; + } + montauk::strcpy(fm->current_path, dpath); + filemanager_push_history(fm); + filemanager_read_dir(fm); + } else if (fm->is_dir[fm->selected]) { filemanager_navigate(fm, fm->entry_names[fm->selected]); } } + } else if (key.scancode == 0x53) { + // Delete key + if (fm->selected >= 0 && fm->selected < fm->entry_count + && !fm->at_drives_root && !fm->is_dir[fm->selected]) { + char fullpath[512]; + montauk::strcpy(fullpath, fm->current_path); + int plen = montauk::slen(fullpath); + if (plen > 0 && fullpath[plen - 1] != '/') + str_append(fullpath, "/", 512); + str_append(fullpath, fm->entry_names[fm->selected], 512); + montauk::fdelete(fullpath); + filemanager_read_dir(fm); + } } else if (key.alt && key.scancode == 0x4B) { // Alt+Left: go back filemanager_go_back(fm); @@ -724,7 +875,6 @@ void open_filemanager(DesktopState* ds) { Window* win = &ds->windows[idx]; FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState)); montauk::memset(fm, 0, sizeof(FileManagerState)); - montauk::strcpy(fm->current_path, "0:/"); fm->selected = -1; fm->last_click_item = -1; fm->history_pos = -1; @@ -734,8 +884,16 @@ void open_filemanager(DesktopState* ds) { fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100); + // Lazy-load file manager icons on first open + if (ds && !ds->icon_drive.pixels) { + Color defColor = colors::ICON_COLOR; + ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor); + ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor); + ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor); + } + + filemanager_read_drives(fm); filemanager_push_history(fm); - filemanager_read_dir(fm); win->app_data = fm; win->on_draw = filemanager_on_draw; diff --git a/programs/src/desktop/apps/app_terminal.cpp b/programs/src/desktop/apps/app_terminal.cpp index 3cc0b0f..7e4c074 100644 --- a/programs/src/desktop/apps/app_terminal.cpp +++ b/programs/src/desktop/apps/app_terminal.cpp @@ -1,6 +1,6 @@ /* * app_terminal.cpp - * MontaukOS Desktop - Terminal application with tab support + * MontaukOS Desktop * Copyright (c) 2026 Daniel Hammer */ @@ -17,12 +17,11 @@ 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 that centers all tabs within bar_w +// Compute the x-offset for left-aligned tabs within bar_w static int term_tabs_origin(int bar_w, int tab_count) { - int total = tab_count * TERM_TAB_W + (tab_count - 1) * TERM_TAB_GAP; - int x = (bar_w - total) / 2; - if (x < 4) x = 4; - return x; + (void)bar_w; + (void)tab_count; + return 8; } struct TermTabState { diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index 27a8d0a..ee2e0ab 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -177,4 +177,5 @@ void open_reboot_dialog(DesktopState* ds); void open_wordprocessor(DesktopState* ds); void open_spreadsheet(DesktopState* ds); void open_shutdown_dialog(DesktopState* ds); +void open_disks(DesktopState* ds); void desktop_poll_external_windows(DesktopState* ds); diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index 2be5f16..c640aab 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -24,7 +24,7 @@ struct MenuRow { int app_id; // -1 for category headers / dividers }; -static constexpr int MENU_ROW_COUNT = 22; +static constexpr int MENU_ROW_COUNT = 23; static const MenuRow menu_rows[MENU_ROW_COUNT] = { { true, "Applications", -1 }, // cat 0 { false, "Terminal", 0 }, @@ -41,6 +41,7 @@ static const MenuRow menu_rows[MENU_ROW_COUNT] = { { false, "Kernel Log", 5 }, { false, "Processes", 6 }, { false, "Devices", 8 }, + { false, "Disks", 17 }, { true, "Games", -1 }, // cat 3 { false, "Mandelbrot", 7 }, { false, "DOOM", 10 }, diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index b3a3b2b..652a080 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -230,6 +230,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { 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; } ds->app_menu_open = false; } diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 88f6eed..18ab9a9 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -52,6 +52,7 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, defColor); ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor); ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor); + // drive icons loaded lazily by file manager to reduce startup heap pressure ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor); ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor); @@ -63,6 +64,7 @@ void gui::desktop_init(DesktopState* ds) { 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); // Settings defaults @@ -82,7 +84,7 @@ void gui::desktop_init(DesktopState* ds) { ds->settings.ui_scale = 1; // Try to load default wallpaper - wallpaper_load(&ds->settings, "0:/home/kristaps-ungurs-llezNN2OGEY-unsplash.jpg", + wallpaper_load(&ds->settings, "0:/home/lucas-alexander-2dJn8XoIKCg-unsplash.jpg", ds->screen_w, ds->screen_h); montauk::win_setscale(1); diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp index 3b1dfc6..13d53b3 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -152,7 +152,7 @@ void desktop_draw_app_menu(DesktopState* ds) { draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); // Icon lookup by app_id - SvgIcon* icons[17] = { + SvgIcon* icons[18] = { &ds->icon_terminal, // 0 &ds->icon_filemanager, // 1 &ds->icon_sysinfo, // 2 @@ -170,6 +170,7 @@ void desktop_draw_app_menu(DesktopState* ds) { &ds->icon_shutdown, // 14 &ds->icon_texteditor, // 15 &ds->icon_spreadsheet, // 16 + &ds->icon_disks, // 17 (Disks) }; int mx = ds->mouse.x; @@ -238,7 +239,7 @@ 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 < 17) { + 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); diff --git a/programs/src/devexplorer/Makefile b/programs/src/devexplorer/Makefile new file mode 100644 index 0000000..a575cad --- /dev/null +++ b/programs/src/devexplorer/Makefile @@ -0,0 +1,86 @@ +# Makefile for devexplorer (standalone Device Explorer) 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 diskdetail.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/os/devexplorer.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/devexplorer/devexplorer.h b/programs/src/devexplorer/devexplorer.h new file mode 100644 index 0000000..33bbf75 --- /dev/null +++ b/programs/src/devexplorer/devexplorer.h @@ -0,0 +1,153 @@ +/* + * devexplorer.h + * Shared header for the MontaukOS Device Explorer + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +using namespace gui; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int INIT_W = 640; +static constexpr int INIT_H = 460; +static constexpr int TOOLBAR_H = 36; +static constexpr int CAT_H = 28; +static constexpr int ITEM_H = 24; +static constexpr int MAX_DEVS = 64; +static constexpr int POLL_MS = 2000; +static constexpr int INDENT = 28; +static constexpr int FONT_SIZE = 18; + +static constexpr int NUM_CATEGORIES = 9; + +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 WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr Color ACCENT_COLOR = Color::from_rgb(0x33, 0x66, 0xCC); + +// ============================================================================ +// Category data +// ============================================================================ + +static const char* category_names[] = { + "CPU", // 0 + "Interrupt", // 1 + "Timer", // 2 + "Input", // 3 + "USB", // 4 + "Network", // 5 + "Display", // 6 + "Storage", // 7 + "PCI", // 8 +}; + +static const Color category_colors[] = { + Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue + Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple + Color::from_rgb(0x22, 0x88, 0x22), // Timer - green + Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber + Color::from_rgb(0x00, 0x88, 0x88), // USB - teal + Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange + Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo + Color::from_rgb(0x99, 0x55, 0x00), // Storage - brown + Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray +}; + +// ============================================================================ +// Display row model +// ============================================================================ + +enum RowType { ROW_CATEGORY, ROW_DEVICE }; + +struct DisplayRow { + RowType type; + int category; + int dev_index; +}; + +static constexpr int MAX_DISPLAY_ROWS = MAX_DEVS + NUM_CATEGORIES; + +// ============================================================================ +// Disk detail window +// ============================================================================ + +static constexpr int DD_TAB_COUNT = 2; +static const char* dd_tab_labels[DD_TAB_COUNT] = { "General", "Features" }; + +static constexpr int DD_INIT_W = 440; +static constexpr int DD_INIT_H = 420; +static constexpr int DD_TAB_BAR_H = 32; + +struct DiskDetailState { + Montauk::DiskInfo info; + int active_tab; + int win_id; + int win_w, win_h; + uint32_t* pixels; + bool open; +}; + +// ============================================================================ +// Main state +// ============================================================================ + +struct DevExplorerState { + Montauk::DevInfo devs[MAX_DEVS]; + int dev_count; + bool collapsed[NUM_CATEGORIES]; + int selected_row; + int scroll_y; + uint64_t last_poll_ms; + + int last_click_row; + uint64_t last_click_ms; + + DiskDetailState detail; +}; + +// ============================================================================ +// Global state (extern — defined in main.cpp) +// ============================================================================ + +extern int g_win_w, g_win_h; +extern DevExplorerState g_state; +extern TrueTypeFont* g_font; + +// ============================================================================ +// Function declarations — render.cpp +// ============================================================================ + +void render(uint32_t* pixels); + +// ============================================================================ +// Function declarations — main.cpp +// ============================================================================ + +int build_display_rows(DevExplorerState* de, DisplayRow* rows); + +// ============================================================================ +// Function declarations — diskdetail.cpp +// ============================================================================ + +void render_disk_detail(); +void open_disk_detail(int port, const char* model); +void close_disk_detail(); +bool handle_detail_mouse(int mx, int my, bool clicked); diff --git a/programs/src/devexplorer/diskdetail.cpp b/programs/src/devexplorer/diskdetail.cpp new file mode 100644 index 0000000..e3de4ff --- /dev/null +++ b/programs/src/devexplorer/diskdetail.cpp @@ -0,0 +1,306 @@ +/* + * diskdetail.cpp + * Disk detail popup window + * Copyright (c) 2026 Daniel Hammer + */ + +#include "px.h" + +// ============================================================================ +// Helpers +// ============================================================================ + +static void fmt_u64(char* buf, int bufsize, uint64_t v) { + if (v == 0) { buf[0] = '0'; buf[1] = '\0'; return; } + char tmp[24]; int i = 0; + while (v > 0) { tmp[i++] = '0' + (int)(v % 10); v /= 10; } + int j = 0; + while (i > 0 && j < bufsize - 1) buf[j++] = tmp[--i]; + buf[j] = '\0'; +} + +static void dd_table_row(uint32_t* px, int bw, int bh, + int x, int y, int col1_w, int row_h, + const char* key, const char* value, + Color key_col, Color val_col) { + int fh = font_h(); + int ty = y + (row_h - fh) / 2; + px_text(px, bw, bh, x + 8, ty, key, key_col); + px_text(px, bw, bh, x + col1_w + 8, ty, value, val_col); +} + +static void dd_feature_row(uint32_t* px, int bw, int bh, + int x, int y, int w, int row_h, + const char* label, bool supported, const char* extra) { + int fh = font_h(); + int ty = y + (row_h - fh) / 2; + + int dot_y = y + (row_h - 8) / 2; + Color dot_col = supported ? Color::from_rgb(0x22, 0x88, 0x22) + : Color::from_rgb(0xBB, 0xBB, 0xBB); + px_fill_rounded(px, bw, bh, x + 10, dot_y, 8, 8, 4, dot_col); + + Color text_col = supported ? TEXT_COLOR : Color::from_rgb(0xAA, 0xAA, 0xAA); + px_text(px, bw, bh, x + 26, ty, label, text_col); + + if (extra && extra[0]) { + int ew = text_w(extra); + px_text(px, bw, bh, x + w - ew - 12, ty, extra, DIM_TEXT); + } +} + +// ============================================================================ +// Tab content: General +// ============================================================================ + +static void dd_draw_general(uint32_t* px, int bw, int bh, DiskDetailState* dd) { + char line[128]; + int fh = font_h(); + int x = 0; + int y = DD_TAB_BAR_H + 12; + int w = bw; + int col1_w = 110; + int row_h = fh + 8; + Color key_col = DIM_TEXT; + Color val_col = TEXT_COLOR; + Color hdr_col = ACCENT_COLOR; + Color border = Color::from_rgb(0xE0, 0xE0, 0xE0); + + // Identification section + px_text(px, bw, bh, x + 8, y, "Identification", hdr_col); + y += fh + 8; + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 1; + + int rowIdx = 0; + auto table_row = [&](const char* key, const char* val) { + if (rowIdx++ % 2 == 0) + px_fill(px, bw, bh, x + 8, y, w - 16, row_h, + Color::from_rgb(0xF7, 0xF7, 0xF7)); + dd_table_row(px, bw, bh, x, y, col1_w, row_h, key, val, key_col, val_col); + y += row_h; + }; + + table_row("Model", dd->info.model); + table_row("Serial", dd->info.serial); + table_row("Firmware", dd->info.firmware); + + const char* typeStr = "Unknown"; + if (dd->info.type == 1) typeStr = "SATA"; + else if (dd->info.type == 2) typeStr = "SATAPI"; + table_row("Type", typeStr); + + snprintf(line, sizeof(line), "%d", (int)dd->info.port); + table_row("AHCI Port", line); + + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 12; + + // Capacity section + px_text(px, bw, bh, x + 8, y, "Capacity", hdr_col); + y += fh + 8; + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 1; + + rowIdx = 0; + uint64_t totalBytes = dd->info.sectorCount * (uint64_t)dd->info.sectorSizeLog; + uint64_t totalMB = totalBytes / (1024 * 1024); + uint64_t totalGB = totalMB / 1024; + if (totalGB > 0) { + int fracGB = (int)((totalMB % 1024) * 10 / 1024); + snprintf(line, sizeof(line), "%d.%d GiB", (int)totalGB, fracGB); + } else { + snprintf(line, sizeof(line), "%d MiB", (int)totalMB); + } + table_row("Size", line); + + char secbuf[24]; + fmt_u64(secbuf, sizeof(secbuf), dd->info.sectorCount); + table_row("Sectors", secbuf); + + snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizeLog); + table_row("Logical", line); + + snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizePhys); + table_row("Physical", line); + + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 12; + + // Interface section + px_text(px, bw, bh, x + 8, y, "Interface", hdr_col); + y += fh + 8; + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 1; + + rowIdx = 0; + const char* sataSpeed = "Unknown"; + if (dd->info.sataGen == 1) sataSpeed = "SATA I (1.5 Gb/s)"; + else if (dd->info.sataGen == 2) sataSpeed = "SATA II (3.0 Gb/s)"; + else if (dd->info.sataGen == 3) sataSpeed = "SATA III (6.0 Gb/s)"; + table_row("Link Speed", sataSpeed); + + if (dd->info.rpm == 0) + table_row("Media", "Not reported"); + else if (dd->info.rpm == 1) + table_row("Media", "Solid State (SSD)"); + else { + snprintf(line, sizeof(line), "%d RPM", (int)dd->info.rpm); + table_row("Media", line); + } + + px_hline(px, bw, bh, x + 8, y, w - 16, border); +} + +// ============================================================================ +// Tab content: Features +// ============================================================================ + +static void dd_draw_features(uint32_t* px, int bw, int bh, DiskDetailState* dd) { + char line[64]; + int fh = font_h(); + int x = 0; + int y = DD_TAB_BAR_H + 12; + int w = bw; + int row_h = fh + 10; + Color hdr_col = ACCENT_COLOR; + Color border = Color::from_rgb(0xE0, 0xE0, 0xE0); + + px_text(px, bw, bh, x + 8, y, "Supported Features", hdr_col); + y += fh + 8; + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 4; + + dd_feature_row(px, bw, bh, x, y, w, row_h, "48-bit LBA", dd->info.supportsLba48, nullptr); + y += row_h; + + if (dd->info.supportsNcq) { + snprintf(line, sizeof(line), "Depth: %d", (int)dd->info.ncqDepth); + dd_feature_row(px, bw, bh, x, y, w, row_h, "Native Command Queuing (NCQ)", true, line); + } else { + dd_feature_row(px, bw, bh, x, y, w, row_h, "Native Command Queuing (NCQ)", false, nullptr); + } + y += row_h; + + dd_feature_row(px, bw, bh, x, y, w, row_h, "TRIM (Data Set Management)", dd->info.supportsTrim, nullptr); + y += row_h; + + dd_feature_row(px, bw, bh, x, y, w, row_h, "S.M.A.R.T.", dd->info.supportsSmart, nullptr); + y += row_h; + + dd_feature_row(px, bw, bh, x, y, w, row_h, "Write Cache", dd->info.supportsWriteCache, nullptr); + y += row_h; + + dd_feature_row(px, bw, bh, x, y, w, row_h, "Read Look-Ahead", dd->info.supportsReadAhead, nullptr); + y += row_h; + + y += 4; + px_hline(px, bw, bh, x + 8, y, w - 16, border); + y += 12; + + // Legend + Color legend_col = Color::from_rgb(0x88, 0x88, 0x88); + int ly = y + (fh - 8) / 2; + px_fill_rounded(px, bw, bh, x + 10, ly, 8, 8, 4, Color::from_rgb(0x22, 0x88, 0x22)); + px_text(px, bw, bh, x + 26, y, "Supported", legend_col); + + px_fill_rounded(px, bw, bh, x + 120, ly, 8, 8, 4, Color::from_rgb(0xBB, 0xBB, 0xBB)); + px_text(px, bw, bh, x + 136, y, "Not supported", legend_col); +} + +// ============================================================================ +// Render +// ============================================================================ + +void render_disk_detail() { + auto& dd = g_state.detail; + if (!dd.open) return; + + uint32_t* px = dd.pixels; + int bw = dd.win_w, bh = dd.win_h; + int fh = font_h(); + + px_fill(px, bw, bh, 0, 0, bw, bh, BG_COLOR); + + // Tab bar + px_fill(px, bw, bh, 0, 0, bw, DD_TAB_BAR_H, TOOLBAR_BG); + px_hline(px, bw, bh, 0, DD_TAB_BAR_H - 1, bw, BORDER_COLOR); + + int tab_w = bw / DD_TAB_COUNT; + for (int i = 0; i < DD_TAB_COUNT; i++) { + int tx = i * tab_w; + bool active = (i == dd.active_tab); + + if (active) { + px_fill(px, bw, bh, tx, 0, tab_w, DD_TAB_BAR_H, BG_COLOR); + px_fill(px, bw, bh, tx + 4, DD_TAB_BAR_H - 3, tab_w - 8, 3, ACCENT_COLOR); + } + + int tw = text_w(dd_tab_labels[i]); + Color tc = active ? ACCENT_COLOR : DIM_TEXT; + px_text(px, bw, bh, tx + (tab_w - tw) / 2, (DD_TAB_BAR_H - fh) / 2, + dd_tab_labels[i], tc); + } + + // Tab content + switch (dd.active_tab) { + case 0: dd_draw_general(px, bw, bh, &dd); break; + case 1: dd_draw_features(px, bw, bh, &dd); break; + } +} + +// ============================================================================ +// Window management +// ============================================================================ + +void open_disk_detail(int port, const char* model) { + auto& dd = g_state.detail; + + if (dd.open) close_disk_detail(); + + char title[64]; + int i = 0; + const char* prefix = "Disk: "; + while (prefix[i]) { title[i] = prefix[i]; i++; } + int j = 0; + while (model[j] && i < 62) { title[i++] = model[j++]; } + title[i] = '\0'; + + dd.win_w = DD_INIT_W; + dd.win_h = DD_INIT_H; + dd.active_tab = 0; + montauk::memset(&dd.info, 0, sizeof(dd.info)); + montauk::diskinfo(&dd.info, port); + + Montauk::WinCreateResult wres; + if (montauk::win_create(title, dd.win_w, dd.win_h, &wres) < 0 || wres.id < 0) + return; + + dd.win_id = wres.id; + dd.pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + dd.open = true; + + render_disk_detail(); + montauk::win_present(dd.win_id); +} + +void close_disk_detail() { + auto& dd = g_state.detail; + if (!dd.open) return; + montauk::win_destroy(dd.win_id); + dd.open = false; +} + +bool handle_detail_mouse(int mx, int my, bool clicked) { + auto& dd = g_state.detail; + if (!clicked) return false; + + if (my >= 0 && my < DD_TAB_BAR_H) { + int tab_w = dd.win_w / DD_TAB_COUNT; + int tab = mx / tab_w; + if (tab >= 0 && tab < DD_TAB_COUNT) + dd.active_tab = tab; + return true; + } + return false; +} diff --git a/programs/src/devexplorer/main.cpp b/programs/src/devexplorer/main.cpp new file mode 100644 index 0000000..c8c8315 --- /dev/null +++ b/programs/src/devexplorer/main.cpp @@ -0,0 +1,304 @@ +/* + * main.cpp + * MontaukOS Device Explorer — entry point, event loop, state + * Copyright (c) 2026 Daniel Hammer + */ + +#include "devexplorer.h" + +// ============================================================================ +// Global state definitions +// ============================================================================ + +int g_win_w = INIT_W; +int g_win_h = INIT_H; + +DevExplorerState g_state; +TrueTypeFont* g_font = nullptr; + +// ============================================================================ +// Display row building +// ============================================================================ + +int build_display_rows(DevExplorerState* de, DisplayRow* rows) { + int count = 0; + for (int cat = 0; cat < NUM_CATEGORIES; cat++) { + int cat_count = 0; + for (int d = 0; d < de->dev_count; d++) + if (de->devs[d].category == cat) cat_count++; + if (cat_count == 0) continue; + + rows[count].type = ROW_CATEGORY; + rows[count].category = cat; + rows[count].dev_index = -1; + count++; + + if (!de->collapsed[cat]) { + for (int d = 0; d < de->dev_count; d++) { + if (de->devs[d].category == cat) { + rows[count].type = ROW_DEVICE; + rows[count].category = cat; + rows[count].dev_index = d; + count++; + } + } + } + } + return count; +} + +// ============================================================================ +// Toolbar hit testing +// ============================================================================ + +static bool handle_toolbar_click(int mx, int my) { + if (my >= TOOLBAR_H) return false; + + int btn_w = 80, btn_h = 26; + int btn_x = 8; + int btn_y = (TOOLBAR_H - btn_h) / 2; + if (mx >= btn_x && mx < btn_x + btn_w && my >= btn_y && my < btn_y + btn_h) { + g_state.last_poll_ms = 0; // force refresh + return true; + } + return false; +} + +// ============================================================================ +// List click handling +// ============================================================================ + +static bool handle_list_click(int mx, int my) { + auto& de = g_state; + int list_y = TOOLBAR_H; + if (my < list_y) return false; + + DisplayRow rows[MAX_DISPLAY_ROWS]; + int row_count = build_display_rows(&de, rows); + + uint64_t now = montauk::get_milliseconds(); + int cur_y = list_y; + for (int i = de.scroll_y; i < row_count; i++) { + int row_h = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + if (my >= cur_y && my < cur_y + row_h) { + if (rows[i].type == ROW_CATEGORY) { + int cat = rows[i].category; + de.collapsed[cat] = !de.collapsed[cat]; + de.selected_row = -1; + de.last_click_row = -1; + } else { + int di = rows[i].dev_index; + bool is_double = (de.last_click_row == i) + && (now - de.last_click_ms < 400); + + if (is_double && de.devs[di].category == 7) { + int port = (int)de.devs[di]._pad[0]; + open_disk_detail(port, de.devs[di].name); + de.last_click_row = -1; + } else { + de.selected_row = i; + de.last_click_row = i; + de.last_click_ms = now; + } + } + return true; + } + cur_y += row_h; + if (cur_y >= g_win_h) break; + } + de.selected_row = -1; + de.last_click_row = -1; + return true; +} + +// ============================================================================ +// Key handling +// ============================================================================ + +static bool handle_key(const Montauk::KeyEvent& key) { + if (!key.pressed) return false; + + auto& de = g_state; + DisplayRow rows[MAX_DISPLAY_ROWS]; + int row_count = build_display_rows(&de, rows); + if (row_count == 0) return true; + + if (key.scancode == 0x48) { // Up + if (de.selected_row <= 0) de.selected_row = 0; + else de.selected_row--; + if (de.selected_row < de.scroll_y) + de.scroll_y = de.selected_row; + } else if (key.scancode == 0x50) { // Down + if (de.selected_row < row_count - 1) + de.selected_row++; + int list_h = g_win_h - TOOLBAR_H; + int cur_h = 0, last_visible = de.scroll_y; + for (int i = de.scroll_y; i < row_count; i++) { + int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + if (cur_h + rh > list_h) break; + cur_h += rh; + last_visible = i; + } + if (de.selected_row > last_visible) + de.scroll_y += (de.selected_row - last_visible); + } else if (key.scancode == 0x4B) { // Left — collapse + if (de.selected_row >= 0 && de.selected_row < row_count) { + int cat = rows[de.selected_row].category; + if (!de.collapsed[cat]) { + de.collapsed[cat] = true; + if (rows[de.selected_row].type == ROW_DEVICE) { + for (int i = de.selected_row - 1; i >= 0; i--) { + if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) { + de.selected_row = i; + break; + } + } + int new_count = build_display_rows(&de, rows); + if (de.selected_row >= new_count) + de.selected_row = new_count - 1; + } + } + } + } else if (key.scancode == 0x4D) { // Right — expand + if (de.selected_row >= 0 && de.selected_row < row_count) { + int cat = rows[de.selected_row].category; + de.collapsed[cat] = false; + } + } else if (key.scancode == 0x1C) { // Enter — toggle category + if (de.selected_row >= 0 && de.selected_row < row_count) { + if (rows[de.selected_row].type == ROW_CATEGORY) { + int cat = rows[de.selected_row].category; + de.collapsed[cat] = !de.collapsed[cat]; + } + } + } else if (key.scancode == 0x01) { // Escape + return false; + } + return true; +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + montauk::memset(&g_state, 0, sizeof(g_state)); + g_state.selected_row = -1; + g_state.last_click_row = -1; + + for (int i = 0; i < NUM_CATEGORIES; i++) + g_state.collapsed[i] = false; + + // 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; + } + + // Initial device poll + g_state.dev_count = montauk::devlist(g_state.devs, MAX_DEVS); + g_state.last_poll_ms = montauk::get_milliseconds(); + + // Create window + Montauk::WinCreateResult wres; + if (montauk::win_create("Devices", 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; + + render(pixels); + montauk::win_present(win_id); + + while (true) { + Montauk::WinEvent ev; + bool redraw_main = false; + bool redraw_detail = false; + + // Poll for device refresh + uint64_t now = montauk::get_milliseconds(); + if (now - g_state.last_poll_ms >= POLL_MS) { + g_state.dev_count = montauk::devlist(g_state.devs, MAX_DEVS); + g_state.last_poll_ms = now; + redraw_main = true; + } + + // Poll disk detail window + if (g_state.detail.open) { + int dr = montauk::win_poll(g_state.detail.win_id, &ev); + if (dr > 0) { + if (ev.type == 3) { // close + close_disk_detail(); + redraw_main = true; + } else if (ev.type == 1) { + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + if (handle_detail_mouse(ev.mouse.x, ev.mouse.y, clicked)) + redraw_detail = true; + } else if (ev.type == 2) { // resize + g_state.detail.win_w = ev.resize.w; + g_state.detail.win_h = ev.resize.h; + g_state.detail.pixels = (uint32_t*)(uintptr_t)montauk::win_resize( + g_state.detail.win_id, ev.resize.w, ev.resize.h); + redraw_detail = true; + } + } + } + + // Poll main window + int r = montauk::win_poll(win_id, &ev); + + if (r < 0) break; + if (r == 0) { + if (redraw_main) { render(pixels); montauk::win_present(win_id); } + if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); } + if (!redraw_main && !redraw_detail) montauk::sleep_ms(16); + continue; + } + + if (ev.type == 3) break; // close + + // Resize + 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_main = true; + } + + // Keyboard + if (ev.type == 0 && ev.key.pressed) { + if (!handle_key(ev.key) && ev.key.scancode == 0x01) + break; + redraw_main = true; + } + + // Mouse + if (ev.type == 1) { + int mx = ev.mouse.x; + int my = ev.mouse.y; + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + + if (ev.mouse.scroll != 0) { + g_state.scroll_y += ev.mouse.scroll; + if (g_state.scroll_y < 0) g_state.scroll_y = 0; + redraw_main = true; + } + + if (clicked) { + if (handle_toolbar_click(mx, my) || handle_list_click(mx, my)) + redraw_main = true; + } + } + + if (redraw_main) { render(pixels); montauk::win_present(win_id); } + if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); } + } + + close_disk_detail(); + montauk::win_destroy(win_id); + montauk::exit(0); +} diff --git a/programs/src/devexplorer/px.h b/programs/src/devexplorer/px.h new file mode 100644 index 0000000..2259bfd --- /dev/null +++ b/programs/src/devexplorer/px.h @@ -0,0 +1,81 @@ +/* + * px.h + * Shared pixel-level drawing helpers for the MontaukOS Device Explorer + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include "devexplorer.h" + +inline 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; +} + +inline 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; +} + +inline void px_fill_rounded(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, int r, Color c) { + if (r <= 0) { px_fill(px, bw, bh, x, y, w, h, c); return; } + 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 in_corner = false; + int cx_off = 0, cy_off = 0; + if (col < r && row < r) { + cx_off = r - col; cy_off = r - row; in_corner = true; + } else if (col >= w - r && row < r) { + cx_off = col - (w - r - 1); cy_off = r - row; in_corner = true; + } else if (col < r && row >= h - r) { + cx_off = r - col; cy_off = row - (h - r - 1); in_corner = true; + } else if (col >= w - r && row >= h - r) { + cx_off = col - (w - r - 1); cy_off = row - (h - r - 1); in_corner = true; + } + if (in_corner && cx_off * cx_off + cy_off * cy_off > r * r) continue; + px[dy * bw + dx] = v; + } + } +} + +inline void px_text(uint32_t* px, int bw, int bh, + int x, int y, const char* text, Color c) { + if (g_font) + g_font->draw_to_buffer(px, bw, bh, x, y, text, c, FONT_SIZE); +} + +inline int text_w(const char* text) { + return g_font ? g_font->measure_text(text, FONT_SIZE) : 0; +} + +inline int font_h() { + if (!g_font) return 16; + auto* cache = g_font->get_cache(FONT_SIZE); + return cache->ascent - cache->descent; +} + +inline 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); +} diff --git a/programs/src/devexplorer/render.cpp b/programs/src/devexplorer/render.cpp new file mode 100644 index 0000000..0d0d39d --- /dev/null +++ b/programs/src/devexplorer/render.cpp @@ -0,0 +1,196 @@ +/* + * render.cpp + * MontaukOS Device Explorer — main window rendering + * Copyright (c) 2026 Daniel Hammer + */ + +#include "px.h" + +// ============================================================================ +// Triangle drawing helpers +// ============================================================================ + +static void draw_triangle_right(uint32_t* px, int bw, int bh, + int x, int y, int size, Color col) { + uint32_t v = col.to_pixel(); + int half = size / 2; + for (int row = 0; row < size; row++) { + int dist = (row <= half) ? row : (size - 1 - row); + for (int col_px = 0; col_px <= dist; col_px++) { + int dx = x + col_px, dy = y + row; + if (dx >= 0 && dx < bw && dy >= 0 && dy < bh) + px[dy * bw + dx] = v; + } + } +} + +static void draw_triangle_down(uint32_t* px, int bw, int bh, + int x, int y, int size, Color col) { + uint32_t v = col.to_pixel(); + int half = size / 2; + for (int row = 0; row < half + 1; row++) { + int w = size - row * 2; + for (int col_px = 0; col_px < w; col_px++) { + int dx = x + row + col_px, dy = y + row; + if (dx >= 0 && dx < bw && dy >= 0 && dy < bh) + px[dy * bw + dx] = v; + } + } +} + +// ============================================================================ +// Render: main device list +// ============================================================================ + +static void render_toolbar(uint32_t* px) { + auto& de = g_state; + int fh = font_h(); + + 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); + + // "Refresh" button + int btn_w = 80, btn_h = 26; + int btn_x = 8; + int btn_y = (TOOLBAR_H - btn_h) / 2; + px_button(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h, + "Refresh", ACCENT_COLOR, WHITE, 4); + + // Device count on right + char count_str[24]; + snprintf(count_str, sizeof(count_str), "%d devices", de.dev_count); + int cw = text_w(count_str); + px_text(px, g_win_w, g_win_h, g_win_w - cw - 12, (TOOLBAR_H - fh) / 2, + count_str, TEXT_COLOR); +} + +static void render_list(uint32_t* px) { + auto& de = g_state; + int fh = font_h(); + + DisplayRow rows[MAX_DISPLAY_ROWS]; + int row_count = build_display_rows(&de, rows); + + int list_y = TOOLBAR_H; + int list_h = g_win_h - list_y; + if (list_h < 1) return; + + // Compute total content height + int total_h = 0; + for (int i = 0; i < row_count; i++) + total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + + int max_scroll_px = total_h - list_h; + if (max_scroll_px < 0) max_scroll_px = 0; + + // Compute scroll pixel offset + int scroll_px = 0; + for (int i = 0; i < de.scroll_y && i < row_count; i++) + scroll_px += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + if (scroll_px > max_scroll_px) { + scroll_px = max_scroll_px; + de.scroll_y = 0; + int acc = 0; + for (int i = 0; i < row_count; i++) { + int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + if (acc + rh > max_scroll_px) break; + acc += rh; + de.scroll_y = i + 1; + } + } + if (de.scroll_y < 0) de.scroll_y = 0; + if (de.selected_row >= row_count) de.selected_row = row_count - 1; + + // Draw rows + int first_visible = de.scroll_y; + int cur_y = list_y; + Color menu_hover = Color::from_rgb(0xD0, 0xE0, 0xF8); + + for (int i = first_visible; i < row_count; i++) { + int row_h = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + if (cur_y >= g_win_h) break; + + if (rows[i].type == ROW_CATEGORY) { + int cat = rows[i].category; + px_fill(px, g_win_w, g_win_h, 0, cur_y, g_win_w, CAT_H, + Color::from_rgb(0xF0, 0xF0, 0xF0)); + + if (i == de.selected_row) + px_fill(px, g_win_w, g_win_h, 0, cur_y, g_win_w, CAT_H, menu_hover); + + // Expand/collapse triangle + int tri_x = 10; + int tri_y = cur_y + (CAT_H - 8) / 2; + Color tri_color = Color::from_rgb(0x55, 0x55, 0x55); + if (de.collapsed[cat]) + draw_triangle_right(px, g_win_w, g_win_h, tri_x, tri_y, 8, tri_color); + else + draw_triangle_down(px, g_win_w, g_win_h, tri_x, tri_y, 8, tri_color); + + // Colored dot + int dot_x = 24; + int dot_y = cur_y + (CAT_H - 8) / 2; + Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES) + ? category_colors[cat] : TEXT_COLOR; + px_fill_rounded(px, g_win_w, g_win_h, dot_x, dot_y, 8, 8, 4, cat_col); + + // Category name + const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES) + ? category_names[cat] : "?"; + int text_y = cur_y + (CAT_H - fh) / 2; + px_text(px, g_win_w, g_win_h, 36, text_y, cat_name, + Color::from_rgb(0x33, 0x33, 0x33)); + + // Device count + int cat_count = 0; + for (int d = 0; d < de.dev_count; d++) + if (de.devs[d].category == cat) cat_count++; + char cnt_buf[16]; + snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count); + int name_w = text_w(cat_name); + px_text(px, g_win_w, g_win_h, 36 + name_w + 8, text_y, cnt_buf, + Color::from_rgb(0x88, 0x88, 0x88)); + + // Bottom border + px_hline(px, g_win_w, g_win_h, 0, cur_y + CAT_H - 1, g_win_w, + Color::from_rgb(0xE0, 0xE0, 0xE0)); + } else { + int di = rows[i].dev_index; + + if (i == de.selected_row) + px_fill(px, g_win_w, g_win_h, 0, cur_y, g_win_w, ITEM_H, menu_hover); + + int text_y = cur_y + (ITEM_H - fh) / 2; + + // Device name (indented) + px_text(px, g_win_w, g_win_h, INDENT + 10, text_y, + de.devs[di].name, TEXT_COLOR); + + // Detail string (right column) + int detail_x = g_win_w / 2 + 20; + px_text(px, g_win_w, g_win_h, detail_x, text_y, + de.devs[di].detail, DIM_TEXT); + } + + cur_y += row_h; + } + + // Scrollbar + if (total_h > list_h) { + int sb_x = g_win_w - 6; + int thumb_h = (list_h * list_h) / total_h; + if (thumb_h < 20) thumb_h = 20; + int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px; + px_fill(px, g_win_w, g_win_h, sb_x, list_y, 4, list_h, + Color::from_rgb(0xE0, 0xE0, 0xE0)); + px_fill(px, g_win_w, g_win_h, sb_x, thumb_y, 4, thumb_h, + Color::from_rgb(0xAA, 0xAA, 0xAA)); + } +} + +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_list(pixels); +} + diff --git a/programs/src/devexplorer/stb_truetype_impl.cpp b/programs/src/devexplorer/stb_truetype_impl.cpp new file mode 100644 index 0000000..9ce2266 --- /dev/null +++ b/programs/src/devexplorer/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +// Override all stb_truetype dependencies before including the implementation + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/disks/Makefile b/programs/src/disks/Makefile new file mode 100644 index 0000000..cb75625 --- /dev/null +++ b/programs/src/disks/Makefile @@ -0,0 +1,86 @@ +# Makefile for disks (standalone Disk Tool) 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)/os/disks.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/disks/actions.cpp b/programs/src/disks/actions.cpp new file mode 100644 index 0000000..8489845 --- /dev/null +++ b/programs/src/disks/actions.cpp @@ -0,0 +1,179 @@ +/* + * actions.cpp + * MontaukOS Disk Tool — disk operations + * Copyright (c) 2026 Daniel Hammer + */ + +#include "disks.h" + +// ============================================================================ +// Refresh +// ============================================================================ + +void disktool_refresh() { + auto& dt = g_state; + dt.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) { + dt.disks[dt.disk_count++] = info; + } + } + dt.part_count = montauk::partlist(dt.parts, MAX_PARTS); + if (dt.selected_disk >= dt.disk_count) + dt.selected_disk = dt.disk_count > 0 ? 0 : -1; + dt.selected_part = -1; + dt.scroll_y = 0; +} + +// ============================================================================ +// Create partition +// ============================================================================ + +void do_create_partition() { + auto& dt = g_state; + if (dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) return; + + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + + if (nparts == 0) { + int r = montauk::gpt_init(dt.selected_disk); + if (r < 0) { + set_status("Failed to initialize GPT on disk"); + return; + } + set_status("Initialized GPT"); + } + + Montauk::GptAddParams params; + montauk::memset(¶ms, 0, sizeof(params)); + params.blockDev = dt.selected_disk; + params.startLba = 0; + params.endLba = 0; + params.typeGuid.Data1 = 0xEBD0A0A2; + params.typeGuid.Data2 = 0xB9E5; + params.typeGuid.Data3 = 0x4433; + params.typeGuid.Data4[0] = 0x87; params.typeGuid.Data4[1] = 0xC0; + params.typeGuid.Data4[2] = 0x68; params.typeGuid.Data4[3] = 0xB6; + params.typeGuid.Data4[4] = 0xB7; params.typeGuid.Data4[5] = 0x26; + params.typeGuid.Data4[6] = 0x99; params.typeGuid.Data4[7] = 0xC7; + + const char* pname = "Data"; + int i = 0; + for (; pname[i] && i < 71; i++) params.name[i] = pname[i]; + params.name[i] = '\0'; + + int r = montauk::gpt_add(¶ms); + if (r < 0) { + set_status("Failed to create partition"); + return; + } + + set_status("Partition created successfully"); + disktool_refresh(); +} + +// ============================================================================ +// Mount partition +// ============================================================================ + +void do_mount_partition() { + auto& dt = g_state; + if (dt.selected_part < 0) { + set_status("Select a partition first"); + return; + } + + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + if (dt.selected_part >= nparts) return; + + int global_idx = part_indices[dt.selected_part]; + int driveNum = 1 + global_idx; + if (driveNum >= 16) driveNum = 15; + + int r = montauk::fs_mount(global_idx, driveNum); + if (r < 0) { + set_status("Mount failed (no recognized filesystem)"); + return; + } + + char msg[80]; + snprintf(msg, sizeof(msg), "Mounted as drive %d:/", driveNum); + set_status(msg); +} + +// ============================================================================ +// Format dialog +// ============================================================================ + +void open_format_dialog() { + auto& dt = g_state; + if (dt.selected_part < 0) { + set_status("Select a partition first"); + return; + } + + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + if (dt.selected_part >= nparts) return; + + int global_idx = part_indices[dt.selected_part]; + + auto& dlg = dt.fmt_dlg; + dlg.global_part_index = global_idx; + dlg.selected_fs = 0; + dlg.hover_format = false; + dlg.hover_cancel = false; + + // Build partition description + Montauk::PartInfo& p = dt.parts[global_idx]; + Montauk::DiskInfo& disk = dt.disks[dt.selected_disk]; + char sz[24]; + format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog); + const char* pname = p.name[0] ? p.name : "Partition"; + snprintf(dlg.part_desc, sizeof(dlg.part_desc), "%s (%s)", pname, sz); + + // Create dialog window + Montauk::WinCreateResult wres; + if (montauk::win_create("Format Partition", FMT_DLG_W, FMT_DLG_H, &wres) < 0 || wres.id < 0) { + set_status("Failed to open format dialog"); + return; + } + dlg.win_id = wres.id; + dlg.pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + dlg.open = true; + + render_format_window(); + montauk::win_present(dlg.win_id); +} + +void close_format_dialog() { + auto& dlg = g_state.fmt_dlg; + if (!dlg.open) return; + montauk::win_destroy(dlg.win_id); + dlg.open = false; +} + +void format_dialog_do_format() { + auto& dt = g_state; + auto& dlg = dt.fmt_dlg; + + Montauk::FsFormatParams params; + montauk::memset(¶ms, 0, sizeof(params)); + params.partIndex = dlg.global_part_index; + params.fsType = g_fsTypes[dlg.selected_fs].id; + + int r = montauk::fs_format(¶ms); + if (r == 0) { + set_status("Format complete"); + disktool_refresh(); + } else { + set_status("Format failed"); + } + + close_format_dialog(); +} diff --git a/programs/src/disks/disks.h b/programs/src/disks/disks.h new file mode 100644 index 0000000..051c2b2 --- /dev/null +++ b/programs/src/disks/disks.h @@ -0,0 +1,138 @@ +/* + * disks.h + * Shared header for the MontaukOS Disk Tool + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +using namespace gui; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int INIT_W = 640; +static constexpr int INIT_H = 460; +static constexpr int TOOLBAR_H = 40; +static constexpr int HEADER_H = 26; +static constexpr int ITEM_H = 32; +static constexpr int MAP_H = 48; +static constexpr int MAP_PAD = 16; +static constexpr int MAX_PARTS = 32; +static constexpr int MAX_DISKS = 8; +static constexpr int STATUS_H = 26; +static constexpr int FONT_SIZE = 16; + +static constexpr int TB_BTN_Y = 7; +static constexpr int TB_BTN_H = 26; +static constexpr int TB_BTN_RAD = 6; + +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 HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); +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 HOVER_BG = Color::from_rgb(0xE8, 0xF0, 0xF8); +static constexpr Color STATUS_BG_COL = Color::from_rgb(0xF0, 0xF0, 0xF0); +static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); + +static constexpr int NUM_PART_COLORS = 8; + +// ============================================================================ +// Filesystem types +// ============================================================================ + +struct FsTypeEntry { + const char* name; + int id; +}; + +static const FsTypeEntry g_fsTypes[] = { + { "FAT32", Montauk::FS_TYPE_FAT32 }, +}; +static constexpr int NUM_FS_TYPES = 1; + +// ============================================================================ +// State +// ============================================================================ + +static constexpr int FMT_DLG_W = 280; +static constexpr int FMT_DLG_H = 220; + +struct FormatDialog { + bool open; + int global_part_index; + int selected_fs; + bool hover_format; + bool hover_cancel; + char part_desc[80]; + int win_id; + uint32_t* pixels; +}; + +struct DiskToolState { + Montauk::DiskInfo disks[MAX_DISKS]; + int disk_count; + Montauk::PartInfo parts[MAX_PARTS]; + int part_count; + int selected_disk; + int selected_part; + int scroll_y; + char status[80]; + uint64_t status_time; + FormatDialog fmt_dlg; +}; + +// ============================================================================ +// Global state (extern — defined in main.cpp) +// ============================================================================ + +extern int g_win_w, g_win_h; +extern DiskToolState g_state; +extern TrueTypeFont* g_font; + +// ============================================================================ +// Partition colors +// ============================================================================ + +extern const Color part_colors[NUM_PART_COLORS]; + +// ============================================================================ +// Function declarations — helpers (main.cpp) +// ============================================================================ + +void set_status(const char* msg); +int get_disk_parts(int* indices, int max); +void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorSize); + +// ============================================================================ +// Function declarations — render.cpp +// ============================================================================ + +void render(uint32_t* pixels); +void render_format_window(); + +// ============================================================================ +// Function declarations — actions.cpp +// ============================================================================ + +void disktool_refresh(); +void do_create_partition(); +void do_mount_partition(); +void open_format_dialog(); +void close_format_dialog(); +void format_dialog_do_format(); diff --git a/programs/src/disks/main.cpp b/programs/src/disks/main.cpp new file mode 100644 index 0000000..3719036 --- /dev/null +++ b/programs/src/disks/main.cpp @@ -0,0 +1,391 @@ +/* + * main.cpp + * MontaukOS Disk Tool — entry point, event loop, state + * Copyright (c) 2026 Daniel Hammer + */ + +#include "disks.h" + +// ============================================================================ +// Global state definitions +// ============================================================================ + +int g_win_w = INIT_W; +int g_win_h = INIT_H; + +DiskToolState g_state; +TrueTypeFont* g_font = nullptr; + +const Color part_colors[NUM_PART_COLORS] = { + Color::from_rgb(0x42, 0x7A, 0xB5), + Color::from_rgb(0x5C, 0xA0, 0x5C), + Color::from_rgb(0xCC, 0x88, 0x33), + Color::from_rgb(0x99, 0x55, 0xBB), + Color::from_rgb(0xCC, 0x55, 0x55), + Color::from_rgb(0x44, 0xAA, 0xAA), + Color::from_rgb(0xBB, 0xBB, 0x44), + Color::from_rgb(0x88, 0x66, 0x44), +}; + +// ============================================================================ +// 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(); +} + +int get_disk_parts(int* indices, int max) { + int count = 0; + for (int i = 0; i < g_state.part_count && count < max; i++) { + if (g_state.parts[i].blockDev == g_state.selected_disk) { + indices[count++] = i; + } + } + return count; +} + +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); + } +} + +// ============================================================================ +// Toolbar hit testing (returns button widths matching render layout) +// ============================================================================ + +static int disk_btn_width(int idx) { + char label[8]; + snprintf(label, sizeof(label), "Disk %d", idx); + int lw = g_font ? g_font->measure_text(label, FONT_SIZE) + 16 : 48; + return lw; +} + +static bool handle_toolbar_click(int mx, int my) { + if (my >= TOOLBAR_H || my < TB_BTN_Y) return false; + + auto& dt = g_state; + + // Disk selector buttons + int bx = 8; + for (int i = 0; i < dt.disk_count; i++) { + int lw = disk_btn_width(i); + if (mx >= bx && mx < bx + lw && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) { + dt.selected_disk = i; + dt.selected_part = -1; + dt.scroll_y = 0; + return true; + } + bx += lw + 6; + } + + // Right-side buttons (must match render layout) + int rx = g_win_w - 8; + + int ref_w = 64; rx -= ref_w; + if (mx >= rx && mx < rx + ref_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) { + disktool_refresh(); + set_status("Refreshed"); + return true; + } + rx -= 6; + + int mnt_w = 60; rx -= mnt_w; + if (mx >= rx && mx < rx + mnt_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) { + do_mount_partition(); + return true; + } + rx -= 6; + + int fmt_w = 64; rx -= fmt_w; + if (mx >= rx && mx < rx + fmt_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) { + open_format_dialog(); + return true; + } + rx -= 6; + + int np_w = 74; rx -= np_w; + if (mx >= rx && mx < rx + np_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) { + do_create_partition(); + return true; + } + + return false; +} + +// ============================================================================ +// Mouse handling for content area +// ============================================================================ + +static bool handle_content_click(int mx, int my) { + auto& dt = g_state; + if (dt.disk_count == 0 || dt.selected_disk < 0) return false; + + int fh = g_font ? g_font->get_cache(FONT_SIZE)->ascent - g_font->get_cache(FONT_SIZE)->descent : 16; + int y = TOOLBAR_H + 8 + fh + 8; + + // Partition map bar click + int map_x = MAP_PAD; + int map_w = g_win_w - MAP_PAD * 2; + if (my >= y && my < y + MAP_H) { + Montauk::DiskInfo& disk = dt.disks[dt.selected_disk]; + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + uint64_t total = disk.sectorCount; + if (total > 0) { + for (int pi = 0; pi < nparts; pi++) { + Montauk::PartInfo& p = dt.parts[part_indices[pi]]; + int px = map_x + (int)((p.startLba * (uint64_t)map_w) / total); + int pw = (int)((p.sectorCount * (uint64_t)map_w) / total); + if (pw < 2) pw = 2; + if (mx >= px && mx < px + pw) { + dt.selected_part = pi; + return true; + } + } + } + dt.selected_part = -1; + return true; + } + + y += MAP_H + 8 + HEADER_H; + + // Partition list click + int list_bottom = g_win_h - STATUS_H; + if (my >= y && my < list_bottom) { + int row = (my - y + dt.scroll_y) / ITEM_H; + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + if (row >= 0 && row < nparts) + dt.selected_part = row; + else + dt.selected_part = -1; + return true; + } + + return false; +} + +// ============================================================================ +// Format dialog mouse handling +// ============================================================================ + +static bool handle_format_dialog_click(int mx, int my, bool clicked) { + auto& dlg = g_state.fmt_dlg; + if (!dlg.open) return false; + + int dw = FMT_DLG_W, dh = FMT_DLG_H; + int fh = g_font ? g_font->get_cache(FONT_SIZE)->ascent - g_font->get_cache(FONT_SIZE)->descent : 16; + + // Button hit testing + int btn_w = 90, btn_h = 30; + int btn_y = dh - btn_h - 16; + int gap = 16; + int total_w = btn_w * 2 + gap; + int bx = (dw - total_w) / 2; + + dlg.hover_format = (mx >= bx && mx < bx + btn_w && my >= btn_y && my < btn_y + btn_h); + dlg.hover_cancel = (mx >= bx + btn_w + gap && mx < bx + btn_w * 2 + gap && my >= btn_y && my < btn_y + btn_h); + + if (!clicked) return true; + + // FS type selector + int sel_y = 12 + fh * 2 + 18; + int opt_y = sel_y + fh + 8; + for (int i = 0; i < NUM_FS_TYPES; i++) { + int iy = opt_y + i * 32; + if (mx >= 24 && mx < dw - 24 && my >= iy && my < iy + 28) { + dlg.selected_fs = i; + return true; + } + } + + if (dlg.hover_format) { + format_dialog_do_format(); + return true; + } + + if (dlg.hover_cancel) { + close_format_dialog(); + return true; + } + + return true; +} + +// ============================================================================ +// Key handling +// ============================================================================ + +static bool handle_key(const Montauk::KeyEvent& key) { + if (!key.pressed) return false; + + auto& dt = g_state; + + // Format dialog keys + if (dt.fmt_dlg.open) { + if (key.scancode == 0x01) { // Escape + close_format_dialog(); + } else if (key.scancode == 0x48 && dt.fmt_dlg.selected_fs > 0) { + dt.fmt_dlg.selected_fs--; + } else if (key.scancode == 0x50 && dt.fmt_dlg.selected_fs < NUM_FS_TYPES - 1) { + dt.fmt_dlg.selected_fs++; + } else if (key.ascii == '\n' || key.ascii == '\r') { + format_dialog_do_format(); + } + return true; + } + + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + + if (key.scancode == 0x01) { // Escape + return false; // signal quit + } else if (key.scancode == 0x48) { // Up + if (dt.selected_part > 0) dt.selected_part--; + else if (nparts > 0) dt.selected_part = 0; + } else if (key.scancode == 0x50) { // Down + if (dt.selected_part < nparts - 1) dt.selected_part++; + } else if (key.scancode == 0x4B) { // Left + if (dt.selected_disk > 0) { dt.selected_disk--; dt.selected_part = -1; dt.scroll_y = 0; } + } else if (key.scancode == 0x4D) { // Right + if (dt.selected_disk < dt.disk_count - 1) { dt.selected_disk++; dt.selected_part = -1; dt.scroll_y = 0; } + } else if (key.ascii == 'n' || key.ascii == 'N') { + do_create_partition(); + } else if (key.ascii == 'm' || key.ascii == 'M') { + do_mount_partition(); + } else if (key.ascii == 'f' || key.ascii == 'F') { + open_format_dialog(); + } else { + return true; // consumed but no action + } + return true; +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + montauk::memset(&g_state, 0, sizeof(g_state)); + g_state.selected_disk = 0; + g_state.selected_part = -1; + + // Load font + { + TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); + if (f) { + montauk::memset(f, 0, sizeof(TrueTypeFont)); + if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; } + } + g_font = f; + } + + disktool_refresh(); + + // Create window + Montauk::WinCreateResult wres; + if (montauk::win_create("Disks", 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; + + render(pixels); + montauk::win_present(win_id); + + while (true) { + Montauk::WinEvent ev; + bool redraw_main = false; + bool redraw_dlg = false; + + // Poll format dialog window + if (g_state.fmt_dlg.open) { + int dr = montauk::win_poll(g_state.fmt_dlg.win_id, &ev); + if (dr > 0) { + if (ev.type == 3) { // close + close_format_dialog(); + redraw_main = true; + } else if (ev.type == 0 && ev.key.pressed) { + handle_key(ev.key); + redraw_dlg = g_state.fmt_dlg.open; + redraw_main = true; + } else if (ev.type == 1) { + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + handle_format_dialog_click(ev.mouse.x, ev.mouse.y, clicked); + redraw_dlg = g_state.fmt_dlg.open; + redraw_main = true; + } + } + } + + // Poll main window + int r = montauk::win_poll(win_id, &ev); + + if (r < 0) break; + if (r == 0) { + // Even with no main event, dialog may have triggered redraws + if (redraw_main) { render(pixels); montauk::win_present(win_id); } + if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); } + if (!redraw_main && !redraw_dlg) montauk::sleep_ms(16); + continue; + } + + if (ev.type == 3) break; // close + + // Resize + 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_main = true; + } + + // Keyboard + if (ev.type == 0 && ev.key.pressed) { + if (!handle_key(ev.key) && ev.key.scancode == 0x01) + break; // Escape quits when not in dialog + redraw_main = true; + } + + // Mouse + if (ev.type == 1) { + int mx = ev.mouse.x; + int my = ev.mouse.y; + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + + // Scroll + if (ev.mouse.scroll != 0) { + g_state.scroll_y -= ev.mouse.scroll * 20; + if (g_state.scroll_y < 0) g_state.scroll_y = 0; + redraw_main = true; + } + + if (clicked) { + if (handle_toolbar_click(mx, my) || handle_content_click(mx, my)) + redraw_main = true; + } + } + + if (redraw_main) { render(pixels); montauk::win_present(win_id); } + if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); } + } + + close_format_dialog(); + montauk::win_destroy(win_id); + montauk::exit(0); +} diff --git a/programs/src/disks/render.cpp b/programs/src/disks/render.cpp new file mode 100644 index 0000000..8ae4835 --- /dev/null +++ b/programs/src/disks/render.cpp @@ -0,0 +1,357 @@ +/* + * render.cpp + * MontaukOS Disk Tool — rendering + * Copyright (c) 2026 Daniel Hammer + */ + +#include "disks.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) { + if (g_font) + g_font->draw_to_buffer(px, bw, bh, x, y, text, c, FONT_SIZE); +} + +static int text_w(const char* text) { + return g_font ? g_font->measure_text(text, FONT_SIZE) : 0; +} + +static int font_h() { + if (!g_font) return 16; + auto* cache = g_font->get_cache(FONT_SIZE); + return cache->ascent - cache->descent; +} + +// ============================================================================ +// Button helper +// ============================================================================ + +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); +} + +// ============================================================================ +// Render: main view +// ============================================================================ + +static void render_toolbar(uint32_t* px) { + auto& dt = g_state; + int fh = font_h(); + + 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); + + int bx = 8; + for (int i = 0; i < dt.disk_count; i++) { + char label[8]; + snprintf(label, sizeof(label), "Disk %d", i); + int lw = text_w(label) + 16; + Color bg = (i == dt.selected_disk) + ? Color::from_rgb(0x42, 0x7A, 0xB5) + : Color::from_rgb(0xDD, 0xDD, 0xDD); + Color fg = (i == dt.selected_disk) ? WHITE : TEXT_COLOR; + px_button(px, g_win_w, g_win_h, bx, TB_BTN_Y, lw, TB_BTN_H, label, bg, fg, TB_BTN_RAD); + bx += lw + 6; + } + + if (dt.disk_count == 0) { + px_text(px, g_win_w, g_win_h, 8, (TOOLBAR_H - fh) / 2, "No disks detected", TEXT_COLOR); + return; + } + + // Right-side action buttons + bool has_sel = dt.selected_part >= 0; + int rx = g_win_w - 8; + + int ref_w = 64; rx -= ref_w; + px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, ref_w, TB_BTN_H, + "Refresh", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, TB_BTN_RAD); + rx -= 6; + + int mnt_w = 60; rx -= mnt_w; + px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, mnt_w, TB_BTN_H, + "Mount", + has_sel ? Color::from_rgb(0x42, 0x7A, 0xB5) : Color::from_rgb(0xBB, 0xBB, 0xBB), + WHITE, TB_BTN_RAD); + rx -= 6; + + int fmt_w = 64; rx -= fmt_w; + px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, fmt_w, TB_BTN_H, + "Format", + has_sel ? Color::from_rgb(0x42, 0x7A, 0xB5) : Color::from_rgb(0xBB, 0xBB, 0xBB), + WHITE, TB_BTN_RAD); + rx -= 6; + + int np_w = 74; rx -= np_w; + px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, np_w, TB_BTN_H, + "New Part", Color::from_rgb(0x42, 0x7A, 0xB5), WHITE, TB_BTN_RAD); +} + +static void render_content(uint32_t* px) { + auto& dt = g_state; + int fh = font_h(); + + if (dt.disk_count == 0 || dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) + return; + + Montauk::DiskInfo& disk = dt.disks[dt.selected_disk]; + int y = TOOLBAR_H + 8; + + // Disk model name + px_text(px, g_win_w, g_win_h, MAP_PAD, y, disk.model, TEXT_COLOR); + + // Size and type on the right + char info_str[64], size_str[24]; + format_disk_size(size_str, sizeof(size_str), disk.sectorCount, disk.sectorSizeLog); + const char* dtype = disk.rpm == 1 ? "SSD" : "HDD"; + snprintf(info_str, sizeof(info_str), "%s %s", size_str, dtype); + int iw = text_w(info_str); + px_text(px, g_win_w, g_win_h, g_win_w - iw - MAP_PAD, y, info_str, DIM_TEXT); + + y += fh + 8; + + // Partition map bar + int map_x = MAP_PAD; + int map_w = g_win_w - MAP_PAD * 2; + px_fill_rounded(px, g_win_w, g_win_h, map_x, y, map_w, MAP_H, 6, + Color::from_rgb(0xE4, 0xE4, 0xE4)); + + int part_indices[MAX_PARTS]; + int nparts = get_disk_parts(part_indices, MAX_PARTS); + uint64_t total_sectors = disk.sectorCount; + + if (total_sectors > 0 && nparts > 0) { + for (int pi = 0; pi < nparts; pi++) { + Montauk::PartInfo& p = dt.parts[part_indices[pi]]; + int ppx = map_x + (int)((p.startLba * (uint64_t)map_w) / total_sectors); + int pw = (int)((p.sectorCount * (uint64_t)map_w) / total_sectors); + if (pw < 2) pw = 2; + if (ppx + pw > map_x + map_w) pw = map_x + map_w - ppx; + + Color col = part_colors[pi % NUM_PART_COLORS]; + if (pi == dt.selected_part) { + col = Color::from_rgb((col.r + 255) / 2, (col.g + 255) / 2, (col.b + 255) / 2); + } + px_fill_rounded(px, g_win_w, g_win_h, ppx, y + 2, pw, MAP_H - 4, 3, col); + + char plabel[8]; + snprintf(plabel, sizeof(plabel), "%d", pi); + int plw = text_w(plabel); + if (pw > plw + 4) + px_text(px, g_win_w, g_win_h, ppx + (pw - plw) / 2, y + (MAP_H - fh) / 2, plabel, WHITE); + } + } + + y += MAP_H + 8; + + // Column header + px_fill(px, g_win_w, g_win_h, 0, y, g_win_w, HEADER_H, HEADER_BG); + int ty = y + (HEADER_H - fh) / 2; + + int col_idx = 12; + int col_name = 40; + int col_type = g_win_w / 2 - 20; + int col_size = g_win_w - 160; + int col_lba = g_win_w - 80; + + px_text(px, g_win_w, g_win_h, col_idx, ty, "#", DIM_TEXT); + px_text(px, g_win_w, g_win_h, col_name, ty, "Name", DIM_TEXT); + px_text(px, g_win_w, g_win_h, col_type, ty, "Type", DIM_TEXT); + px_text(px, g_win_w, g_win_h, col_size, ty, "Size", DIM_TEXT); + px_text(px, g_win_w, g_win_h, col_lba, ty, "LBA", DIM_TEXT); + px_hline(px, g_win_w, g_win_h, 0, y + HEADER_H - 1, g_win_w, BORDER_COLOR); + y += HEADER_H; + + // Partition rows + int list_bottom = g_win_h - STATUS_H; + int list_y = y; + for (int pi = 0; pi < nparts; pi++) { + int row_y = list_y + pi * ITEM_H - dt.scroll_y; + if (row_y + ITEM_H <= list_y) continue; + if (row_y >= list_bottom) break; + + Montauk::PartInfo& p = dt.parts[part_indices[pi]]; + + if (pi == dt.selected_part) + px_fill(px, g_win_w, g_win_h, 0, row_y, g_win_w, ITEM_H, HOVER_BG); + + px_fill_rounded(px, g_win_w, g_win_h, col_idx, row_y + (ITEM_H - 10) / 2, 10, 10, 5, + part_colors[pi % NUM_PART_COLORS]); + + int ry = row_y + (ITEM_H - fh) / 2; + px_text(px, g_win_w, g_win_h, col_name, ry, p.name[0] ? p.name : "(unnamed)", TEXT_COLOR); + px_text(px, g_win_w, g_win_h, col_type, ry, p.typeName, DIM_TEXT); + + char sz[24]; + format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog); + px_text(px, g_win_w, g_win_h, col_size, ry, sz, TEXT_COLOR); + + char lba_str[24]; + snprintf(lba_str, sizeof(lba_str), "%lu", (unsigned)p.startLba); + px_text(px, g_win_w, g_win_h, col_lba, ry, lba_str, FAINT_TEXT); + } + + if (nparts == 0) + px_text(px, g_win_w, g_win_h, col_name, list_y + 8, "No partitions found", FAINT_TEXT); +} + +static void render_status(uint32_t* px) { + auto& dt = 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, STATUS_BG_COL); + px_hline(px, g_win_w, g_win_h, 0, sy, g_win_w, BORDER_COLOR); + if (dt.status[0]) { + uint64_t age = montauk::get_milliseconds() - dt.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, dt.status, sc); + } +} + +// ============================================================================ +// Render: format dialog window +// ============================================================================ + +void render_format_window() { + auto& dlg = g_state.fmt_dlg; + if (!dlg.open) return; + + uint32_t* px = dlg.pixels; + int dw = FMT_DLG_W, dh = FMT_DLG_H; + int fh = font_h(); + + px_fill(px, dw, dh, 0, 0, dw, dh, BG_COLOR); + + // Title + const char* title = "Format Partition"; + int tw = text_w(title); + px_text(px, dw, dh, (dw - tw) / 2, 12, title, TEXT_COLOR); + + // Partition description + int ddw = text_w(dlg.part_desc); + px_text(px, dw, dh, (dw - ddw) / 2, 12 + fh + 6, dlg.part_desc, DIM_TEXT); + + // Filesystem selector + int sel_y = 12 + fh * 2 + 18; + px_text(px, dw, dh, 16, sel_y, "Filesystem:", TEXT_COLOR); + + int opt_y = sel_y + fh + 8; + for (int i = 0; i < NUM_FS_TYPES; i++) { + int ox = 24; + int ow = dw - 48; + int oh = 28; + int iy = opt_y + i * (oh + 4); + + if (i == dlg.selected_fs) { + px_fill_rounded(px, dw, dh, ox, iy, ow, oh, 6, + Color::from_rgb(0xD0, 0xE0, 0xF0)); + px_fill_rounded(px, dw, dh, ox - 1, iy - 1, ow + 2, oh + 2, 7, Color::from_rgb(0x42, 0x7A, 0xB5)); + px_fill_rounded(px, dw, dh, ox, iy, ow, oh, 6, Color::from_rgb(0xD0, 0xE0, 0xF0)); + } else { + px_fill_rounded(px, dw, dh, ox - 1, iy - 1, ow + 2, oh + 2, 7, BORDER_COLOR); + px_fill_rounded(px, dw, dh, ox, iy, ow, oh, 6, BG_COLOR); + } + + // Radio indicator (circular) + int cx = ox + 14; + int cy = iy + oh / 2; + px_fill_rounded(px, dw, dh, cx - 5, cy - 5, 10, 10, 5, DIM_TEXT); + px_fill_rounded(px, dw, dh, cx - 4, cy - 4, 8, 8, 4, BG_COLOR); + if (i == dlg.selected_fs) + px_fill_rounded(px, dw, dh, cx - 3, cy - 3, 6, 6, 3, Color::from_rgb(0x42, 0x7A, 0xB5)); + + px_text(px, dw, dh, cx + 12, iy + (oh - fh) / 2, g_fsTypes[i].name, TEXT_COLOR); + } + + // Buttons + int btn_w = 90, btn_h = 30; + int btn_y = dh - btn_h - 16; + int gap = 16; + int total_w = btn_w * 2 + gap; + int bx = (dw - total_w) / 2; + + Color fmt_bg = dlg.hover_format + ? Color::from_rgb(0xDD, 0x44, 0x44) + : Color::from_rgb(0xCC, 0x33, 0x33); + px_button(px, dw, dh, bx, btn_y, btn_w, btn_h, "Format", fmt_bg, WHITE, 6); + + Color can_bg = dlg.hover_cancel + ? Color::from_rgb(0x99, 0x99, 0x99) + : Color::from_rgb(0x88, 0x88, 0x88); + px_button(px, dw, dh, bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", can_bg, WHITE, 6); +} + +// ============================================================================ +// 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_content(pixels); + render_status(pixels); +} diff --git a/programs/src/disks/stb_truetype_impl.cpp b/programs/src/disks/stb_truetype_impl.cpp new file mode 100644 index 0000000..9ce2266 --- /dev/null +++ b/programs/src/disks/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +// Override all stb_truetype dependencies before including the implementation + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/edit/main.cpp b/programs/src/edit/main.cpp index 94ceffb..3256cef 100644 --- a/programs/src/edit/main.cpp +++ b/programs/src/edit/main.cpp @@ -279,15 +279,11 @@ static bool save_file() { if (i < numLines - 1) totalSize++; // newline } - // Try to open existing file first - int handle = montauk::open(path); + // Create (or truncate existing) file + int handle = montauk::fcreate(path); if (handle < 0) { - // Create new file - handle = montauk::fcreate(path); - if (handle < 0) { - set_status("Error: could not create file"); - return false; - } + set_status("Error: could not create file"); + return false; } // Build content buffer and write diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index 4494908..b4c1977 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -28,14 +28,17 @@ static void scat(char* dst, const char* src, int maxLen) { dst[dLen + i] = '\0'; } -// Current working directory (relative to 0:/). +// Current working directory (relative to N:/). // "" = root, "man" = 0:/man, "man/sub" = 0:/man/sub static char cwd[128] = ""; +static int current_drive = 0; -// Build VFS directory path: "0:/" or "0:/" -static void build_dir_path(const char* dir, char* out, int outMax) { +// Build VFS path with explicit drive number +static void build_drive_path(int drive, const char* dir, char* out, int outMax) { int i = 0; - out[i++] = '0'; out[i++] = ':'; out[i++] = '/'; + if (drive >= 10) { out[i++] = '0' + drive / 10; } + out[i++] = '0' + drive % 10; + out[i++] = ':'; out[i++] = '/'; if (dir && dir[0]) { int j = 0; while (dir[j] && i < outMax - 1) out[i++] = dir[j++]; @@ -43,6 +46,32 @@ static void build_dir_path(const char* dir, char* out, int outMax) { out[i] = '\0'; } +// Build VFS directory path: "N:/" or "N:/" using current_drive +static void build_dir_path(const char* dir, char* out, int outMax) { + build_drive_path(current_drive, dir, out, outMax); +} + +// Parse drive number from a drive prefix like "0:" or "12:". Returns -1 if invalid. +static int parse_drive_prefix(const char* s) { + if (s[0] < '0' || s[0] > '9') return -1; + int n = s[0] - '0'; + if (s[1] >= '0' && s[1] <= '9') { + n = n * 10 + (s[1] - '0'); + if (s[2] != ':') return -1; + } else if (s[1] == ':') { + // single digit drive + } else { + return -1; + } + return n; +} + +// Length of drive prefix ("0:" = 2, "12:" = 3). Assumes valid prefix. +static int drive_prefix_len(const char* s) { + if (s[1] >= '0' && s[1] <= '9') return 3; // e.g. "12:" + return 2; // e.g. "0:" +} + // ---- Command history ---- static constexpr int HISTORY_MAX = 32; @@ -72,7 +101,17 @@ static const char* history_get(int idx) { // ---- Prompt ---- static void prompt() { - montauk::print("0:/"); + char drv[4]; + if (current_drive >= 10) { + drv[0] = '0' + current_drive / 10; + drv[1] = '0' + current_drive % 10; + drv[2] = '\0'; + } else { + drv[0] = '0' + current_drive; + drv[1] = '\0'; + } + montauk::print(drv); + montauk::print(":/"); if (cwd[0]) montauk::print(cwd); montauk::print("> "); } @@ -108,6 +147,7 @@ static void cmd_help() { montauk::print(" help Show this help message\n"); montauk::print(" ls [dir] List files in directory\n"); montauk::print(" cd [dir] Change working directory\n"); + montauk::print(" N: Switch to drive N (e.g. 1:)\n"); montauk::print(" exit Exit the shell\n"); montauk::print("\n"); montauk::print("System commands:\n"); @@ -138,9 +178,9 @@ static void cmd_help() { montauk::print("Any .elf on the ramdisk is executable.\n"); } -// Check if a string already has a VFS drive prefix (e.g. "0:/") +// Check if a string already has a VFS drive prefix (e.g. "0:/" or "12:/") static bool has_drive_prefix(const char* s) { - return s[0] >= '0' && s[0] <= '9' && s[1] == ':'; + return parse_drive_prefix(s) >= 0; } // ---- Builtin: ls ---- @@ -150,10 +190,13 @@ static void cmd_ls(const char* arg) { // Build the target directory (relative path from root) char dir[128]; + int drive = current_drive; if (*arg) { if (has_drive_prefix(arg)) { - // Absolute VFS path: "0:/something" - scopy(dir, arg + 3, sizeof(dir)); + // Absolute VFS path: "N:/something" + drive = parse_drive_prefix(arg); + int plen = drive_prefix_len(arg); + scopy(dir, arg + plen + 1, sizeof(dir)); // skip "N:/" } else if (arg[0] == '/') { // Absolute path from root: "/something" scopy(dir, arg + 1, sizeof(dir)); @@ -172,7 +215,7 @@ static void cmd_ls(const char* arg) { } char path[128]; - build_dir_path(dir, path, sizeof(path)); + build_drive_path(drive, dir, path, sizeof(path)); const char* entries[64]; int count = montauk::readdir(path, entries, 64); @@ -198,6 +241,18 @@ static void cmd_ls(const char* arg) { // ---- Builtin: cd ---- +// Switch to a drive. Returns true if valid. +static bool switch_drive(int drive) { + // Validate drive exists by trying to readdir its root + char path[8]; + build_drive_path(drive, "", path, sizeof(path)); + const char* entries[1]; + if (montauk::readdir(path, entries, 1) < 0) return false; + current_drive = drive; + cwd[0] = '\0'; + return true; +} + static void cmd_cd(const char* arg) { arg = skip_spaces(arg); @@ -247,20 +302,41 @@ static void cmd_cd(const char* arg) { return; } - // cd 0:/path -> absolute VFS path + // cd N:/ or cd N:/path -> switch drive and optionally cd into path if (has_drive_prefix(arg)) { - const char* rel = arg + 3; // skip "0:/" - if (*rel == '\0') { cwd[0] = '\0'; return; } - char path[128]; - build_dir_path(rel, path, sizeof(path)); - const char* entries[1]; - if (montauk::readdir(path, entries, 1) < 0) { - montauk::print("cd: no such directory: "); + int drive = parse_drive_prefix(arg); + int plen = drive_prefix_len(arg); + const char* rel = arg + plen; // points at ":/" or ":" + if (*rel == '/') rel++; // skip the '/' + + // Validate drive exists + char rootPath[8]; + build_drive_path(drive, "", rootPath, sizeof(rootPath)); + const char* rootEntries[1]; + if (montauk::readdir(rootPath, rootEntries, 1) < 0) { + montauk::print("cd: no such drive: "); montauk::print(arg); montauk::putchar('\n'); return; } - scopy(cwd, rel, sizeof(cwd)); + + // If there's a path after the drive, validate it + if (*rel != '\0') { + char path[128]; + build_drive_path(drive, rel, path, sizeof(path)); + const char* entries[1]; + if (montauk::readdir(path, entries, 1) < 0) { + montauk::print("cd: no such directory: "); + montauk::print(arg); + montauk::putchar('\n'); + return; + } + current_drive = drive; + scopy(cwd, rel, sizeof(cwd)); + } else { + current_drive = drive; + cwd[0] = '\0'; + } return; } @@ -346,28 +422,39 @@ static void resolve_args(const char* args, char* out, int outMax) { // Decide whether to resolve this token as a path. // Don't resolve if it already has a drive prefix, or starts with '-' - bool resolve = cwd[0] && !has_drive_prefix(tokStart) && tokStart[0] != '-'; + bool resolve = (cwd[0] || current_drive != 0) && !has_drive_prefix(tokStart) && tokStart[0] != '-'; if (resolve) { // Build candidate resolved path and check if the file exists char candidate[256]; int r = 0; - candidate[r++] = '0'; candidate[r++] = ':'; candidate[r++] = '/'; + if (current_drive >= 10) candidate[r++] = '0' + current_drive / 10; + candidate[r++] = '0' + current_drive % 10; + candidate[r++] = ':'; candidate[r++] = '/'; int j = 0; while (cwd[j] && r < 255) candidate[r++] = cwd[j++]; - if (r < 255) candidate[r++] = '/'; + if (cwd[0] && r < 255) candidate[r++] = '/'; for (int k = 0; k < tokLen && r < 255; k++) candidate[r++] = tokStart[k]; candidate[r] = '\0'; - // Only use the resolved path if the file actually exists; - // otherwise pass the argument through unchanged (it may be - // a hostname, IP address, URL, or other non-path argument). + // Use the resolved path if the file exists. If it doesn't + // exist, still use the resolved path when the token looks + // like a filename (contains a dot) so that programs creating + // new files get the correct drive/directory prefix. int h = montauk::open(candidate); if (h >= 0) { montauk::close(h); for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k]; } else { - for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k]; + bool looksLikeFile = false; + for (int k = 0; k < tokLen; k++) { + if (tokStart[k] == '.') { looksLikeFile = true; break; } + } + if (looksLikeFile) { + for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k]; + } else { + for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k]; + } } } else { for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k]; @@ -386,6 +473,7 @@ static void exec_external(const char* cmd, const char* args) { resolve_args(args, resolvedArgs, sizeof(resolvedArgs)); const char* finalArgs = resolvedArgs[0] ? resolvedArgs : nullptr; + // Always search drive 0 for system commands (os/, games/) // 1. Try 0:/os/.elf scopy(path, "0:/os/", sizeof(path)); scat(path, cmd, sizeof(path)); @@ -398,9 +486,9 @@ static void exec_external(const char* cmd, const char* args) { scat(path, ".elf", sizeof(path)); if (try_exec(path, finalArgs)) return; - // 3. Try 0://.elf (if cwd is set) + // 3. Try N://.elf on current drive (if cwd is set) if (cwd[0]) { - scopy(path, "0:/", sizeof(path)); + build_drive_path(current_drive, "", path, sizeof(path)); scat(path, cwd, sizeof(path)); scat(path, "/", sizeof(path)); scat(path, cmd, sizeof(path)); @@ -408,12 +496,20 @@ static void exec_external(const char* cmd, const char* args) { if (try_exec(path, finalArgs)) return; } - // 4. Try 0:/.elf - scopy(path, "0:/", sizeof(path)); + // 4. Try N:/.elf on current drive + build_drive_path(current_drive, "", path, sizeof(path)); scat(path, cmd, sizeof(path)); scat(path, ".elf", sizeof(path)); if (try_exec(path, finalArgs)) return; + // 5. If on a non-zero drive, also try 0:/.elf + if (current_drive != 0) { + scopy(path, "0:/", sizeof(path)); + scat(path, cmd, sizeof(path)); + scat(path, ".elf", sizeof(path)); + if (try_exec(path, finalArgs)) return; + } + // Not found montauk::print(cmd); montauk::print(": command not found\n"); @@ -440,6 +536,17 @@ static void process_command(const char* line) { if (*args == '\0') args = nullptr; } + // Bare drive switch: "1:", "2:", etc. + if (has_drive_prefix(cmd) && cmd[drive_prefix_len(cmd)] == '\0' && args == nullptr) { + int drive = parse_drive_prefix(cmd); + if (!switch_drive(drive)) { + montauk::print("No such drive: "); + montauk::print(cmd); + montauk::print("/\n"); + } + return; + } + // Builtins if (streq(cmd, "help")) { cmd_help(); diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 18a1067..f301f14 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -85,6 +85,9 @@ ICONS=( "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