feat: split desktop implementation into multiple source files, reorganize apps

This commit is contained in:
2026-03-06 08:15:06 +01:00
parent b148f0605a
commit 3985188baa
24 changed files with 1591 additions and 1517 deletions
@@ -0,0 +1,404 @@
/*
* app_calculator.cpp
* MontaukOS Desktop - Calculator application
* Integer-only 4-function calculator (values scaled by 100 for 2 decimal places)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Calculator state
// ============================================================================
struct CalcState {
int64_t display_val; // current display value * 100
int64_t accumulator; // stored accumulator * 100
char pending_op; // '+', '-', '*', '/', or 0
bool start_new; // next digit starts a new number
bool has_decimal; // decimal point pressed
int decimal_digits; // number of decimal digits entered
char display_str[32]; // formatted display string
};
static constexpr int CALC_DISPLAY_H = 56;
static constexpr int CALC_BTN_W = 52;
static constexpr int CALC_BTN_H = 40;
static constexpr int CALC_BTN_PAD = 4;
// ============================================================================
// Display formatting
// ============================================================================
static void calc_format_display(CalcState* cs) {
int64_t val = cs->display_val;
bool neg = val < 0;
if (neg) val = -val;
int64_t integer = val / 100;
int64_t frac = val % 100;
if (cs->has_decimal || frac != 0) {
// Show decimal
char int_buf[20];
int pos = 0;
if (integer == 0) {
int_buf[pos++] = '0';
} else {
char tmp[20]; int ti = 0;
int64_t v = integer;
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
while (ti > 0) int_buf[pos++] = tmp[--ti];
}
int_buf[pos] = '\0';
if (neg) {
snprintf(cs->display_str, 32, "-%s.%02d", int_buf, (int)frac);
} else {
snprintf(cs->display_str, 32, "%s.%02d", int_buf, (int)frac);
}
// Trim trailing zeros after decimal point (unless user is entering decimals)
if (!cs->has_decimal) {
int len = montauk::slen(cs->display_str);
while (len > 1 && cs->display_str[len - 1] == '0') {
cs->display_str[--len] = '\0';
}
if (len > 0 && cs->display_str[len - 1] == '.') {
cs->display_str[--len] = '\0';
}
}
} else {
// Integer display
if (neg) {
char int_buf[20];
int pos = 0;
int64_t v = integer;
if (v == 0) {
int_buf[pos++] = '0';
} else {
char tmp[20]; int ti = 0;
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
while (ti > 0) int_buf[pos++] = tmp[--ti];
}
int_buf[pos] = '\0';
snprintf(cs->display_str, 32, "-%s", int_buf);
} else {
int pos = 0;
int64_t v = integer;
if (v == 0) {
cs->display_str[pos++] = '0';
} else {
char tmp[20]; int ti = 0;
while (v > 0) { tmp[ti++] = '0' + (int)(v % 10); v /= 10; }
while (ti > 0) cs->display_str[pos++] = tmp[--ti];
}
cs->display_str[pos] = '\0';
}
}
}
// ============================================================================
// Calculator operations
// ============================================================================
static void calc_apply_op(CalcState* cs) {
if (cs->pending_op == 0) {
cs->accumulator = cs->display_val;
return;
}
switch (cs->pending_op) {
case '+': cs->accumulator += cs->display_val; break;
case '-': cs->accumulator -= cs->display_val; break;
case '*': cs->accumulator = (cs->accumulator * cs->display_val) / 100; break;
case '/':
if (cs->display_val != 0)
cs->accumulator = (cs->accumulator * 100) / cs->display_val;
break;
}
}
static void calc_input_digit(CalcState* cs, int digit) {
if (cs->start_new) {
cs->display_val = 0;
cs->start_new = false;
cs->has_decimal = false;
cs->decimal_digits = 0;
}
if (cs->has_decimal) {
if (cs->decimal_digits < 2) {
cs->decimal_digits++;
bool neg = cs->display_val < 0;
int64_t abs_val = neg ? -cs->display_val : cs->display_val;
if (cs->decimal_digits == 1) {
abs_val = (abs_val / 100) * 100 + digit * 10;
} else {
abs_val = abs_val + digit;
}
cs->display_val = neg ? -abs_val : abs_val;
}
} else {
bool neg = cs->display_val < 0;
int64_t abs_val = neg ? -cs->display_val : cs->display_val;
int64_t integer = abs_val / 100;
if (integer < 999999999) {
integer = integer * 10 + digit;
cs->display_val = integer * 100;
if (neg) cs->display_val = -cs->display_val;
}
}
calc_format_display(cs);
}
static void calc_press_operator(CalcState* cs, char op) {
if (!cs->start_new) {
calc_apply_op(cs);
cs->display_val = cs->accumulator;
}
cs->pending_op = op;
cs->start_new = true;
cs->has_decimal = false;
cs->decimal_digits = 0;
calc_format_display(cs);
}
static void calc_press_equals(CalcState* cs) {
calc_apply_op(cs);
cs->display_val = cs->accumulator;
cs->pending_op = 0;
cs->start_new = true;
cs->has_decimal = false;
cs->decimal_digits = 0;
calc_format_display(cs);
}
static void calc_press_clear(CalcState* cs) {
cs->display_val = 0;
cs->accumulator = 0;
cs->pending_op = 0;
cs->start_new = false;
cs->has_decimal = false;
cs->decimal_digits = 0;
calc_format_display(cs);
}
static void calc_press_negate(CalcState* cs) {
cs->display_val = -cs->display_val;
calc_format_display(cs);
}
static void calc_press_percent(CalcState* cs) {
// Divide by 100: display_val is already scaled by 100, so dividing by 100 means /100
cs->display_val = cs->display_val / 100;
calc_format_display(cs);
}
static void calc_press_decimal(CalcState* cs) {
if (cs->start_new) {
cs->display_val = 0;
cs->start_new = false;
cs->decimal_digits = 0;
}
cs->has_decimal = true;
calc_format_display(cs);
}
// ============================================================================
// Drawing
// ============================================================================
// Button layout: labels[row][col]
static const char* calc_labels[5][4] = {
{ "C", "+/-", "%", "/" },
{ "7", "8", "9", "*" },
{ "4", "5", "6", "-" },
{ "1", "2", "3", "+" },
{ "0", "0", ".", "=" },
};
static void calculator_on_draw(Window* win, Framebuffer& fb) {
CalcState* cs = (CalcState*)win->app_data;
if (!cs) return;
Canvas c(win);
// Background
c.fill(Color::from_rgb(0xF0, 0xF0, 0xF0));
// Display area
c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D));
// Display text (right-aligned, 2x scale)
int text_w;
int large_h;
if (fonts::system_font && fonts::system_font->valid) {
text_w = fonts::system_font->measure_text(cs->display_str, fonts::LARGE_SIZE);
large_h = fonts::system_font->get_line_height(fonts::LARGE_SIZE);
} else {
int text_len = montauk::slen(cs->display_str);
text_w = text_len * FONT_WIDTH * 2;
large_h = FONT_HEIGHT * 2;
}
int tx = c.w - text_w - 12;
int ty = (CALC_DISPLAY_H - large_h) / 2;
if (tx < 4) tx = 4;
c.text_2x(tx, ty, cs->display_str, colors::WHITE);
// Button grid
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
for (int row = 0; row < 5; row++) {
for (int col = 0; col < 4; col++) {
// Skip second column of "0" button (it spans 2 cols)
if (row == 4 && col == 1) continue;
int bx = CALC_BTN_PAD + col * (CALC_BTN_W + CALC_BTN_PAD);
int by = grid_y + row * (CALC_BTN_H + CALC_BTN_PAD);
int bw = CALC_BTN_W;
// "0" button spans 2 columns
if (row == 4 && col == 0) {
bw = CALC_BTN_W * 2 + CALC_BTN_PAD;
}
// Button color
Color btn_color;
if (col == 3) {
btn_color = colors::ACCENT;
} else if (row == 0) {
btn_color = Color::from_rgb(0xD0, 0xD0, 0xD0);
} else {
btn_color = Color::from_rgb(0xE8, 0xE8, 0xE8);
}
// Draw button
c.fill_rect(bx, by, bw, CALC_BTN_H, btn_color);
// Button text
const char* label = calc_labels[row][col];
Color label_color = (col == 3) ? colors::WHITE : colors::TEXT_COLOR;
int label_w = text_width(label);
int lx = bx + (bw - label_w) / 2;
int ly = by + (CALC_BTN_H - system_font_height()) / 2;
c.text(lx, ly, label, label_color);
}
}
}
// ============================================================================
// Mouse handling
// ============================================================================
static void calculator_on_mouse(Window* win, MouseEvent& ev) {
CalcState* cs = (CalcState*)win->app_data;
if (!cs) return;
if (!ev.left_pressed()) return;
Rect cr = win->content_rect();
int local_x = ev.x - cr.x;
int local_y = ev.y - cr.y;
// Check if click is in button grid
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
if (local_y < grid_y) return;
int row = (local_y - grid_y) / (CALC_BTN_H + CALC_BTN_PAD);
int col = (local_x - CALC_BTN_PAD) / (CALC_BTN_W + CALC_BTN_PAD);
if (row < 0 || row > 4 || col < 0 || col > 3) return;
// Handle "0" button spanning 2 columns
if (row == 4 && col <= 1) col = 0;
// Dispatch button press
if (row == 0) {
switch (col) {
case 0: calc_press_clear(cs); break;
case 1: calc_press_negate(cs); break;
case 2: calc_press_percent(cs); break;
case 3: calc_press_operator(cs, '/'); break;
}
} else if (row >= 1 && row <= 3) {
if (col == 3) {
char ops[] = {'*', '-', '+'};
calc_press_operator(cs, ops[row - 1]);
} else {
int digits[3][3] = {{7,8,9},{4,5,6},{1,2,3}};
calc_input_digit(cs, digits[row - 1][col]);
}
} else if (row == 4) {
switch (col) {
case 0: calc_input_digit(cs, 0); break;
case 2: calc_press_decimal(cs); break;
case 3: calc_press_equals(cs); break;
}
}
}
// ============================================================================
// Keyboard handling
// ============================================================================
static void calculator_on_key(Window* win, const Montauk::KeyEvent& key) {
CalcState* cs = (CalcState*)win->app_data;
if (!cs || !key.pressed) return;
if (key.ascii >= '0' && key.ascii <= '9') {
calc_input_digit(cs, key.ascii - '0');
} else if (key.ascii == '+') {
calc_press_operator(cs, '+');
} else if (key.ascii == '-') {
calc_press_operator(cs, '-');
} else if (key.ascii == '*') {
calc_press_operator(cs, '*');
} else if (key.ascii == '/') {
calc_press_operator(cs, '/');
} else if (key.ascii == '=' || key.ascii == '\n' || key.ascii == '\r') {
calc_press_equals(cs);
} else if (key.ascii == '.') {
calc_press_decimal(cs);
} else if (key.ascii == 'c' || key.ascii == 'C' || key.scancode == 0x0E) {
calc_press_clear(cs);
} else if (key.ascii == '%') {
calc_press_percent(cs);
}
}
static void calculator_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Calculator launcher
// ============================================================================
void open_calculator(DesktopState* ds) {
int calc_w = CALC_BTN_PAD + 4 * (CALC_BTN_W + CALC_BTN_PAD);
int calc_h = CALC_DISPLAY_H + CALC_BTN_PAD + 5 * (CALC_BTN_H + CALC_BTN_PAD);
int idx = desktop_create_window(ds, "Calculator", 350, 150, calc_w, calc_h + TITLEBAR_HEIGHT + BORDER_WIDTH);
if (idx < 0) return;
Window* win = &ds->windows[idx];
CalcState* cs = (CalcState*)montauk::malloc(sizeof(CalcState));
montauk::memset(cs, 0, sizeof(CalcState));
cs->display_val = 0;
cs->accumulator = 0;
cs->pending_op = 0;
cs->start_new = false;
cs->has_decimal = false;
cs->decimal_digits = 0;
calc_format_display(cs);
win->app_data = cs;
win->on_draw = calculator_on_draw;
win->on_mouse = calculator_on_mouse;
win->on_key = calculator_on_key;
win->on_close = calculator_on_close;
}
@@ -0,0 +1,475 @@
/*
* app_devexplorer.cpp
* MontaukOS Desktop - Device Explorer (lists hardware detected by the kernel)
* 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
"PCI", // 7
};
static constexpr int NUM_CATEGORIES = 8;
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(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;
};
// ============================================================================
// 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));
}
}
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
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;
} else {
de->selected_row = i;
}
return;
}
cur_y += row_h;
if (cur_y >= win->content_h) break;
}
// Clicked below all rows
de->selected_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;
// 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;
}
+12
View File
@@ -0,0 +1,12 @@
/*
* app_doom.cpp
* MontaukOS Desktop - DOOM launcher (spawns standalone doom.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_doom(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/games/doom.elf");
}
@@ -0,0 +1,745 @@
/*
* app_filemanager.cpp
* MontaukOS Desktop - Enhanced File Manager application
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// File Manager state
// ============================================================================
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_sizes[64];
int entry_count;
int selected;
int scroll_offset;
bool is_dir[64];
int last_click_item;
uint64_t last_click_time;
Scrollbar scrollbar;
DesktopState* desktop;
bool grid_view;
};
static constexpr int FM_TOOLBAR_H = 32;
static constexpr int FM_PATHBAR_H = 24;
static constexpr int FM_HEADER_H = 20;
static constexpr int FM_ITEM_H = 24;
static constexpr int FM_SCROLLBAR_W = 12;
static constexpr int FM_GRID_CELL_W = 80;
static constexpr int FM_GRID_CELL_H = 80;
static constexpr int FM_GRID_ICON = 48;
static constexpr int FM_GRID_PAD = 4;
// ============================================================================
// File type detection
// ============================================================================
static bool str_ends_with(const char* s, const char* suffix) {
int slen = montauk::slen(s);
int suflen = montauk::slen(suffix);
if (suflen > slen) return false;
for (int i = 0; i < suflen; i++) {
char sc = s[slen - suflen + i];
char ec = suffix[i];
if (sc >= 'A' && sc <= 'Z') sc += 32;
if (ec >= 'A' && ec <= 'Z') ec += 32;
if (sc != ec) return false;
}
return true;
}
static bool is_image_file(const char* name) {
return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg");
}
static bool is_font_file(const char* name) {
return str_ends_with(name, ".ttf");
}
static bool is_pdf_file(const char* name) {
return str_ends_with(name, ".pdf");
}
static bool is_spreadsheet_file(const char* name) {
return str_ends_with(name, ".mss");
}
static int detect_file_type(const char* name, bool is_dir) {
if (is_dir) return 1;
if (str_ends_with(name, ".elf")) return 2;
return 0;
}
// ============================================================================
// Directory reading with sorting and file sizes
// ============================================================================
static void filemanager_read_dir(FileManagerState* fm) {
const char* names[64];
fm->entry_count = montauk::readdir(fm->current_path, names, 64);
if (fm->entry_count < 0) fm->entry_count = 0;
// readdir returns full paths from the VFS (e.g. "man/fetch.1" instead
// of just "fetch.1"). Compute the prefix to strip so we get basenames.
const char* after_drive = fm->current_path;
for (int k = 0; after_drive[k]; k++) {
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
after_drive += k + 2;
break;
}
}
char prefix[256] = {0};
int prefix_len = 0;
if (after_drive[0] != '\0') {
montauk::strcpy(prefix, after_drive);
prefix_len = montauk::slen(prefix);
if (prefix_len > 0 && prefix[prefix_len - 1] != '/') {
prefix[prefix_len++] = '/';
prefix[prefix_len] = '\0';
}
}
for (int i = 0; i < fm->entry_count; i++) {
const char* raw = names[i];
// Strip directory prefix if it matches
if (prefix_len > 0) {
bool match = true;
for (int k = 0; k < prefix_len; k++) {
if (raw[k] != prefix[k]) { match = false; break; }
}
if (match) raw += prefix_len;
}
montauk::strncpy(fm->entry_names[i], raw, 63);
int len = montauk::slen(fm->entry_names[i]);
// Detect directory
if (len > 0 && fm->entry_names[i][len - 1] == '/') {
fm->is_dir[i] = true;
fm->entry_names[i][len - 1] = '\0';
} else {
bool has_dot = false;
for (int j = 0; j < len; j++) {
if (fm->entry_names[i][j] == '.') { has_dot = true; break; }
}
fm->is_dir[i] = !has_dot;
}
fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]);
// Get file size
fm->entry_sizes[i] = 0;
if (!fm->is_dir[i]) {
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[i], 512);
int fd = montauk::open(fullpath);
if (fd >= 0) {
fm->entry_sizes[i] = (int)montauk::getsize(fd);
montauk::close(fd);
}
}
}
// Sort: directories first, then alphabetical (case-insensitive)
for (int i = 1; i < fm->entry_count; i++) {
char tmp_name[64];
int tmp_type = fm->entry_types[i];
int tmp_size = fm->entry_sizes[i];
bool tmp_isdir = fm->is_dir[i];
montauk::strcpy(tmp_name, fm->entry_names[i]);
int j = i - 1;
while (j >= 0) {
bool swap = false;
if (tmp_isdir && !fm->is_dir[j]) {
swap = true;
} else if (tmp_isdir == fm->is_dir[j]) {
if (str_compare_ci(tmp_name, fm->entry_names[j]) < 0) {
swap = true;
}
}
if (!swap) break;
montauk::strcpy(fm->entry_names[j + 1], fm->entry_names[j]);
fm->entry_types[j + 1] = fm->entry_types[j];
fm->entry_sizes[j + 1] = fm->entry_sizes[j];
fm->is_dir[j + 1] = fm->is_dir[j];
j--;
}
montauk::strcpy(fm->entry_names[j + 1], tmp_name);
fm->entry_types[j + 1] = tmp_type;
fm->entry_sizes[j + 1] = tmp_size;
fm->is_dir[j + 1] = tmp_isdir;
}
fm->selected = -1;
fm->scroll_offset = 0;
fm->scrollbar.scroll_offset = 0;
fm->last_click_item = -1;
fm->last_click_time = 0;
}
// ============================================================================
// History management
// ============================================================================
static void filemanager_push_history(FileManagerState* fm) {
// Don't push if same as current position
if (fm->history_count > 0 && fm->history_pos >= 0) {
if (montauk::streq(fm->history[fm->history_pos], fm->current_path)) return;
}
fm->history_pos++;
if (fm->history_pos >= 16) fm->history_pos = 15;
montauk::strcpy(fm->history[fm->history_pos], fm->current_path);
fm->history_count = fm->history_pos + 1;
}
static void filemanager_navigate(FileManagerState* fm, const char* name) {
int path_len = montauk::slen(fm->current_path);
if (path_len > 0 && fm->current_path[path_len - 1] != '/') {
str_append(fm->current_path, "/", 256);
}
str_append(fm->current_path, name, 256);
filemanager_push_history(fm);
filemanager_read_dir(fm);
}
static void filemanager_go_up(FileManagerState* fm) {
int len = montauk::slen(fm->current_path);
if (len <= 3) return; // "0:/" is root
if (len > 0 && fm->current_path[len - 1] == '/') {
fm->current_path[len - 1] = '\0';
len--;
}
int last_slash = -1;
for (int i = len - 1; i >= 0; i--) {
if (fm->current_path[i] == '/') { last_slash = i; break; }
}
if (last_slash >= 0) {
fm->current_path[last_slash + 1] = '\0';
}
filemanager_push_history(fm);
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);
}
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);
}
static void filemanager_go_home(FileManagerState* fm) {
montauk::strcpy(fm->current_path, "0:/");
filemanager_push_history(fm);
filemanager_read_dir(fm);
}
// ============================================================================
// Drawing
// ============================================================================
static void filemanager_draw_header(Canvas& c, FileManagerState* fm,
Color toolbar_color, Color btn_bg) {
DesktopState* ds = fm->desktop;
// ---- Toolbar (32px) ----
c.fill_rect(0, 0, c.w, FM_TOOLBAR_H, toolbar_color);
struct ToolBtn { int x; SvgIcon* icon; };
ToolBtn btns[4] = {
{ 4, ds ? &ds->icon_go_back : nullptr },
{ 32, ds ? &ds->icon_go_forward : nullptr },
{ 60, ds ? &ds->icon_go_up : nullptr },
{ 88, ds ? &ds->icon_home : nullptr },
};
for (int i = 0; i < 4; i++) {
int bx = btns[i].x;
int by = 4;
c.fill_rect(bx, by, 24, 24, btn_bg);
if (btns[i].icon) {
int ix = bx + (24 - btns[i].icon->width) / 2;
int iy = by + (24 - btns[i].icon->height) / 2;
c.icon(ix, iy, *btns[i].icon);
}
}
// Toggle view button (5th toolbar button)
{
int bx = 120, by = 4;
c.fill_rect(bx, by, 24, 24, btn_bg);
if (fm->grid_view) {
for (int r = 0; r < 2; r++)
for (int cc = 0; cc < 2; cc++)
c.fill_rect(bx + 5 + cc * 8, by + 5 + r * 8, 6, 6, colors::TEXT_COLOR);
} else {
for (int r = 0; r < 3; r++)
c.fill_rect(bx + 5, by + 5 + r * 5, 14, 2, colors::TEXT_COLOR);
}
}
// 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);
// Path bar separator
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
}
static void filemanager_on_draw(Window* win, Framebuffer& fb) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color toolbar_color = Color::from_rgb(0xF5, 0xF5, 0xF5);
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
DesktopState* ds = fm->desktop;
// Draw header (toolbar + path bar)
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
if (fm->grid_view) {
// ---- Grid View ----
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
int list_h = c.h - list_y;
int cols = (c.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
int rows = (fm->entry_count + cols - 1) / cols;
int content_h = rows * FM_GRID_CELL_H;
// Update scrollbar
fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
fm->scrollbar.content_height = content_h;
fm->scrollbar.view_height = list_h;
for (int i = 0; i < fm->entry_count; i++) {
int col = i % cols;
int row = i / cols;
int cell_x = col * FM_GRID_CELL_W;
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
// Skip if entirely off-screen
if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= c.h) continue;
// Selection highlight
if (i == fm->selected) {
int sy = gui_max(cell_y, list_y);
int sh = gui_min(cell_y + FM_GRID_CELL_H, c.h) - sy;
int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x);
if (sh > 0 && sw > 0)
c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER);
}
// 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) {
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);
} else if (ds && ds->icon_file_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_file_lg);
} else {
Color icon_c = fm->is_dir[i]
? Color::from_rgb(0xFF, 0xBD, 0x2E)
: Color::from_rgb(0x90, 0x90, 0x90);
int iy_clip = gui_max(icon_y, list_y);
int ih_clip = FM_GRID_ICON - (iy_clip - icon_y);
if (ih_clip > 0)
c.fill_rect(icon_x, iy_clip, FM_GRID_ICON, ih_clip, icon_c);
}
// Filename centered below icon, truncated if needed
char label[16];
int nlen = montauk::slen(fm->entry_names[i]);
if (nlen > 9) {
for (int k = 0; k < 9; k++) label[k] = fm->entry_names[i][k];
label[9] = '.';
label[10] = '.';
label[11] = '\0';
} else {
montauk::strncpy(label, fm->entry_names[i], 15);
}
int tw = text_width(label);
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
if (tx < cell_x) tx = cell_x;
int ty = icon_y + FM_GRID_ICON + 2;
if (ty >= list_y && ty + system_font_height() <= c.h)
c.text(tx, ty, label, colors::TEXT_COLOR);
}
} else {
// ---- List View ----
// ---- Column headers ----
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
int name_col_x = 8;
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
int type_col_x = c.w - FM_SCROLLBAR_W - 60;
c.text(name_col_x, header_y + 2, "Name", dim);
if (size_col_x > 100)
c.text(size_col_x, header_y + 2, "Size", dim);
if (type_col_x > 160)
c.text(type_col_x, header_y + 2, "Type", dim);
// Header separator
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
// Column separator lines
if (size_col_x > 100) {
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
}
// ---- File entries ----
int list_y = header_y + FM_HEADER_H;
int list_h = c.h - list_y;
int visible_items = list_h / FM_ITEM_H;
int content_h = fm->entry_count * FM_ITEM_H;
// Update scrollbar
fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
fm->scrollbar.content_height = content_h;
fm->scrollbar.view_height = list_h;
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue;
// Highlight selected
if (i == fm->selected) {
int sy = gui_max(iy, list_y);
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
if (sh > 0)
c.fill_rect(0, sy, c.w - FM_SCROLLBAR_W, sh, colors::MENU_HOVER);
}
// 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) {
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);
} else if (ds && ds->icon_file.pixels) {
c.icon(ico_x, ico_y, ds->icon_file);
} else {
Color icon_c = fm->is_dir[i]
? Color::from_rgb(0xFF, 0xBD, 0x2E)
: Color::from_rgb(0x90, 0x90, 0x90);
int iy_clip = gui_max(ico_y, list_y);
int ih_clip = 16 - (iy_clip - ico_y);
if (ih_clip > 0)
c.fill_rect(ico_x, iy_clip, 16, ih_clip, icon_c);
}
// Name
int tx = 30;
int fm_sfh = system_font_height();
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
if (ty >= list_y && ty + fm_sfh <= c.h)
c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR);
// Size
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
char size_str[16];
format_size(size_str, fm->entry_sizes[i]);
c.text(size_col_x, ty, size_str, dim);
}
// 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";
else if (fm->entry_types[i] == 2) type_str = "Exec";
c.text(type_col_x, ty, type_str, dim);
}
}
}
// ---- Scrollbar ----
if (fm->scrollbar.content_height > fm->scrollbar.view_height) {
Color sb_fg_color = (fm->scrollbar.hovered || fm->scrollbar.dragging)
? fm->scrollbar.hover_fg : fm->scrollbar.fg;
int sbx = fm->scrollbar.bounds.x;
int sby = fm->scrollbar.bounds.y;
int sbw = fm->scrollbar.bounds.w;
int sbh = fm->scrollbar.bounds.h;
c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG);
int th = fm->scrollbar.thumb_height();
int tty = fm->scrollbar.thumb_y();
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color);
}
// Repaint header on top of content to clip scrolled icon overflow
if (fm->scrollbar.scroll_offset > 0)
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
}
// ============================================================================
// Mouse handling
// ============================================================================
static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm) return;
Rect cr = win->content_rect();
int local_x = ev.x - cr.x;
int local_y = ev.y - cr.y;
int cw = cr.w;
// Scrollbar interaction
MouseEvent local_ev = ev;
local_ev.x = local_x;
local_ev.y = local_y;
fm->scrollbar.handle_mouse(local_ev);
if (ev.left_pressed()) {
// Toolbar button clicks
if (local_y < FM_TOOLBAR_H) {
if (local_x >= 4 && local_x < 28) filemanager_go_back(fm);
else if (local_x >= 32 && local_x < 56) filemanager_go_forward(fm);
else if (local_x >= 60 && local_x < 84) filemanager_go_up(fm);
else if (local_x >= 88 && local_x < 112) filemanager_go_home(fm);
else if (local_x >= 120 && local_x < 144) {
fm->grid_view = !fm->grid_view;
fm->scrollbar.scroll_offset = 0;
}
return;
}
// File clicks (grid vs list)
if (fm->grid_view) {
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
int col = local_x / FM_GRID_CELL_W;
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
int clicked_idx = row * cols + col;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count && col < cols) {
uint64_t now = montauk::get_milliseconds();
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
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[clicked_idx], 512);
if (is_image_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/imageviewer.elf", fullpath);
} else if (is_font_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/fontpreview.elf", fullpath);
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
} else if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
}
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
}
}
} else {
// List view clicks
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
int clicked_idx = rel_y / FM_ITEM_H;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = montauk::get_milliseconds();
// Double-click detection
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
// Open file in appropriate viewer
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[clicked_idx], 512);
if (is_image_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/imageviewer.elf", fullpath);
} else if (is_font_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/fontpreview.elf", fullpath);
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
} else if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
}
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
}
}
}
}
// Scroll handling
if (ev.scroll != 0) {
int list_y_start = fm->grid_view
? FM_TOOLBAR_H + FM_PATHBAR_H
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
if (local_y >= list_y_start) {
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
int ms = fm->scrollbar.max_scroll();
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
if (fm->scrollbar.scroll_offset > ms) fm->scrollbar.scroll_offset = ms;
}
}
}
// ============================================================================
// Keyboard handling
// ============================================================================
static void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm || !key.pressed) return;
if (key.ascii == '\b' || key.scancode == 0x0E) {
filemanager_go_up(fm);
} else if (key.scancode == 0x48) {
// Up arrow
if (fm->grid_view) {
Rect cr = win->content_rect();
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected >= cols) fm->selected -= cols;
} else {
if (fm->selected > 0) fm->selected--;
}
} else if (key.scancode == 0x50) {
// Down arrow
if (fm->grid_view) {
Rect cr = win->content_rect();
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
} else {
if (fm->selected < fm->entry_count - 1) fm->selected++;
}
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
// Left arrow (grid view only)
if (fm->selected > 0) fm->selected--;
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
// Right arrow (grid view only)
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]) {
filemanager_navigate(fm, fm->entry_names[fm->selected]);
}
}
} else if (key.alt && key.scancode == 0x4B) {
// Alt+Left: go back
filemanager_go_back(fm);
} else if (key.alt && key.scancode == 0x4D) {
// Alt+Right: go forward
filemanager_go_forward(fm);
}
}
static void filemanager_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// File Manager launcher
// ============================================================================
void open_filemanager(DesktopState* ds) {
int idx = desktop_create_window(ds, "Files", 150, 120, 560, 420);
if (idx < 0) return;
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;
fm->history_count = 0;
fm->desktop = ds;
fm->grid_view = true;
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
filemanager_push_history(fm);
filemanager_read_dir(fm);
win->app_data = fm;
win->on_draw = filemanager_on_draw;
win->on_mouse = filemanager_on_mouse;
win->on_key = filemanager_on_key;
win->on_close = filemanager_on_close;
}
+200
View File
@@ -0,0 +1,200 @@
/*
* app_klog.cpp
* MontaukOS Desktop - Kernel Log viewer (tails the kernel ring buffer)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Kernel Log state
// ============================================================================
static constexpr int KLOG_READ_SIZE = 65536; // matches kernel ring buffer size
static constexpr int KLOG_POLL_MS = 250;
struct KlogState {
TerminalState term;
char* klog_buf;
int last_len;
char last_tail_byte;
uint64_t last_poll_ms;
};
// ============================================================================
// Re-feed log into the terminal (fills scrollback + visible screen)
// ============================================================================
static void klog_refeed(KlogState* klog, int n) {
// Find enough lines to fill scrollback + visible screen
int lines_needed = klog->term.rows + klog->term.max_scrollback;
int start = 0;
int line_count = 0;
for (int i = n - 1; i >= 0; i--) {
if (klog->klog_buf[i] == '\n') {
line_count++;
if (line_count >= lines_needed) { start = i + 1; break; }
}
}
// Reset terminal state
klog->term.scrollback_lines = 0;
klog->term.view_offset = 0;
klog->term.cursor_x = 0;
klog->term.cursor_y = 0;
klog->term.current_fg = colors::TERM_FG;
klog->term.current_bg = colors::TERM_BG;
// Clear visible screen
TermCell* screen = term_screen_row(&klog->term, 0);
int total = klog->term.cols * klog->term.rows;
for (int i = 0; i < total; i++)
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
terminal_feed(&klog->term, klog->klog_buf + start, n - start);
}
// ============================================================================
// Callbacks
// ============================================================================
static void klog_on_draw(Window* win, Framebuffer& fb) {
KlogState* klog = (KlogState*)win->app_data;
if (!klog) return;
Rect cr = win->content_rect();
// Handle resize
int new_cols = cr.w / mono_cell_width();
int new_rows = cr.h / mono_cell_height();
if (new_cols != klog->term.cols || new_rows != klog->term.rows) {
terminal_resize(&klog->term, new_cols, new_rows);
// Refeed to fill the new grid with log content
if (klog->last_len > 0) {
klog_refeed(klog, klog->last_len);
}
}
bool was_dirty = klog->term.dirty;
terminal_render(&klog->term, win->content, cr.w, cr.h);
// Draw scrollbar overlay when scrollback exists
if (was_dirty && klog->term.scrollback_lines > 0) {
int cell_h = mono_cell_height();
int total_rows = klog->term.scrollback_lines + klog->term.rows;
int total_h = total_rows * cell_h;
int view_h = cr.h;
if (total_h > view_h) {
Canvas c(win->content, cr.w, cr.h);
int sb_w = 4;
int sb_x = cr.w - sb_w - 2;
int thumb_h = (view_h * view_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int scroll_from_top = klog->term.scrollback_lines - klog->term.view_offset;
int max_scroll = klog->term.scrollback_lines;
int thumb_y = (max_scroll > 0)
? (scroll_from_top * (view_h - thumb_h)) / max_scroll : 0;
c.fill_rect(sb_x, 0, sb_w, view_h, Color::from_rgb(0x33, 0x33, 0x33));
c.fill_rect(sb_x, thumb_y, sb_w, thumb_h, Color::from_rgb(0x88, 0x88, 0x88));
}
}
}
static void klog_on_mouse(Window* win, MouseEvent& ev) {
KlogState* klog = (KlogState*)win->app_data;
if (!klog) return;
if (ev.scroll != 0) {
klog->term.view_offset += (ev.scroll < 0) ? 3 : -3;
if (klog->term.view_offset < 0) klog->term.view_offset = 0;
if (klog->term.view_offset > klog->term.scrollback_lines)
klog->term.view_offset = klog->term.scrollback_lines;
klog->term.dirty = true;
}
}
static void klog_on_key(Window* win, const Montauk::KeyEvent& key) {
// Read-only viewer — no keyboard input
}
static void klog_on_poll(Window* win) {
KlogState* klog = (KlogState*)win->app_data;
if (!klog) return;
uint64_t now = montauk::get_milliseconds();
if (now - klog->last_poll_ms < KLOG_POLL_MS) return;
klog->last_poll_ms = now;
int n = (int)montauk::read_klog(klog->klog_buf, KLOG_READ_SIZE);
if (n <= 0 && klog->last_len <= 0) return;
if (n > klog->last_len) {
// Buffer grew — feed only the new portion
terminal_feed(&klog->term, klog->klog_buf + klog->last_len, n - klog->last_len);
} else if (n == klog->last_len && n > 0) {
// Same size — check if the tail changed (ring buffer wrapped)
if (klog->klog_buf[n - 1] != klog->last_tail_byte) {
klog_refeed(klog, n);
} else {
return; // nothing changed
}
} else if (n < klog->last_len) {
// Unexpected shrink — full re-render
klog_refeed(klog, n);
}
klog->last_len = n;
if (n > 0) klog->last_tail_byte = klog->klog_buf[n - 1];
}
static void klog_on_close(Window* win) {
KlogState* klog = (KlogState*)win->app_data;
if (klog) {
if (klog->term.cells) montauk::free(klog->term.cells);
if (klog->term.alt_cells) montauk::free(klog->term.alt_cells);
if (klog->klog_buf) montauk::mfree(klog->klog_buf);
montauk::mfree(klog);
win->app_data = nullptr;
}
}
// ============================================================================
// Kernel Log launcher
// ============================================================================
void open_klog(DesktopState* ds) {
int idx = desktop_create_window(ds, "Kernel Log", 160, 60, 720, 480);
if (idx < 0) return;
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
int cols = cr.w / mono_cell_width();
int rows = cr.h / mono_cell_height();
KlogState* klog = (KlogState*)montauk::malloc(sizeof(KlogState));
montauk::memset(klog, 0, sizeof(KlogState));
// Initialize with scrollback enabled
terminal_init_cells(&klog->term, cols, rows, TERM_MAX_SCROLLBACK);
// Allocate klog read buffer
klog->klog_buf = (char*)montauk::malloc(KLOG_READ_SIZE);
klog->last_len = 0;
klog->last_tail_byte = 0;
klog->last_poll_ms = 0;
// Do an initial read to show existing log content
int n = (int)montauk::read_klog(klog->klog_buf, KLOG_READ_SIZE);
if (n > 0) {
klog_refeed(klog, n);
klog->last_len = n;
klog->last_tail_byte = klog->klog_buf[n - 1];
}
win->app_data = klog;
win->on_draw = klog_on_draw;
win->on_mouse = klog_on_mouse;
win->on_key = klog_on_key;
win->on_close = klog_on_close;
win->on_poll = klog_on_poll;
}
@@ -0,0 +1,296 @@
/*
* app_mandelbrot.cpp
* MontaukOS Desktop - Mandelbrot set visualizer
* Supports zoom (scroll wheel), pan (drag), and reset (R key)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Fixed-point Mandelbrot (avoids FPU/SSE dependency issues)
// Uses 28.36 fixed-point: 36 fractional bits gives ~1e-10 precision
// ============================================================================
using fp_t = int64_t;
static constexpr int FP_SHIFT = 36;
static constexpr fp_t FP_ONE = (fp_t)1 << FP_SHIFT;
static inline fp_t fp_from_int(int v) { return (fp_t)v << FP_SHIFT; }
static inline fp_t fp_mul(fp_t a, fp_t b) {
// Split to avoid overflow: a * b >> SHIFT
// Use 128-bit intermediate via compiler builtin
__int128 r = (__int128)a * b;
return (fp_t)(r >> FP_SHIFT);
}
// fp_div for small numerators (avoids __divti3 by using 64-bit division)
// Only valid when a is small enough that a << FP_SHIFT fits in int64_t
static inline fp_t fp_div_small(int a, int b) {
return ((fp_t)a << FP_SHIFT) / (fp_t)b;
}
// ============================================================================
// State
// ============================================================================
static constexpr int MB_MAX_ITER = 256;
static constexpr int MB_TOOLBAR_H = 32;
struct MandelbrotState {
DesktopState* desktop;
fp_t center_x, center_y; // center of view in fractal coords
fp_t scale; // units per pixel (fixed-point)
int max_iter;
bool needs_render;
// Drag state
bool dragging;
int drag_start_x, drag_start_y;
fp_t drag_start_cx, drag_start_cy;
};
// ============================================================================
// Color palette
// ============================================================================
static uint32_t mandelbrot_color(int iter, int max_iter) {
if (iter >= max_iter) return 0xFF000000; // black for inside set
// Smooth cycling palette using bit manipulation
int t = (iter * 7) & 0xFF;
int phase = (iter * 7) >> 8;
uint8_t r, g, b;
switch (phase % 6) {
case 0: r = 255; g = (uint8_t)t; b = 0; break;
case 1: r = (uint8_t)(255 - t); g = 255; b = 0; break;
case 2: r = 0; g = 255; b = (uint8_t)t; break;
case 3: r = 0; g = (uint8_t)(255 - t); b = 255; break;
case 4: r = (uint8_t)t; g = 0; b = 255; break;
default: r = 255; g = 0; b = (uint8_t)(255 - t); break;
}
return 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
// ============================================================================
// Render
// ============================================================================
static void mandelbrot_render(MandelbrotState* mb, uint32_t* pixels, int w, int h) {
int render_h = h - MB_TOOLBAR_H;
if (render_h <= 0) return;
fp_t half_w = fp_mul(fp_from_int(w / 2), mb->scale);
fp_t half_h = fp_mul(fp_from_int(render_h / 2), mb->scale);
fp_t x_min = mb->center_x - half_w;
fp_t y_min = mb->center_y - half_h;
fp_t bailout = fp_from_int(4);
for (int py = 0; py < render_h; py++) {
fp_t ci = y_min + fp_mul(fp_from_int(py), mb->scale);
uint32_t* row = pixels + (py + MB_TOOLBAR_H) * w;
for (int px = 0; px < w; px++) {
fp_t cr = x_min + fp_mul(fp_from_int(px), mb->scale);
fp_t zr = 0, zi = 0;
int iter = 0;
while (iter < mb->max_iter) {
fp_t zr2 = fp_mul(zr, zr);
fp_t zi2 = fp_mul(zi, zi);
if (zr2 + zi2 > bailout) break;
fp_t new_zr = zr2 - zi2 + cr;
zi = fp_mul(fp_from_int(2), fp_mul(zr, zi)) + ci;
zr = new_zr;
iter++;
}
row[px] = mandelbrot_color(iter, mb->max_iter);
}
}
mb->needs_render = false;
}
// ============================================================================
// Callbacks
// ============================================================================
static void mb_on_draw(Window* win, Framebuffer& fb) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb) return;
Canvas c(win);
if (mb->needs_render) {
mandelbrot_render(mb, win->content, c.w, c.h);
}
// Draw toolbar over the render
c.fill_rect(0, 0, c.w, MB_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, MB_TOOLBAR_H - 1, c.w, colors::BORDER);
// Reset button
c.button(8, 4, 60, 24, "Reset", colors::ACCENT, colors::WHITE, 4);
// Iter +/- buttons
char iter_str[24];
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
int fh = system_font_height();
c.text(80, (MB_TOOLBAR_H - fh) / 2, iter_str, colors::TEXT_COLOR);
int iter_text_w = text_width(iter_str);
int btn_x = 80 + iter_text_w + 8;
c.button(btn_x, 4, 24, 24, "-", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
c.button(btn_x + 28, 4, 24, 24, "+", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
// Hint
const char* hint = "Scroll=zoom Drag=pan";
int hw = text_width(hint);
c.text(c.w - hw - 8, (MB_TOOLBAR_H - fh) / 2, hint, Color::from_rgb(0x99, 0x99, 0x99));
}
static void mb_on_mouse(Window* win, MouseEvent& ev) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
// Handle toolbar clicks
if (ev.left_pressed() && ly < MB_TOOLBAR_H) {
// Reset button
Rect reset_r = {8, 4, 60, 24};
if (reset_r.contains(lx, ly)) {
mb->center_x = -fp_from_int(1) / 2; // -0.5
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
return;
}
// Iter buttons
char iter_str[24];
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
int iter_text_w = text_width(iter_str);
int btn_x = 80 + iter_text_w + 8;
Rect minus_r = {btn_x, 4, 24, 24};
Rect plus_r = {btn_x + 28, 4, 24, 24};
if (minus_r.contains(lx, ly)) {
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
return;
}
if (plus_r.contains(lx, ly)) {
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
return;
}
return;
}
// Drag panning
if (ev.left_pressed() && ly >= MB_TOOLBAR_H) {
mb->dragging = true;
mb->drag_start_x = lx;
mb->drag_start_y = ly;
mb->drag_start_cx = mb->center_x;
mb->drag_start_cy = mb->center_y;
return;
}
if (mb->dragging && ev.left_held()) {
int dx = lx - mb->drag_start_x;
int dy = ly - mb->drag_start_y;
mb->center_x = mb->drag_start_cx - fp_mul(fp_from_int(dx), mb->scale);
mb->center_y = mb->drag_start_cy - fp_mul(fp_from_int(dy), mb->scale);
mb->needs_render = true;
return;
}
if (mb->dragging && !ev.left_held()) {
mb->dragging = false;
}
// Scroll zoom (centered on mouse position)
if (ev.scroll != 0 && ly >= MB_TOOLBAR_H) {
int render_h = cr.h - MB_TOOLBAR_H;
// Mouse position in fractal coords before zoom
fp_t mx_frac = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
fp_t my_frac = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
if (ev.scroll < 0) {
// Zoom in
mb->scale = fp_mul(mb->scale, fp_from_int(3) / 4); // * 0.75
} else {
// Zoom out
mb->scale = fp_mul(mb->scale, fp_from_int(4) / 3); // * 1.33
}
// Adjust center so mouse stays at same fractal point
fp_t new_mx = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
fp_t new_my = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
mb->center_x += mx_frac - new_mx;
mb->center_y += my_frac - new_my;
mb->needs_render = true;
}
}
static void mb_on_key(Window* win, const Montauk::KeyEvent& key) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb || !key.pressed) return;
if (key.ascii == 'r' || key.ascii == 'R') {
Rect cr = win->content_rect();
mb->center_x = -fp_from_int(1) / 2;
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
} else if (key.ascii == '+' || key.ascii == '=') {
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
} else if (key.ascii == '-') {
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
}
}
static void mb_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Mandelbrot launcher
// ============================================================================
void open_mandelbrot(DesktopState* ds) {
int idx = desktop_create_window(ds, "Mandelbrot", 120, 60, 500, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
MandelbrotState* mb = (MandelbrotState*)montauk::malloc(sizeof(MandelbrotState));
montauk::memset(mb, 0, sizeof(MandelbrotState));
mb->desktop = ds;
mb->center_x = -fp_from_int(1) / 2; // -0.5
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
mb->dragging = false;
win->app_data = mb;
win->on_draw = mb_on_draw;
win->on_mouse = mb_on_mouse;
win->on_key = mb_on_key;
win->on_close = mb_on_close;
}
+244
View File
@@ -0,0 +1,244 @@
/*
* app_procmgr.cpp
* MontaukOS Desktop - Process Manager
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Process Manager state
// ============================================================================
static constexpr int PM_TOOLBAR_H = 36;
static constexpr int PM_HEADER_H = 24;
static constexpr int PM_ITEM_H = 28;
static constexpr int PM_MAX_PROCS = 16;
static constexpr int PM_POLL_MS = 1000;
struct ProcMgrState {
DesktopState* desktop;
Montauk::ProcInfo procs[PM_MAX_PROCS];
int proc_count;
int selected; // selected row index (-1 = none)
uint64_t last_poll_ms;
};
// ============================================================================
// Callbacks
// ============================================================================
static void procmgr_on_poll(Window* win) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
uint64_t now = montauk::get_milliseconds();
if (now - pm->last_poll_ms < PM_POLL_MS) return;
pm->last_poll_ms = now;
// Remember previously selected PID
int prev_pid = -1;
if (pm->selected >= 0 && pm->selected < pm->proc_count) {
prev_pid = pm->procs[pm->selected].pid;
}
pm->proc_count = montauk::proclist(pm->procs, PM_MAX_PROCS);
// Restore selection by matching PID
pm->selected = -1;
if (prev_pid >= 0) {
for (int i = 0; i < pm->proc_count; i++) {
if (pm->procs[i].pid == prev_pid) {
pm->selected = i;
break;
}
}
}
}
static void procmgr_on_draw(Window* win, Framebuffer& fb) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int fh = system_font_height();
// --- Toolbar ---
c.fill_rect(0, 0, c.w, PM_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, PM_TOOLBAR_H - 1, c.w, colors::BORDER);
// "End Process" button
int btn_w = 100;
int btn_h = 26;
int btn_x = 8;
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
Color btn_bg = (pm->selected >= 0 && pm->procs[pm->selected].pid != 0)
? Color::from_rgb(0xCC, 0x33, 0x33)
: Color::from_rgb(0xAA, 0xAA, 0xAA);
c.button(btn_x, btn_y, btn_w, btn_h, "End Process", btn_bg, colors::WHITE, 4);
// Process count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d processes", pm->proc_count);
int cw = text_width(count_str);
c.text(c.w - cw - 12, (PM_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
// --- Header row ---
int header_y = PM_TOOLBAR_H;
c.fill_rect(0, header_y, c.w, PM_HEADER_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
int col_pid = 12;
int col_name = 64;
int col_mem = c.w - 120;
int col_state = c.w - 52;
int ty = header_y + (PM_HEADER_H - fh) / 2;
c.text(col_pid, ty, "PID", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_name, ty, "Name", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_mem, ty, "Mem", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_state, ty, "St", Color::from_rgb(0x66, 0x66, 0x66));
c.hline(0, header_y + PM_HEADER_H - 1, c.w, colors::BORDER);
// --- Process rows ---
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
for (int i = 0; i < pm->proc_count; i++) {
int row_y = list_y + i * PM_ITEM_H;
if (row_y + PM_ITEM_H > c.h) break;
// Selected row highlight
if (i == pm->selected) {
c.fill_rect(0, row_y, c.w, PM_ITEM_H, colors::MENU_HOVER);
}
int ry = row_y + (PM_ITEM_H - fh) / 2;
// PID (right-aligned in PID column)
char pid_str[8];
snprintf(pid_str, sizeof(pid_str), "%d", (int)pm->procs[i].pid);
int pid_w = text_width(pid_str);
c.text(col_pid + 32 - pid_w, ry, pid_str, colors::TEXT_COLOR);
// Name (truncated to fit)
c.text(col_name, ry, pm->procs[i].name, colors::TEXT_COLOR);
// Memory
char mem_str[16];
format_size(mem_str, (int)pm->procs[i].heapUsed);
c.text(col_mem, ry, mem_str, colors::TEXT_COLOR);
// State
const char* st_str;
Color st_color;
switch (pm->procs[i].state) {
case 2: // Running
st_str = "Run";
st_color = Color::from_rgb(0x22, 0x88, 0x22);
break;
case 1: // Ready
st_str = "Rdy";
st_color = Color::from_rgb(0x33, 0x66, 0xCC);
break;
case 3: // Terminated
st_str = "Term";
st_color = Color::from_rgb(0xCC, 0x33, 0x33);
break;
default:
st_str = "?";
st_color = colors::TEXT_COLOR;
break;
}
c.text(col_state, ry, st_str, st_color);
}
}
static void procmgr_on_mouse(Window* win, MouseEvent& ev) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
if (ev.left_pressed()) {
// Check "End Process" button click
int btn_w = 100;
int btn_h = 26;
int btn_x = 8;
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
if (btn_rect.contains(lx, ly)) {
if (pm->selected >= 0 && pm->selected < pm->proc_count
&& pm->procs[pm->selected].pid != 0) {
montauk::kill(pm->procs[pm->selected].pid);
pm->last_poll_ms = 0; // force refresh
}
return;
}
// Check row click
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
if (ly >= list_y) {
int row = (ly - list_y) / PM_ITEM_H;
if (row >= 0 && row < pm->proc_count) {
pm->selected = row;
} else {
pm->selected = -1;
}
}
}
}
static void procmgr_on_key(Window* win, const Montauk::KeyEvent& key) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm || !key.pressed) return;
if (key.scancode == 0x48) { // Up arrow
if (pm->selected > 0) pm->selected--;
else if (pm->proc_count > 0) pm->selected = 0;
} else if (key.scancode == 0x50) { // Down arrow
if (pm->selected < pm->proc_count - 1) pm->selected++;
} else if (key.scancode == 0x53) { // Delete key
if (pm->selected >= 0 && pm->selected < pm->proc_count
&& pm->procs[pm->selected].pid != 0) {
montauk::kill(pm->procs[pm->selected].pid);
pm->last_poll_ms = 0; // force refresh
}
}
}
static void procmgr_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Process Manager launcher
// ============================================================================
void open_procmgr(DesktopState* ds) {
int idx = desktop_create_window(ds, "Processes", 180, 80, 520, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
ProcMgrState* pm = (ProcMgrState*)montauk::malloc(sizeof(ProcMgrState));
montauk::memset(pm, 0, sizeof(ProcMgrState));
pm->desktop = ds;
pm->selected = -1;
pm->last_poll_ms = 0;
// Initial poll
pm->proc_count = montauk::proclist(pm->procs, PM_MAX_PROCS);
win->app_data = pm;
win->on_draw = procmgr_on_draw;
win->on_mouse = procmgr_on_mouse;
win->on_key = procmgr_on_key;
win->on_close = procmgr_on_close;
win->on_poll = procmgr_on_poll;
}
+608
View File
@@ -0,0 +1,608 @@
/*
* app_settings.cpp
* MontaukOS Desktop - Settings application
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
#include "../wallpaper.hpp"
// ============================================================================
// Settings state
// ============================================================================
struct SettingsState {
DesktopState* desktop;
int active_tab; // 0=Appearance, 1=Display, 2=About
Montauk::SysInfo sys_info;
uint64_t uptime_ms;
WallpaperFileList wp_files;
bool wp_scanned;
};
// ============================================================================
// Color palette presets
// ============================================================================
static constexpr int SWATCH_COUNT = 8;
static constexpr int SWATCH_SIZE = 24;
static constexpr int SWATCH_GAP = 6;
// Background colors (light tones)
static const Color bg_palette[SWATCH_COUNT] = {
Color::from_rgb(0xD0, 0xD8, 0xE8), // light blue-gray (default)
Color::from_rgb(0xE8, 0xDD, 0xCB), // warm beige
Color::from_rgb(0xC8, 0xE6, 0xD0), // mint green
Color::from_rgb(0xD8, 0xD0, 0xE8), // lavender
Color::from_rgb(0xB8, 0xBE, 0xC8), // slate
Color::from_rgb(0xF0, 0xF0, 0xF0), // white
Color::from_rgb(0xE8, 0xD0, 0xD8), // soft pink
Color::from_rgb(0xE8, 0xE0, 0xC8), // light gold
};
// Panel colors (dark tones)
static const Color panel_palette[SWATCH_COUNT] = {
Color::from_rgb(0x2B, 0x3E, 0x50), // dark blue-gray (default)
Color::from_rgb(0x2D, 0x2D, 0x2D), // dark charcoal
Color::from_rgb(0x1B, 0x2A, 0x4A), // navy
Color::from_rgb(0x1A, 0x3A, 0x3A), // dark teal
Color::from_rgb(0x1A, 0x3A, 0x1A), // dark green
Color::from_rgb(0x30, 0x20, 0x40), // dark purple
Color::from_rgb(0x40, 0x1A, 0x1A), // dark red
Color::from_rgb(0x10, 0x10, 0x10), // black
};
// Accent colors
static const Color accent_palette[SWATCH_COUNT] = {
Color::from_rgb(0x36, 0x7B, 0xF0), // blue (default)
Color::from_rgb(0x00, 0x9B, 0x9B), // teal
Color::from_rgb(0x2E, 0x9E, 0x3E), // green
Color::from_rgb(0xE0, 0x8A, 0x20), // orange
Color::from_rgb(0xD0, 0x3E, 0x3E), // red
Color::from_rgb(0x7B, 0x3E, 0xB8), // purple
Color::from_rgb(0xD0, 0x5C, 0x9E), // pink
Color::from_rgb(0x44, 0x44, 0xCC), // indigo
};
// ============================================================================
// Layout constants
// ============================================================================
static constexpr int TAB_BAR_H = 36;
static constexpr int TAB_COUNT = 3;
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "About" };
// ============================================================================
// Helper: check if two colors match
// ============================================================================
static bool color_eq(Color a, Color b) {
return a.r == b.r && a.g == b.g && a.b == b.b;
}
// ============================================================================
// Helper: find selected swatch index in a palette
// ============================================================================
static int find_swatch(const Color* palette, Color current) {
for (int i = 0; i < SWATCH_COUNT; i++) {
if (color_eq(palette[i], current)) return i;
}
return -1;
}
// ============================================================================
// Drawing
// ============================================================================
static void draw_swatch_row(Canvas& c, int x, int y, const Color* palette,
Color selected, Color accent) {
int sel = find_swatch(palette, selected);
for (int i = 0; i < SWATCH_COUNT; i++) {
int sx = x + i * (SWATCH_SIZE + SWATCH_GAP);
// Draw selection border
if (i == sel) {
c.fill_rounded_rect(sx - 2, y - 2, SWATCH_SIZE + 4, SWATCH_SIZE + 4, 4, accent);
}
c.fill_rounded_rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, 3, palette[i]);
// Thin border for light colors
c.rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, Color::from_rgb(0xCC, 0xCC, 0xCC));
}
}
static void draw_radio(Canvas& c, int x, int y, bool selected, Color accent) {
int r = 7;
// Outer circle (simple square approximation with rounded rect)
c.fill_rounded_rect(x, y, r * 2, r * 2, r, Color::from_rgb(0xCC, 0xCC, 0xCC));
c.fill_rounded_rect(x + 1, y + 1, r * 2 - 2, r * 2 - 2, r - 1, colors::WHITE);
if (selected) {
c.fill_rounded_rect(x + 4, y + 4, r * 2 - 8, r * 2 - 8, r - 4, accent);
}
}
static void draw_toggle_btn(Canvas& c, int x, int y, int bw, int bh,
const char* label, bool active, Color accent) {
Color bg = active ? accent : colors::WINDOW_BG;
Color fg = active ? colors::WHITE : colors::TEXT_COLOR;
c.fill_rounded_rect(x, y, bw, bh, 4, bg);
if (!active) {
c.rect(x, y, bw, bh, colors::BORDER);
}
int tw = text_width(label);
int fh = system_font_height();
c.text(x + (bw - tw) / 2, y + (bh - fh) / 2, label, fg);
}
static constexpr int WP_ITEM_H = 24;
static void settings_draw_appearance(Canvas& c, SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
Color accent = s.accent_color;
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
int x = 16;
int y = 12;
int sfh = system_font_height();
int line_h = sfh + 10;
// Section: Background
c.text(x, y, "Background", colors::TEXT_COLOR);
y += line_h;
// Radio buttons: Gradient / Solid Color / Image
bool mode_grad = s.bg_gradient && !s.bg_image;
bool mode_solid = !s.bg_gradient && !s.bg_image;
bool mode_image = s.bg_image;
draw_radio(c, x, y, mode_grad, accent);
c.text(x + 20, y + 2, "Gradient", colors::TEXT_COLOR);
draw_radio(c, x + 120, y, mode_solid, accent);
c.text(x + 140, y + 2, "Solid", colors::TEXT_COLOR);
draw_radio(c, x + 220, y, mode_image, accent);
c.text(x + 240, y + 2, "Image", colors::TEXT_COLOR);
y += line_h + 4;
if (mode_image) {
// Scan for images lazily
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
st->wp_scanned = true;
}
c.text(x, y, "Images in 0:/home/", dim);
y += sfh + 6;
if (st->wp_files.count == 0) {
c.text(x + 8, y, "No .jpg files found", dim);
y += WP_ITEM_H;
} else {
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
// Build full path for comparison
char fullpath[256];
montauk::strcpy(fullpath, "0:/home/");
str_append(fullpath, st->wp_files.names[i], 256);
bool selected = s.bg_image &&
montauk::streq(s.bg_image_path, fullpath);
if (selected) {
c.fill_rounded_rect(x, y, c.w - 2 * x, WP_ITEM_H, 3, accent);
}
// Truncate long filenames
char label[40];
int nlen = montauk::slen(st->wp_files.names[i]);
if (nlen > 35) {
montauk::strncpy(label, st->wp_files.names[i], 32);
label[32] = '.';
label[33] = '.';
label[34] = '.';
label[35] = '\0';
} else {
montauk::strncpy(label, st->wp_files.names[i], 39);
}
Color tc = selected ? colors::WHITE : colors::TEXT_COLOR;
c.text(x + 8, y + (WP_ITEM_H - sfh) / 2, label, tc);
y += WP_ITEM_H;
}
}
y += 6;
} else if (mode_grad) {
// Top color swatches
c.text(x, y + 4, "Top", dim);
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_top, accent);
y += SWATCH_SIZE + 14;
// Bottom color swatches
c.text(x, y + 4, "Bottom", dim);
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_bottom, accent);
y += SWATCH_SIZE + 14;
} else {
// Solid color swatches
c.text(x, y + 4, "Color", dim);
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_solid, accent);
y += SWATCH_SIZE + 14;
}
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
// Panel color
c.text(x, y + 4, "Panel Color", colors::TEXT_COLOR);
draw_swatch_row(c, x + 110, y, panel_palette, s.panel_color, accent);
y += SWATCH_SIZE + 14;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
// Accent color
c.text(x, y + 4, "Accent Color", colors::TEXT_COLOR);
draw_swatch_row(c, x + 110, y, accent_palette, s.accent_color, accent);
}
static void apply_ui_scale(int scale) {
switch (scale) {
case 0: fonts::UI_SIZE=14; fonts::TITLE_SIZE=14; fonts::TERM_SIZE=14; fonts::LARGE_SIZE=22; break;
case 2: fonts::UI_SIZE=22; fonts::TITLE_SIZE=22; fonts::TERM_SIZE=22; fonts::LARGE_SIZE=34; break;
default: fonts::UI_SIZE=18; fonts::TITLE_SIZE=18; fonts::TERM_SIZE=18; fonts::LARGE_SIZE=28; break;
}
}
static void settings_draw_display(Canvas& c, SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
Color accent = s.accent_color;
int x = 16;
int y = 20;
int btn_w = 60;
int btn_h = 28;
// Window Shadows
c.text(x, y + 6, "Window Shadows", colors::TEXT_COLOR);
int bx = x + 180;
draw_toggle_btn(c, bx, y, btn_w, btn_h, "On", s.show_shadows, accent);
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "Off", !s.show_shadows, accent);
y += btn_h + 20;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 16;
// Clock Format
c.text(x, y + 6, "Clock Format", colors::TEXT_COLOR);
bx = x + 180;
draw_toggle_btn(c, bx, y, btn_w, btn_h, "24h", s.clock_24h, accent);
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "12h", !s.clock_24h, accent);
y += btn_h + 20;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 16;
// UI Scale
c.text(x, y + 6, "UI Scale", colors::TEXT_COLOR);
bx = x + 180;
int sbw = 68;
draw_toggle_btn(c, bx, y, sbw, btn_h, "Small", s.ui_scale == 0, accent);
draw_toggle_btn(c, bx + sbw + 8, y, sbw, btn_h, "Default", s.ui_scale == 1, accent);
draw_toggle_btn(c, bx + (sbw + 8) * 2, y, sbw, btn_h, "Large", s.ui_scale == 2, accent);
}
static void settings_draw_about(Canvas& c, SettingsState* st) {
st->uptime_ms = montauk::get_milliseconds();
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
int x = 16;
int y = 20;
char line[128];
int sfh = system_font_height();
int line_h = sfh + 6;
// OS name in 2x size
c.text_2x(x, y, st->sys_info.osName, st->desktop->settings.accent_color);
int large_h = (fonts::system_font && fonts::system_font->valid)
? fonts::system_font->get_line_height(fonts::LARGE_SIZE) : (FONT_HEIGHT * 2);
y += large_h + 8;
snprintf(line, sizeof(line), "Version %s", st->sys_info.osVersion);
c.text(x, y, line, colors::TEXT_COLOR);
y += line_h + 8;
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
snprintf(line, sizeof(line), "API version: %d", (int)st->sys_info.apiVersion);
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
int up_sec = (int)(st->uptime_ms / 1000);
int up_min = up_sec / 60;
int up_hr = up_min / 60;
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
// snprintf(line, sizeof(line), "Build: %s %s", __DATE__, __TIME__);
// c.text(x, y, line, colors::TEXT_COLOR);
y += 8;
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
}
static void settings_on_draw(Window* win, Framebuffer& fb) {
SettingsState* st = (SettingsState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color accent = st->desktop->settings.accent_color;
int sfh = system_font_height();
// Draw tab bar background
c.fill_rect(0, 0, c.w, TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, TAB_BAR_H - 1, c.w, colors::BORDER);
// Draw tabs
int tab_w = c.w / TAB_COUNT;
for (int i = 0; i < TAB_COUNT; i++) {
int tx = i * tab_w;
bool active = (i == st->active_tab);
if (active) {
c.fill_rect(tx, 0, tab_w, TAB_BAR_H, colors::WINDOW_BG);
// Active tab underline
c.fill_rect(tx + 4, TAB_BAR_H - 3, tab_w - 8, 3, accent);
}
int tw = text_width(tab_labels[i]);
Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66);
c.text(tx + (tab_w - tw) / 2, (TAB_BAR_H - sfh) / 2, tab_labels[i], tc);
}
// Draw tab content below the tab bar
// Create a sub-canvas offset by TAB_BAR_H
Canvas content(win->content + TAB_BAR_H * win->content_w,
win->content_w, win->content_h - TAB_BAR_H);
switch (st->active_tab) {
case 0: settings_draw_appearance(content, st); break;
case 1: settings_draw_display(content, st); break;
case 2: settings_draw_about(content, st); break;
}
}
// ============================================================================
// Mouse interaction
// ============================================================================
static bool swatch_hit(int mx, int my, int row_x, int row_y, int* out_idx) {
for (int i = 0; i < SWATCH_COUNT; i++) {
int sx = row_x + i * (SWATCH_SIZE + SWATCH_GAP);
if (mx >= sx && mx < sx + SWATCH_SIZE && my >= row_y && my < row_y + SWATCH_SIZE) {
*out_idx = i;
return true;
}
}
return false;
}
static void settings_on_mouse(Window* win, MouseEvent& ev) {
SettingsState* st = (SettingsState*)win->app_data;
if (!st) return;
if (!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 < TAB_BAR_H) {
int tab_w = win->content_w / TAB_COUNT;
int tab = mx / tab_w;
if (tab >= 0 && tab < TAB_COUNT) {
st->active_tab = tab;
}
return;
}
// Content area (offset by TAB_BAR_H)
int cy = my - TAB_BAR_H;
DesktopSettings& s = st->desktop->settings;
if (st->active_tab == 0) {
// Appearance tab
int x = 16;
int sfh = system_font_height();
int line_h = sfh + 10;
int y = 12;
bool mode_grad = s.bg_gradient && !s.bg_image;
bool mode_image = s.bg_image;
// "Background" label
y += line_h;
// Radio: Gradient
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
s.bg_gradient = true;
s.bg_image = false;
return;
}
// Radio: Solid
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
s.bg_gradient = false;
s.bg_image = false;
return;
}
// Radio: Image
if (mx >= x + 220 && mx < x + 320 && cy >= y && cy < y + 16) {
s.bg_image = true;
s.bg_gradient = false;
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
st->wp_scanned = true;
}
return;
}
y += line_h + 4;
int idx;
if (mode_image) {
// "Images in 0:/home/" label
y += sfh + 6;
// File list clicks
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
if (cy >= y && cy < y + WP_ITEM_H &&
mx >= x && mx < win->content_w - x) {
// Build full path and load wallpaper
char fullpath[256];
montauk::strcpy(fullpath, "0:/home/");
str_append(fullpath, st->wp_files.names[i], 256);
wallpaper_load(&s, fullpath,
st->desktop->screen_w, st->desktop->screen_h);
return;
}
y += WP_ITEM_H;
}
if (st->wp_files.count == 0) y += WP_ITEM_H;
y += 6;
} else if (mode_grad) {
// Top swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_top = bg_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
// Bottom swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_bottom = bg_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
} else {
// Solid color swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_solid = bg_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
}
// Separator
y += 12;
// Panel color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.panel_color = panel_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
// Separator
y += 12;
// Accent color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.accent_color = accent_palette[idx];
return;
}
} else if (st->active_tab == 1) {
// Display tab
int x = 16;
int y = 20;
int btn_w = 60;
int btn_h = 28;
int bx = x + 180;
// Window Shadows: On
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.show_shadows = true;
return;
}
// Window Shadows: Off
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
s.show_shadows = false;
return;
}
y += btn_h + 20 + 16;
// Clock: 24h
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.clock_24h = true;
return;
}
// Clock: 12h
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
s.clock_24h = false;
return;
}
y += btn_h + 20 + 16;
// UI Scale buttons
int sbw = 68;
// Small
if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) {
s.ui_scale = 0;
apply_ui_scale(0);
montauk::win_setscale(0);
return;
}
// Default
if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) {
s.ui_scale = 1;
apply_ui_scale(1);
montauk::win_setscale(1);
return;
}
// Large
if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) {
s.ui_scale = 2;
apply_ui_scale(2);
montauk::win_setscale(2);
return;
}
}
}
// ============================================================================
// Cleanup
// ============================================================================
static void settings_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Settings launcher
// ============================================================================
void open_settings(DesktopState* ds) {
int idx = desktop_create_window(ds, "Settings", 200, 100, 480, 420);
if (idx < 0) return;
Window* win = &ds->windows[idx];
SettingsState* st = (SettingsState*)montauk::malloc(sizeof(SettingsState));
montauk::memset(st, 0, sizeof(SettingsState));
st->desktop = ds;
st->active_tab = 0;
montauk::get_info(&st->sys_info);
st->uptime_ms = montauk::get_milliseconds();
st->wp_scanned = false;
st->wp_files.count = 0;
win->app_data = st;
win->on_draw = settings_on_draw;
win->on_mouse = settings_on_mouse;
win->on_close = settings_on_close;
}
@@ -0,0 +1,12 @@
/*
* app_spreadsheet.cpp
* MontaukOS Desktop - Spreadsheet launcher (spawns standalone spreadsheet.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_spreadsheet(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/spreadsheet.elf");
}
+135
View File
@@ -0,0 +1,135 @@
/*
* app_sysinfo.cpp
* MontaukOS Desktop - System Info application
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// System Info state and callbacks
// ============================================================================
struct SysInfoState {
Montauk::SysInfo sys_info;
Montauk::NetCfg net_cfg;
uint64_t uptime_ms;
};
static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
SysInfoState* si = (SysInfoState*)win->app_data;
if (!si) return;
si->uptime_ms = montauk::get_milliseconds();
Canvas c(win);
c.fill(colors::WINDOW_BG);
int y = 16;
int x = 16;
char line[128];
// Title
c.text(x, y, "System Information", colors::ACCENT);
y += system_font_height() + 12;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 8;
// OS Name
snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName);
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// OS Version
snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion);
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// API Version
snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion);
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// Max Processes
snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses);
c.text(x, y, line, colors::TEXT_COLOR);
y += system_font_height() + 12;
// Uptime
int up_sec = (int)(si->uptime_ms / 1000);
int up_min = up_sec / 60;
int up_hr = up_min / 60;
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
c.text(x, y, line, colors::TEXT_COLOR);
y += system_font_height() + 12;
// Network section
c.text(x, y, "Network", colors::ACCENT);
y += system_font_height() + 8;
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 8;
// IP Address
uint32_t ip = si->net_cfg.ipAddress;
snprintf(line, sizeof(line), "IP: %d.%d.%d.%d",
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// Subnet
uint32_t mask = si->net_cfg.subnetMask;
snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d",
(int)(mask & 0xFF), (int)((mask >> 8) & 0xFF),
(int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF));
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// Gateway
uint32_t gw = si->net_cfg.gateway;
snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d",
(int)(gw & 0xFF), (int)((gw >> 8) & 0xFF),
(int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF));
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// DNS
uint32_t dns = si->net_cfg.dnsServer;
snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d",
(int)(dns & 0xFF), (int)((dns >> 8) & 0xFF),
(int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF));
c.kv_line(x, &y, line, colors::TEXT_COLOR);
// MAC Address
snprintf(line, sizeof(line), "MAC: %02x:%02x:%02x:%02x:%02x:%02x",
(unsigned)si->net_cfg.macAddress[0], (unsigned)si->net_cfg.macAddress[1],
(unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3],
(unsigned)si->net_cfg.macAddress[4], (unsigned)si->net_cfg.macAddress[5]);
c.text(x, y, line, colors::TEXT_COLOR);
}
static void sysinfo_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// System Info launcher
// ============================================================================
void open_sysinfo(DesktopState* ds) {
int idx = desktop_create_window(ds, "System Info", 300, 100, 400, 380);
if (idx < 0) return;
Window* win = &ds->windows[idx];
SysInfoState* si = (SysInfoState*)montauk::malloc(sizeof(SysInfoState));
montauk::memset(si, 0, sizeof(SysInfoState));
montauk::get_info(&si->sys_info);
montauk::get_netcfg(&si->net_cfg);
si->uptime_ms = montauk::get_milliseconds();
win->app_data = si;
win->on_draw = sysinfo_on_draw;
win->on_mouse = nullptr;
win->on_key = nullptr;
win->on_close = sysinfo_on_close;
}
+359
View File
@@ -0,0 +1,359 @@
/*
* app_terminal.cpp
* MontaukOS Desktop - Terminal application with tab support
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Tab data model
// ============================================================================
static constexpr int TERM_TAB_BAR_H = 34;
static constexpr int TERM_MAX_TABS = 8;
static constexpr int TERM_TAB_W = 130;
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
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;
}
struct TermTabState {
TerminalState* tabs[TERM_MAX_TABS];
int tab_count;
int active_tab;
int prev_active_tab; // track tab switches to force redraw
uint32_t* last_content; // detect content buffer reallocation
DesktopState* desktop;
};
// ============================================================================
// Tab helpers
// ============================================================================
static void term_free_tab(TerminalState* ts) {
if (!ts) return;
if (ts->child_pid > 0) montauk::kill(ts->child_pid);
if (ts->cells) montauk::free(ts->cells);
if (ts->alt_cells) montauk::free(ts->alt_cells);
montauk::mfree(ts);
}
static TerminalState* term_create_tab(DesktopState* ds, int cols, int rows) {
TerminalState* ts = (TerminalState*)montauk::malloc(sizeof(TerminalState));
montauk::memset(ts, 0, sizeof(TerminalState));
terminal_init(ts, cols, rows);
ts->desktop = ds;
return ts;
}
static void term_close_tab(TermTabState* tts, Window* win, int idx) {
if (idx < 0 || idx >= tts->tab_count) return;
term_free_tab(tts->tabs[idx]);
// Shift remaining tabs left
for (int i = idx; i < tts->tab_count - 1; i++)
tts->tabs[i] = tts->tabs[i + 1];
tts->tab_count--;
tts->tabs[tts->tab_count] = nullptr;
if (tts->tab_count == 0) {
// Last tab closed — close the window
DesktopState* ds = tts->desktop;
if (ds) {
for (int i = 0; i < ds->window_count; i++) {
if (&ds->windows[i] == win) {
desktop_close_window(ds, i);
return;
}
}
}
return;
}
// Adjust active tab index
if (idx == tts->active_tab) {
// Closed the active tab: prefer left neighbor, fall back to right
if (tts->active_tab > 0)
tts->active_tab--;
// else stays at 0 (right neighbor shifted here)
} else if (tts->active_tab > idx) {
tts->active_tab--;
}
if (tts->active_tab >= tts->tab_count)
tts->active_tab = tts->tab_count - 1;
// Mark new active tab dirty so it redraws
tts->tabs[tts->active_tab]->dirty = true;
}
static void term_add_tab(TermTabState* tts, int cols, int rows) {
if (tts->tab_count >= TERM_MAX_TABS) return;
TerminalState* ts = term_create_tab(tts->desktop, cols, rows);
tts->tabs[tts->tab_count] = ts;
tts->active_tab = tts->tab_count;
tts->tab_count++;
}
// ============================================================================
// Terminal callbacks
// ============================================================================
static void terminal_on_draw(Window* win, Framebuffer& fb) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return;
if (tts->active_tab < 0 || tts->active_tab >= tts->tab_count)
tts->active_tab = 0;
Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H;
if (term_h < 1) term_h = 1;
// Force full redraw when active tab changed
if (tts->active_tab != tts->prev_active_tab) {
tts->tabs[tts->active_tab]->dirty = true;
tts->prev_active_tab = tts->active_tab;
}
// Force full redraw when content buffer was reallocated (resize/maximize)
if (win->content != tts->last_content) {
tts->tabs[tts->active_tab]->dirty = true;
tts->last_content = win->content;
}
TerminalState* ts = tts->tabs[tts->active_tab];
// Check if window was resized and update terminal grid
int new_cols = cr.w / mono_cell_width();
int new_rows = term_h / mono_cell_height();
if (new_cols < 1) new_cols = 1;
if (new_rows < 1) new_rows = 1;
if (new_cols != ts->cols || new_rows != ts->rows) {
for (int i = 0; i < tts->tab_count; i++)
terminal_resize(tts->tabs[i], new_cols, new_rows);
}
// Only redraw when terminal content changed
if (!ts->dirty) return;
// ---- Draw tab bar ----
// Bar is darker than TERM_BG; active tab matches TERM_BG to merge with content
Canvas c(win->content, cr.w, cr.h);
Color bar_bg = Color::from_hex(0x1C1C1C);
c.fill_rect(0, 0, cr.w, TERM_TAB_BAR_H, bar_bg);
int fh = system_font_height();
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
for (int i = 0; i < tts->tab_count; i++) {
bool active = (i == tts->active_tab);
char label[16];
snprintf(label, sizeof(label), "Tab %d", i + 1);
if (active) {
// Active tab: TERM_BG pill with squared-off bottom → merges into content
int ty = 5;
int th = TERM_TAB_BAR_H - ty;
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 6, colors::TERM_BG);
c.fill_rect(tab_x, TERM_TAB_BAR_H - 6, TERM_TAB_W, 6, colors::TERM_BG);
int text_y = ty + (th - fh) / 2;
c.text(tab_x + 12, text_y, label, Color::from_hex(0xE0E0E0));
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x707070));
} else {
// Inactive tab: subtle rounded pill, sits above the bottom edge
int ty = 7;
int th = 22;
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 5, Color::from_hex(0x262626));
int text_y = ty + (th - fh) / 2;
c.text(tab_x + 12, text_y, label, Color::from_hex(0x6E6E6E));
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x444444));
}
tab_x += TERM_TAB_W + TERM_TAB_GAP;
}
// "+" button pinned to right edge
if (tts->tab_count < TERM_MAX_TABS) {
int plus_h = 22;
int py = 7;
int px = cr.w - TERM_PLUS_PAD - TERM_PLUS_W;
c.fill_rounded_rect(px, py, TERM_PLUS_W, plus_h, 5, Color::from_hex(0x262626));
int pw_text = text_width("+");
c.text(px + (TERM_PLUS_W - pw_text) / 2, py + (plus_h - fh) / 2, "+",
Color::from_hex(0x6E6E6E));
}
// ---- Render active terminal into offset buffer ----
uint32_t* term_pixels = win->content + TERM_TAB_BAR_H * cr.w;
terminal_render(ts, term_pixels, cr.w, term_h);
// Draw scrollbar overlay when scrollback exists
if (ts->scrollback_lines > 0 && !ts->alt_screen_active) {
int cell_h = mono_cell_height();
int total_rows = ts->scrollback_lines + ts->rows;
int total_h = total_rows * cell_h;
int view_h = term_h;
if (total_h > view_h) {
Canvas sc(term_pixels, cr.w, term_h);
int sb_w = 4;
int sb_x = cr.w - sb_w - 2;
int thumb_h = (view_h * view_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int scroll_from_top = ts->scrollback_lines - ts->view_offset;
int max_scroll = ts->scrollback_lines;
int thumb_y = (max_scroll > 0)
? (scroll_from_top * (view_h - thumb_h)) / max_scroll : 0;
sc.fill_rect(sb_x, 0, sb_w, view_h, Color::from_rgb(0x33, 0x33, 0x33));
sc.fill_rect(sb_x, thumb_y, sb_w, thumb_h, Color::from_rgb(0x88, 0x88, 0x88));
}
}
}
static void terminal_on_mouse(Window* win, MouseEvent& ev) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return;
// Scroll wheel: forward to active tab regardless of mouse position
TerminalState* ts = tts->tabs[tts->active_tab];
if (ev.scroll != 0 && !ts->alt_screen_active) {
ts->view_offset += (ev.scroll < 0) ? 3 : -3;
if (ts->view_offset < 0) ts->view_offset = 0;
if (ts->view_offset > ts->scrollback_lines)
ts->view_offset = ts->scrollback_lines;
ts->dirty = true;
}
// Tab bar click handling
if (!ev.left_pressed()) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
if (ly >= TERM_TAB_BAR_H) return; // click in terminal area, ignore
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
for (int i = 0; i < tts->tab_count; i++) {
if (lx >= tab_x && lx < tab_x + TERM_TAB_W) {
// Check if click is on the close "x" (last 24px of tab)
if (lx >= tab_x + TERM_TAB_W - 24) {
term_close_tab(tts, win, i);
} else if (i != tts->active_tab) {
tts->active_tab = i;
tts->tabs[i]->dirty = true;
}
return;
}
tab_x += TERM_TAB_W + TERM_TAB_GAP;
}
// "+" button pinned to right edge
int px = cr.w - TERM_PLUS_PAD - TERM_PLUS_W;
if (tts->tab_count < TERM_MAX_TABS && lx >= px && lx < px + TERM_PLUS_W) {
int term_h = cr.h - TERM_TAB_BAR_H;
int cols = cr.w / mono_cell_width();
int rows = term_h / mono_cell_height();
if (cols < 1) cols = 1;
if (rows < 1) rows = 1;
term_add_tab(tts, cols, rows);
}
}
static void terminal_on_key(Window* win, const Montauk::KeyEvent& key) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return;
// Ctrl+T: new tab (scancode 0x14 = 'T')
if (key.ctrl && key.pressed && key.scancode == 0x14) {
if (tts->tab_count < TERM_MAX_TABS) {
Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H;
int cols = cr.w / mono_cell_width();
int rows = term_h / mono_cell_height();
if (cols < 1) cols = 1;
if (rows < 1) rows = 1;
term_add_tab(tts, cols, rows);
}
return;
}
// Ctrl+W: close current tab (scancode 0x11 = 'W')
if (key.ctrl && key.pressed && key.scancode == 0x11) {
term_close_tab(tts, win, tts->active_tab);
return;
}
// Forward all other keys to active tab
TerminalState* ts = tts->tabs[tts->active_tab];
terminal_handle_key(ts, key);
}
static void terminal_on_close(Window* win) {
TermTabState* tts = (TermTabState*)win->app_data;
if (tts) {
for (int i = 0; i < tts->tab_count; i++)
term_free_tab(tts->tabs[i]);
montauk::mfree(tts);
win->app_data = nullptr;
}
}
static void terminal_on_poll(Window* win) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts) return;
// Poll ALL tabs — background shells still produce output
for (int i = tts->tab_count - 1; i >= 0; i--) {
if (!terminal_poll(tts->tabs[i])) {
// This tab's shell exited
term_close_tab(tts, win, i);
// term_close_tab may have closed the window (last tab).
// desktop_close_window calls on_close which nulls app_data
// before the window slot is recycled, so win is still valid here.
if (!win->app_data) return;
}
}
}
// ============================================================================
// Terminal launcher
// ============================================================================
void open_terminal(DesktopState* ds) {
int idx = desktop_create_window(ds, "Terminal", 200, 80, 648, 480);
if (idx < 0) return;
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H;
int cols = cr.w / mono_cell_width();
int rows = term_h / mono_cell_height();
if (cols < 1) cols = 1;
if (rows < 1) rows = 1;
TermTabState* tts = (TermTabState*)montauk::malloc(sizeof(TermTabState));
montauk::memset(tts, 0, sizeof(TermTabState));
tts->desktop = ds;
tts->tabs[0] = term_create_tab(ds, cols, rows);
tts->tab_count = 1;
tts->active_tab = 0;
tts->prev_active_tab = 0;
tts->last_content = win->content;
win->app_data = tts;
win->on_draw = terminal_on_draw;
win->on_mouse = terminal_on_mouse;
win->on_key = terminal_on_key;
win->on_close = terminal_on_close;
win->on_poll = terminal_on_poll;
}
@@ -0,0 +1,759 @@
/*
* app_texteditor.cpp
* MontaukOS Desktop - Text Editor application
* Single-buffer text editor with line numbers, cursor, scrolling, file I/O
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Text Editor state
// ============================================================================
static constexpr int TE_TOOLBAR_H = 36;
static constexpr int TE_PATHBAR_H = 32;
static constexpr int TE_STATUS_H = 24;
static constexpr int TE_LINE_NUM_W = 48;
static constexpr int TE_INIT_CAP = 4096;
static constexpr int TE_MAX_CAP = 262144; // 256KB
static constexpr int TE_MAX_LINES = 16384;
static constexpr int TE_TAB_WIDTH = 4;
struct TextEditorState {
char* buffer;
int buf_len;
int buf_cap;
int* line_offsets;
int line_count;
int cursor_pos; // byte position in buffer
int cursor_line;
int cursor_col;
int scroll_y; // first visible line
int scroll_x; // horizontal scroll in pixels
bool modified;
char filepath[256];
char filename[64];
DesktopState* desktop;
bool show_pathbar;
char pathbar_text[256];
int pathbar_cursor;
int pathbar_len;
};
// ============================================================================
// Line index management
// ============================================================================
static void te_recompute_lines(TextEditorState* te) {
if (!te->line_offsets) {
te->line_offsets = (int*)montauk::malloc(TE_MAX_LINES * sizeof(int));
}
te->line_count = 0;
te->line_offsets[te->line_count++] = 0;
for (int i = 0; i < te->buf_len; i++) {
if (te->buffer[i] == '\n' && te->line_count < TE_MAX_LINES) {
te->line_offsets[te->line_count++] = i + 1;
}
}
}
static void te_update_cursor_pos(TextEditorState* te) {
// Find which line the cursor is on
te->cursor_line = 0;
for (int i = te->line_count - 1; i >= 0; i--) {
if (te->cursor_pos >= te->line_offsets[i]) {
te->cursor_line = i;
break;
}
}
te->cursor_col = te->cursor_pos - te->line_offsets[te->cursor_line];
}
static int te_line_length(TextEditorState* te, int line) {
if (line < 0 || line >= te->line_count) return 0;
int start = te->line_offsets[line];
int end;
if (line + 1 < te->line_count) {
end = te->line_offsets[line + 1] - 1; // exclude newline
} else {
end = te->buf_len;
}
return end - start;
}
// ============================================================================
// Buffer operations
// ============================================================================
static void te_ensure_capacity(TextEditorState* te, int needed) {
if (te->buf_len + needed <= te->buf_cap) return;
int new_cap = te->buf_cap * 2;
if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP;
if (new_cap < te->buf_len + needed) new_cap = te->buf_len + needed;
te->buffer = (char*)montauk::realloc(te->buffer, new_cap);
te->buf_cap = new_cap;
}
static void te_insert_char(TextEditorState* te, char c) {
if (te->buf_len >= TE_MAX_CAP - 1) return;
te_ensure_capacity(te, 1);
// Shift everything after cursor right
for (int i = te->buf_len; i > te->cursor_pos; i--) {
te->buffer[i] = te->buffer[i - 1];
}
te->buffer[te->cursor_pos] = c;
te->buf_len++;
te->cursor_pos++;
te->modified = true;
te_recompute_lines(te);
te_update_cursor_pos(te);
}
static void te_insert_string(TextEditorState* te, const char* s, int len) {
if (te->buf_len + len >= TE_MAX_CAP) return;
te_ensure_capacity(te, len);
for (int i = te->buf_len - 1; i >= te->cursor_pos; i--) {
te->buffer[i + len] = te->buffer[i];
}
for (int i = 0; i < len; i++) {
te->buffer[te->cursor_pos + i] = s[i];
}
te->buf_len += len;
te->cursor_pos += len;
te->modified = true;
te_recompute_lines(te);
te_update_cursor_pos(te);
}
static void te_backspace(TextEditorState* te) {
if (te->cursor_pos <= 0) return;
te->cursor_pos--;
for (int i = te->cursor_pos; i < te->buf_len - 1; i++) {
te->buffer[i] = te->buffer[i + 1];
}
te->buf_len--;
te->modified = true;
te_recompute_lines(te);
te_update_cursor_pos(te);
}
static void te_delete_char(TextEditorState* te) {
if (te->cursor_pos >= te->buf_len) return;
for (int i = te->cursor_pos; i < te->buf_len - 1; i++) {
te->buffer[i] = te->buffer[i + 1];
}
te->buf_len--;
te->modified = true;
te_recompute_lines(te);
te_update_cursor_pos(te);
}
// ============================================================================
// Cursor movement
// ============================================================================
static void te_move_up(TextEditorState* te) {
if (te->cursor_line <= 0) return;
int target_col = te->cursor_col;
int prev_line = te->cursor_line - 1;
int prev_len = te_line_length(te, prev_line);
if (target_col > prev_len) target_col = prev_len;
te->cursor_pos = te->line_offsets[prev_line] + target_col;
te_update_cursor_pos(te);
}
static void te_move_down(TextEditorState* te) {
if (te->cursor_line >= te->line_count - 1) return;
int target_col = te->cursor_col;
int next_line = te->cursor_line + 1;
int next_len = te_line_length(te, next_line);
if (target_col > next_len) target_col = next_len;
te->cursor_pos = te->line_offsets[next_line] + target_col;
te_update_cursor_pos(te);
}
static void te_move_left(TextEditorState* te) {
if (te->cursor_pos > 0) {
te->cursor_pos--;
te_update_cursor_pos(te);
}
}
static void te_move_right(TextEditorState* te) {
if (te->cursor_pos < te->buf_len) {
te->cursor_pos++;
te_update_cursor_pos(te);
}
}
static void te_move_home(TextEditorState* te) {
te->cursor_pos = te->line_offsets[te->cursor_line];
te_update_cursor_pos(te);
}
static void te_move_end(TextEditorState* te) {
te->cursor_pos = te->line_offsets[te->cursor_line] + te_line_length(te, te->cursor_line);
te_update_cursor_pos(te);
}
// ============================================================================
// Scrolling
// ============================================================================
static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines, int content_w) {
if (te->cursor_line < te->scroll_y) {
te->scroll_y = te->cursor_line;
}
if (te->cursor_line >= te->scroll_y + visible_lines) {
te->scroll_y = te->cursor_line - visible_lines + 1;
}
// Horizontal scroll
int cell_w = mono_cell_width();
int cursor_px = te->cursor_col * cell_w;
int view_w = content_w - TE_LINE_NUM_W;
if (cursor_px - te->scroll_x > view_w - cell_w * 2) {
te->scroll_x = cursor_px - view_w + cell_w * 4;
}
if (cursor_px < te->scroll_x) {
te->scroll_x = cursor_px - cell_w * 2;
if (te->scroll_x < 0) te->scroll_x = 0;
}
}
// ============================================================================
// File I/O
// ============================================================================
static void te_load_file(TextEditorState* te, const char* path) {
int fd = montauk::open(path);
if (fd < 0) return;
uint64_t size = montauk::getsize(fd);
if (size > TE_MAX_CAP) size = TE_MAX_CAP;
if ((int)size >= te->buf_cap) {
int new_cap = (int)size + 1024;
if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP;
te->buffer = (char*)montauk::realloc(te->buffer, new_cap);
te->buf_cap = new_cap;
}
montauk::read(fd, (uint8_t*)te->buffer, 0, size);
montauk::close(fd);
te->buf_len = (int)size;
te->cursor_pos = 0;
te->scroll_y = 0;
te->scroll_x = 0;
te->modified = false;
montauk::strncpy(te->filepath, path, 255);
// Extract filename from path
int last_slash = -1;
for (int i = 0; path[i]; i++) {
if (path[i] == '/') last_slash = i;
}
if (last_slash >= 0) {
montauk::strncpy(te->filename, path + last_slash + 1, 63);
} else {
montauk::strncpy(te->filename, path, 63);
}
te_recompute_lines(te);
te_update_cursor_pos(te);
}
static void te_save_file(TextEditorState* te) {
if (te->filepath[0] == '\0') return;
int fd = montauk::fcreate(te->filepath);
if (fd < 0) return;
montauk::fwrite(fd, (const uint8_t*)te->buffer, 0, te->buf_len);
montauk::close(fd);
te->modified = false;
}
// ============================================================================
// Drawing
// ============================================================================
static void texteditor_on_draw(Window* win, Framebuffer& fb) {
TextEditorState* te = (TextEditorState*)win->app_data;
if (!te) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int cell_w = mono_cell_width();
int cell_h = mono_cell_height();
// ---- Toolbar (36px) ----
c.fill_rect(0, 0, c.w, TE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
// Open button
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
c.fill_rounded_rect(4, 6, 24, 24, 3, btn_bg);
if (te->desktop && te->desktop->icon_folder.pixels)
c.icon(8, 10, te->desktop->icon_folder);
// Save button
c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg);
if (te->desktop && te->desktop->icon_save.pixels)
c.icon(36, 10, te->desktop->icon_save);
// Vertical separator
c.vline(60, 4, 28, colors::BORDER);
// Filename + modified flag
char toolbar_label[128];
if (te->filename[0]) {
snprintf(toolbar_label, 128, "%s%s", te->filename, te->modified ? " [modified]" : "");
} else {
snprintf(toolbar_label, 128, "Untitled%s", te->modified ? " [modified]" : "");
}
int sfh = system_font_height();
c.text(68, (TE_TOOLBAR_H - sfh) / 2, toolbar_label, colors::TEXT_COLOR);
// Toolbar bottom separator
c.hline(0, TE_TOOLBAR_H - 1, c.w, colors::BORDER);
// ---- Path bar (conditional, 32px) ----
int editor_y_start = TE_TOOLBAR_H;
if (te->show_pathbar) {
int pb_y = TE_TOOLBAR_H;
c.fill_rect(0, pb_y, c.w, TE_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Text input box
int inp_x = 8;
int inp_y = pb_y + 4;
int btn_w = 56;
int inp_w = c.w - inp_x - btn_w - 12;
int inp_h = 24;
c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE);
c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT);
// Path text
int text_y = inp_y + (inp_h - sfh) / 2;
c.text(inp_x + 4, text_y, te->pathbar_text, colors::TEXT_COLOR);
// Blinking cursor in path input
char prefix[256];
int plen = te->pathbar_cursor;
if (plen > 255) plen = 255;
for (int i = 0; i < plen; i++) prefix[i] = te->pathbar_text[i];
prefix[plen] = '\0';
int cx = inp_x + 4 + text_width(prefix);
c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT);
// Open button
int ob_x = inp_x + inp_w + 6;
c.button(ob_x, inp_y, btn_w, inp_h, "Open", colors::ACCENT, colors::WHITE, 3);
// Path bar bottom separator
c.hline(0, pb_y + TE_PATHBAR_H - 1, c.w, colors::BORDER);
editor_y_start = TE_TOOLBAR_H + TE_PATHBAR_H;
}
// ---- Editor area ----
int text_area_h = c.h - editor_y_start - TE_STATUS_H;
int visible_lines = text_area_h / cell_h;
if (visible_lines < 1) visible_lines = 1;
te_ensure_cursor_visible(te, visible_lines, c.w);
// Line number gutter background
c.fill_rect(0, editor_y_start, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Gutter separator
c.vline(TE_LINE_NUM_W, editor_y_start, text_area_h, colors::BORDER);
// Draw lines
Color linenum_color = Color::from_rgb(0x99, 0x99, 0x99);
Color cursor_line_color = Color::from_rgb(0xFF, 0xFD, 0xE8);
Color text_color = colors::TEXT_COLOR;
int text_start_x = TE_LINE_NUM_W + 4;
for (int vis = 0; vis < visible_lines + 1; vis++) {
int line = te->scroll_y + vis;
if (line >= te->line_count) break;
int py = editor_y_start + vis * cell_h;
if (py >= editor_y_start + text_area_h) break;
// Cursor line highlighting
if (line == te->cursor_line) {
int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py);
if (hl_h > 0)
c.fill_rect(TE_LINE_NUM_W + 1, py, c.w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color);
}
// Line number
char num_str[8];
snprintf(num_str, 8, "%4d", line + 1);
c.text_mono(4, py, num_str, linenum_color);
// Line text (per-character rendering with horizontal scroll clipping)
int line_start = te->line_offsets[line];
int line_len = te_line_length(te, line);
for (int ci = 0; ci < line_len; ci++) {
int px = text_start_x + ci * cell_w - te->scroll_x;
if (px + cell_w <= TE_LINE_NUM_W + 1) continue;
if (px >= c.w) break;
char ch = te->buffer[line_start + ci];
if (ch >= 32 || ch < 0) {
if (fonts::mono && fonts::mono->valid) {
GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE);
fonts::mono->draw_char_to_buffer(
c.pixels, c.w, c.h, px, py + gc->ascent,
ch, text_color, gc);
} else {
// bitmap fallback
uint32_t text_px = text_color.to_pixel();
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT && py + fy < editor_y_start + text_area_h; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = px + fx;
int dy = py + fy;
if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < c.h)
c.pixels[dy * c.w + dx] = text_px;
}
}
}
}
}
}
// Draw cursor
if (line == te->cursor_line) {
int cx = text_start_x + te->cursor_col * cell_w - te->scroll_x;
if (cx > TE_LINE_NUM_W && cx + 2 <= c.w) {
int cur_h = gui_min(cell_h, editor_y_start + text_area_h - py);
if (cur_h > 0)
c.fill_rect(cx, py, 2, cur_h, colors::ACCENT);
}
}
}
// ---- Status bar ----
int status_y = c.h - TE_STATUS_H;
c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
// Cursor position (right side)
char status_right[32];
snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1);
int sr_w = text_width(status_right);
int status_text_y = status_y + (TE_STATUS_H - sfh) / 2;
c.text(c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
// Filename + modified flag (left side)
char status_left[128];
if (te->filename[0]) {
snprintf(status_left, 128, " %s%s", te->filename, te->modified ? " [modified]" : "");
} else {
snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : "");
}
c.text(4, status_text_y, status_left, colors::PANEL_TEXT);
}
// ============================================================================
// Mouse handling
// ============================================================================
static void texteditor_on_mouse(Window* win, MouseEvent& ev) {
TextEditorState* te = (TextEditorState*)win->app_data;
if (!te) return;
Rect cr = win->content_rect();
int local_x = ev.x - cr.x;
int local_y = ev.y - cr.y;
int cell_w = mono_cell_width();
int cell_h = mono_cell_height();
int editor_y_start = TE_TOOLBAR_H + (te->show_pathbar ? TE_PATHBAR_H : 0);
int text_area_h = cr.h - editor_y_start - TE_STATUS_H;
// ---- Toolbar clicks ----
if (ev.left_pressed() && local_y < TE_TOOLBAR_H) {
// Open button
if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) {
te->show_pathbar = !te->show_pathbar;
if (te->show_pathbar) {
// Pre-fill with current filepath
montauk::strncpy(te->pathbar_text, te->filepath, 255);
te->pathbar_len = montauk::slen(te->pathbar_text);
te->pathbar_cursor = te->pathbar_len;
}
return;
}
// Save button
if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) {
te_save_file(te);
return;
}
return;
}
// ---- Path bar clicks ----
if (te->show_pathbar && local_y >= TE_TOOLBAR_H && local_y < TE_TOOLBAR_H + TE_PATHBAR_H) {
if (ev.left_pressed()) {
// Check "Open" button region
int btn_w = 56;
int inp_w = cr.w - 8 - btn_w - 12;
int ob_x = 8 + inp_w + 6;
if (local_x >= ob_x && local_x < ob_x + btn_w) {
if (te->pathbar_text[0]) {
te_load_file(te, te->pathbar_text);
// Update window title
char title[64];
snprintf(title, 64, "%s - Editor", te->filename);
montauk::strncpy(win->title, title, 63);
te->show_pathbar = false;
}
}
}
return;
}
// ---- Editor area clicks ----
if (ev.left_pressed() && local_y >= editor_y_start && local_y < editor_y_start + text_area_h && local_x > TE_LINE_NUM_W) {
int clicked_line = te->scroll_y + (local_y - editor_y_start) / cell_h;
if (clicked_line >= te->line_count) clicked_line = te->line_count - 1;
if (clicked_line < 0) clicked_line = 0;
int clicked_col = (local_x - TE_LINE_NUM_W - 4 + te->scroll_x + cell_w / 2) / cell_w;
if (clicked_col < 0) clicked_col = 0;
int line_len = te_line_length(te, clicked_line);
if (clicked_col > line_len) clicked_col = line_len;
te->cursor_pos = te->line_offsets[clicked_line] + clicked_col;
te_update_cursor_pos(te);
}
// ---- Scroll ----
if (ev.scroll != 0 && local_y >= editor_y_start && local_y < editor_y_start + text_area_h) {
te->scroll_y -= ev.scroll * 3;
if (te->scroll_y < 0) te->scroll_y = 0;
int max_scroll = te->line_count - (text_area_h / cell_h) + 1;
if (max_scroll < 0) max_scroll = 0;
if (te->scroll_y > max_scroll) te->scroll_y = max_scroll;
}
}
// ============================================================================
// Keyboard handling
// ============================================================================
static void texteditor_on_key(Window* win, const Montauk::KeyEvent& key) {
TextEditorState* te = (TextEditorState*)win->app_data;
if (!te || !key.pressed) return;
// ---- Path bar input mode ----
if (te->show_pathbar) {
if (key.ascii == '\n' || key.ascii == '\r') {
if (te->pathbar_text[0]) {
te_load_file(te, te->pathbar_text);
char title[64];
snprintf(title, 64, "%s - Editor", te->filename);
montauk::strncpy(win->title, title, 63);
te->show_pathbar = false;
}
return;
}
if (key.scancode == 0x01) { // Escape
te->show_pathbar = false;
return;
}
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (te->pathbar_cursor > 0) {
for (int i = te->pathbar_cursor - 1; i < te->pathbar_len - 1; i++)
te->pathbar_text[i] = te->pathbar_text[i + 1];
te->pathbar_len--;
te->pathbar_cursor--;
te->pathbar_text[te->pathbar_len] = '\0';
}
return;
}
if (key.scancode == 0x4B) { // Left
if (te->pathbar_cursor > 0) te->pathbar_cursor--;
return;
}
if (key.scancode == 0x4D) { // Right
if (te->pathbar_cursor < te->pathbar_len) te->pathbar_cursor++;
return;
}
if (key.ascii >= 32 && key.ascii < 127 && te->pathbar_len < 254) {
for (int i = te->pathbar_len; i > te->pathbar_cursor; i--)
te->pathbar_text[i] = te->pathbar_text[i - 1];
te->pathbar_text[te->pathbar_cursor] = key.ascii;
te->pathbar_cursor++;
te->pathbar_len++;
te->pathbar_text[te->pathbar_len] = '\0';
return;
}
return;
}
// ---- Normal editor mode ----
// Ctrl+S: save
if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) {
te_save_file(te);
return;
}
// Ctrl+O: open
if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) {
te->show_pathbar = !te->show_pathbar;
if (te->show_pathbar) {
montauk::strncpy(te->pathbar_text, te->filepath, 255);
te->pathbar_len = montauk::slen(te->pathbar_text);
te->pathbar_cursor = te->pathbar_len;
}
return;
}
// Arrow keys
if (key.scancode == 0x48) { te_move_up(te); return; }
if (key.scancode == 0x50) { te_move_down(te); return; }
if (key.scancode == 0x4B) { te_move_left(te); return; }
if (key.scancode == 0x4D) { te_move_right(te); return; }
// Home
if (key.scancode == 0x47) { te_move_home(te); return; }
// End
if (key.scancode == 0x4F) { te_move_end(te); return; }
// Delete
if (key.scancode == 0x53) { te_delete_char(te); return; }
// Backspace
if (key.ascii == '\b' || key.scancode == 0x0E) {
te_backspace(te);
return;
}
// Enter
if (key.ascii == '\n' || key.ascii == '\r') {
te_insert_char(te, '\n');
return;
}
// Tab
if (key.ascii == '\t') {
for (int i = 0; i < TE_TAB_WIDTH; i++) {
te_insert_char(te, ' ');
}
return;
}
// Printable characters
if (key.ascii >= 32 && key.ascii < 127) {
te_insert_char(te, key.ascii);
return;
}
}
static void texteditor_on_close(Window* win) {
TextEditorState* te = (TextEditorState*)win->app_data;
if (te) {
if (te->buffer) montauk::mfree(te->buffer);
if (te->line_offsets) montauk::mfree(te->line_offsets);
montauk::mfree(te);
win->app_data = nullptr;
}
}
// ============================================================================
// Text Editor launchers
// ============================================================================
void open_texteditor(DesktopState* ds) {
int idx = desktop_create_window(ds, "Text Editor", 180, 60, 600, 450);
if (idx < 0) return;
Window* win = &ds->windows[idx];
TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState));
montauk::memset(te, 0, sizeof(TextEditorState));
te->buffer = (char*)montauk::malloc(TE_INIT_CAP);
te->buf_cap = TE_INIT_CAP;
te->buf_len = 0;
te->modified = false;
te->desktop = ds;
te->show_pathbar = false;
te->pathbar_text[0] = '\0';
te->pathbar_cursor = 0;
te->pathbar_len = 0;
te_recompute_lines(te);
te_update_cursor_pos(te);
win->app_data = te;
win->on_draw = texteditor_on_draw;
win->on_mouse = texteditor_on_mouse;
win->on_key = texteditor_on_key;
win->on_close = texteditor_on_close;
}
void open_texteditor_with_file(DesktopState* ds, const char* path) {
// Extract filename for window title
const char* name = path;
for (int i = 0; path[i]; i++) {
if (path[i] == '/') name = path + i + 1;
}
char title[64];
snprintf(title, 64, "%s - Editor", name);
int idx = desktop_create_window(ds, title, 180, 60, 600, 450);
if (idx < 0) return;
Window* win = &ds->windows[idx];
TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState));
montauk::memset(te, 0, sizeof(TextEditorState));
te->buffer = (char*)montauk::malloc(TE_INIT_CAP);
te->buf_cap = TE_INIT_CAP;
te->buf_len = 0;
te->modified = false;
te->desktop = ds;
te->show_pathbar = false;
te->pathbar_text[0] = '\0';
te->pathbar_cursor = 0;
te->pathbar_len = 0;
// Initialize line index for empty document first (ensures line_offsets
// is non-null even if te_load_file fails to open the file)
te_recompute_lines(te);
te_update_cursor_pos(te);
te_load_file(te, path);
win->app_data = te;
win->on_draw = texteditor_on_draw;
win->on_mouse = texteditor_on_mouse;
win->on_key = texteditor_on_key;
win->on_close = texteditor_on_close;
}
+13
View File
@@ -0,0 +1,13 @@
/*
* app_weather.cpp
* MontaukOS Desktop - Weather app launcher
* Spawns weather.elf as a standalone Window Server process
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_weather(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/weather.elf");
}
+13
View File
@@ -0,0 +1,13 @@
/*
* app_wiki.cpp
* MontaukOS Desktop - Wikipedia launcher
* Spawns wikipedia.elf as a standalone Window Server process
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_wiki(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/wikipedia.elf");
}
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
/*
* apps_common.hpp
* Shared inline utilities and forward declarations for desktop apps
* 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/framebuffer.hpp>
#include <gui/font.hpp>
#include <gui/draw.hpp>
#include <gui/svg.hpp>
#include <gui/widgets.hpp>
#include <gui/window.hpp>
#include <gui/canvas.hpp>
#include <gui/terminal.hpp>
#include <gui/desktop.hpp>
// Placement new for freestanding environment
inline void* operator new(unsigned long, void* p) { return p; }
using namespace gui;
// ============================================================================
// Minimal snprintf
// ============================================================================
using va_list = __builtin_va_list;
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
struct PfState { char* buf; int pos; int max; };
inline void pf_putc(PfState* st, char c) {
if (st->pos < st->max) st->buf[st->pos] = c;
st->pos++;
}
inline void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) {
char tmp[24]; int i = 0;
const char* digits = "0123456789abcdef";
if (val == 0) { tmp[i++] = '0'; }
else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } }
int total = (neg ? 1 : 0) + i;
if (neg && pad == '0') pf_putc(st, '-');
for (int w = total; w < width; w++) pf_putc(st, pad);
if (neg && pad != '0') pf_putc(st, '-');
while (i > 0) pf_putc(st, tmp[--i]);
}
inline int snprintf(char* buf, int size, const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0;
while (*fmt) {
if (*fmt != '%') { pf_putc(&st, *fmt++); continue; }
fmt++;
char pad = ' ';
if (*fmt == '0') { pad = '0'; fmt++; }
int width = 0;
while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; }
if (*fmt == 'l') fmt++;
switch (*fmt) {
case 'd': case 'i': {
long val = va_arg(ap, int);
int neg = 0; unsigned long uval;
if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val;
pf_putnum(&st, uval, 10, width, pad, neg); break;
}
case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; }
case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; }
case 's': {
const char* s = va_arg(ap, const char*); if (!s) s = "(null)";
int slen = 0; while (s[slen]) slen++;
for (int w = slen; w < width; w++) pf_putc(&st, ' ');
for (int j = 0; j < slen; j++) pf_putc(&st, s[j]);
break;
}
case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; }
case '%': pf_putc(&st, '%'); break;
default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break;
}
if (*fmt) fmt++;
}
if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; }
va_end(ap); return st.pos;
}
// ============================================================================
// String helpers
// ============================================================================
inline void str_append(char* dst, const char* src, int max) {
int len = montauk::slen(dst);
int i = 0;
while (src[i] && len < max - 1) {
dst[len++] = src[i++];
}
dst[len] = '\0';
}
inline int str_compare_ci(const char* a, const char* b) {
while (*a && *b) {
char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a;
char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b;
if (ca != cb) return ca - cb;
a++; b++;
}
return (unsigned char)*a - (unsigned char)*b;
}
// ============================================================================
// Network formatting helpers
// ============================================================================
inline void format_ip(char* buf, uint32_t ip) {
snprintf(buf, 20, "%d.%d.%d.%d",
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
}
inline void format_mac(char* buf, const uint8_t* mac) {
snprintf(buf, 20, "%02x:%02x:%02x:%02x:%02x:%02x",
(unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2],
(unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]);
}
// ============================================================================
// File size formatting
// ============================================================================
inline void format_size(char* buf, int size) {
if (size < 1024) {
snprintf(buf, 16, "%d B", size);
} else if (size < 1024 * 1024) {
int kb = size / 1024;
int frac = ((size % 1024) * 10) / 1024;
if (kb < 10) {
snprintf(buf, 16, "%d.%d KB", kb, frac);
} else {
snprintf(buf, 16, "%d KB", kb);
}
} else {
int mb = size / (1024 * 1024);
int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024);
if (mb < 10) {
snprintf(buf, 16, "%d.%d MB", mb, frac);
} else {
snprintf(buf, 16, "%d MB", mb);
}
}
}
// ============================================================================
// Forward declarations for app launchers
// ============================================================================
void open_terminal(DesktopState* ds);
void open_filemanager(DesktopState* ds);
void open_sysinfo(DesktopState* ds);
void open_calculator(DesktopState* ds);
void open_texteditor(DesktopState* ds);
void open_texteditor_with_file(DesktopState* ds, const char* path);
void open_klog(DesktopState* ds);
void open_wiki(DesktopState* ds);
void open_weather(DesktopState* ds);
void open_procmgr(DesktopState* ds);
void open_mandelbrot(DesktopState* ds);
void open_devexplorer(DesktopState* ds);
void open_settings(DesktopState* ds);
void open_doom(DesktopState* ds);
void open_reboot_dialog(DesktopState* ds);
void open_wordprocessor(DesktopState* ds);
void open_spreadsheet(DesktopState* ds);
void open_shutdown_dialog(DesktopState* ds);
void desktop_poll_external_windows(DesktopState* ds);