feat: multi-user system, bug fixes, security & performance fixes, and more

This commit is contained in:
2026-03-14 13:28:46 +01:00
parent 576ad34f95
commit 261b536041
389 changed files with 231853 additions and 591 deletions
@@ -360,7 +360,12 @@ static void filemanager_go_up(FileManagerState* fm) {
if (fm->current_path[i] == '/') { last_slash = i; break; }
}
if (last_slash >= 0) {
fm->current_path[last_slash + 1] = '\0';
// Keep the slash only if it's the root slash (right after "N:")
if (last_slash > 0 && fm->current_path[last_slash - 1] == ':') {
fm->current_path[last_slash + 1] = '\0';
} else {
fm->current_path[last_slash] = '\0';
}
}
filemanager_push_history(fm);
filemanager_read_dir(fm);
+799 -10
View File
@@ -6,6 +6,8 @@
#include "apps_common.hpp"
#include "../wallpaper.hpp"
#include <montauk/config.h>
#include <montauk/user.h>
// ============================================================================
// Settings state
@@ -13,11 +15,20 @@
struct SettingsState {
DesktopState* desktop;
int active_tab; // 0=Appearance, 1=Display, 2=About
int active_tab; // 0=Appearance, 1=Display, 2=Users, 3=About
Montauk::SysInfo sys_info;
uint64_t uptime_ms;
WallpaperFileList wp_files;
bool wp_scanned;
// Users tab
montauk::user::UserInfo users[16];
int user_count;
int selected_user; // index into users[], or -1
bool users_loaded;
char status_msg[128];
uint64_t status_time;
};
// ============================================================================
@@ -69,8 +80,8 @@ static const Color accent_palette[SWATCH_COUNT] = {
// ============================================================================
static constexpr int TAB_BAR_H = 36;
static constexpr int TAB_COUNT = 3;
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "About" };
static constexpr int TAB_COUNT = 4;
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "Users", "About" };
// ============================================================================
// Helper: check if two colors match
@@ -166,11 +177,11 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
if (mode_image) {
// Scan for images lazily
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
wallpaper_scan_dir(st->desktop->home_dir, &st->wp_files);
st->wp_scanned = true;
}
c.text(x, y, "Images in 0:/home/", dim);
c.text(x, y, "Wallpapers", dim);
y += sfh + 6;
if (st->wp_files.count == 0) {
@@ -180,7 +191,8 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
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/");
montauk::strcpy(fullpath, st->desktop->home_dir);
str_append(fullpath, "/", 256);
str_append(fullpath, st->wp_files.names[i], 256);
bool selected = s.bg_image &&
@@ -333,6 +345,702 @@ static void settings_draw_about(Canvas& c, SettingsState* st) {
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
}
// ============================================================================
// Config persistence
// ============================================================================
static int64_t color_to_int(Color c) {
return ((int64_t)c.r << 16) | ((int64_t)c.g << 8) | c.b;
}
static Color int_to_color(int64_t v) {
return Color::from_rgb((uint8_t)((v >> 16) & 0xFF),
(uint8_t)((v >> 8) & 0xFF),
(uint8_t)(v & 0xFF));
}
static void settings_persist(SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
const char* user = st->desktop->current_user;
montauk::toml::Doc doc;
doc.init();
// Background mode
const char* mode = s.bg_image ? "image" : (s.bg_gradient ? "gradient" : "solid");
montauk::config::set_string(&doc, "background.mode", mode);
// Wallpaper path
if (s.bg_image && s.bg_image_path[0])
montauk::config::set_string(&doc, "wallpaper.path", s.bg_image_path);
// Background colors
montauk::config::set_int(&doc, "background.solid_color", color_to_int(s.bg_solid));
montauk::config::set_int(&doc, "background.grad_top", color_to_int(s.bg_grad_top));
montauk::config::set_int(&doc, "background.grad_bottom", color_to_int(s.bg_grad_bottom));
// Appearance
montauk::config::set_int(&doc, "appearance.panel_color", color_to_int(s.panel_color));
montauk::config::set_int(&doc, "appearance.accent_color", color_to_int(s.accent_color));
// Display
montauk::config::set_int(&doc, "display.ui_scale", s.ui_scale);
montauk::config::set_bool(&doc, "display.clock_24h", s.clock_24h);
montauk::config::set_bool(&doc, "display.show_shadows", s.show_shadows);
montauk::config::save_user(user, "desktop", &doc);
doc.destroy();
}
// ============================================================================
// Users tab
// ============================================================================
static void users_reload(SettingsState* st) {
st->user_count = montauk::user::load_users(st->users, 16);
st->users_loaded = true;
}
static constexpr int USER_BTN_H = 30;
static constexpr int USER_BTN_W = 100;
static constexpr int USER_FIELD_H = 32;
// Forward declarations for dialog openers
static void open_add_user_dialog(SettingsState* st);
static void open_change_pwd_dialog(SettingsState* st);
static void open_delete_user_dialog(SettingsState* st);
static int user_row_height() {
return system_font_height() * 2 + 12; // Two lines of text + padding
}
static void settings_draw_users(Canvas& c, SettingsState* st) {
if (!st->users_loaded) users_reload(st);
Color accent = st->desktop->settings.accent_color;
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
Color card_bg = Color::from_rgb(0xF8, 0xF8, 0xF8);
int x = 16;
int y = 20;
int sfh = system_font_height();
int content_w = c.w - 2 * x;
int row_h = user_row_height();
if (!st->desktop->is_admin) {
c.text(x, y, "Admin access required to manage users.", dim);
return;
}
// User list
for (int i = 0; i < st->user_count; i++) {
bool sel = (i == st->selected_user);
bool is_current = montauk::streq(st->users[i].username, st->desktop->current_user);
if (sel) {
c.fill_rounded_rect(x, y, content_w, row_h, 4, accent);
} else if (i % 2 == 0) {
c.fill_rounded_rect(x, y, content_w, row_h, 4, card_bg);
}
Color tc = sel ? colors::WHITE : colors::TEXT_COLOR;
Color sc = sel ? Color::from_rgb(0xDD, 0xDD, 0xFF) : dim;
// Display name (primary line)
int text_y = y + 4;
c.text(x + 12, text_y, st->users[i].display_name, tc);
// Username (secondary line)
char sub[48];
if (is_current) {
snprintf(sub, sizeof(sub), "@%s (you)", st->users[i].username);
} else {
snprintf(sub, sizeof(sub), "@%s", st->users[i].username);
}
c.text(x + 12, text_y + sfh + 2, sub, sc);
// Role badge
const char* role = st->users[i].role;
int rw = text_width(role) + 12;
int badge_x = c.w - x - rw - 8;
int badge_y = y + (row_h - sfh - 4) / 2;
if (sel) {
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2,
Color::from_rgb(0xFF, 0xFF, 0xFF));
c.text(badge_x + 6, badge_y + 2, role, accent);
} else {
bool is_admin_role = montauk::streq(role, "admin");
Color badge_bg = is_admin_role
? Color::from_rgb(0xE8, 0xD8, 0xF0)
: Color::from_rgb(0xE0, 0xE8, 0xF0);
Color badge_fg = is_admin_role
? Color::from_rgb(0x7B, 0x3E, 0xB8)
: Color::from_rgb(0x36, 0x7B, 0xF0);
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2, badge_bg);
c.text(badge_x + 6, badge_y + 2, role, badge_fg);
}
y += row_h + 4;
}
// Separator
y += 8;
c.hline(x, y, content_w, colors::BORDER);
y += 16;
// Status message
if (st->status_msg[0] && (montauk::get_milliseconds() - st->status_time < 3000)) {
c.text(x, y, st->status_msg, accent);
y += sfh + 8;
}
// Action buttons
int btn_x = x;
// Add User
c.fill_rounded_rect(btn_x, y, 90, USER_BTN_H, 4, accent);
{
int tw = text_width("Add User");
c.text(btn_x + (90 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Add User", colors::WHITE);
}
btn_x += 98;
// Change Password (disabled when no selection)
{
bool enabled = st->selected_user >= 0;
Color bg = enabled ? accent : Color::from_rgb(0xCC, 0xCC, 0xCC);
c.fill_rounded_rect(btn_x, y, USER_BTN_W, USER_BTN_H, 4, bg);
int tw = text_width("Change Pwd");
c.text(btn_x + (USER_BTN_W - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Change Pwd", colors::WHITE);
}
btn_x += USER_BTN_W + 8;
// Delete (disabled when no selection)
{
bool enabled = st->selected_user >= 0;
Color del_bg = enabled ? Color::from_rgb(0xD0, 0x3E, 0x3E) : Color::from_rgb(0xCC, 0xCC, 0xCC);
c.fill_rounded_rect(btn_x, y, 70, USER_BTN_H, 4, del_bg);
int tw = text_width("Delete");
c.text(btn_x + (70 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Delete", colors::WHITE);
}
}
// ============================================================================
// Dialog helpers
// ============================================================================
static void dialog_append(char* buf, int* len, int max, char ch) {
if (*len < max - 1) { buf[*len] = ch; (*len)++; buf[*len] = '\0'; }
}
static void dialog_backspace(char* buf, int* len) {
if (*len > 0) { (*len)--; buf[*len] = '\0'; }
}
static void dialog_close_self(DesktopState* ds, void* app_data) {
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].app_data == app_data) {
desktop_close_window(ds, i);
return;
}
}
}
static void draw_input_field(Canvas& c, int x, int y, int w, int h,
const char* label, const char* value,
bool focused, bool masked, Color accent) {
int sfh = system_font_height();
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
c.text(x, y, label, dim);
y += sfh + 2;
Color border = focused ? accent : colors::BORDER;
c.fill_rounded_rect(x, y, w, h, 3, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.rect(x, y, w, h, border);
if (masked) {
int vlen = montauk::slen(value);
char dots[65];
for (int i = 0; i < vlen && i < 64; i++) dots[i] = '*';
dots[vlen < 64 ? vlen : 64] = '\0';
c.text(x + 8, y + (h - sfh) / 2, dots, colors::TEXT_COLOR);
} else {
c.text(x + 8, y + (h - sfh) / 2, value, colors::TEXT_COLOR);
}
if (focused) {
int tw = masked ? text_width("*") * montauk::slen(value) : text_width(value);
c.fill_rect(x + 8 + tw, y + 6, 2, h - 12, accent);
}
}
// ============================================================================
// Add User Dialog
// ============================================================================
struct AddUserDialogState {
DesktopState* ds;
SettingsState* parent;
Color accent;
int field; // 0=username, 1=display_name, 2=password
char username[32]; int username_len;
char display[64]; int display_len;
char password[64]; int password_len;
bool role_admin;
char error[64];
};
static void adduser_on_draw(Window* win, Framebuffer& fb) {
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color accent = st->accent;
int sfh = system_font_height();
int pad = 16;
int fw = c.w - 2 * pad;
int y = pad;
// Fields
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Username", st->username,
st->field == 0, false, accent);
y += sfh + 2 + USER_FIELD_H + 10;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Display Name", st->display,
st->field == 1, false, accent);
y += sfh + 2 + USER_FIELD_H + 10;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Password", st->password,
st->field == 2, true, accent);
y += sfh + 2 + USER_FIELD_H + 12;
// Role toggle
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
c.text(pad, y + 4, "Role:", dim);
int rbx = pad + 60;
draw_toggle_btn(c, rbx, y, 60, USER_BTN_H, "User", !st->role_admin, accent);
draw_toggle_btn(c, rbx + 68, y, 60, USER_BTN_H, "Admin", st->role_admin, accent);
y += USER_BTN_H + 14;
// Error message
if (st->error[0]) {
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
y += sfh + 6;
}
// Buttons at bottom
int btn_y = c.h - USER_BTN_H - pad;
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
int tw = text_width("Create");
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Create", colors::WHITE);
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
tw = text_width("Cancel");
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
}
static void adduser_submit(AddUserDialogState* st) {
if (st->username_len == 0) {
montauk::strcpy(st->error, "Username is required");
return;
}
if (st->password_len == 0) {
montauk::strcpy(st->error, "Password is required");
return;
}
const char* dname = st->display_len > 0 ? st->display : st->username;
const char* role = st->role_admin ? "admin" : "user";
if (montauk::user::create_user(st->username, dname, st->password, role)) {
st->parent->users_loaded = false;
montauk::strcpy(st->parent->status_msg, "User created");
st->parent->status_time = montauk::get_milliseconds();
dialog_close_self(st->ds, st);
} else {
montauk::strcpy(st->error, "Failed (username taken?)");
}
}
static void adduser_on_mouse(Window* win, MouseEvent& ev) {
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
if (!st || !ev.left_pressed()) return;
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
int pad = 16;
int sfh = system_font_height();
int fw = win->content_w - 2 * pad;
int y = pad;
// Username field hit
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
y += USER_FIELD_H + 10;
// Display field hit
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
y += USER_FIELD_H + 10;
// Password field hit
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 2; return; }
y += USER_FIELD_H + 12;
// Role toggle
int rbx = pad + 60;
if (mx >= rbx && mx < rbx + 60 && my >= y && my < y + USER_BTN_H) {
st->role_admin = false; return;
}
if (mx >= rbx + 68 && mx < rbx + 128 && my >= y && my < y + USER_BTN_H) {
st->role_admin = true; return;
}
// Bottom buttons
int btn_y = win->content_h - USER_BTN_H - pad;
if (my >= btn_y && my < btn_y + USER_BTN_H) {
if (mx >= pad && mx < pad + 80) { adduser_submit(st); return; }
if (mx >= pad + 88 && mx < pad + 168) { dialog_close_self(st->ds, st); return; }
}
}
static void adduser_on_key(Window* win, const Montauk::KeyEvent& key) {
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
if (!st || !key.pressed) return;
if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) {
adduser_submit(st); return;
}
if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; }
if (key.scancode == 0x0F) { st->field = (st->field + 1) % 3; return; }
if (key.ascii == '\b' || key.scancode == 0x0E) {
switch (st->field) {
case 0: dialog_backspace(st->username, &st->username_len); break;
case 1: dialog_backspace(st->display, &st->display_len); break;
case 2: dialog_backspace(st->password, &st->password_len); break;
}
return;
}
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
switch (st->field) {
case 0: dialog_append(st->username, &st->username_len, 31, key.ascii); break;
case 1: dialog_append(st->display, &st->display_len, 63, key.ascii); break;
case 2: dialog_append(st->password, &st->password_len, 63, key.ascii); break;
}
}
}
static void adduser_on_close(Window* win) {
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
}
static void open_add_user_dialog(SettingsState* parent) {
DesktopState* ds = parent->desktop;
int w = 340, h = 340;
int wx = (ds->screen_w - w) / 2;
int wy = (ds->screen_h - h) / 2;
int idx = desktop_create_window(ds, "Add User", wx, wy, w, h);
if (idx < 0) return;
Window* win = &ds->windows[idx];
AddUserDialogState* st = (AddUserDialogState*)montauk::malloc(sizeof(AddUserDialogState));
montauk::memset(st, 0, sizeof(AddUserDialogState));
st->ds = ds;
st->parent = parent;
st->accent = ds->settings.accent_color;
win->app_data = st;
win->on_draw = adduser_on_draw;
win->on_mouse = adduser_on_mouse;
win->on_key = adduser_on_key;
win->on_close = adduser_on_close;
}
// ============================================================================
// Change Password Dialog
// ============================================================================
struct ChPwdDialogState {
DesktopState* ds;
SettingsState* parent;
Color accent;
char username[32];
char display_name[64];
int field; // 0=new, 1=confirm
char new_pwd[64]; int new_len;
char confirm[64]; int confirm_len;
char error[64];
};
static void chpwd_on_draw(Window* win, Framebuffer& fb) {
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color accent = st->accent;
int sfh = system_font_height();
int pad = 16;
int fw = c.w - 2 * pad;
int y = pad;
// Title
char title[80];
snprintf(title, sizeof(title), "Change password for %s", st->display_name);
c.text(pad, y, title, colors::TEXT_COLOR);
y += sfh + 12;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "New Password", st->new_pwd,
st->field == 0, true, accent);
y += sfh + 2 + USER_FIELD_H + 10;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Confirm Password", st->confirm,
st->field == 1, true, accent);
y += sfh + 2 + USER_FIELD_H + 12;
// Error
if (st->error[0]) {
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
}
// Buttons at bottom
int btn_y = c.h - USER_BTN_H - pad;
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
int tw = text_width("Save");
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Save", colors::WHITE);
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
tw = text_width("Cancel");
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
}
static void chpwd_submit(ChPwdDialogState* st) {
if (st->new_len == 0) {
montauk::strcpy(st->error, "Password cannot be empty");
return;
}
if (!montauk::streq(st->new_pwd, st->confirm)) {
montauk::strcpy(st->error, "Passwords don't match");
return;
}
montauk::user::change_password(st->username, st->new_pwd);
montauk::strcpy(st->parent->status_msg, "Password changed");
st->parent->status_time = montauk::get_milliseconds();
dialog_close_self(st->ds, st);
}
static void chpwd_on_mouse(Window* win, MouseEvent& ev) {
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
if (!st || !ev.left_pressed()) return;
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
int pad = 16;
int sfh = system_font_height();
int y = pad + sfh + 12;
// New password field
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
y += USER_FIELD_H + 10;
// Confirm field
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
// Bottom buttons
int btn_y = win->content_h - USER_BTN_H - pad;
if (my >= btn_y && my < btn_y + USER_BTN_H) {
if (mx >= pad && mx < pad + 80) { chpwd_submit(st); return; }
if (mx >= pad + 88 && mx < pad + 168) { dialog_close_self(st->ds, st); return; }
}
}
static void chpwd_on_key(Window* win, const Montauk::KeyEvent& key) {
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
if (!st || !key.pressed) return;
if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) {
chpwd_submit(st); return;
}
if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; }
if (key.scancode == 0x0F) { st->field = (st->field + 1) % 2; return; }
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (st->field == 0) dialog_backspace(st->new_pwd, &st->new_len);
else dialog_backspace(st->confirm, &st->confirm_len);
return;
}
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
if (st->field == 0) dialog_append(st->new_pwd, &st->new_len, 63, key.ascii);
else dialog_append(st->confirm, &st->confirm_len, 63, key.ascii);
}
}
static void chpwd_on_close(Window* win) {
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
}
static void open_change_pwd_dialog(SettingsState* parent) {
if (parent->selected_user < 0) return;
DesktopState* ds = parent->desktop;
int w = 340, h = 260;
int wx = (ds->screen_w - w) / 2;
int wy = (ds->screen_h - h) / 2;
int idx = desktop_create_window(ds, "Change Password", wx, wy, w, h);
if (idx < 0) return;
Window* win = &ds->windows[idx];
ChPwdDialogState* st = (ChPwdDialogState*)montauk::malloc(sizeof(ChPwdDialogState));
montauk::memset(st, 0, sizeof(ChPwdDialogState));
st->ds = ds;
st->parent = parent;
st->accent = ds->settings.accent_color;
montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31);
montauk::strncpy(st->display_name, parent->users[parent->selected_user].display_name, 63);
win->app_data = st;
win->on_draw = chpwd_on_draw;
win->on_mouse = chpwd_on_mouse;
win->on_key = chpwd_on_key;
win->on_close = chpwd_on_close;
}
// ============================================================================
// Delete User Dialog
// ============================================================================
struct DeleteUserDialogState {
DesktopState* ds;
SettingsState* parent;
char username[32];
bool hover_delete, hover_cancel;
};
static void deluser_on_draw(Window* win, Framebuffer& fb) {
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int sfh = system_font_height();
char msg[96];
snprintf(msg, sizeof(msg), "Delete user \"%s\"?", st->username);
int tw = text_width(msg);
c.text((c.w - tw) / 2, 24, msg, colors::TEXT_COLOR);
const char* warn = "This action cannot be undone.";
tw = text_width(warn);
c.text((c.w - tw) / 2, 24 + sfh + 8, warn, Color::from_rgb(0x88, 0x88, 0x88));
// Buttons
int btn_w = 100, btn_h = 32;
int btn_y = c.h - btn_h - 20;
int gap = 20;
int total = btn_w * 2 + gap;
int bx = (c.w - total) / 2;
Color del_bg = st->hover_delete
? Color::from_rgb(0xDD, 0x44, 0x44)
: Color::from_rgb(0xD0, 0x3E, 0x3E);
c.button(bx, btn_y, btn_w, btn_h, "Delete", del_bg, colors::WHITE, 4);
Color cancel_bg = st->hover_cancel
? Color::from_rgb(0x99, 0x99, 0x99)
: Color::from_rgb(0x88, 0x88, 0x88);
c.button(bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4);
}
static void deluser_on_mouse(Window* win, MouseEvent& ev) {
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
if (!st) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
int btn_w = 100, btn_h = 32;
int btn_y = win->content_h - btn_h - 20;
int gap = 20;
int total = btn_w * 2 + gap;
int bx = (win->content_w - total) / 2;
Rect db = {bx, btn_y, btn_w, btn_h};
Rect cb = {bx + btn_w + gap, btn_y, btn_w, btn_h};
st->hover_delete = db.contains(lx, ly);
st->hover_cancel = cb.contains(lx, ly);
if (ev.left_pressed()) {
if (st->hover_delete) {
montauk::user::delete_user(st->username);
st->parent->users_loaded = false;
st->parent->selected_user = -1;
montauk::strcpy(st->parent->status_msg, "User deleted");
st->parent->status_time = montauk::get_milliseconds();
dialog_close_self(st->ds, st);
return;
}
if (st->hover_cancel) {
dialog_close_self(st->ds, st);
return;
}
}
}
static void deluser_on_key(Window* win, const Montauk::KeyEvent& key) {
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
if (!st || !key.pressed) return;
if (key.ascii == '\n' || key.ascii == '\r') {
montauk::user::delete_user(st->username);
st->parent->users_loaded = false;
st->parent->selected_user = -1;
montauk::strcpy(st->parent->status_msg, "User deleted");
st->parent->status_time = montauk::get_milliseconds();
dialog_close_self(st->ds, st);
}
if (key.scancode == 0x01) {
dialog_close_self(st->ds, st);
}
}
static void deluser_on_close(Window* win) {
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
}
static void open_delete_user_dialog(SettingsState* parent) {
if (parent->selected_user < 0) return;
DesktopState* ds = parent->desktop;
// Don't allow deleting yourself
if (montauk::streq(parent->users[parent->selected_user].username, ds->current_user)) {
montauk::strcpy(parent->status_msg, "Cannot delete current user");
parent->status_time = montauk::get_milliseconds();
return;
}
int w = 300, h = 150;
int wx = (ds->screen_w - w) / 2;
int wy = (ds->screen_h - h) / 2;
int idx = desktop_create_window(ds, "Delete User", wx, wy, w, h);
if (idx < 0) return;
Window* win = &ds->windows[idx];
DeleteUserDialogState* st = (DeleteUserDialogState*)montauk::malloc(sizeof(DeleteUserDialogState));
montauk::memset(st, 0, sizeof(DeleteUserDialogState));
st->ds = ds;
st->parent = parent;
montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31);
win->app_data = st;
win->on_draw = deluser_on_draw;
win->on_mouse = deluser_on_mouse;
win->on_key = deluser_on_key;
win->on_close = deluser_on_close;
}
static void settings_on_draw(Window* win, Framebuffer& fb) {
SettingsState* st = (SettingsState*)win->app_data;
if (!st) return;
@@ -372,7 +1080,8 @@ static void settings_on_draw(Window* win, Framebuffer& fb) {
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;
case 2: settings_draw_users(content, st); break;
case 3: settings_draw_about(content, st); break;
}
}
@@ -432,12 +1141,14 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
s.bg_gradient = true;
s.bg_image = false;
settings_persist(st);
return;
}
// Radio: Solid
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
s.bg_gradient = false;
s.bg_image = false;
settings_persist(st);
return;
}
// Radio: Image
@@ -445,16 +1156,17 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.bg_image = true;
s.bg_gradient = false;
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
wallpaper_scan_dir(st->desktop->home_dir, &st->wp_files);
st->wp_scanned = true;
}
settings_persist(st);
return;
}
y += line_h + 4;
int idx;
if (mode_image) {
// "Images in 0:/home/" label
// Wallpaper file list
y += sfh + 6;
// File list clicks
@@ -463,10 +1175,12 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
mx >= x && mx < win->content_w - x) {
// Build full path and load wallpaper
char fullpath[256];
montauk::strcpy(fullpath, "0:/home/");
montauk::strcpy(fullpath, st->desktop->home_dir);
str_append(fullpath, "/", 256);
str_append(fullpath, st->wp_files.names[i], 256);
wallpaper_load(&s, fullpath,
st->desktop->screen_w, st->desktop->screen_h);
settings_persist(st);
return;
}
y += WP_ITEM_H;
@@ -477,6 +1191,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Top swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_top = bg_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -484,6 +1199,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Bottom swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_bottom = bg_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -491,6 +1207,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Solid color swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_solid = bg_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -502,6 +1219,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Panel color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.panel_color = panel_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -512,6 +1230,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Accent color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.accent_color = accent_palette[idx];
settings_persist(st);
return;
}
} else if (st->active_tab == 1) {
@@ -525,11 +1244,13 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Window Shadows: On
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.show_shadows = true;
settings_persist(st);
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;
settings_persist(st);
return;
}
y += btn_h + 20 + 16;
@@ -537,11 +1258,13 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Clock: 24h
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.clock_24h = true;
settings_persist(st);
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;
settings_persist(st);
return;
}
y += btn_h + 20 + 16;
@@ -553,6 +1276,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.ui_scale = 0;
apply_ui_scale(0);
montauk::win_setscale(0);
settings_persist(st);
return;
}
// Default
@@ -560,6 +1284,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.ui_scale = 1;
apply_ui_scale(1);
montauk::win_setscale(1);
settings_persist(st);
return;
}
// Large
@@ -567,11 +1292,71 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.ui_scale = 2;
apply_ui_scale(2);
montauk::win_setscale(2);
settings_persist(st);
return;
}
} else if (st->active_tab == 2) {
// Users tab
if (!st->desktop->is_admin) return;
int x = 16;
int sfh = system_font_height();
int y = 20;
int content_w = win->content_w - 2 * x;
int row_h = user_row_height();
// User list rows
for (int i = 0; i < st->user_count; i++) {
if (cy >= y && cy < y + row_h && mx >= x && mx < x + content_w) {
st->selected_user = (st->selected_user == i) ? -1 : i;
return;
}
y += row_h + 4;
}
// Separator + gap
y += 8 + 1 + 16;
// Status message (if shown, takes space)
if (st->status_msg[0] && (montauk::get_milliseconds() - st->status_time < 3000)) {
y += sfh + 8;
}
// Action buttons
int btn_x = x;
// Add User
if (mx >= btn_x && mx < btn_x + 90 && cy >= y && cy < y + USER_BTN_H) {
open_add_user_dialog(st);
return;
}
btn_x += 98;
// Change Password
if (mx >= btn_x && mx < btn_x + USER_BTN_W && cy >= y && cy < y + USER_BTN_H) {
if (st->selected_user >= 0) open_change_pwd_dialog(st);
return;
}
btn_x += USER_BTN_W + 8;
// Delete
if (mx >= btn_x && mx < btn_x + 70 && cy >= y && cy < y + USER_BTN_H) {
if (st->selected_user >= 0) open_delete_user_dialog(st);
return;
}
}
}
// ============================================================================
// Keyboard
// ============================================================================
static void settings_on_key(Window* win, const Montauk::KeyEvent& key) {
// Settings main window no longer needs keyboard handling —
// text input is handled by dialog windows.
(void)win; (void)key;
}
// ============================================================================
// Cleanup
// ============================================================================
@@ -600,9 +1385,13 @@ void open_settings(DesktopState* ds) {
st->uptime_ms = montauk::get_milliseconds();
st->wp_scanned = false;
st->wp_files.count = 0;
st->selected_user = -1;
st->users_loaded = false;
st->status_msg[0] = '\0';
win->app_data = st;
win->on_draw = settings_on_draw;
win->on_mouse = settings_on_mouse;
win->on_key = settings_on_key;
win->on_close = settings_on_close;
}