feat: GPT, FAT32 driver, disks app, userspace adapted for multiple drives, and more

This commit is contained in:
2026-03-07 15:58:27 +01:00
parent 8c5c259f5d
commit 4d0177d55e
48 changed files with 5777 additions and 873 deletions
+4
View File
@@ -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);
}
};
+123
View File
@@ -0,0 +1,123 @@
/*
* Storage.hpp
* Storage, GPT, format, and mount syscall implementations
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Drivers/Storage/BlockDevice.hpp>
#include <Drivers/Storage/Gpt.hpp>
#include <Fs/FsProbe.hpp>
#include <Fs/Fat32.hpp>
#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, &params->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
}
}
};
+20 -1
View File
@@ -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;
}
+48
View File
@@ -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;
+4 -3
View File
@@ -11,6 +11,7 @@
#include <Drivers/Net/E1000E.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/Storage/Ahci.hpp>
#include <Drivers/Storage/Gpt.hpp>
#include <Graphics/Cursor.hpp>
#include <Net/Net.hpp>
#include <Terminal/Terminal.hpp>
@@ -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();
}
}
+26 -1
View File
@@ -5,6 +5,7 @@
*/
#include "Ahci.hpp"
#include "BlockDevice.hpp"
#include <Pci/Pci.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -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);
}
}
}
@@ -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;
}
};
@@ -0,0 +1,32 @@
/*
* BlockDevice.hpp
* Driver-agnostic block device registry
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
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();
};
+710
View File
@@ -0,0 +1,710 @@
/*
* Gpt.cpp
* GUID Partition Table (GPT) parser
* Copyright (c) 2026 Daniel Hammer
*/
#include "Gpt.hpp"
#include "BlockDevice.hpp"
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/PageFrameAllocator.hpp>
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;
}
};
+147
View File
@@ -0,0 +1,147 @@
/*
* Gpt.hpp
* GUID Partition Table (GPT) parser
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
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);
};
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
/*
* Fat32.hpp
* FAT32 filesystem driver
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Fs/Vfs.hpp>
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);
};
+70
View File
@@ -0,0 +1,70 @@
/*
* FsProbe.cpp
* Filesystem probe registry
* Copyright (c) 2026 Daniel Hammer
*/
#include "FsProbe.hpp"
#include <Drivers/Storage/Gpt.hpp>
#include <Terminal/Terminal.hpp>
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;
}
};
+29
View File
@@ -0,0 +1,29 @@
/*
* FsProbe.hpp
* Filesystem probe registry
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <Fs/Vfs.hpp>
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);
};
+13
View File
@@ -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;
+2
View File
@@ -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);
+8 -1
View File
@@ -33,6 +33,8 @@
#include <Hal/Cpu.hpp>
#include <Fs/Ramdisk.hpp>
#include <Fs/Vfs.hpp>
#include <Fs/Fat32.hpp>
#include <Fs/FsProbe.hpp>
#include <Sched/Scheduler.hpp>
#include <Api/Syscall.hpp>
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();
+13 -3
View File
@@ -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
+48
View File
@@ -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;
+4
View File
@@ -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;
+70 -1
View File
@@ -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 <g fill="..."> 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 <path, <circle, <rect elements — rasterize each individually
// Skip elements inside <defs> 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 </g> closing tags to pop the group fill stack
if (remaining > 3 && svg_strncmp(p, "</g>", 4)) {
if (g_depth > 0) g_depth--;
p += 4;
continue;
}
// Track <g> opening tags for fill inheritance
if (remaining > 2 && svg_strncmp(p, "<g", 2) && (svg_char_is_ws(p[2]) || p[2] == '>')) {
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 <path
if (remaining > 5 && svg_strncmp(p, "<path", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
const char* elem_start = p;
@@ -1244,8 +1303,12 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
continue;
}
// Determine fill color for this element
// Determine fill color: element's own fill, or inherit from nearest <g>
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; }
+32
View File
@@ -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); }
+3
View File
@@ -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)
+3 -798
View File
@@ -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");
}
+11
View File
@@ -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");
}
+175 -17
View File
@@ -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;
+5 -6
View File
@@ -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 {
@@ -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);
+2 -1
View File
@@ -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 },
+1
View File
@@ -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;
}
+3 -1
View File
@@ -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);
+3 -2
View File
@@ -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);
+86
View File
@@ -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)
+153
View File
@@ -0,0 +1,153 @@
/*
* devexplorer.h
* Shared header for the MontaukOS Device Explorer
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 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);
+306
View File
@@ -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;
}
+304
View File
@@ -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);
}
+81
View File
@@ -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);
}
+196
View File
@@ -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);
}
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+86
View File
@@ -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)
+179
View File
@@ -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(&params, 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(&params);
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(&params, 0, sizeof(params));
params.partIndex = dlg.global_part_index;
params.fsType = g_fsTypes[dlg.selected_fs].id;
int r = montauk::fs_format(&params);
if (r == 0) {
set_status("Format complete");
disktool_refresh();
} else {
set_status("Format failed");
}
close_format_dialog();
}
+138
View File
@@ -0,0 +1,138 @@
/*
* disks.h
* Shared header for the MontaukOS Disk Tool
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 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();
+391
View File
@@ -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);
}
+357
View File
@@ -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);
}
+35
View File
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+2 -6
View File
@@ -279,16 +279,12 @@ static bool save_file() {
if (i < numLines - 1) totalSize++; // newline
}
// Try to open existing file first
int handle = montauk::open(path);
if (handle < 0) {
// Create new file
handle = montauk::fcreate(path);
// Create (or truncate existing) file
int handle = montauk::fcreate(path);
if (handle < 0) {
set_status("Error: could not create file");
return false;
}
}
// Build content buffer and write
uint8_t* buf = (uint8_t*)montauk::malloc(totalSize + 1);
+130 -23
View File
@@ -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:/<dir>"
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:/<dir>" 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,12 +302,28 @@ 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; }
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;
}
// If there's a path after the drive, validate it
if (*rel != '\0') {
char path[128];
build_dir_path(rel, path, sizeof(path));
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: ");
@@ -260,7 +331,12 @@ static void cmd_cd(const char* arg) {
montauk::putchar('\n');
return;
}
current_drive = drive;
scopy(cwd, rel, sizeof(cwd));
} else {
current_drive = drive;
cwd[0] = '\0';
}
return;
}
@@ -346,29 +422,40 @@ 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 {
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/<cmd>.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:/<cwd>/<cmd>.elf (if cwd is set)
// 3. Try N:/<cwd>/<cmd>.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,11 +496,19 @@ static void exec_external(const char* cmd, const char* args) {
if (try_exec(path, finalArgs)) return;
}
// 4. Try 0:/<cmd>.elf
// 4. Try N:/<cmd>.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:/<cmd>.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);
@@ -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();
+3
View File
@@ -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