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

This commit is contained in:
2026-03-07 15:58:27 +01:00
parent 8c5c259f5d
commit 4d0177d55e
48 changed files with 5777 additions and 873 deletions
+13 -3
View File
@@ -59,7 +59,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately).
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer desktop
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, games are handled separately.
@@ -84,9 +84,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop icons fonts bearssl libc tls libjpeg
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer desktop icons fonts bearssl libc tls libjpeg
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
# Build BearSSL static library (cross-compiled for freestanding x86_64).
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
@@ -143,6 +143,14 @@ spreadsheet: libc
pdfviewer: libc
$(MAKE) -C src/pdfviewer
# Build disks standalone GUI tool (depends on libc).
disks: libc
$(MAKE) -C src/disks
# Build devexplorer standalone GUI tool (depends on libc).
devexplorer: libc
$(MAKE) -C src/devexplorer
# Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
desktop: libc libjpeg
$(MAKE) -C src/desktop
@@ -205,3 +213,5 @@ clean:
$(MAKE) -C src/fontpreview clean
$(MAKE) -C src/spreadsheet clean
$(MAKE) -C src/pdfviewer clean
$(MAKE) -C src/disks clean
$(MAKE) -C src/devexplorer clean
+48
View File
@@ -54,6 +54,7 @@ namespace Montauk {
static constexpr uint64_t SYS_RECVFROM = 40;
static constexpr uint64_t SYS_FWRITE = 41;
static constexpr uint64_t SYS_FCREATE = 42;
static constexpr uint64_t SYS_FDELETE = 77;
static constexpr uint64_t SYS_TERMSCALE = 43;
static constexpr uint64_t SYS_RESOLVE = 44;
static constexpr uint64_t SYS_GETRANDOM = 45;
@@ -88,6 +89,15 @@ namespace Montauk {
// Kernel introspection syscalls
static constexpr uint64_t SYS_MEMSTATS = 67;
// Storage / partition syscalls
static constexpr uint64_t SYS_PARTLIST = 70;
static constexpr uint64_t SYS_DISKREAD = 71;
static constexpr uint64_t SYS_DISKWRITE = 72;
static constexpr uint64_t SYS_GPTINIT = 73;
static constexpr uint64_t SYS_GPTADD = 74;
static constexpr uint64_t SYS_FSMOUNT = 75;
static constexpr uint64_t SYS_FSFORMAT = 76;
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
@@ -198,6 +208,44 @@ namespace Montauk {
char _pad2[1];
};
struct PartGuid {
uint32_t Data1;
uint16_t Data2;
uint16_t Data3;
uint8_t Data4[8];
};
struct PartInfo {
int32_t blockDev; // block device index
uint32_t _pad0;
uint64_t startLba;
uint64_t endLba;
uint64_t sectorCount;
PartGuid typeGuid;
PartGuid uniqueGuid;
uint64_t attributes;
char name[72]; // ASCII partition name
char typeName[24]; // human-readable type name
};
struct GptAddParams {
int32_t blockDev;
uint32_t _pad0;
uint64_t startLba; // 0 = auto (fill largest free region)
uint64_t endLba; // 0 = auto
PartGuid typeGuid;
char name[72];
};
// Filesystem type IDs for SYS_FSFORMAT
static constexpr int FS_TYPE_FAT32 = 1;
struct FsFormatParams {
int32_t partIndex; // global partition index
int32_t fsType; // FS_TYPE_FAT32, etc.
char label[32]; // volume label
};
struct ProcInfo {
int32_t pid;
int32_t parentPid;
+4
View File
@@ -76,6 +76,9 @@ struct DesktopState {
SvgIcon icon_folder_lg;
SvgIcon icon_file_lg;
SvgIcon icon_exec_lg;
SvgIcon icon_drive;
SvgIcon icon_drive_lg;
SvgIcon icon_delete;
SvgIcon icon_settings;
SvgIcon icon_reboot;
@@ -88,6 +91,7 @@ struct DesktopState {
SvgIcon icon_mandelbrot;
SvgIcon icon_devexplorer;
SvgIcon icon_spreadsheet;
SvgIcon icon_disks;
bool ctx_menu_open;
int ctx_menu_x, ctx_menu_y;
+70 -1
View File
@@ -161,6 +161,23 @@ inline int svg_parse_fixed(const char* s, fixed_t* out) {
val += (int32_t)(((int64_t)frac << 16) / frac_div);
}
// Scientific notation (e.g. 2e-3, 7e-3)
if (*p == 'e' || *p == 'E') {
++p;
bool exp_neg = false;
if (*p == '-') { exp_neg = true; ++p; }
else if (*p == '+') { ++p; }
int exp_val = 0;
while (*p >= '0' && *p <= '9') {
exp_val = exp_val * 10 + (*p - '0');
++p;
}
for (int i = 0; i < exp_val; i++) {
if (exp_neg) val /= 10;
else val *= 10;
}
}
if (neg) val = -val;
*out = val;
return (int)(p - s);
@@ -1207,6 +1224,12 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
// Heap-allocated path data buffer (avoids 8 KiB on the stack)
char* d_buf = (char*)montauk::malloc(SVG_MAX_PATH_LEN);
// Group fill inheritance stack: track <g fill="..."> so child elements inherit
static constexpr int MAX_GROUP_DEPTH = 8;
struct GroupFill { Color color; bool has_fill; };
GroupFill g_stack[MAX_GROUP_DEPTH];
int g_depth = 0;
// Scan for <path, <circle, <rect elements — rasterize each individually
// Skip elements inside <defs> blocks (they define reusable items, not rendered directly)
const char* p = svg_data;
@@ -1230,6 +1253,42 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
continue;
}
// Track </g> closing tags to pop the group fill stack
if (remaining > 3 && svg_strncmp(p, "</g>", 4)) {
if (g_depth > 0) g_depth--;
p += 4;
continue;
}
// Track <g> opening tags for fill inheritance
if (remaining > 2 && svg_strncmp(p, "<g", 2) && (svg_char_is_ws(p[2]) || p[2] == '>')) {
const char* g_start = p;
const char* g_end = p;
while (g_end < end && *g_end != '>') ++g_end;
if (g_end < end) ++g_end;
int g_len = (int)(g_end - g_start);
if (g_depth < MAX_GROUP_DEPTH) {
g_stack[g_depth].has_fill = false;
Color gc = fill_color;
char fbuf[64];
int fLen = svg_get_attr(g_start, g_len, " fill", fbuf, sizeof(fbuf));
if (fLen > 0) {
int fr = svg_resolve_fill_value(fbuf, &gc, &grads);
if (fr == 1) {
g_stack[g_depth].color = gc;
g_stack[g_depth].has_fill = true;
} else if (fr == -1) {
// fill="none" on group — children inherit none
g_stack[g_depth].has_fill = false;
}
}
g_depth++;
}
p = g_end;
continue;
}
// Check for <path
if (remaining > 5 && svg_strncmp(p, "<path", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
const char* elem_start = p;
@@ -1244,8 +1303,12 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
continue;
}
// Determine fill color for this element
// Determine fill color: element's own fill, or inherit from nearest <g>
Color elem_color = fill_color;
// Apply inherited group fill as default
for (int gi = g_depth - 1; gi >= 0; gi--) {
if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; }
}
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css);
if (fillResult == -1) { p = elem_end; continue; } // fill="none"
@@ -1278,6 +1341,9 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
}
Color elem_color = fill_color;
for (int gi = g_depth - 1; gi >= 0; gi--) {
if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; }
}
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css);
if (fillResult == -1) { p = elem_end; continue; }
@@ -1321,6 +1387,9 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
}
Color elem_color = fill_color;
for (int gi = g_depth - 1; gi >= 0; gi--) {
if (g_stack[gi].has_fill) { elem_color = g_stack[gi].color; break; }
}
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css);
if (fillResult == -1) { p = elem_end; continue; }
+32
View File
@@ -143,6 +143,9 @@ namespace montauk {
inline int fcreate(const char* path) {
return (int)syscall1(Montauk::SYS_FCREATE, (uint64_t)path);
}
inline int fdelete(const char* path) {
return (int)syscall1(Montauk::SYS_FDELETE, (uint64_t)path);
}
// Memory
inline void* alloc(uint64_t size) { return (void*)syscall1(Montauk::SYS_ALLOC, size); }
@@ -298,6 +301,35 @@ namespace montauk {
return (int)syscall2(Montauk::SYS_DISKINFO, (uint64_t)buf, (uint64_t)port);
}
// Partition table
inline int partlist(Montauk::PartInfo* buf, int max) {
return (int)syscall2(Montauk::SYS_PARTLIST, (uint64_t)buf, (uint64_t)max);
}
// Raw block device I/O (driver-agnostic)
inline int64_t disk_read(int blockDev, uint64_t lba, uint32_t sectorCount, void* buf) {
return syscall4(Montauk::SYS_DISKREAD, (uint64_t)blockDev, lba,
(uint64_t)sectorCount, (uint64_t)buf);
}
inline int64_t disk_write(int blockDev, uint64_t lba, uint32_t sectorCount, const void* buf) {
return syscall4(Montauk::SYS_DISKWRITE, (uint64_t)blockDev, lba,
(uint64_t)sectorCount, (uint64_t)buf);
}
// GPT management
inline int gpt_init(int blockDev) {
return (int)syscall1(Montauk::SYS_GPTINIT, (uint64_t)blockDev);
}
inline int gpt_add(const Montauk::GptAddParams* params) {
return (int)syscall1(Montauk::SYS_GPTADD, (uint64_t)params);
}
inline int fs_mount(int partIndex, int driveNum) {
return (int)syscall2(Montauk::SYS_FSMOUNT, (uint64_t)partIndex, (uint64_t)driveNum);
}
inline int fs_format(const Montauk::FsFormatParams* params) {
return (int)syscall1(Montauk::SYS_FSFORMAT, (uint64_t)params);
}
// Kernel introspection
inline void memstats(Montauk::MemStats* out) { syscall1(Montauk::SYS_MEMSTATS, (uint64_t)out); }
+3
View File
@@ -46,6 +46,7 @@ CXXFLAGS := \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-MMD -MP \
-I $(PROG_INC) \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
@@ -94,5 +95,7 @@ $(OBJDIR)/%.o: apps/%.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
clean:
rm -rf $(OBJDIR) $(TARGET)
+3 -798
View File
@@ -1,807 +1,12 @@
/*
* app_devexplorer.cpp
* MontaukOS Desktop - Device Explorer
* MontaukOS Desktop - Device Explorer launcher (spawns standalone devexplorer.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Device Explorer state
// ============================================================================
static constexpr int DE_TOOLBAR_H = 36;
static constexpr int DE_CAT_H = 28;
static constexpr int DE_ITEM_H = 24;
static constexpr int DE_MAX_DEVS = 64;
static constexpr int DE_POLL_MS = 2000;
static constexpr int DE_INDENT = 28;
// Category names matching DevInfo.category values
static const char* category_names[] = {
"CPU", // 0
"Interrupt", // 1
"Timer", // 2
"Input", // 3
"USB", // 4
"Network", // 5
"Display", // 6
"Storage", // 7
"PCI", // 8
};
static constexpr int NUM_CATEGORIES = 9;
static Color category_colors[] = {
Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue
Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple
Color::from_rgb(0x22, 0x88, 0x22), // Timer - green
Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber
Color::from_rgb(0x00, 0x88, 0x88), // USB - teal
Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange
Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo
Color::from_rgb(0x99, 0x55, 0x00), // Storage - brown
Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray
};
struct DevExplorerState {
DesktopState* desktop;
Montauk::DevInfo devs[DE_MAX_DEVS];
int dev_count;
bool collapsed[NUM_CATEGORIES]; // per-category collapse state
int selected_row; // index into visible display rows (-1 = none)
int scroll_y; // scroll offset in display rows
uint64_t last_poll_ms;
// Double-click tracking
int last_click_row;
uint64_t last_click_ms;
};
// ============================================================================
// Display row model
// ============================================================================
enum RowType { ROW_CATEGORY, ROW_DEVICE };
struct DisplayRow {
RowType type;
int category; // category index (0..7)
int dev_index; // index into devs[] (only valid for ROW_DEVICE)
};
static constexpr int MAX_DISPLAY_ROWS = DE_MAX_DEVS + NUM_CATEGORIES;
static int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
int count = 0;
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
// Count devices in this category
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) cat_count++;
}
if (cat_count == 0) continue;
// Emit category header
rows[count].type = ROW_CATEGORY;
rows[count].category = cat;
rows[count].dev_index = -1;
count++;
// Emit device rows if expanded
if (!de->collapsed[cat]) {
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) {
rows[count].type = ROW_DEVICE;
rows[count].category = cat;
rows[count].dev_index = d;
count++;
}
}
}
}
return count;
}
// ============================================================================
// Triangle drawing helpers
// ============================================================================
static void draw_triangle_right(Canvas& c, int x, int y, int size, Color col) {
// Right-pointing filled triangle (▶)
int half = size / 2;
for (int row = 0; row < size; row++) {
int dist = (row <= half) ? row : (size - 1 - row);
for (int col_px = 0; col_px <= dist; col_px++) {
c.put_pixel(x + col_px, y + row, col);
}
}
}
static void draw_triangle_down(Canvas& c, int x, int y, int size, Color col) {
// Down-pointing filled triangle (▼)
int half = size / 2;
for (int row = 0; row < half + 1; row++) {
int w = size - row * 2;
for (int col_px = 0; col_px < w; col_px++) {
c.put_pixel(x + row + col_px, y + row, col);
}
}
}
// ============================================================================
// Callbacks
// ============================================================================
static void devexplorer_on_poll(Window* win) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
uint64_t now = montauk::get_milliseconds();
if (now - de->last_poll_ms < DE_POLL_MS) return;
de->last_poll_ms = now;
de->dev_count = montauk::devlist(de->devs, DE_MAX_DEVS);
}
static void devexplorer_on_draw(Window* win, Framebuffer& fb) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int fh = system_font_height();
// --- Toolbar ---
c.fill_rect(0, 0, c.w, DE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, DE_TOOLBAR_H - 1, c.w, colors::BORDER);
// "Refresh" button
int btn_w = 80;
int btn_h = 26;
int btn_x = 8;
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
c.button(btn_x, btn_y, btn_w, btn_h, "Refresh",
Color::from_rgb(0x33, 0x66, 0xCC), colors::WHITE, 4);
// Device count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d devices", de->dev_count);
int cw = text_width(count_str);
c.text(c.w - cw - 12, (DE_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
// --- Build display rows ---
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
// --- Compute visible area ---
int list_y = DE_TOOLBAR_H;
int list_h = c.h - list_y;
if (list_h < 1) return;
// Clamp scroll
// Calculate total content height
int total_h = 0;
for (int i = 0; i < row_count; i++) {
total_h += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
int max_scroll_px = total_h - list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
// Convert scroll_y (in rows) to pixel offset for simplicity,
// but keep the row-based model: scroll_y = row offset
// We'll compute cumulative pixel offsets
int scroll_px = 0;
for (int i = 0; i < de->scroll_y && i < row_count; i++) {
scroll_px += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
if (scroll_px > max_scroll_px) {
scroll_px = max_scroll_px;
// Recalculate scroll_y to match
de->scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de->scroll_y = i + 1;
}
}
if (de->scroll_y < 0) de->scroll_y = 0;
// Clamp selected_row
if (de->selected_row >= row_count) de->selected_row = row_count - 1;
// --- Draw rows ---
int draw_y = list_y - scroll_px;
// Advance to first visible area by recomputing from scroll offset
draw_y = list_y;
int first_visible = de->scroll_y;
// Compute starting y by accumulating heights of skipped rows
// Actually, let's just iterate all rows and skip those above the viewport
draw_y = list_y;
for (int i = 0; i < first_visible && i < row_count; i++) {
draw_y -= (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
// Draw from first_visible onward
int cur_y = list_y;
for (int i = first_visible; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (cur_y >= c.h) break; // Past bottom of window
if (rows[i].type == ROW_CATEGORY) {
// Category header
int cat = rows[i].category;
c.fill_rect(0, cur_y, c.w, DE_CAT_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Highlight if selected
if (i == de->selected_row) {
c.fill_rect(0, cur_y, c.w, DE_CAT_H, colors::MENU_HOVER);
}
// Expand/collapse triangle
int tri_x = 10;
int tri_y = cur_y + (DE_CAT_H - 8) / 2;
Color tri_color = Color::from_rgb(0x55, 0x55, 0x55);
if (de->collapsed[cat]) {
draw_triangle_right(c, tri_x, tri_y, 8, tri_color);
} else {
draw_triangle_down(c, tri_x, tri_y, 8, tri_color);
}
// Colored dot
int dot_x = 24;
int dot_y = cur_y + (DE_CAT_H - 8) / 2;
Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES)
? category_colors[cat] : colors::TEXT_COLOR;
c.fill_rounded_rect(dot_x, dot_y, 8, 8, 4, cat_col);
// Category name
const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES)
? category_names[cat] : "?";
int text_y = cur_y + (DE_CAT_H - fh) / 2;
c.text(36, text_y, cat_name, Color::from_rgb(0x33, 0x33, 0x33));
// Device count in parentheses on right
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) cat_count++;
}
char cnt_buf[16];
snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count);
int name_w = text_width(cat_name);
c.text(36 + name_w + 8, text_y, cnt_buf,
Color::from_rgb(0x88, 0x88, 0x88));
// Bottom border
c.hline(0, cur_y + DE_CAT_H - 1, c.w, Color::from_rgb(0xE0, 0xE0, 0xE0));
} else {
// Device item row
int di = rows[i].dev_index;
// Selected row highlight
if (i == de->selected_row) {
c.fill_rect(0, cur_y, c.w, DE_ITEM_H, colors::MENU_HOVER);
}
int text_y = cur_y + (DE_ITEM_H - fh) / 2;
// Device name (indented)
c.text(DE_INDENT + 10, text_y, de->devs[di].name, colors::TEXT_COLOR);
// Detail string (right column, gray)
int detail_x = c.w / 2 + 20;
c.text(detail_x, text_y, de->devs[di].detail,
Color::from_rgb(0x66, 0x66, 0x66));
}
cur_y += row_h;
}
// --- Scrollbar ---
if (total_h > list_h) {
int sb_x = c.w - 6;
int thumb_h = (list_h * list_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px;
c.fill_rect(sb_x, list_y, 4, list_h, Color::from_rgb(0xE0, 0xE0, 0xE0));
c.fill_rect(sb_x, thumb_y, 4, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA));
}
}
// ============================================================================
// Disk Detail Window
// ============================================================================
static constexpr int DD_TAB_BAR_H = 32;
static constexpr int DD_TAB_COUNT = 2;
static const char* dd_tab_labels[DD_TAB_COUNT] = { "General", "Features" };
struct DiskDetailState {
DesktopState* desktop;
Montauk::DiskInfo info;
int active_tab;
};
// Helper: format uint64 to string (snprintf %d can't handle 64-bit)
static void fmt_u64(char* buf, int bufsize, uint64_t v) {
if (v == 0) { buf[0] = '0'; buf[1] = '\0'; return; }
char tmp[24]; int i = 0;
while (v > 0) { tmp[i++] = '0' + (int)(v % 10); v /= 10; }
int j = 0;
while (i > 0 && j < bufsize - 1) buf[j++] = tmp[--i];
buf[j] = '\0';
}
// Draw a two-column table row
static void dd_table_row(Canvas& c, int x, int y, int col1_w, int row_h,
const char* key, const char* value, Color key_col, Color val_col) {
int fh = system_font_height();
int ty = y + (row_h - fh) / 2;
c.text(x + 8, ty, key, key_col);
c.text(x + col1_w + 8, ty, value, val_col);
}
// Draw a feature row with colored status indicator
static void dd_feature_row(Canvas& c, int x, int y, int w, int row_h,
const char* label, bool supported, const char* extra) {
int fh = system_font_height();
int ty = y + (row_h - fh) / 2;
// Status dot
int dot_y = y + (row_h - 8) / 2;
Color dot_col = supported ? Color::from_rgb(0x22, 0x88, 0x22)
: Color::from_rgb(0xBB, 0xBB, 0xBB);
c.fill_rounded_rect(x + 10, dot_y, 8, 8, 4, dot_col);
// Label
Color text_col = supported ? colors::TEXT_COLOR : Color::from_rgb(0xAA, 0xAA, 0xAA);
c.text(x + 26, ty, label, text_col);
// Extra info on right
if (extra && extra[0]) {
int ew = text_width(extra);
c.text(x + w - ew - 12, ty, extra, Color::from_rgb(0x66, 0x66, 0x66));
}
}
static void diskdetail_draw_general(Canvas& c, DiskDetailState* dd) {
char line[128];
int fh = system_font_height();
int x = 0;
int y = 12;
int w = c.w;
int col1_w = 110;
int row_h = fh + 8;
Color key_col = Color::from_rgb(0x66, 0x66, 0x66);
Color val_col = colors::TEXT_COLOR;
Color hdr_col = colors::ACCENT;
Color border = Color::from_rgb(0xE0, 0xE0, 0xE0);
// --- Identification section ---
c.text(x + 8, y, "Identification", hdr_col);
y += fh + 8;
c.hline(x + 8, y, w - 16, border);
y += 1;
// Table rows with alternating backgrounds
auto row_bg = [&](int ry) {
static int rowIdx = 0;
if (rowIdx++ % 2 == 0)
c.fill_rect(x + 8, ry, w - 16, row_h, Color::from_rgb(0xF7, 0xF7, 0xF7));
};
int rowIdx = 0;
auto table_row = [&](const char* key, const char* val) {
if (rowIdx++ % 2 == 0)
c.fill_rect(x + 8, y, w - 16, row_h, Color::from_rgb(0xF7, 0xF7, 0xF7));
dd_table_row(c, x, y, col1_w, row_h, key, val, key_col, val_col);
y += row_h;
};
table_row("Model", dd->info.model);
table_row("Serial", dd->info.serial);
table_row("Firmware", dd->info.firmware);
const char* typeStr = "Unknown";
if (dd->info.type == 1) typeStr = "SATA";
else if (dd->info.type == 2) typeStr = "SATAPI";
table_row("Type", typeStr);
snprintf(line, sizeof(line), "%d", (int)dd->info.port);
table_row("AHCI Port", line);
c.hline(x + 8, y, w - 16, border);
y += 12;
// --- Capacity section ---
c.text(x + 8, y, "Capacity", hdr_col);
y += fh + 8;
c.hline(x + 8, y, w - 16, border);
y += 1;
rowIdx = 0;
uint64_t totalBytes = dd->info.sectorCount * (uint64_t)dd->info.sectorSizeLog;
uint64_t totalMB = totalBytes / (1024 * 1024);
uint64_t totalGB = totalMB / 1024;
if (totalGB > 0) {
int fracGB = (int)((totalMB % 1024) * 10 / 1024);
snprintf(line, sizeof(line), "%d.%d GiB", (int)totalGB, fracGB);
} else {
snprintf(line, sizeof(line), "%d MiB", (int)totalMB);
}
table_row("Size", line);
char secbuf[24];
fmt_u64(secbuf, sizeof(secbuf), dd->info.sectorCount);
table_row("Sectors", secbuf);
snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizeLog);
table_row("Logical", line);
snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizePhys);
table_row("Physical", line);
c.hline(x + 8, y, w - 16, border);
y += 12;
// --- Interface section ---
c.text(x + 8, y, "Interface", hdr_col);
y += fh + 8;
c.hline(x + 8, y, w - 16, border);
y += 1;
rowIdx = 0;
const char* sataSpeed = "Unknown";
if (dd->info.sataGen == 1) sataSpeed = "SATA I (1.5 Gb/s)";
else if (dd->info.sataGen == 2) sataSpeed = "SATA II (3.0 Gb/s)";
else if (dd->info.sataGen == 3) sataSpeed = "SATA III (6.0 Gb/s)";
table_row("Link Speed", sataSpeed);
if (dd->info.rpm == 0)
table_row("Media", "Not reported");
else if (dd->info.rpm == 1)
table_row("Media", "Solid State (SSD)");
else {
snprintf(line, sizeof(line), "%d RPM", (int)dd->info.rpm);
table_row("Media", line);
}
c.hline(x + 8, y, w - 16, border);
}
static void diskdetail_draw_features(Canvas& c, DiskDetailState* dd) {
char line[64];
int fh = system_font_height();
int x = 0;
int y = 12;
int w = c.w;
int row_h = fh + 10;
Color hdr_col = colors::ACCENT;
Color border = Color::from_rgb(0xE0, 0xE0, 0xE0);
c.text(x + 8, y, "Supported Features", hdr_col);
y += fh + 8;
c.hline(x + 8, y, w - 16, border);
y += 4;
dd_feature_row(c, x, y, w, row_h, "48-bit LBA", dd->info.supportsLba48, nullptr);
y += row_h;
if (dd->info.supportsNcq) {
snprintf(line, sizeof(line), "Depth: %d", (int)dd->info.ncqDepth);
dd_feature_row(c, x, y, w, row_h, "Native Command Queuing (NCQ)", true, line);
} else {
dd_feature_row(c, x, y, w, row_h, "Native Command Queuing (NCQ)", false, nullptr);
}
y += row_h;
dd_feature_row(c, x, y, w, row_h, "TRIM (Data Set Management)", dd->info.supportsTrim, nullptr);
y += row_h;
dd_feature_row(c, x, y, w, row_h, "S.M.A.R.T.", dd->info.supportsSmart, nullptr);
y += row_h;
dd_feature_row(c, x, y, w, row_h, "Write Cache", dd->info.supportsWriteCache, nullptr);
y += row_h;
dd_feature_row(c, x, y, w, row_h, "Read Look-Ahead", dd->info.supportsReadAhead, nullptr);
y += row_h;
y += 4;
c.hline(x + 8, y, w - 16, border);
y += 12;
// Legend
Color legend_col = Color::from_rgb(0x88, 0x88, 0x88);
int ly = y + (fh - 8) / 2;
c.fill_rounded_rect(x + 10, ly, 8, 8, 4, Color::from_rgb(0x22, 0x88, 0x22));
c.text(x + 26, y, "Supported", legend_col);
c.fill_rounded_rect(x + 120, ly, 8, 8, 4, Color::from_rgb(0xBB, 0xBB, 0xBB));
c.text(x + 136, y, "Not supported", legend_col);
}
static void diskdetail_on_draw(Window* win, Framebuffer& fb) {
DiskDetailState* dd = (DiskDetailState*)win->app_data;
if (!dd) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int sfh = system_font_height();
Color accent = colors::ACCENT;
// --- Tab bar ---
c.fill_rect(0, 0, c.w, DD_TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, DD_TAB_BAR_H - 1, c.w, colors::BORDER);
int tab_w = c.w / DD_TAB_COUNT;
for (int i = 0; i < DD_TAB_COUNT; i++) {
int tx = i * tab_w;
bool active = (i == dd->active_tab);
if (active) {
c.fill_rect(tx, 0, tab_w, DD_TAB_BAR_H, colors::WINDOW_BG);
c.fill_rect(tx + 4, DD_TAB_BAR_H - 3, tab_w - 8, 3, accent);
}
int tw = text_width(dd_tab_labels[i]);
Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66);
c.text(tx + (tab_w - tw) / 2, (DD_TAB_BAR_H - sfh) / 2, dd_tab_labels[i], tc);
}
// --- Tab content ---
Canvas content(win->content + DD_TAB_BAR_H * win->content_w,
win->content_w, win->content_h - DD_TAB_BAR_H);
switch (dd->active_tab) {
case 0: diskdetail_draw_general(content, dd); break;
case 1: diskdetail_draw_features(content, dd); break;
}
}
static void diskdetail_on_mouse(Window* win, MouseEvent& ev) {
DiskDetailState* dd = (DiskDetailState*)win->app_data;
if (!dd || !ev.left_pressed()) return;
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
// Tab bar click
if (my >= 0 && my < DD_TAB_BAR_H) {
int tab_w = win->content_w / DD_TAB_COUNT;
int tab = mx / tab_w;
if (tab >= 0 && tab < DD_TAB_COUNT) {
dd->active_tab = tab;
}
}
}
static void diskdetail_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
static void open_disk_detail(DesktopState* ds, int port, const char* model) {
char title[64];
int i = 0;
const char* prefix = "Disk: ";
while (prefix[i]) { title[i] = prefix[i]; i++; }
int j = 0;
while (model[j] && i < 62) { title[i++] = model[j++]; }
title[i] = '\0';
int idx = desktop_create_window(ds, title, 180, 90, 440, 420);
if (idx < 0) return;
Window* win = &ds->windows[idx];
DiskDetailState* dd = (DiskDetailState*)montauk::malloc(sizeof(DiskDetailState));
montauk::memset(dd, 0, sizeof(DiskDetailState));
dd->desktop = ds;
dd->active_tab = 0;
montauk::diskinfo(&dd->info, port);
win->app_data = dd;
win->on_draw = diskdetail_on_draw;
win->on_mouse = diskdetail_on_mouse;
win->on_close = diskdetail_on_close;
}
// ============================================================================
// Callbacks
// ============================================================================
static void devexplorer_on_mouse(Window* win, MouseEvent& ev) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
// Scroll
if (ev.scroll != 0) {
de->scroll_y += ev.scroll;
if (de->scroll_y < 0) de->scroll_y = 0;
return;
}
if (ev.left_pressed()) {
// Check "Refresh" button click
int btn_w = 80;
int btn_h = 26;
int btn_x = 8;
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
if (btn_rect.contains(lx, ly)) {
de->last_poll_ms = 0; // force refresh
return;
}
// Check row click in list area
int list_y = DE_TOOLBAR_H;
if (ly >= list_y) {
// Build display rows to map click to row
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
// Walk rows from scroll offset, accumulating y
uint64_t now = montauk::get_milliseconds();
int cur_y = list_y;
for (int i = de->scroll_y; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (ly >= cur_y && ly < cur_y + row_h) {
// Hit this row
if (rows[i].type == ROW_CATEGORY) {
// Toggle collapse
int cat = rows[i].category;
de->collapsed[cat] = !de->collapsed[cat];
de->selected_row = -1;
de->last_click_row = -1;
} else {
// Check for double-click on storage device
int di = rows[i].dev_index;
bool is_double = (de->last_click_row == i)
&& (now - de->last_click_ms < 400);
if (is_double && de->devs[di].category == 7) {
// Storage device — open detail window
int port = (int)de->devs[di]._pad[0];
open_disk_detail(de->desktop, port, de->devs[di].name);
de->last_click_row = -1;
} else {
de->selected_row = i;
de->last_click_row = i;
de->last_click_ms = now;
}
}
return;
}
cur_y += row_h;
if (cur_y >= win->content_h) break;
}
// Clicked below all rows
de->selected_row = -1;
de->last_click_row = -1;
}
}
}
static void devexplorer_on_key(Window* win, const Montauk::KeyEvent& key) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de || !key.pressed) return;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
if (row_count == 0) return;
if (key.scancode == 0x48) { // Up arrow
if (de->selected_row <= 0) {
de->selected_row = 0;
} else {
de->selected_row--;
}
// Scroll to keep selection visible
if (de->selected_row < de->scroll_y) {
de->scroll_y = de->selected_row;
}
} else if (key.scancode == 0x50) { // Down arrow
if (de->selected_row < row_count - 1) {
de->selected_row++;
}
// Scroll to keep selection visible — estimate visible rows
int list_h = win->content_h - DE_TOOLBAR_H;
int cur_h = 0;
int last_visible = de->scroll_y;
for (int i = de->scroll_y; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (cur_h + rh > list_h) break;
cur_h += rh;
last_visible = i;
}
if (de->selected_row > last_visible) {
de->scroll_y += (de->selected_row - last_visible);
}
} else if (key.scancode == 0x4B) { // Left arrow — collapse
if (de->selected_row >= 0 && de->selected_row < row_count) {
int cat = rows[de->selected_row].category;
if (!de->collapsed[cat]) {
de->collapsed[cat] = true;
// If we were on a device row, move selection to the category header
if (rows[de->selected_row].type == ROW_DEVICE) {
// Find the category header row for this category
for (int i = de->selected_row - 1; i >= 0; i--) {
if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) {
de->selected_row = i;
break;
}
}
// After collapsing, rebuild and clamp
int new_count = build_display_rows(de, rows);
if (de->selected_row >= new_count)
de->selected_row = new_count - 1;
}
}
}
} else if (key.scancode == 0x4D) { // Right arrow — expand
if (de->selected_row >= 0 && de->selected_row < row_count) {
int cat = rows[de->selected_row].category;
de->collapsed[cat] = false;
}
} else if (key.scancode == 0x1C) { // Enter — toggle on category row
if (de->selected_row >= 0 && de->selected_row < row_count) {
if (rows[de->selected_row].type == ROW_CATEGORY) {
int cat = rows[de->selected_row].category;
de->collapsed[cat] = !de->collapsed[cat];
}
}
}
}
static void devexplorer_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Device Explorer launcher
// ============================================================================
void open_devexplorer(DesktopState* ds) {
int idx = desktop_create_window(ds, "Devices", 140, 70, 640, 460);
if (idx < 0) return;
Window* win = &ds->windows[idx];
DevExplorerState* de = (DevExplorerState*)montauk::malloc(sizeof(DevExplorerState));
montauk::memset(de, 0, sizeof(DevExplorerState));
de->desktop = ds;
de->selected_row = -1;
de->scroll_y = 0;
de->last_poll_ms = 0;
de->last_click_row = -1;
de->last_click_ms = 0;
// All categories start expanded
for (int i = 0; i < NUM_CATEGORIES; i++)
de->collapsed[i] = false;
// Initial poll
de->dev_count = montauk::devlist(de->devs, DE_MAX_DEVS);
win->app_data = de;
win->on_draw = devexplorer_on_draw;
win->on_mouse = devexplorer_on_mouse;
win->on_key = devexplorer_on_key;
win->on_close = devexplorer_on_close;
win->on_poll = devexplorer_on_poll;
(void)ds;
montauk::spawn("0:/os/devexplorer.elf");
}
+11
View File
@@ -0,0 +1,11 @@
/*
* app_disks.cpp
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_disks(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/disks.elf");
}
+175 -17
View File
@@ -10,13 +10,15 @@
// File Manager state
// ============================================================================
static constexpr int FM_MAX_DRIVES = 16;
struct FileManagerState {
char current_path[256];
char history[16][256];
int history_pos;
int history_count;
char entry_names[64][64];
int entry_types[64]; // 0=file, 1=directory, 2=executable
int entry_types[64]; // 0=file, 1=directory, 2=executable, 3=drive
int entry_sizes[64];
int entry_count;
int selected;
@@ -27,6 +29,8 @@ struct FileManagerState {
Scrollbar scrollbar;
DesktopState* desktop;
bool grid_view;
bool at_drives_root;
int drive_indices[FM_MAX_DRIVES]; // which drive number each entry maps to
};
static constexpr int FM_TOOLBAR_H = 32;
@@ -83,7 +87,51 @@ static int detect_file_type(const char* name, bool is_dir) {
// Directory reading with sorting and file sizes
// ============================================================================
static void filemanager_read_drives(FileManagerState* fm) {
fm->entry_count = 0;
fm->at_drives_root = true;
fm->current_path[0] = '\0';
for (int d = 0; d < FM_MAX_DRIVES; d++) {
char probe[8];
if (d < 10) {
probe[0] = '0' + d;
probe[1] = ':';
probe[2] = '/';
probe[3] = '\0';
} else {
probe[0] = '1';
probe[1] = '0' + (d - 10);
probe[2] = ':';
probe[3] = '/';
probe[4] = '\0';
}
const char* tmp[1];
int r = montauk::readdir(probe, tmp, 1);
if (r >= 0) {
int i = fm->entry_count;
char label[64];
montauk::strcpy(label, "Drive ");
str_append(label, probe, 64);
montauk::strncpy(fm->entry_names[i], label, 63);
fm->entry_types[i] = 3; // drive
fm->entry_sizes[i] = 0;
fm->is_dir[i] = true;
fm->drive_indices[i] = d;
fm->entry_count++;
if (fm->entry_count >= 64) break;
}
}
fm->selected = -1;
fm->scroll_offset = 0;
fm->scrollbar.scroll_offset = 0;
fm->last_click_item = -1;
fm->last_click_time = 0;
}
static void filemanager_read_dir(FileManagerState* fm) {
fm->at_drives_root = false;
const char* names[64];
fm->entry_count = montauk::readdir(fm->current_path, names, 64);
if (fm->entry_count < 0) fm->entry_count = 0;
@@ -218,8 +266,25 @@ static void filemanager_navigate(FileManagerState* fm, const char* name) {
}
static void filemanager_go_up(FileManagerState* fm) {
if (fm->at_drives_root) return;
int len = montauk::slen(fm->current_path);
if (len <= 3) return; // "0:/" is root
// At drive root (e.g. "0:/" or "10:/") — go to drives view
bool is_drive_root = false;
if (len >= 3 && fm->current_path[len - 1] == '/') {
// Check if path is just "N:/" or "NN:/"
int colon = -1;
for (int i = 0; i < len; i++) {
if (fm->current_path[i] == ':') { colon = i; break; }
}
if (colon >= 0 && colon + 2 == len) is_drive_root = true;
}
if (is_drive_root) {
filemanager_read_drives(fm);
filemanager_push_history(fm);
return;
}
if (len > 0 && fm->current_path[len - 1] == '/') {
fm->current_path[len - 1] = '\0';
@@ -237,24 +302,30 @@ static void filemanager_go_up(FileManagerState* fm) {
filemanager_read_dir(fm);
}
static void filemanager_navigate_to_history(FileManagerState* fm) {
montauk::strcpy(fm->current_path, fm->history[fm->history_pos]);
if (fm->current_path[0] == '\0') {
filemanager_read_drives(fm);
} else {
filemanager_read_dir(fm);
}
}
static void filemanager_go_back(FileManagerState* fm) {
if (fm->history_pos <= 0) return;
fm->history_pos--;
montauk::strcpy(fm->current_path, fm->history[fm->history_pos]);
filemanager_read_dir(fm);
filemanager_navigate_to_history(fm);
}
static void filemanager_go_forward(FileManagerState* fm) {
if (fm->history_pos >= fm->history_count - 1) return;
fm->history_pos++;
montauk::strcpy(fm->current_path, fm->history[fm->history_pos]);
filemanager_read_dir(fm);
filemanager_navigate_to_history(fm);
}
static void filemanager_go_home(FileManagerState* fm) {
montauk::strcpy(fm->current_path, "0:/");
filemanager_read_drives(fm);
filemanager_push_history(fm);
filemanager_read_dir(fm);
}
// ============================================================================
@@ -302,13 +373,27 @@ static void filemanager_draw_header(Canvas& c, FileManagerState* fm,
}
}
// Delete button (6th toolbar button) — only active when a file is selected
{
int bx = 148, by = 4;
bool has_sel = fm->selected >= 0 && fm->selected < fm->entry_count
&& !fm->at_drives_root && !fm->is_dir[fm->selected];
Color del_bg = has_sel ? btn_bg : Color::from_rgb(0xF0, 0xF0, 0xF0);
c.fill_rect(bx, by, 24, 24, del_bg);
if (ds && ds->icon_delete.pixels) {
int ix = bx + (24 - ds->icon_delete.width) / 2;
int iy = by + (24 - ds->icon_delete.height) / 2;
c.icon(ix, iy, ds->icon_delete);
}
}
// Toolbar separator
c.hline(0, FM_TOOLBAR_H - 1, c.w, colors::BORDER);
// ---- Path bar ----
int pathbar_y = FM_TOOLBAR_H;
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
c.text(8, pathbar_y + 4, fm->current_path, colors::TEXT_COLOR);
c.text(8, pathbar_y + 4, fm->at_drives_root ? "Drives" : fm->current_path, colors::TEXT_COLOR);
// Path bar separator
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
@@ -364,7 +449,9 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Large icon centered horizontally
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
int icon_y = cell_y + FM_GRID_PAD;
if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
if (ds && fm->entry_types[i] == 3 && ds->icon_drive_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_drive_lg);
} else if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_folder_lg);
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_exec_lg);
@@ -451,7 +538,9 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Icon
int ico_x = 8;
int ico_y = iy + (FM_ITEM_H - 16) / 2;
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
if (ds && fm->entry_types[i] == 3 && ds->icon_drive.pixels) {
c.icon(ico_x, ico_y, ds->icon_drive);
} else if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
c.icon(ico_x, ico_y, ds->icon_folder);
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
c.icon(ico_x, ico_y, ds->icon_exec);
@@ -484,7 +573,8 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Type
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
const char* type_str = "File";
if (fm->entry_types[i] == 1) type_str = "Dir";
if (fm->entry_types[i] == 3) type_str = "Drive";
else if (fm->entry_types[i] == 1) type_str = "Dir";
else if (fm->entry_types[i] == 2) type_str = "Exec";
c.text(type_col_x, ty, type_str, dim);
}
@@ -543,6 +633,20 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
fm->grid_view = !fm->grid_view;
fm->scrollbar.scroll_offset = 0;
}
else if (local_x >= 148 && local_x < 172) {
// Delete selected file
if (fm->selected >= 0 && fm->selected < fm->entry_count
&& !fm->at_drives_root && !fm->is_dir[fm->selected]) {
char fullpath[512];
montauk::strcpy(fullpath, fm->current_path);
int plen = montauk::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/')
str_append(fullpath, "/", 512);
str_append(fullpath, fm->entry_names[fm->selected], 512);
montauk::fdelete(fullpath);
filemanager_read_dir(fm);
}
}
return;
}
@@ -561,7 +665,19 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
if (fm->at_drives_root && fm->entry_types[clicked_idx] == 3) {
// Navigate into drive
int d = fm->drive_indices[clicked_idx];
char dpath[8];
if (d < 10) {
dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0';
} else {
dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0';
}
montauk::strcpy(fm->current_path, dpath);
filemanager_push_history(fm);
filemanager_read_dir(fm);
} else if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
char fullpath[512];
@@ -605,7 +721,18 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
// Double-click detection
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
if (fm->at_drives_root && fm->entry_types[clicked_idx] == 3) {
int d = fm->drive_indices[clicked_idx];
char dpath[8];
if (d < 10) {
dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0';
} else {
dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0';
}
montauk::strcpy(fm->current_path, dpath);
filemanager_push_history(fm);
filemanager_read_dir(fm);
} else if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
// Open file in appropriate viewer
@@ -693,10 +820,34 @@ static void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) {
if (fm->selected < fm->entry_count - 1) fm->selected++;
} else if (key.ascii == '\n' || key.ascii == '\r') {
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
if (fm->is_dir[fm->selected]) {
if (fm->at_drives_root && fm->entry_types[fm->selected] == 3) {
int d = fm->drive_indices[fm->selected];
char dpath[8];
if (d < 10) {
dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0';
} else {
dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0';
}
montauk::strcpy(fm->current_path, dpath);
filemanager_push_history(fm);
filemanager_read_dir(fm);
} else if (fm->is_dir[fm->selected]) {
filemanager_navigate(fm, fm->entry_names[fm->selected]);
}
}
} else if (key.scancode == 0x53) {
// Delete key
if (fm->selected >= 0 && fm->selected < fm->entry_count
&& !fm->at_drives_root && !fm->is_dir[fm->selected]) {
char fullpath[512];
montauk::strcpy(fullpath, fm->current_path);
int plen = montauk::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/')
str_append(fullpath, "/", 512);
str_append(fullpath, fm->entry_names[fm->selected], 512);
montauk::fdelete(fullpath);
filemanager_read_dir(fm);
}
} else if (key.alt && key.scancode == 0x4B) {
// Alt+Left: go back
filemanager_go_back(fm);
@@ -724,7 +875,6 @@ void open_filemanager(DesktopState* ds) {
Window* win = &ds->windows[idx];
FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
montauk::memset(fm, 0, sizeof(FileManagerState));
montauk::strcpy(fm->current_path, "0:/");
fm->selected = -1;
fm->last_click_item = -1;
fm->history_pos = -1;
@@ -734,8 +884,16 @@ void open_filemanager(DesktopState* ds) {
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
// Lazy-load file manager icons on first open
if (ds && !ds->icon_drive.pixels) {
Color defColor = colors::ICON_COLOR;
ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor);
ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor);
ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor);
}
filemanager_read_drives(fm);
filemanager_push_history(fm);
filemanager_read_dir(fm);
win->app_data = fm;
win->on_draw = filemanager_on_draw;
+5 -6
View File
@@ -1,6 +1,6 @@
/*
* app_terminal.cpp
* MontaukOS Desktop - Terminal application with tab support
* MontaukOS Desktop
* Copyright (c) 2026 Daniel Hammer
*/
@@ -17,12 +17,11 @@ static constexpr int TERM_TAB_GAP = 4;
static constexpr int TERM_PLUS_W = 28;
static constexpr int TERM_PLUS_PAD = 8;
// Compute the x-offset that centers all tabs within bar_w
// Compute the x-offset for left-aligned tabs within bar_w
static int term_tabs_origin(int bar_w, int tab_count) {
int total = tab_count * TERM_TAB_W + (tab_count - 1) * TERM_TAB_GAP;
int x = (bar_w - total) / 2;
if (x < 4) x = 4;
return x;
(void)bar_w;
(void)tab_count;
return 8;
}
struct TermTabState {
@@ -177,4 +177,5 @@ void open_reboot_dialog(DesktopState* ds);
void open_wordprocessor(DesktopState* ds);
void open_spreadsheet(DesktopState* ds);
void open_shutdown_dialog(DesktopState* ds);
void open_disks(DesktopState* ds);
void desktop_poll_external_windows(DesktopState* ds);
+2 -1
View File
@@ -24,7 +24,7 @@ struct MenuRow {
int app_id; // -1 for category headers / dividers
};
static constexpr int MENU_ROW_COUNT = 22;
static constexpr int MENU_ROW_COUNT = 23;
static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ true, "Applications", -1 }, // cat 0
{ false, "Terminal", 0 },
@@ -41,6 +41,7 @@ static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ false, "Kernel Log", 5 },
{ false, "Processes", 6 },
{ false, "Devices", 8 },
{ false, "Disks", 17 },
{ true, "Games", -1 }, // cat 3
{ false, "Mandelbrot", 7 },
{ false, "DOOM", 10 },
+1
View File
@@ -230,6 +230,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
case 14: open_shutdown_dialog(ds); break;
case 15: open_wordprocessor(ds); break;
case 16: open_spreadsheet(ds); break;
case 17: open_disks(ds); break;
}
ds->app_menu_open = false;
}
+3 -1
View File
@@ -52,6 +52,7 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, defColor);
ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor);
ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor);
// drive icons loaded lazily by file manager to reduce startup heap pressure
ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor);
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
@@ -63,6 +64,7 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
ds->icon_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor);
ds->icon_disks = svg_load("0:/icons/gparted.svg", 20, 20, defColor); // gparted icon, i.e. disk/partition management
ds->icon_spreadsheet = svg_load("0:/icons/spreadsheet.svg", 20, 20, defColor);
// Settings defaults
@@ -82,7 +84,7 @@ void gui::desktop_init(DesktopState* ds) {
ds->settings.ui_scale = 1;
// Try to load default wallpaper
wallpaper_load(&ds->settings, "0:/home/kristaps-ungurs-llezNN2OGEY-unsplash.jpg",
wallpaper_load(&ds->settings, "0:/home/lucas-alexander-2dJn8XoIKCg-unsplash.jpg",
ds->screen_w, ds->screen_h);
montauk::win_setscale(1);
+3 -2
View File
@@ -152,7 +152,7 @@ void desktop_draw_app_menu(DesktopState* ds) {
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
// Icon lookup by app_id
SvgIcon* icons[17] = {
SvgIcon* icons[18] = {
&ds->icon_terminal, // 0
&ds->icon_filemanager, // 1
&ds->icon_sysinfo, // 2
@@ -170,6 +170,7 @@ void desktop_draw_app_menu(DesktopState* ds) {
&ds->icon_shutdown, // 14
&ds->icon_texteditor, // 15
&ds->icon_spreadsheet, // 16
&ds->icon_disks, // 17 (Disks)
};
int mx = ds->mouse.x;
@@ -238,7 +239,7 @@ void desktop_draw_app_menu(DesktopState* ds) {
// Icon
int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (row_h - 20) / 2;
if (row.app_id >= 0 && row.app_id < 17) {
if (row.app_id >= 0 && row.app_id < 18) {
SvgIcon* icon = icons[row.app_id];
if (icon && icon->pixels) {
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels);
+86
View File
@@ -0,0 +1,86 @@
# Makefile for devexplorer (standalone Device Explorer) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
# ---- Paths ----
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Source files ----
SRCS := main.cpp render.cpp diskdetail.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/os/devexplorer.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+153
View File
@@ -0,0 +1,153 @@
/*
* devexplorer.h
* Shared header for the MontaukOS Device Explorer
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 640;
static constexpr int INIT_H = 460;
static constexpr int TOOLBAR_H = 36;
static constexpr int CAT_H = 28;
static constexpr int ITEM_H = 24;
static constexpr int MAX_DEVS = 64;
static constexpr int POLL_MS = 2000;
static constexpr int INDENT = 28;
static constexpr int FONT_SIZE = 18;
static constexpr int NUM_CATEGORIES = 9;
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
static constexpr Color BORDER_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC);
static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22);
static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color ACCENT_COLOR = Color::from_rgb(0x33, 0x66, 0xCC);
// ============================================================================
// Category data
// ============================================================================
static const char* category_names[] = {
"CPU", // 0
"Interrupt", // 1
"Timer", // 2
"Input", // 3
"USB", // 4
"Network", // 5
"Display", // 6
"Storage", // 7
"PCI", // 8
};
static const Color category_colors[] = {
Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue
Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple
Color::from_rgb(0x22, 0x88, 0x22), // Timer - green
Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber
Color::from_rgb(0x00, 0x88, 0x88), // USB - teal
Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange
Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo
Color::from_rgb(0x99, 0x55, 0x00), // Storage - brown
Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray
};
// ============================================================================
// Display row model
// ============================================================================
enum RowType { ROW_CATEGORY, ROW_DEVICE };
struct DisplayRow {
RowType type;
int category;
int dev_index;
};
static constexpr int MAX_DISPLAY_ROWS = MAX_DEVS + NUM_CATEGORIES;
// ============================================================================
// Disk detail window
// ============================================================================
static constexpr int DD_TAB_COUNT = 2;
static const char* dd_tab_labels[DD_TAB_COUNT] = { "General", "Features" };
static constexpr int DD_INIT_W = 440;
static constexpr int DD_INIT_H = 420;
static constexpr int DD_TAB_BAR_H = 32;
struct DiskDetailState {
Montauk::DiskInfo info;
int active_tab;
int win_id;
int win_w, win_h;
uint32_t* pixels;
bool open;
};
// ============================================================================
// Main state
// ============================================================================
struct DevExplorerState {
Montauk::DevInfo devs[MAX_DEVS];
int dev_count;
bool collapsed[NUM_CATEGORIES];
int selected_row;
int scroll_y;
uint64_t last_poll_ms;
int last_click_row;
uint64_t last_click_ms;
DiskDetailState detail;
};
// ============================================================================
// Global state (extern — defined in main.cpp)
// ============================================================================
extern int g_win_w, g_win_h;
extern DevExplorerState g_state;
extern TrueTypeFont* g_font;
// ============================================================================
// Function declarations — render.cpp
// ============================================================================
void render(uint32_t* pixels);
// ============================================================================
// Function declarations — main.cpp
// ============================================================================
int build_display_rows(DevExplorerState* de, DisplayRow* rows);
// ============================================================================
// Function declarations — diskdetail.cpp
// ============================================================================
void render_disk_detail();
void open_disk_detail(int port, const char* model);
void close_disk_detail();
bool handle_detail_mouse(int mx, int my, bool clicked);
+306
View File
@@ -0,0 +1,306 @@
/*
* diskdetail.cpp
* Disk detail popup window
* Copyright (c) 2026 Daniel Hammer
*/
#include "px.h"
// ============================================================================
// Helpers
// ============================================================================
static void fmt_u64(char* buf, int bufsize, uint64_t v) {
if (v == 0) { buf[0] = '0'; buf[1] = '\0'; return; }
char tmp[24]; int i = 0;
while (v > 0) { tmp[i++] = '0' + (int)(v % 10); v /= 10; }
int j = 0;
while (i > 0 && j < bufsize - 1) buf[j++] = tmp[--i];
buf[j] = '\0';
}
static void dd_table_row(uint32_t* px, int bw, int bh,
int x, int y, int col1_w, int row_h,
const char* key, const char* value,
Color key_col, Color val_col) {
int fh = font_h();
int ty = y + (row_h - fh) / 2;
px_text(px, bw, bh, x + 8, ty, key, key_col);
px_text(px, bw, bh, x + col1_w + 8, ty, value, val_col);
}
static void dd_feature_row(uint32_t* px, int bw, int bh,
int x, int y, int w, int row_h,
const char* label, bool supported, const char* extra) {
int fh = font_h();
int ty = y + (row_h - fh) / 2;
int dot_y = y + (row_h - 8) / 2;
Color dot_col = supported ? Color::from_rgb(0x22, 0x88, 0x22)
: Color::from_rgb(0xBB, 0xBB, 0xBB);
px_fill_rounded(px, bw, bh, x + 10, dot_y, 8, 8, 4, dot_col);
Color text_col = supported ? TEXT_COLOR : Color::from_rgb(0xAA, 0xAA, 0xAA);
px_text(px, bw, bh, x + 26, ty, label, text_col);
if (extra && extra[0]) {
int ew = text_w(extra);
px_text(px, bw, bh, x + w - ew - 12, ty, extra, DIM_TEXT);
}
}
// ============================================================================
// Tab content: General
// ============================================================================
static void dd_draw_general(uint32_t* px, int bw, int bh, DiskDetailState* dd) {
char line[128];
int fh = font_h();
int x = 0;
int y = DD_TAB_BAR_H + 12;
int w = bw;
int col1_w = 110;
int row_h = fh + 8;
Color key_col = DIM_TEXT;
Color val_col = TEXT_COLOR;
Color hdr_col = ACCENT_COLOR;
Color border = Color::from_rgb(0xE0, 0xE0, 0xE0);
// Identification section
px_text(px, bw, bh, x + 8, y, "Identification", hdr_col);
y += fh + 8;
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 1;
int rowIdx = 0;
auto table_row = [&](const char* key, const char* val) {
if (rowIdx++ % 2 == 0)
px_fill(px, bw, bh, x + 8, y, w - 16, row_h,
Color::from_rgb(0xF7, 0xF7, 0xF7));
dd_table_row(px, bw, bh, x, y, col1_w, row_h, key, val, key_col, val_col);
y += row_h;
};
table_row("Model", dd->info.model);
table_row("Serial", dd->info.serial);
table_row("Firmware", dd->info.firmware);
const char* typeStr = "Unknown";
if (dd->info.type == 1) typeStr = "SATA";
else if (dd->info.type == 2) typeStr = "SATAPI";
table_row("Type", typeStr);
snprintf(line, sizeof(line), "%d", (int)dd->info.port);
table_row("AHCI Port", line);
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 12;
// Capacity section
px_text(px, bw, bh, x + 8, y, "Capacity", hdr_col);
y += fh + 8;
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 1;
rowIdx = 0;
uint64_t totalBytes = dd->info.sectorCount * (uint64_t)dd->info.sectorSizeLog;
uint64_t totalMB = totalBytes / (1024 * 1024);
uint64_t totalGB = totalMB / 1024;
if (totalGB > 0) {
int fracGB = (int)((totalMB % 1024) * 10 / 1024);
snprintf(line, sizeof(line), "%d.%d GiB", (int)totalGB, fracGB);
} else {
snprintf(line, sizeof(line), "%d MiB", (int)totalMB);
}
table_row("Size", line);
char secbuf[24];
fmt_u64(secbuf, sizeof(secbuf), dd->info.sectorCount);
table_row("Sectors", secbuf);
snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizeLog);
table_row("Logical", line);
snprintf(line, sizeof(line), "%d bytes", (int)dd->info.sectorSizePhys);
table_row("Physical", line);
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 12;
// Interface section
px_text(px, bw, bh, x + 8, y, "Interface", hdr_col);
y += fh + 8;
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 1;
rowIdx = 0;
const char* sataSpeed = "Unknown";
if (dd->info.sataGen == 1) sataSpeed = "SATA I (1.5 Gb/s)";
else if (dd->info.sataGen == 2) sataSpeed = "SATA II (3.0 Gb/s)";
else if (dd->info.sataGen == 3) sataSpeed = "SATA III (6.0 Gb/s)";
table_row("Link Speed", sataSpeed);
if (dd->info.rpm == 0)
table_row("Media", "Not reported");
else if (dd->info.rpm == 1)
table_row("Media", "Solid State (SSD)");
else {
snprintf(line, sizeof(line), "%d RPM", (int)dd->info.rpm);
table_row("Media", line);
}
px_hline(px, bw, bh, x + 8, y, w - 16, border);
}
// ============================================================================
// Tab content: Features
// ============================================================================
static void dd_draw_features(uint32_t* px, int bw, int bh, DiskDetailState* dd) {
char line[64];
int fh = font_h();
int x = 0;
int y = DD_TAB_BAR_H + 12;
int w = bw;
int row_h = fh + 10;
Color hdr_col = ACCENT_COLOR;
Color border = Color::from_rgb(0xE0, 0xE0, 0xE0);
px_text(px, bw, bh, x + 8, y, "Supported Features", hdr_col);
y += fh + 8;
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 4;
dd_feature_row(px, bw, bh, x, y, w, row_h, "48-bit LBA", dd->info.supportsLba48, nullptr);
y += row_h;
if (dd->info.supportsNcq) {
snprintf(line, sizeof(line), "Depth: %d", (int)dd->info.ncqDepth);
dd_feature_row(px, bw, bh, x, y, w, row_h, "Native Command Queuing (NCQ)", true, line);
} else {
dd_feature_row(px, bw, bh, x, y, w, row_h, "Native Command Queuing (NCQ)", false, nullptr);
}
y += row_h;
dd_feature_row(px, bw, bh, x, y, w, row_h, "TRIM (Data Set Management)", dd->info.supportsTrim, nullptr);
y += row_h;
dd_feature_row(px, bw, bh, x, y, w, row_h, "S.M.A.R.T.", dd->info.supportsSmart, nullptr);
y += row_h;
dd_feature_row(px, bw, bh, x, y, w, row_h, "Write Cache", dd->info.supportsWriteCache, nullptr);
y += row_h;
dd_feature_row(px, bw, bh, x, y, w, row_h, "Read Look-Ahead", dd->info.supportsReadAhead, nullptr);
y += row_h;
y += 4;
px_hline(px, bw, bh, x + 8, y, w - 16, border);
y += 12;
// Legend
Color legend_col = Color::from_rgb(0x88, 0x88, 0x88);
int ly = y + (fh - 8) / 2;
px_fill_rounded(px, bw, bh, x + 10, ly, 8, 8, 4, Color::from_rgb(0x22, 0x88, 0x22));
px_text(px, bw, bh, x + 26, y, "Supported", legend_col);
px_fill_rounded(px, bw, bh, x + 120, ly, 8, 8, 4, Color::from_rgb(0xBB, 0xBB, 0xBB));
px_text(px, bw, bh, x + 136, y, "Not supported", legend_col);
}
// ============================================================================
// Render
// ============================================================================
void render_disk_detail() {
auto& dd = g_state.detail;
if (!dd.open) return;
uint32_t* px = dd.pixels;
int bw = dd.win_w, bh = dd.win_h;
int fh = font_h();
px_fill(px, bw, bh, 0, 0, bw, bh, BG_COLOR);
// Tab bar
px_fill(px, bw, bh, 0, 0, bw, DD_TAB_BAR_H, TOOLBAR_BG);
px_hline(px, bw, bh, 0, DD_TAB_BAR_H - 1, bw, BORDER_COLOR);
int tab_w = bw / DD_TAB_COUNT;
for (int i = 0; i < DD_TAB_COUNT; i++) {
int tx = i * tab_w;
bool active = (i == dd.active_tab);
if (active) {
px_fill(px, bw, bh, tx, 0, tab_w, DD_TAB_BAR_H, BG_COLOR);
px_fill(px, bw, bh, tx + 4, DD_TAB_BAR_H - 3, tab_w - 8, 3, ACCENT_COLOR);
}
int tw = text_w(dd_tab_labels[i]);
Color tc = active ? ACCENT_COLOR : DIM_TEXT;
px_text(px, bw, bh, tx + (tab_w - tw) / 2, (DD_TAB_BAR_H - fh) / 2,
dd_tab_labels[i], tc);
}
// Tab content
switch (dd.active_tab) {
case 0: dd_draw_general(px, bw, bh, &dd); break;
case 1: dd_draw_features(px, bw, bh, &dd); break;
}
}
// ============================================================================
// Window management
// ============================================================================
void open_disk_detail(int port, const char* model) {
auto& dd = g_state.detail;
if (dd.open) close_disk_detail();
char title[64];
int i = 0;
const char* prefix = "Disk: ";
while (prefix[i]) { title[i] = prefix[i]; i++; }
int j = 0;
while (model[j] && i < 62) { title[i++] = model[j++]; }
title[i] = '\0';
dd.win_w = DD_INIT_W;
dd.win_h = DD_INIT_H;
dd.active_tab = 0;
montauk::memset(&dd.info, 0, sizeof(dd.info));
montauk::diskinfo(&dd.info, port);
Montauk::WinCreateResult wres;
if (montauk::win_create(title, dd.win_w, dd.win_h, &wres) < 0 || wres.id < 0)
return;
dd.win_id = wres.id;
dd.pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
dd.open = true;
render_disk_detail();
montauk::win_present(dd.win_id);
}
void close_disk_detail() {
auto& dd = g_state.detail;
if (!dd.open) return;
montauk::win_destroy(dd.win_id);
dd.open = false;
}
bool handle_detail_mouse(int mx, int my, bool clicked) {
auto& dd = g_state.detail;
if (!clicked) return false;
if (my >= 0 && my < DD_TAB_BAR_H) {
int tab_w = dd.win_w / DD_TAB_COUNT;
int tab = mx / tab_w;
if (tab >= 0 && tab < DD_TAB_COUNT)
dd.active_tab = tab;
return true;
}
return false;
}
+304
View File
@@ -0,0 +1,304 @@
/*
* main.cpp
* MontaukOS Device Explorer — entry point, event loop, state
* Copyright (c) 2026 Daniel Hammer
*/
#include "devexplorer.h"
// ============================================================================
// Global state definitions
// ============================================================================
int g_win_w = INIT_W;
int g_win_h = INIT_H;
DevExplorerState g_state;
TrueTypeFont* g_font = nullptr;
// ============================================================================
// Display row building
// ============================================================================
int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
int count = 0;
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++)
if (de->devs[d].category == cat) cat_count++;
if (cat_count == 0) continue;
rows[count].type = ROW_CATEGORY;
rows[count].category = cat;
rows[count].dev_index = -1;
count++;
if (!de->collapsed[cat]) {
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) {
rows[count].type = ROW_DEVICE;
rows[count].category = cat;
rows[count].dev_index = d;
count++;
}
}
}
}
return count;
}
// ============================================================================
// Toolbar hit testing
// ============================================================================
static bool handle_toolbar_click(int mx, int my) {
if (my >= TOOLBAR_H) return false;
int btn_w = 80, btn_h = 26;
int btn_x = 8;
int btn_y = (TOOLBAR_H - btn_h) / 2;
if (mx >= btn_x && mx < btn_x + btn_w && my >= btn_y && my < btn_y + btn_h) {
g_state.last_poll_ms = 0; // force refresh
return true;
}
return false;
}
// ============================================================================
// List click handling
// ============================================================================
static bool handle_list_click(int mx, int my) {
auto& de = g_state;
int list_y = TOOLBAR_H;
if (my < list_y) return false;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows);
uint64_t now = montauk::get_milliseconds();
int cur_y = list_y;
for (int i = de.scroll_y; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (my >= cur_y && my < cur_y + row_h) {
if (rows[i].type == ROW_CATEGORY) {
int cat = rows[i].category;
de.collapsed[cat] = !de.collapsed[cat];
de.selected_row = -1;
de.last_click_row = -1;
} else {
int di = rows[i].dev_index;
bool is_double = (de.last_click_row == i)
&& (now - de.last_click_ms < 400);
if (is_double && de.devs[di].category == 7) {
int port = (int)de.devs[di]._pad[0];
open_disk_detail(port, de.devs[di].name);
de.last_click_row = -1;
} else {
de.selected_row = i;
de.last_click_row = i;
de.last_click_ms = now;
}
}
return true;
}
cur_y += row_h;
if (cur_y >= g_win_h) break;
}
de.selected_row = -1;
de.last_click_row = -1;
return true;
}
// ============================================================================
// Key handling
// ============================================================================
static bool handle_key(const Montauk::KeyEvent& key) {
if (!key.pressed) return false;
auto& de = g_state;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows);
if (row_count == 0) return true;
if (key.scancode == 0x48) { // Up
if (de.selected_row <= 0) de.selected_row = 0;
else de.selected_row--;
if (de.selected_row < de.scroll_y)
de.scroll_y = de.selected_row;
} else if (key.scancode == 0x50) { // Down
if (de.selected_row < row_count - 1)
de.selected_row++;
int list_h = g_win_h - TOOLBAR_H;
int cur_h = 0, last_visible = de.scroll_y;
for (int i = de.scroll_y; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (cur_h + rh > list_h) break;
cur_h += rh;
last_visible = i;
}
if (de.selected_row > last_visible)
de.scroll_y += (de.selected_row - last_visible);
} else if (key.scancode == 0x4B) { // Left — collapse
if (de.selected_row >= 0 && de.selected_row < row_count) {
int cat = rows[de.selected_row].category;
if (!de.collapsed[cat]) {
de.collapsed[cat] = true;
if (rows[de.selected_row].type == ROW_DEVICE) {
for (int i = de.selected_row - 1; i >= 0; i--) {
if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) {
de.selected_row = i;
break;
}
}
int new_count = build_display_rows(&de, rows);
if (de.selected_row >= new_count)
de.selected_row = new_count - 1;
}
}
}
} else if (key.scancode == 0x4D) { // Right — expand
if (de.selected_row >= 0 && de.selected_row < row_count) {
int cat = rows[de.selected_row].category;
de.collapsed[cat] = false;
}
} else if (key.scancode == 0x1C) { // Enter — toggle category
if (de.selected_row >= 0 && de.selected_row < row_count) {
if (rows[de.selected_row].type == ROW_CATEGORY) {
int cat = rows[de.selected_row].category;
de.collapsed[cat] = !de.collapsed[cat];
}
}
} else if (key.scancode == 0x01) { // Escape
return false;
}
return true;
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
montauk::memset(&g_state, 0, sizeof(g_state));
g_state.selected_row = -1;
g_state.last_click_row = -1;
for (int i = 0; i < NUM_CATEGORIES; i++)
g_state.collapsed[i] = false;
// Load font
{
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (f) {
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; }
}
g_font = f;
}
// Initial device poll
g_state.dev_count = montauk::devlist(g_state.devs, MAX_DEVS);
g_state.last_poll_ms = montauk::get_milliseconds();
// Create window
Montauk::WinCreateResult wres;
if (montauk::win_create("Devices", INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
montauk::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
render(pixels);
montauk::win_present(win_id);
while (true) {
Montauk::WinEvent ev;
bool redraw_main = false;
bool redraw_detail = false;
// Poll for device refresh
uint64_t now = montauk::get_milliseconds();
if (now - g_state.last_poll_ms >= POLL_MS) {
g_state.dev_count = montauk::devlist(g_state.devs, MAX_DEVS);
g_state.last_poll_ms = now;
redraw_main = true;
}
// Poll disk detail window
if (g_state.detail.open) {
int dr = montauk::win_poll(g_state.detail.win_id, &ev);
if (dr > 0) {
if (ev.type == 3) { // close
close_disk_detail();
redraw_main = true;
} else if (ev.type == 1) {
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (handle_detail_mouse(ev.mouse.x, ev.mouse.y, clicked))
redraw_detail = true;
} else if (ev.type == 2) { // resize
g_state.detail.win_w = ev.resize.w;
g_state.detail.win_h = ev.resize.h;
g_state.detail.pixels = (uint32_t*)(uintptr_t)montauk::win_resize(
g_state.detail.win_id, ev.resize.w, ev.resize.h);
redraw_detail = true;
}
}
}
// Poll main window
int r = montauk::win_poll(win_id, &ev);
if (r < 0) break;
if (r == 0) {
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); }
if (!redraw_main && !redraw_detail) montauk::sleep_ms(16);
continue;
}
if (ev.type == 3) break; // close
// Resize
if (ev.type == 2) {
g_win_w = ev.resize.w;
g_win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
redraw_main = true;
}
// Keyboard
if (ev.type == 0 && ev.key.pressed) {
if (!handle_key(ev.key) && ev.key.scancode == 0x01)
break;
redraw_main = true;
}
// Mouse
if (ev.type == 1) {
int mx = ev.mouse.x;
int my = ev.mouse.y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (ev.mouse.scroll != 0) {
g_state.scroll_y += ev.mouse.scroll;
if (g_state.scroll_y < 0) g_state.scroll_y = 0;
redraw_main = true;
}
if (clicked) {
if (handle_toolbar_click(mx, my) || handle_list_click(mx, my))
redraw_main = true;
}
}
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); }
}
close_disk_detail();
montauk::win_destroy(win_id);
montauk::exit(0);
}
+81
View File
@@ -0,0 +1,81 @@
/*
* px.h
* Shared pixel-level drawing helpers for the MontaukOS Device Explorer
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "devexplorer.h"
inline void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w > bw ? bw : x + w;
int y1 = y + h > bh ? bh : y + h;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
px[row * bw + col] = v;
}
inline void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) {
if (y < 0 || y >= bh) return;
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x;
int x1 = x + w > bw ? bw : x + w;
for (int col = x0; col < x1; col++)
px[y * bw + col] = v;
}
inline void px_fill_rounded(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, int r, Color c) {
if (r <= 0) { px_fill(px, bw, bh, x, y, w, h, c); return; }
uint32_t v = c.to_pixel();
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= bh) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= bw) continue;
bool in_corner = false;
int cx_off = 0, cy_off = 0;
if (col < r && row < r) {
cx_off = r - col; cy_off = r - row; in_corner = true;
} else if (col >= w - r && row < r) {
cx_off = col - (w - r - 1); cy_off = r - row; in_corner = true;
} else if (col < r && row >= h - r) {
cx_off = r - col; cy_off = row - (h - r - 1); in_corner = true;
} else if (col >= w - r && row >= h - r) {
cx_off = col - (w - r - 1); cy_off = row - (h - r - 1); in_corner = true;
}
if (in_corner && cx_off * cx_off + cy_off * cy_off > r * r) continue;
px[dy * bw + dx] = v;
}
}
}
inline void px_text(uint32_t* px, int bw, int bh,
int x, int y, const char* text, Color c) {
if (g_font)
g_font->draw_to_buffer(px, bw, bh, x, y, text, c, FONT_SIZE);
}
inline int text_w(const char* text) {
return g_font ? g_font->measure_text(text, FONT_SIZE) : 0;
}
inline int font_h() {
if (!g_font) return 16;
auto* cache = g_font->get_cache(FONT_SIZE);
return cache->ascent - cache->descent;
}
inline void px_button(uint32_t* px, int bw, int bh,
int x, int y, int w, int h,
const char* label, Color bg, Color fg, int r) {
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
int tw = text_w(label);
int fh = font_h();
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg);
}
+196
View File
@@ -0,0 +1,196 @@
/*
* render.cpp
* MontaukOS Device Explorer — main window rendering
* Copyright (c) 2026 Daniel Hammer
*/
#include "px.h"
// ============================================================================
// Triangle drawing helpers
// ============================================================================
static void draw_triangle_right(uint32_t* px, int bw, int bh,
int x, int y, int size, Color col) {
uint32_t v = col.to_pixel();
int half = size / 2;
for (int row = 0; row < size; row++) {
int dist = (row <= half) ? row : (size - 1 - row);
for (int col_px = 0; col_px <= dist; col_px++) {
int dx = x + col_px, dy = y + row;
if (dx >= 0 && dx < bw && dy >= 0 && dy < bh)
px[dy * bw + dx] = v;
}
}
}
static void draw_triangle_down(uint32_t* px, int bw, int bh,
int x, int y, int size, Color col) {
uint32_t v = col.to_pixel();
int half = size / 2;
for (int row = 0; row < half + 1; row++) {
int w = size - row * 2;
for (int col_px = 0; col_px < w; col_px++) {
int dx = x + row + col_px, dy = y + row;
if (dx >= 0 && dx < bw && dy >= 0 && dy < bh)
px[dy * bw + dx] = v;
}
}
}
// ============================================================================
// Render: main device list
// ============================================================================
static void render_toolbar(uint32_t* px) {
auto& de = g_state;
int fh = font_h();
px_fill(px, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG);
px_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR);
// "Refresh" button
int btn_w = 80, btn_h = 26;
int btn_x = 8;
int btn_y = (TOOLBAR_H - btn_h) / 2;
px_button(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h,
"Refresh", ACCENT_COLOR, WHITE, 4);
// Device count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d devices", de.dev_count);
int cw = text_w(count_str);
px_text(px, g_win_w, g_win_h, g_win_w - cw - 12, (TOOLBAR_H - fh) / 2,
count_str, TEXT_COLOR);
}
static void render_list(uint32_t* px) {
auto& de = g_state;
int fh = font_h();
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows);
int list_y = TOOLBAR_H;
int list_h = g_win_h - list_y;
if (list_h < 1) return;
// Compute total content height
int total_h = 0;
for (int i = 0; i < row_count; i++)
total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
int max_scroll_px = total_h - list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
// Compute scroll pixel offset
int scroll_px = 0;
for (int i = 0; i < de.scroll_y && i < row_count; i++)
scroll_px += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (scroll_px > max_scroll_px) {
scroll_px = max_scroll_px;
de.scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de.scroll_y = i + 1;
}
}
if (de.scroll_y < 0) de.scroll_y = 0;
if (de.selected_row >= row_count) de.selected_row = row_count - 1;
// Draw rows
int first_visible = de.scroll_y;
int cur_y = list_y;
Color menu_hover = Color::from_rgb(0xD0, 0xE0, 0xF8);
for (int i = first_visible; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (cur_y >= g_win_h) break;
if (rows[i].type == ROW_CATEGORY) {
int cat = rows[i].category;
px_fill(px, g_win_w, g_win_h, 0, cur_y, g_win_w, CAT_H,
Color::from_rgb(0xF0, 0xF0, 0xF0));
if (i == de.selected_row)
px_fill(px, g_win_w, g_win_h, 0, cur_y, g_win_w, CAT_H, menu_hover);
// Expand/collapse triangle
int tri_x = 10;
int tri_y = cur_y + (CAT_H - 8) / 2;
Color tri_color = Color::from_rgb(0x55, 0x55, 0x55);
if (de.collapsed[cat])
draw_triangle_right(px, g_win_w, g_win_h, tri_x, tri_y, 8, tri_color);
else
draw_triangle_down(px, g_win_w, g_win_h, tri_x, tri_y, 8, tri_color);
// Colored dot
int dot_x = 24;
int dot_y = cur_y + (CAT_H - 8) / 2;
Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES)
? category_colors[cat] : TEXT_COLOR;
px_fill_rounded(px, g_win_w, g_win_h, dot_x, dot_y, 8, 8, 4, cat_col);
// Category name
const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES)
? category_names[cat] : "?";
int text_y = cur_y + (CAT_H - fh) / 2;
px_text(px, g_win_w, g_win_h, 36, text_y, cat_name,
Color::from_rgb(0x33, 0x33, 0x33));
// Device count
int cat_count = 0;
for (int d = 0; d < de.dev_count; d++)
if (de.devs[d].category == cat) cat_count++;
char cnt_buf[16];
snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count);
int name_w = text_w(cat_name);
px_text(px, g_win_w, g_win_h, 36 + name_w + 8, text_y, cnt_buf,
Color::from_rgb(0x88, 0x88, 0x88));
// Bottom border
px_hline(px, g_win_w, g_win_h, 0, cur_y + CAT_H - 1, g_win_w,
Color::from_rgb(0xE0, 0xE0, 0xE0));
} else {
int di = rows[i].dev_index;
if (i == de.selected_row)
px_fill(px, g_win_w, g_win_h, 0, cur_y, g_win_w, ITEM_H, menu_hover);
int text_y = cur_y + (ITEM_H - fh) / 2;
// Device name (indented)
px_text(px, g_win_w, g_win_h, INDENT + 10, text_y,
de.devs[di].name, TEXT_COLOR);
// Detail string (right column)
int detail_x = g_win_w / 2 + 20;
px_text(px, g_win_w, g_win_h, detail_x, text_y,
de.devs[di].detail, DIM_TEXT);
}
cur_y += row_h;
}
// Scrollbar
if (total_h > list_h) {
int sb_x = g_win_w - 6;
int thumb_h = (list_h * list_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px;
px_fill(px, g_win_w, g_win_h, sb_x, list_y, 4, list_h,
Color::from_rgb(0xE0, 0xE0, 0xE0));
px_fill(px, g_win_w, g_win_h, sb_x, thumb_y, 4, thumb_h,
Color::from_rgb(0xAA, 0xAA, 0xAA));
}
}
void render(uint32_t* pixels) {
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR);
render_toolbar(pixels);
render_list(pixels);
}
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+86
View File
@@ -0,0 +1,86 @@
# Makefile for disks (standalone Disk Tool) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
# ---- Paths ----
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Source files ----
SRCS := main.cpp render.cpp actions.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/os/disks.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+179
View File
@@ -0,0 +1,179 @@
/*
* actions.cpp
* MontaukOS Disk Tool — disk operations
* Copyright (c) 2026 Daniel Hammer
*/
#include "disks.h"
// ============================================================================
// Refresh
// ============================================================================
void disktool_refresh() {
auto& dt = g_state;
dt.disk_count = 0;
for (int port = 0; port < MAX_DISKS; port++) {
Montauk::DiskInfo info;
montauk::memset(&info, 0, sizeof(info));
int r = montauk::diskinfo(&info, port);
if (r == 0 && info.type != 0) {
dt.disks[dt.disk_count++] = info;
}
}
dt.part_count = montauk::partlist(dt.parts, MAX_PARTS);
if (dt.selected_disk >= dt.disk_count)
dt.selected_disk = dt.disk_count > 0 ? 0 : -1;
dt.selected_part = -1;
dt.scroll_y = 0;
}
// ============================================================================
// Create partition
// ============================================================================
void do_create_partition() {
auto& dt = g_state;
if (dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) return;
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
if (nparts == 0) {
int r = montauk::gpt_init(dt.selected_disk);
if (r < 0) {
set_status("Failed to initialize GPT on disk");
return;
}
set_status("Initialized GPT");
}
Montauk::GptAddParams params;
montauk::memset(&params, 0, sizeof(params));
params.blockDev = dt.selected_disk;
params.startLba = 0;
params.endLba = 0;
params.typeGuid.Data1 = 0xEBD0A0A2;
params.typeGuid.Data2 = 0xB9E5;
params.typeGuid.Data3 = 0x4433;
params.typeGuid.Data4[0] = 0x87; params.typeGuid.Data4[1] = 0xC0;
params.typeGuid.Data4[2] = 0x68; params.typeGuid.Data4[3] = 0xB6;
params.typeGuid.Data4[4] = 0xB7; params.typeGuid.Data4[5] = 0x26;
params.typeGuid.Data4[6] = 0x99; params.typeGuid.Data4[7] = 0xC7;
const char* pname = "Data";
int i = 0;
for (; pname[i] && i < 71; i++) params.name[i] = pname[i];
params.name[i] = '\0';
int r = montauk::gpt_add(&params);
if (r < 0) {
set_status("Failed to create partition");
return;
}
set_status("Partition created successfully");
disktool_refresh();
}
// ============================================================================
// Mount partition
// ============================================================================
void do_mount_partition() {
auto& dt = g_state;
if (dt.selected_part < 0) {
set_status("Select a partition first");
return;
}
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
if (dt.selected_part >= nparts) return;
int global_idx = part_indices[dt.selected_part];
int driveNum = 1 + global_idx;
if (driveNum >= 16) driveNum = 15;
int r = montauk::fs_mount(global_idx, driveNum);
if (r < 0) {
set_status("Mount failed (no recognized filesystem)");
return;
}
char msg[80];
snprintf(msg, sizeof(msg), "Mounted as drive %d:/", driveNum);
set_status(msg);
}
// ============================================================================
// Format dialog
// ============================================================================
void open_format_dialog() {
auto& dt = g_state;
if (dt.selected_part < 0) {
set_status("Select a partition first");
return;
}
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
if (dt.selected_part >= nparts) return;
int global_idx = part_indices[dt.selected_part];
auto& dlg = dt.fmt_dlg;
dlg.global_part_index = global_idx;
dlg.selected_fs = 0;
dlg.hover_format = false;
dlg.hover_cancel = false;
// Build partition description
Montauk::PartInfo& p = dt.parts[global_idx];
Montauk::DiskInfo& disk = dt.disks[dt.selected_disk];
char sz[24];
format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog);
const char* pname = p.name[0] ? p.name : "Partition";
snprintf(dlg.part_desc, sizeof(dlg.part_desc), "%s (%s)", pname, sz);
// Create dialog window
Montauk::WinCreateResult wres;
if (montauk::win_create("Format Partition", FMT_DLG_W, FMT_DLG_H, &wres) < 0 || wres.id < 0) {
set_status("Failed to open format dialog");
return;
}
dlg.win_id = wres.id;
dlg.pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
dlg.open = true;
render_format_window();
montauk::win_present(dlg.win_id);
}
void close_format_dialog() {
auto& dlg = g_state.fmt_dlg;
if (!dlg.open) return;
montauk::win_destroy(dlg.win_id);
dlg.open = false;
}
void format_dialog_do_format() {
auto& dt = g_state;
auto& dlg = dt.fmt_dlg;
Montauk::FsFormatParams params;
montauk::memset(&params, 0, sizeof(params));
params.partIndex = dlg.global_part_index;
params.fsType = g_fsTypes[dlg.selected_fs].id;
int r = montauk::fs_format(&params);
if (r == 0) {
set_status("Format complete");
disktool_refresh();
} else {
set_status("Format failed");
}
close_format_dialog();
}
+138
View File
@@ -0,0 +1,138 @@
/*
* disks.h
* Shared header for the MontaukOS Disk Tool
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 640;
static constexpr int INIT_H = 460;
static constexpr int TOOLBAR_H = 40;
static constexpr int HEADER_H = 26;
static constexpr int ITEM_H = 32;
static constexpr int MAP_H = 48;
static constexpr int MAP_PAD = 16;
static constexpr int MAX_PARTS = 32;
static constexpr int MAX_DISKS = 8;
static constexpr int STATUS_H = 26;
static constexpr int FONT_SIZE = 16;
static constexpr int TB_BTN_Y = 7;
static constexpr int TB_BTN_H = 26;
static constexpr int TB_BTN_RAD = 6;
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
static constexpr Color HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
static constexpr Color BORDER_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC);
static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22);
static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color FAINT_TEXT = Color::from_rgb(0x88, 0x88, 0x88);
static constexpr Color HOVER_BG = Color::from_rgb(0xE8, 0xF0, 0xF8);
static constexpr Color STATUS_BG_COL = Color::from_rgb(0xF0, 0xF0, 0xF0);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr int NUM_PART_COLORS = 8;
// ============================================================================
// Filesystem types
// ============================================================================
struct FsTypeEntry {
const char* name;
int id;
};
static const FsTypeEntry g_fsTypes[] = {
{ "FAT32", Montauk::FS_TYPE_FAT32 },
};
static constexpr int NUM_FS_TYPES = 1;
// ============================================================================
// State
// ============================================================================
static constexpr int FMT_DLG_W = 280;
static constexpr int FMT_DLG_H = 220;
struct FormatDialog {
bool open;
int global_part_index;
int selected_fs;
bool hover_format;
bool hover_cancel;
char part_desc[80];
int win_id;
uint32_t* pixels;
};
struct DiskToolState {
Montauk::DiskInfo disks[MAX_DISKS];
int disk_count;
Montauk::PartInfo parts[MAX_PARTS];
int part_count;
int selected_disk;
int selected_part;
int scroll_y;
char status[80];
uint64_t status_time;
FormatDialog fmt_dlg;
};
// ============================================================================
// Global state (extern — defined in main.cpp)
// ============================================================================
extern int g_win_w, g_win_h;
extern DiskToolState g_state;
extern TrueTypeFont* g_font;
// ============================================================================
// Partition colors
// ============================================================================
extern const Color part_colors[NUM_PART_COLORS];
// ============================================================================
// Function declarations — helpers (main.cpp)
// ============================================================================
void set_status(const char* msg);
int get_disk_parts(int* indices, int max);
void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorSize);
// ============================================================================
// Function declarations — render.cpp
// ============================================================================
void render(uint32_t* pixels);
void render_format_window();
// ============================================================================
// Function declarations — actions.cpp
// ============================================================================
void disktool_refresh();
void do_create_partition();
void do_mount_partition();
void open_format_dialog();
void close_format_dialog();
void format_dialog_do_format();
+391
View File
@@ -0,0 +1,391 @@
/*
* main.cpp
* MontaukOS Disk Tool — entry point, event loop, state
* Copyright (c) 2026 Daniel Hammer
*/
#include "disks.h"
// ============================================================================
// Global state definitions
// ============================================================================
int g_win_w = INIT_W;
int g_win_h = INIT_H;
DiskToolState g_state;
TrueTypeFont* g_font = nullptr;
const Color part_colors[NUM_PART_COLORS] = {
Color::from_rgb(0x42, 0x7A, 0xB5),
Color::from_rgb(0x5C, 0xA0, 0x5C),
Color::from_rgb(0xCC, 0x88, 0x33),
Color::from_rgb(0x99, 0x55, 0xBB),
Color::from_rgb(0xCC, 0x55, 0x55),
Color::from_rgb(0x44, 0xAA, 0xAA),
Color::from_rgb(0xBB, 0xBB, 0x44),
Color::from_rgb(0x88, 0x66, 0x44),
};
// ============================================================================
// Helpers
// ============================================================================
void set_status(const char* msg) {
int i = 0;
for (; i < 79 && msg[i]; i++) g_state.status[i] = msg[i];
g_state.status[i] = '\0';
g_state.status_time = montauk::get_milliseconds();
}
int get_disk_parts(int* indices, int max) {
int count = 0;
for (int i = 0; i < g_state.part_count && count < max; i++) {
if (g_state.parts[i].blockDev == g_state.selected_disk) {
indices[count++] = i;
}
}
return count;
}
void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorSize) {
uint64_t bytes = sectors * sectorSize;
uint64_t gb = bytes / (1024ULL * 1024 * 1024);
if (gb >= 1024) {
uint64_t tb = gb / 1024;
uint64_t frac = ((gb % 1024) * 10) / 1024;
snprintf(buf, bufsize, "%lu.%lu TB", (unsigned)tb, (unsigned)frac);
} else if (gb > 0) {
uint64_t frac = ((bytes % (1024ULL * 1024 * 1024)) * 10) / (1024ULL * 1024 * 1024);
snprintf(buf, bufsize, "%lu.%lu GB", (unsigned)gb, (unsigned)frac);
} else {
uint64_t mb = bytes / (1024ULL * 1024);
snprintf(buf, bufsize, "%lu MB", (unsigned)mb);
}
}
// ============================================================================
// Toolbar hit testing (returns button widths matching render layout)
// ============================================================================
static int disk_btn_width(int idx) {
char label[8];
snprintf(label, sizeof(label), "Disk %d", idx);
int lw = g_font ? g_font->measure_text(label, FONT_SIZE) + 16 : 48;
return lw;
}
static bool handle_toolbar_click(int mx, int my) {
if (my >= TOOLBAR_H || my < TB_BTN_Y) return false;
auto& dt = g_state;
// Disk selector buttons
int bx = 8;
for (int i = 0; i < dt.disk_count; i++) {
int lw = disk_btn_width(i);
if (mx >= bx && mx < bx + lw && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) {
dt.selected_disk = i;
dt.selected_part = -1;
dt.scroll_y = 0;
return true;
}
bx += lw + 6;
}
// Right-side buttons (must match render layout)
int rx = g_win_w - 8;
int ref_w = 64; rx -= ref_w;
if (mx >= rx && mx < rx + ref_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) {
disktool_refresh();
set_status("Refreshed");
return true;
}
rx -= 6;
int mnt_w = 60; rx -= mnt_w;
if (mx >= rx && mx < rx + mnt_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) {
do_mount_partition();
return true;
}
rx -= 6;
int fmt_w = 64; rx -= fmt_w;
if (mx >= rx && mx < rx + fmt_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) {
open_format_dialog();
return true;
}
rx -= 6;
int np_w = 74; rx -= np_w;
if (mx >= rx && mx < rx + np_w && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_H) {
do_create_partition();
return true;
}
return false;
}
// ============================================================================
// Mouse handling for content area
// ============================================================================
static bool handle_content_click(int mx, int my) {
auto& dt = g_state;
if (dt.disk_count == 0 || dt.selected_disk < 0) return false;
int fh = g_font ? g_font->get_cache(FONT_SIZE)->ascent - g_font->get_cache(FONT_SIZE)->descent : 16;
int y = TOOLBAR_H + 8 + fh + 8;
// Partition map bar click
int map_x = MAP_PAD;
int map_w = g_win_w - MAP_PAD * 2;
if (my >= y && my < y + MAP_H) {
Montauk::DiskInfo& disk = dt.disks[dt.selected_disk];
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
uint64_t total = disk.sectorCount;
if (total > 0) {
for (int pi = 0; pi < nparts; pi++) {
Montauk::PartInfo& p = dt.parts[part_indices[pi]];
int px = map_x + (int)((p.startLba * (uint64_t)map_w) / total);
int pw = (int)((p.sectorCount * (uint64_t)map_w) / total);
if (pw < 2) pw = 2;
if (mx >= px && mx < px + pw) {
dt.selected_part = pi;
return true;
}
}
}
dt.selected_part = -1;
return true;
}
y += MAP_H + 8 + HEADER_H;
// Partition list click
int list_bottom = g_win_h - STATUS_H;
if (my >= y && my < list_bottom) {
int row = (my - y + dt.scroll_y) / ITEM_H;
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
if (row >= 0 && row < nparts)
dt.selected_part = row;
else
dt.selected_part = -1;
return true;
}
return false;
}
// ============================================================================
// Format dialog mouse handling
// ============================================================================
static bool handle_format_dialog_click(int mx, int my, bool clicked) {
auto& dlg = g_state.fmt_dlg;
if (!dlg.open) return false;
int dw = FMT_DLG_W, dh = FMT_DLG_H;
int fh = g_font ? g_font->get_cache(FONT_SIZE)->ascent - g_font->get_cache(FONT_SIZE)->descent : 16;
// Button hit testing
int btn_w = 90, btn_h = 30;
int btn_y = dh - btn_h - 16;
int gap = 16;
int total_w = btn_w * 2 + gap;
int bx = (dw - total_w) / 2;
dlg.hover_format = (mx >= bx && mx < bx + btn_w && my >= btn_y && my < btn_y + btn_h);
dlg.hover_cancel = (mx >= bx + btn_w + gap && mx < bx + btn_w * 2 + gap && my >= btn_y && my < btn_y + btn_h);
if (!clicked) return true;
// FS type selector
int sel_y = 12 + fh * 2 + 18;
int opt_y = sel_y + fh + 8;
for (int i = 0; i < NUM_FS_TYPES; i++) {
int iy = opt_y + i * 32;
if (mx >= 24 && mx < dw - 24 && my >= iy && my < iy + 28) {
dlg.selected_fs = i;
return true;
}
}
if (dlg.hover_format) {
format_dialog_do_format();
return true;
}
if (dlg.hover_cancel) {
close_format_dialog();
return true;
}
return true;
}
// ============================================================================
// Key handling
// ============================================================================
static bool handle_key(const Montauk::KeyEvent& key) {
if (!key.pressed) return false;
auto& dt = g_state;
// Format dialog keys
if (dt.fmt_dlg.open) {
if (key.scancode == 0x01) { // Escape
close_format_dialog();
} else if (key.scancode == 0x48 && dt.fmt_dlg.selected_fs > 0) {
dt.fmt_dlg.selected_fs--;
} else if (key.scancode == 0x50 && dt.fmt_dlg.selected_fs < NUM_FS_TYPES - 1) {
dt.fmt_dlg.selected_fs++;
} else if (key.ascii == '\n' || key.ascii == '\r') {
format_dialog_do_format();
}
return true;
}
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
if (key.scancode == 0x01) { // Escape
return false; // signal quit
} else if (key.scancode == 0x48) { // Up
if (dt.selected_part > 0) dt.selected_part--;
else if (nparts > 0) dt.selected_part = 0;
} else if (key.scancode == 0x50) { // Down
if (dt.selected_part < nparts - 1) dt.selected_part++;
} else if (key.scancode == 0x4B) { // Left
if (dt.selected_disk > 0) { dt.selected_disk--; dt.selected_part = -1; dt.scroll_y = 0; }
} else if (key.scancode == 0x4D) { // Right
if (dt.selected_disk < dt.disk_count - 1) { dt.selected_disk++; dt.selected_part = -1; dt.scroll_y = 0; }
} else if (key.ascii == 'n' || key.ascii == 'N') {
do_create_partition();
} else if (key.ascii == 'm' || key.ascii == 'M') {
do_mount_partition();
} else if (key.ascii == 'f' || key.ascii == 'F') {
open_format_dialog();
} else {
return true; // consumed but no action
}
return true;
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
montauk::memset(&g_state, 0, sizeof(g_state));
g_state.selected_disk = 0;
g_state.selected_part = -1;
// Load font
{
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (f) {
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; }
}
g_font = f;
}
disktool_refresh();
// Create window
Montauk::WinCreateResult wres;
if (montauk::win_create("Disks", INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
montauk::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
render(pixels);
montauk::win_present(win_id);
while (true) {
Montauk::WinEvent ev;
bool redraw_main = false;
bool redraw_dlg = false;
// Poll format dialog window
if (g_state.fmt_dlg.open) {
int dr = montauk::win_poll(g_state.fmt_dlg.win_id, &ev);
if (dr > 0) {
if (ev.type == 3) { // close
close_format_dialog();
redraw_main = true;
} else if (ev.type == 0 && ev.key.pressed) {
handle_key(ev.key);
redraw_dlg = g_state.fmt_dlg.open;
redraw_main = true;
} else if (ev.type == 1) {
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
handle_format_dialog_click(ev.mouse.x, ev.mouse.y, clicked);
redraw_dlg = g_state.fmt_dlg.open;
redraw_main = true;
}
}
}
// Poll main window
int r = montauk::win_poll(win_id, &ev);
if (r < 0) break;
if (r == 0) {
// Even with no main event, dialog may have triggered redraws
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); }
if (!redraw_main && !redraw_dlg) montauk::sleep_ms(16);
continue;
}
if (ev.type == 3) break; // close
// Resize
if (ev.type == 2) {
g_win_w = ev.resize.w;
g_win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
redraw_main = true;
}
// Keyboard
if (ev.type == 0 && ev.key.pressed) {
if (!handle_key(ev.key) && ev.key.scancode == 0x01)
break; // Escape quits when not in dialog
redraw_main = true;
}
// Mouse
if (ev.type == 1) {
int mx = ev.mouse.x;
int my = ev.mouse.y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
// Scroll
if (ev.mouse.scroll != 0) {
g_state.scroll_y -= ev.mouse.scroll * 20;
if (g_state.scroll_y < 0) g_state.scroll_y = 0;
redraw_main = true;
}
if (clicked) {
if (handle_toolbar_click(mx, my) || handle_content_click(mx, my))
redraw_main = true;
}
}
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); }
}
close_format_dialog();
montauk::win_destroy(win_id);
montauk::exit(0);
}
+357
View File
@@ -0,0 +1,357 @@
/*
* render.cpp
* MontaukOS Disk Tool — rendering
* Copyright (c) 2026 Daniel Hammer
*/
#include "disks.h"
// ============================================================================
// Pixel helpers
// ============================================================================
static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w > bw ? bw : x + w;
int y1 = y + h > bh ? bh : y + h;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
px[row * bw + col] = v;
}
static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) {
if (y < 0 || y >= bh) return;
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x;
int x1 = x + w > bw ? bw : x + w;
for (int col = x0; col < x1; col++)
px[y * bw + col] = v;
}
static void px_fill_rounded(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, int r, Color c) {
uint32_t v = c.to_pixel();
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= bh) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= bw) continue;
bool skip = false;
int cx, cy;
if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; }
else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; }
else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; }
else if (col >= w - r && row >= h - r) { cx = col - (w - r); cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; }
if (!skip) px[dy * bw + dx] = v;
}
}
}
static void px_rect(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
px_hline(px, bw, bh, x, y, w, c);
px_hline(px, bw, bh, x, y + h - 1, w, c);
for (int row = y; row < y + h; row++) {
if (row < 0 || row >= bh) continue;
if (x >= 0 && x < bw) px[row * bw + x] = c.to_pixel();
int rx = x + w - 1;
if (rx >= 0 && rx < bw) px[row * bw + rx] = c.to_pixel();
}
}
static void px_text(uint32_t* px, int bw, int bh,
int x, int y, const char* text, Color c) {
if (g_font)
g_font->draw_to_buffer(px, bw, bh, x, y, text, c, FONT_SIZE);
}
static int text_w(const char* text) {
return g_font ? g_font->measure_text(text, FONT_SIZE) : 0;
}
static int font_h() {
if (!g_font) return 16;
auto* cache = g_font->get_cache(FONT_SIZE);
return cache->ascent - cache->descent;
}
// ============================================================================
// Button helper
// ============================================================================
static void px_button(uint32_t* px, int bw, int bh,
int x, int y, int w, int h,
const char* label, Color bg, Color fg, int r) {
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
int tw = text_w(label);
int fh = font_h();
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg);
}
// ============================================================================
// Render: main view
// ============================================================================
static void render_toolbar(uint32_t* px) {
auto& dt = g_state;
int fh = font_h();
px_fill(px, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG);
px_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR);
int bx = 8;
for (int i = 0; i < dt.disk_count; i++) {
char label[8];
snprintf(label, sizeof(label), "Disk %d", i);
int lw = text_w(label) + 16;
Color bg = (i == dt.selected_disk)
? Color::from_rgb(0x42, 0x7A, 0xB5)
: Color::from_rgb(0xDD, 0xDD, 0xDD);
Color fg = (i == dt.selected_disk) ? WHITE : TEXT_COLOR;
px_button(px, g_win_w, g_win_h, bx, TB_BTN_Y, lw, TB_BTN_H, label, bg, fg, TB_BTN_RAD);
bx += lw + 6;
}
if (dt.disk_count == 0) {
px_text(px, g_win_w, g_win_h, 8, (TOOLBAR_H - fh) / 2, "No disks detected", TEXT_COLOR);
return;
}
// Right-side action buttons
bool has_sel = dt.selected_part >= 0;
int rx = g_win_w - 8;
int ref_w = 64; rx -= ref_w;
px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, ref_w, TB_BTN_H,
"Refresh", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, TB_BTN_RAD);
rx -= 6;
int mnt_w = 60; rx -= mnt_w;
px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, mnt_w, TB_BTN_H,
"Mount",
has_sel ? Color::from_rgb(0x42, 0x7A, 0xB5) : Color::from_rgb(0xBB, 0xBB, 0xBB),
WHITE, TB_BTN_RAD);
rx -= 6;
int fmt_w = 64; rx -= fmt_w;
px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, fmt_w, TB_BTN_H,
"Format",
has_sel ? Color::from_rgb(0x42, 0x7A, 0xB5) : Color::from_rgb(0xBB, 0xBB, 0xBB),
WHITE, TB_BTN_RAD);
rx -= 6;
int np_w = 74; rx -= np_w;
px_button(px, g_win_w, g_win_h, rx, TB_BTN_Y, np_w, TB_BTN_H,
"New Part", Color::from_rgb(0x42, 0x7A, 0xB5), WHITE, TB_BTN_RAD);
}
static void render_content(uint32_t* px) {
auto& dt = g_state;
int fh = font_h();
if (dt.disk_count == 0 || dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count)
return;
Montauk::DiskInfo& disk = dt.disks[dt.selected_disk];
int y = TOOLBAR_H + 8;
// Disk model name
px_text(px, g_win_w, g_win_h, MAP_PAD, y, disk.model, TEXT_COLOR);
// Size and type on the right
char info_str[64], size_str[24];
format_disk_size(size_str, sizeof(size_str), disk.sectorCount, disk.sectorSizeLog);
const char* dtype = disk.rpm == 1 ? "SSD" : "HDD";
snprintf(info_str, sizeof(info_str), "%s %s", size_str, dtype);
int iw = text_w(info_str);
px_text(px, g_win_w, g_win_h, g_win_w - iw - MAP_PAD, y, info_str, DIM_TEXT);
y += fh + 8;
// Partition map bar
int map_x = MAP_PAD;
int map_w = g_win_w - MAP_PAD * 2;
px_fill_rounded(px, g_win_w, g_win_h, map_x, y, map_w, MAP_H, 6,
Color::from_rgb(0xE4, 0xE4, 0xE4));
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
uint64_t total_sectors = disk.sectorCount;
if (total_sectors > 0 && nparts > 0) {
for (int pi = 0; pi < nparts; pi++) {
Montauk::PartInfo& p = dt.parts[part_indices[pi]];
int ppx = map_x + (int)((p.startLba * (uint64_t)map_w) / total_sectors);
int pw = (int)((p.sectorCount * (uint64_t)map_w) / total_sectors);
if (pw < 2) pw = 2;
if (ppx + pw > map_x + map_w) pw = map_x + map_w - ppx;
Color col = part_colors[pi % NUM_PART_COLORS];
if (pi == dt.selected_part) {
col = Color::from_rgb((col.r + 255) / 2, (col.g + 255) / 2, (col.b + 255) / 2);
}
px_fill_rounded(px, g_win_w, g_win_h, ppx, y + 2, pw, MAP_H - 4, 3, col);
char plabel[8];
snprintf(plabel, sizeof(plabel), "%d", pi);
int plw = text_w(plabel);
if (pw > plw + 4)
px_text(px, g_win_w, g_win_h, ppx + (pw - plw) / 2, y + (MAP_H - fh) / 2, plabel, WHITE);
}
}
y += MAP_H + 8;
// Column header
px_fill(px, g_win_w, g_win_h, 0, y, g_win_w, HEADER_H, HEADER_BG);
int ty = y + (HEADER_H - fh) / 2;
int col_idx = 12;
int col_name = 40;
int col_type = g_win_w / 2 - 20;
int col_size = g_win_w - 160;
int col_lba = g_win_w - 80;
px_text(px, g_win_w, g_win_h, col_idx, ty, "#", DIM_TEXT);
px_text(px, g_win_w, g_win_h, col_name, ty, "Name", DIM_TEXT);
px_text(px, g_win_w, g_win_h, col_type, ty, "Type", DIM_TEXT);
px_text(px, g_win_w, g_win_h, col_size, ty, "Size", DIM_TEXT);
px_text(px, g_win_w, g_win_h, col_lba, ty, "LBA", DIM_TEXT);
px_hline(px, g_win_w, g_win_h, 0, y + HEADER_H - 1, g_win_w, BORDER_COLOR);
y += HEADER_H;
// Partition rows
int list_bottom = g_win_h - STATUS_H;
int list_y = y;
for (int pi = 0; pi < nparts; pi++) {
int row_y = list_y + pi * ITEM_H - dt.scroll_y;
if (row_y + ITEM_H <= list_y) continue;
if (row_y >= list_bottom) break;
Montauk::PartInfo& p = dt.parts[part_indices[pi]];
if (pi == dt.selected_part)
px_fill(px, g_win_w, g_win_h, 0, row_y, g_win_w, ITEM_H, HOVER_BG);
px_fill_rounded(px, g_win_w, g_win_h, col_idx, row_y + (ITEM_H - 10) / 2, 10, 10, 5,
part_colors[pi % NUM_PART_COLORS]);
int ry = row_y + (ITEM_H - fh) / 2;
px_text(px, g_win_w, g_win_h, col_name, ry, p.name[0] ? p.name : "(unnamed)", TEXT_COLOR);
px_text(px, g_win_w, g_win_h, col_type, ry, p.typeName, DIM_TEXT);
char sz[24];
format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog);
px_text(px, g_win_w, g_win_h, col_size, ry, sz, TEXT_COLOR);
char lba_str[24];
snprintf(lba_str, sizeof(lba_str), "%lu", (unsigned)p.startLba);
px_text(px, g_win_w, g_win_h, col_lba, ry, lba_str, FAINT_TEXT);
}
if (nparts == 0)
px_text(px, g_win_w, g_win_h, col_name, list_y + 8, "No partitions found", FAINT_TEXT);
}
static void render_status(uint32_t* px) {
auto& dt = g_state;
int fh = font_h();
int sy = g_win_h - STATUS_H;
px_fill(px, g_win_w, g_win_h, 0, sy, g_win_w, STATUS_H, STATUS_BG_COL);
px_hline(px, g_win_w, g_win_h, 0, sy, g_win_w, BORDER_COLOR);
if (dt.status[0]) {
uint64_t age = montauk::get_milliseconds() - dt.status_time;
Color sc = (age < 5000)
? Color::from_rgb(0x33, 0x33, 0x33)
: Color::from_rgb(0xAA, 0xAA, 0xAA);
px_text(px, g_win_w, g_win_h, 8, sy + (STATUS_H - fh) / 2, dt.status, sc);
}
}
// ============================================================================
// Render: format dialog window
// ============================================================================
void render_format_window() {
auto& dlg = g_state.fmt_dlg;
if (!dlg.open) return;
uint32_t* px = dlg.pixels;
int dw = FMT_DLG_W, dh = FMT_DLG_H;
int fh = font_h();
px_fill(px, dw, dh, 0, 0, dw, dh, BG_COLOR);
// Title
const char* title = "Format Partition";
int tw = text_w(title);
px_text(px, dw, dh, (dw - tw) / 2, 12, title, TEXT_COLOR);
// Partition description
int ddw = text_w(dlg.part_desc);
px_text(px, dw, dh, (dw - ddw) / 2, 12 + fh + 6, dlg.part_desc, DIM_TEXT);
// Filesystem selector
int sel_y = 12 + fh * 2 + 18;
px_text(px, dw, dh, 16, sel_y, "Filesystem:", TEXT_COLOR);
int opt_y = sel_y + fh + 8;
for (int i = 0; i < NUM_FS_TYPES; i++) {
int ox = 24;
int ow = dw - 48;
int oh = 28;
int iy = opt_y + i * (oh + 4);
if (i == dlg.selected_fs) {
px_fill_rounded(px, dw, dh, ox, iy, ow, oh, 6,
Color::from_rgb(0xD0, 0xE0, 0xF0));
px_fill_rounded(px, dw, dh, ox - 1, iy - 1, ow + 2, oh + 2, 7, Color::from_rgb(0x42, 0x7A, 0xB5));
px_fill_rounded(px, dw, dh, ox, iy, ow, oh, 6, Color::from_rgb(0xD0, 0xE0, 0xF0));
} else {
px_fill_rounded(px, dw, dh, ox - 1, iy - 1, ow + 2, oh + 2, 7, BORDER_COLOR);
px_fill_rounded(px, dw, dh, ox, iy, ow, oh, 6, BG_COLOR);
}
// Radio indicator (circular)
int cx = ox + 14;
int cy = iy + oh / 2;
px_fill_rounded(px, dw, dh, cx - 5, cy - 5, 10, 10, 5, DIM_TEXT);
px_fill_rounded(px, dw, dh, cx - 4, cy - 4, 8, 8, 4, BG_COLOR);
if (i == dlg.selected_fs)
px_fill_rounded(px, dw, dh, cx - 3, cy - 3, 6, 6, 3, Color::from_rgb(0x42, 0x7A, 0xB5));
px_text(px, dw, dh, cx + 12, iy + (oh - fh) / 2, g_fsTypes[i].name, TEXT_COLOR);
}
// Buttons
int btn_w = 90, btn_h = 30;
int btn_y = dh - btn_h - 16;
int gap = 16;
int total_w = btn_w * 2 + gap;
int bx = (dw - total_w) / 2;
Color fmt_bg = dlg.hover_format
? Color::from_rgb(0xDD, 0x44, 0x44)
: Color::from_rgb(0xCC, 0x33, 0x33);
px_button(px, dw, dh, bx, btn_y, btn_w, btn_h, "Format", fmt_bg, WHITE, 6);
Color can_bg = dlg.hover_cancel
? Color::from_rgb(0x99, 0x99, 0x99)
: Color::from_rgb(0x88, 0x88, 0x88);
px_button(px, dw, dh, bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", can_bg, WHITE, 6);
}
// ============================================================================
// Top-level render
// ============================================================================
void render(uint32_t* pixels) {
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR);
render_toolbar(pixels);
render_content(pixels);
render_status(pixels);
}
+35
View File
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+4 -8
View File
@@ -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
View File
@@ -28,14 +28,17 @@ static void scat(char* dst, const char* src, int maxLen) {
dst[dLen + i] = '\0';
}
// Current working directory (relative to 0:/).
// Current working directory (relative to N:/).
// "" = root, "man" = 0:/man, "man/sub" = 0:/man/sub
static char cwd[128] = "";
static int current_drive = 0;
// Build VFS directory path: "0:/" or "0:/<dir>"
static void build_dir_path(const char* dir, char* out, int outMax) {
// Build VFS path with explicit drive number
static void build_drive_path(int drive, const char* dir, char* out, int outMax) {
int i = 0;
out[i++] = '0'; out[i++] = ':'; out[i++] = '/';
if (drive >= 10) { out[i++] = '0' + drive / 10; }
out[i++] = '0' + drive % 10;
out[i++] = ':'; out[i++] = '/';
if (dir && dir[0]) {
int j = 0;
while (dir[j] && i < outMax - 1) out[i++] = dir[j++];
@@ -43,6 +46,32 @@ static void build_dir_path(const char* dir, char* out, int outMax) {
out[i] = '\0';
}
// Build VFS directory path: "N:/" or "N:/<dir>" using current_drive
static void build_dir_path(const char* dir, char* out, int outMax) {
build_drive_path(current_drive, dir, out, outMax);
}
// Parse drive number from a drive prefix like "0:" or "12:". Returns -1 if invalid.
static int parse_drive_prefix(const char* s) {
if (s[0] < '0' || s[0] > '9') return -1;
int n = s[0] - '0';
if (s[1] >= '0' && s[1] <= '9') {
n = n * 10 + (s[1] - '0');
if (s[2] != ':') return -1;
} else if (s[1] == ':') {
// single digit drive
} else {
return -1;
}
return n;
}
// Length of drive prefix ("0:" = 2, "12:" = 3). Assumes valid prefix.
static int drive_prefix_len(const char* s) {
if (s[1] >= '0' && s[1] <= '9') return 3; // e.g. "12:"
return 2; // e.g. "0:"
}
// ---- Command history ----
static constexpr int HISTORY_MAX = 32;
@@ -72,7 +101,17 @@ static const char* history_get(int idx) {
// ---- Prompt ----
static void prompt() {
montauk::print("0:/");
char drv[4];
if (current_drive >= 10) {
drv[0] = '0' + current_drive / 10;
drv[1] = '0' + current_drive % 10;
drv[2] = '\0';
} else {
drv[0] = '0' + current_drive;
drv[1] = '\0';
}
montauk::print(drv);
montauk::print(":/");
if (cwd[0]) montauk::print(cwd);
montauk::print("> ");
}
@@ -108,6 +147,7 @@ static void cmd_help() {
montauk::print(" help Show this help message\n");
montauk::print(" ls [dir] List files in directory\n");
montauk::print(" cd [dir] Change working directory\n");
montauk::print(" N: Switch to drive N (e.g. 1:)\n");
montauk::print(" exit Exit the shell\n");
montauk::print("\n");
montauk::print("System commands:\n");
@@ -138,9 +178,9 @@ static void cmd_help() {
montauk::print("Any .elf on the ramdisk is executable.\n");
}
// Check if a string already has a VFS drive prefix (e.g. "0:/")
// Check if a string already has a VFS drive prefix (e.g. "0:/" or "12:/")
static bool has_drive_prefix(const char* s) {
return s[0] >= '0' && s[0] <= '9' && s[1] == ':';
return parse_drive_prefix(s) >= 0;
}
// ---- Builtin: ls ----
@@ -150,10 +190,13 @@ static void cmd_ls(const char* arg) {
// Build the target directory (relative path from root)
char dir[128];
int drive = current_drive;
if (*arg) {
if (has_drive_prefix(arg)) {
// Absolute VFS path: "0:/something"
scopy(dir, arg + 3, sizeof(dir));
// Absolute VFS path: "N:/something"
drive = parse_drive_prefix(arg);
int plen = drive_prefix_len(arg);
scopy(dir, arg + plen + 1, sizeof(dir)); // skip "N:/"
} else if (arg[0] == '/') {
// Absolute path from root: "/something"
scopy(dir, arg + 1, sizeof(dir));
@@ -172,7 +215,7 @@ static void cmd_ls(const char* arg) {
}
char path[128];
build_dir_path(dir, path, sizeof(path));
build_drive_path(drive, dir, path, sizeof(path));
const char* entries[64];
int count = montauk::readdir(path, entries, 64);
@@ -198,6 +241,18 @@ static void cmd_ls(const char* arg) {
// ---- Builtin: cd ----
// Switch to a drive. Returns true if valid.
static bool switch_drive(int drive) {
// Validate drive exists by trying to readdir its root
char path[8];
build_drive_path(drive, "", path, sizeof(path));
const char* entries[1];
if (montauk::readdir(path, entries, 1) < 0) return false;
current_drive = drive;
cwd[0] = '\0';
return true;
}
static void cmd_cd(const char* arg) {
arg = skip_spaces(arg);
@@ -247,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();