feat: GPT, FAT32 driver, disks app, userspace adapted for multiple drives, and more
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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, ¶ms->typeGuid, sizeof(typeGuid));
|
||||
|
||||
return (int64_t)Drivers::Storage::Gpt::AddPartition(
|
||||
params->blockDev, params->startLba, params->endLba,
|
||||
typeGuid, params->name);
|
||||
}
|
||||
|
||||
// Mount a partition as a VFS drive. Returns 0 on success, -1 on error.
|
||||
static int64_t Sys_FsMount(int partIndex, int driveNum) {
|
||||
return (int64_t)Fs::FsProbe::MountPartition(partIndex, driveNum);
|
||||
}
|
||||
|
||||
// Format a partition with a filesystem. Returns 0 on success, -1 on error.
|
||||
static int64_t Sys_FsFormat(const FsFormatParams* params) {
|
||||
if (params == nullptr) return -1;
|
||||
|
||||
auto* part = Drivers::Storage::Gpt::GetPartition(params->partIndex);
|
||||
if (!part) return -1;
|
||||
|
||||
switch (params->fsType) {
|
||||
case FS_TYPE_FAT32:
|
||||
return (int64_t)Fs::Fat32::Format(
|
||||
part->BlockDevIndex, part->StartLba, part->SectorCount,
|
||||
params->label[0] ? params->label : nullptr);
|
||||
default:
|
||||
return -1; // unknown filesystem type
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* actions.cpp
|
||||
* MontaukOS Disk Tool — disk operations
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "disks.h"
|
||||
|
||||
// ============================================================================
|
||||
// Refresh
|
||||
// ============================================================================
|
||||
|
||||
void disktool_refresh() {
|
||||
auto& dt = g_state;
|
||||
dt.disk_count = 0;
|
||||
for (int port = 0; port < MAX_DISKS; port++) {
|
||||
Montauk::DiskInfo info;
|
||||
montauk::memset(&info, 0, sizeof(info));
|
||||
int r = montauk::diskinfo(&info, port);
|
||||
if (r == 0 && info.type != 0) {
|
||||
dt.disks[dt.disk_count++] = info;
|
||||
}
|
||||
}
|
||||
dt.part_count = montauk::partlist(dt.parts, MAX_PARTS);
|
||||
if (dt.selected_disk >= dt.disk_count)
|
||||
dt.selected_disk = dt.disk_count > 0 ? 0 : -1;
|
||||
dt.selected_part = -1;
|
||||
dt.scroll_y = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Create partition
|
||||
// ============================================================================
|
||||
|
||||
void do_create_partition() {
|
||||
auto& dt = g_state;
|
||||
if (dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) return;
|
||||
|
||||
int part_indices[MAX_PARTS];
|
||||
int nparts = get_disk_parts(part_indices, MAX_PARTS);
|
||||
|
||||
if (nparts == 0) {
|
||||
int r = montauk::gpt_init(dt.selected_disk);
|
||||
if (r < 0) {
|
||||
set_status("Failed to initialize GPT on disk");
|
||||
return;
|
||||
}
|
||||
set_status("Initialized GPT");
|
||||
}
|
||||
|
||||
Montauk::GptAddParams params;
|
||||
montauk::memset(¶ms, 0, sizeof(params));
|
||||
params.blockDev = dt.selected_disk;
|
||||
params.startLba = 0;
|
||||
params.endLba = 0;
|
||||
params.typeGuid.Data1 = 0xEBD0A0A2;
|
||||
params.typeGuid.Data2 = 0xB9E5;
|
||||
params.typeGuid.Data3 = 0x4433;
|
||||
params.typeGuid.Data4[0] = 0x87; params.typeGuid.Data4[1] = 0xC0;
|
||||
params.typeGuid.Data4[2] = 0x68; params.typeGuid.Data4[3] = 0xB6;
|
||||
params.typeGuid.Data4[4] = 0xB7; params.typeGuid.Data4[5] = 0x26;
|
||||
params.typeGuid.Data4[6] = 0x99; params.typeGuid.Data4[7] = 0xC7;
|
||||
|
||||
const char* pname = "Data";
|
||||
int i = 0;
|
||||
for (; pname[i] && i < 71; i++) params.name[i] = pname[i];
|
||||
params.name[i] = '\0';
|
||||
|
||||
int r = montauk::gpt_add(¶ms);
|
||||
if (r < 0) {
|
||||
set_status("Failed to create partition");
|
||||
return;
|
||||
}
|
||||
|
||||
set_status("Partition created successfully");
|
||||
disktool_refresh();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mount partition
|
||||
// ============================================================================
|
||||
|
||||
void do_mount_partition() {
|
||||
auto& dt = g_state;
|
||||
if (dt.selected_part < 0) {
|
||||
set_status("Select a partition first");
|
||||
return;
|
||||
}
|
||||
|
||||
int part_indices[MAX_PARTS];
|
||||
int nparts = get_disk_parts(part_indices, MAX_PARTS);
|
||||
if (dt.selected_part >= nparts) return;
|
||||
|
||||
int global_idx = part_indices[dt.selected_part];
|
||||
int driveNum = 1 + global_idx;
|
||||
if (driveNum >= 16) driveNum = 15;
|
||||
|
||||
int r = montauk::fs_mount(global_idx, driveNum);
|
||||
if (r < 0) {
|
||||
set_status("Mount failed (no recognized filesystem)");
|
||||
return;
|
||||
}
|
||||
|
||||
char msg[80];
|
||||
snprintf(msg, sizeof(msg), "Mounted as drive %d:/", driveNum);
|
||||
set_status(msg);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Format dialog
|
||||
// ============================================================================
|
||||
|
||||
void open_format_dialog() {
|
||||
auto& dt = g_state;
|
||||
if (dt.selected_part < 0) {
|
||||
set_status("Select a partition first");
|
||||
return;
|
||||
}
|
||||
|
||||
int part_indices[MAX_PARTS];
|
||||
int nparts = get_disk_parts(part_indices, MAX_PARTS);
|
||||
if (dt.selected_part >= nparts) return;
|
||||
|
||||
int global_idx = part_indices[dt.selected_part];
|
||||
|
||||
auto& dlg = dt.fmt_dlg;
|
||||
dlg.global_part_index = global_idx;
|
||||
dlg.selected_fs = 0;
|
||||
dlg.hover_format = false;
|
||||
dlg.hover_cancel = false;
|
||||
|
||||
// Build partition description
|
||||
Montauk::PartInfo& p = dt.parts[global_idx];
|
||||
Montauk::DiskInfo& disk = dt.disks[dt.selected_disk];
|
||||
char sz[24];
|
||||
format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog);
|
||||
const char* pname = p.name[0] ? p.name : "Partition";
|
||||
snprintf(dlg.part_desc, sizeof(dlg.part_desc), "%s (%s)", pname, sz);
|
||||
|
||||
// Create dialog window
|
||||
Montauk::WinCreateResult wres;
|
||||
if (montauk::win_create("Format Partition", FMT_DLG_W, FMT_DLG_H, &wres) < 0 || wres.id < 0) {
|
||||
set_status("Failed to open format dialog");
|
||||
return;
|
||||
}
|
||||
dlg.win_id = wres.id;
|
||||
dlg.pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
|
||||
dlg.open = true;
|
||||
|
||||
render_format_window();
|
||||
montauk::win_present(dlg.win_id);
|
||||
}
|
||||
|
||||
void close_format_dialog() {
|
||||
auto& dlg = g_state.fmt_dlg;
|
||||
if (!dlg.open) return;
|
||||
montauk::win_destroy(dlg.win_id);
|
||||
dlg.open = false;
|
||||
}
|
||||
|
||||
void format_dialog_do_format() {
|
||||
auto& dt = g_state;
|
||||
auto& dlg = dt.fmt_dlg;
|
||||
|
||||
Montauk::FsFormatParams params;
|
||||
montauk::memset(¶ms, 0, sizeof(params));
|
||||
params.partIndex = dlg.global_part_index;
|
||||
params.fsType = g_fsTypes[dlg.selected_fs].id;
|
||||
|
||||
int r = montauk::fs_format(¶ms);
|
||||
if (r == 0) {
|
||||
set_status("Format complete");
|
||||
disktool_refresh();
|
||||
} else {
|
||||
set_status("Format failed");
|
||||
}
|
||||
|
||||
close_format_dialog();
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -279,15 +279,11 @@ static bool save_file() {
|
||||
if (i < numLines - 1) totalSize++; // newline
|
||||
}
|
||||
|
||||
// Try to open existing file first
|
||||
int handle = montauk::open(path);
|
||||
// Create (or truncate existing) file
|
||||
int handle = montauk::fcreate(path);
|
||||
if (handle < 0) {
|
||||
// Create new file
|
||||
handle = montauk::fcreate(path);
|
||||
if (handle < 0) {
|
||||
set_status("Error: could not create file");
|
||||
return false;
|
||||
}
|
||||
set_status("Error: could not create file");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build content buffer and write
|
||||
|
||||
+137
-30
@@ -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,20 +302,41 @@ static void cmd_cd(const char* arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cd 0:/path -> absolute VFS path
|
||||
// cd N:/ or cd N:/path -> switch drive and optionally cd into path
|
||||
if (has_drive_prefix(arg)) {
|
||||
const char* rel = arg + 3; // skip "0:/"
|
||||
if (*rel == '\0') { cwd[0] = '\0'; return; }
|
||||
char path[128];
|
||||
build_dir_path(rel, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
int drive = parse_drive_prefix(arg);
|
||||
int plen = drive_prefix_len(arg);
|
||||
const char* rel = arg + plen; // points at ":/" or ":"
|
||||
if (*rel == '/') rel++; // skip the '/'
|
||||
|
||||
// Validate drive exists
|
||||
char rootPath[8];
|
||||
build_drive_path(drive, "", rootPath, sizeof(rootPath));
|
||||
const char* rootEntries[1];
|
||||
if (montauk::readdir(rootPath, rootEntries, 1) < 0) {
|
||||
montauk::print("cd: no such drive: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return;
|
||||
}
|
||||
scopy(cwd, rel, sizeof(cwd));
|
||||
|
||||
// If there's a path after the drive, validate it
|
||||
if (*rel != '\0') {
|
||||
char path[128];
|
||||
build_drive_path(drive, rel, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return;
|
||||
}
|
||||
current_drive = drive;
|
||||
scopy(cwd, rel, sizeof(cwd));
|
||||
} else {
|
||||
current_drive = drive;
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,28 +422,39 @@ static void resolve_args(const char* args, char* out, int outMax) {
|
||||
|
||||
// Decide whether to resolve this token as a path.
|
||||
// Don't resolve if it already has a drive prefix, or starts with '-'
|
||||
bool resolve = cwd[0] && !has_drive_prefix(tokStart) && tokStart[0] != '-';
|
||||
bool resolve = (cwd[0] || current_drive != 0) && !has_drive_prefix(tokStart) && tokStart[0] != '-';
|
||||
|
||||
if (resolve) {
|
||||
// Build candidate resolved path and check if the file exists
|
||||
char candidate[256];
|
||||
int r = 0;
|
||||
candidate[r++] = '0'; candidate[r++] = ':'; candidate[r++] = '/';
|
||||
if (current_drive >= 10) candidate[r++] = '0' + current_drive / 10;
|
||||
candidate[r++] = '0' + current_drive % 10;
|
||||
candidate[r++] = ':'; candidate[r++] = '/';
|
||||
int j = 0;
|
||||
while (cwd[j] && r < 255) candidate[r++] = cwd[j++];
|
||||
if (r < 255) candidate[r++] = '/';
|
||||
if (cwd[0] && r < 255) candidate[r++] = '/';
|
||||
for (int k = 0; k < tokLen && r < 255; k++) candidate[r++] = tokStart[k];
|
||||
candidate[r] = '\0';
|
||||
|
||||
// Only use the resolved path if the file actually exists;
|
||||
// otherwise pass the argument through unchanged (it may be
|
||||
// a hostname, IP address, URL, or other non-path argument).
|
||||
// Use the resolved path if the file exists. If it doesn't
|
||||
// exist, still use the resolved path when the token looks
|
||||
// like a filename (contains a dot) so that programs creating
|
||||
// new files get the correct drive/directory prefix.
|
||||
int h = montauk::open(candidate);
|
||||
if (h >= 0) {
|
||||
montauk::close(h);
|
||||
for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k];
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
bool looksLikeFile = false;
|
||||
for (int k = 0; k < tokLen; k++) {
|
||||
if (tokStart[k] == '.') { looksLikeFile = true; break; }
|
||||
}
|
||||
if (looksLikeFile) {
|
||||
for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k];
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
@@ -386,6 +473,7 @@ static void exec_external(const char* cmd, const char* args) {
|
||||
resolve_args(args, resolvedArgs, sizeof(resolvedArgs));
|
||||
const char* finalArgs = resolvedArgs[0] ? resolvedArgs : nullptr;
|
||||
|
||||
// Always search drive 0 for system commands (os/, games/)
|
||||
// 1. Try 0:/os/<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,12 +496,20 @@ static void exec_external(const char* cmd, const char* args) {
|
||||
if (try_exec(path, finalArgs)) return;
|
||||
}
|
||||
|
||||
// 4. Try 0:/<cmd>.elf
|
||||
scopy(path, "0:/", sizeof(path));
|
||||
// 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);
|
||||
montauk::print(": command not found\n");
|
||||
@@ -440,6 +536,17 @@ static void process_command(const char* line) {
|
||||
if (*args == '\0') args = nullptr;
|
||||
}
|
||||
|
||||
// Bare drive switch: "1:", "2:", etc.
|
||||
if (has_drive_prefix(cmd) && cmd[drive_prefix_len(cmd)] == '\0' && args == nullptr) {
|
||||
int drive = parse_drive_prefix(cmd);
|
||||
if (!switch_drive(drive)) {
|
||||
montauk::print("No such drive: ");
|
||||
montauk::print(cmd);
|
||||
montauk::print("/\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Builtins
|
||||
if (streq(cmd, "help")) {
|
||||
cmd_help();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user