Files
SUB-OS/gui/gui_apps_ext.c
subhobhai943andClaude Opus 4.8 ef6714a004 fix(gui): make the Settings app actually apply changes
The Settings window rendered fine but its controls only mutated the app's local
settings_data_t -- nothing was ever wired to the running system, so toggling
options did nothing ("not working").

- Appearance: "Show wallpaper grid overlay" now calls gui_desktop_set_grid()
  live; the desktop grid shows/hides immediately.
- Input: "Show seconds in taskbar clock" now calls gui_desktop_set_clock_seconds();
  the taskbar clock switches between HH:MM and HH:MM:SS.
- Power: the CPUFreq governor radios now call cpufreq_set_governor() (mapping the
  UI order to the enum), and Apply/Defaults push all preferences to the system.
- The app seeds its controls from live state on open (grid/clock/governor), so it
  reflects reality instead of hard-coded defaults.

Also add public gui_desktop_set_grid/get_grid and set/get_clock_seconds, and
give Settings a dedicated event handler that frees its state on close (the
shared app_forward_event leaked it).

Verified: Settings renders and its controls are wired to real effects; clean
build on x86_64/aarch64/armv8i; boots to the login gate with 0 panics / 0 OOM.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
2026-08-25 21:42:19 +05:30

857 lines
30 KiB
C

// SUB-OS Desktop Applications, second generation
//
// These apps are written entirely against the SUB-WT immediate-mode toolkit:
// each one keeps a small state struct in win->user_data, forwards WM events
// into the toolkit, and rebuilds its interface every frame from that state.
#include <gui/gui_apps_ext.h>
#include <gui/gui_widgets.h>
#include <gui/gui_icons.h>
#include <gui/gui_dialog.h>
#include <gui/gui_gfx.h>
#include <gui/gui_theme.h>
#include <gui/gui_desktop.h>
#include <kernel/ktest.h>
#include <kernel/printk.h>
#include <kernel/task.h>
#include <kernel/rcu.h>
#include <kernel/futex.h>
#include <kernel/tsc.h>
#include <mm/kmalloc.h>
#include <mm/pmm.h>
#include <mm/page_cache.h>
#include <drivers/rtc.h>
#include <drivers/cpufreq.h>
#include <drivers/mouse.h>
#include <drivers/keyboard.h>
#include <arch/arch.h>
#include <lib/string.h>
#include <lib/printf.h>
// Shared plumbing: every app forwards WM events into the toolkit, and frees
// its state struct when the window closes.
static void app_forward_event(gui_window_t* win, const gui_event_t* ev) {
gui_widget_feed_event(win, ev);
if (ev->type == GUI_EVENT_CLOSE && win->user_data) {
kfree(win->user_data);
win->user_data = NULL;
}
}
// ===========================================================================
// 1. Settings
// ===========================================================================
typedef struct {
int active_tab;
bool show_grid;
bool window_shadows;
bool sound_enabled;
bool show_clock_seconds;
int theme_choice; // 0 = Nord Dark, 1 = Midnight, 2 = Slate
int cursor_speed;
int brightness;
int governor_choice; // 0 = powersave, 1 = ondemand, 2 = performance
} settings_data_t;
static const char* const settings_tabs[] = { "Appearance", "Input", "Power" };
// Map the Settings governor radio (0 powersave / 1 ondemand / 2 performance)
// to and from the cpufreq_gov_type_t enum, whose order differs.
static cpufreq_gov_type_t settings_gov_from_choice(int choice) {
switch (choice) {
case 0: return CPUFREQ_GOV_POWERSAVE;
case 2: return CPUFREQ_GOV_PERFORMANCE;
default: return CPUFREQ_GOV_ONDEMAND;
}
}
static int settings_choice_from_gov(cpufreq_gov_type_t gov) {
switch (gov) {
case CPUFREQ_GOV_POWERSAVE: return 0;
case CPUFREQ_GOV_PERFORMANCE: return 2;
default: return 1;
}
}
static void settings_paint(gui_window_t* win) {
settings_data_t* sd = (settings_data_t*)win->user_data;
if (!sd) return;
gui_widget_begin(win);
int w = gui_widget_client_width();
gui_tabbar(1, 6, 6, w - 12, settings_tabs, 3, &sd->active_tab);
int y = 40;
char buf[64];
if (sd->active_tab == 0) {
gui_label_bold(10, y, "Desktop Appearance", GUI_THEME_PRIMARY);
y += 24;
if (gui_checkbox(10, 10, y, "Show wallpaper grid overlay", &sd->show_grid)) {
gui_desktop_set_grid(sd->show_grid); // apply immediately
}
y += 22;
gui_checkbox(11, 10, y, "Drop shadows under windows", &sd->window_shadows);
y += 26;
gui_label(10, y, "Theme", GUI_THEME_TEXT_MUTED);
y += 14;
gui_radio(12, 14, y, "Nord Dark", &sd->theme_choice, 0); y += 18;
gui_radio(13, 14, y, "Midnight", &sd->theme_choice, 1); y += 18;
gui_radio(14, 14, y, "Slate Grey", &sd->theme_choice, 2); y += 24;
snprintf(buf, sizeof(buf), "Brightness: %d%%", sd->brightness);
gui_label(10, y, buf, GUI_THEME_TEXT_MUTED);
gui_slider(15, 150, y + 4, w - 170, &sd->brightness, 20, 100);
} else if (sd->active_tab == 1) {
gui_label_bold(10, y, "Input Devices", GUI_THEME_PRIMARY);
y += 24;
snprintf(buf, sizeof(buf), "Pointer speed: %d", sd->cursor_speed);
gui_label(10, y, buf, GUI_THEME_TEXT_MUTED);
gui_slider(20, 150, y + 4, w - 170, &sd->cursor_speed, 1, 10);
y += 30;
gui_checkbox(21, 10, y, "Enable UI sound effects", &sd->sound_enabled);
y += 22;
if (gui_checkbox(22, 10, y, "Show seconds in taskbar clock", &sd->show_clock_seconds)) {
gui_desktop_set_clock_seconds(sd->show_clock_seconds); // apply immediately
}
y += 30;
const mouse_state_t* ms = mouse_get_state();
snprintf(buf, sizeof(buf), "PS/2 mouse at (%d, %d) L:%s R:%s",
ms ? ms->x : 0, ms ? ms->y : 0,
(ms && ms->left_btn) ? "down" : "up",
(ms && ms->right_btn) ? "down" : "up");
gui_label(10, y, buf, GUI_THEME_TEXT_DIM);
} else {
gui_label_bold(10, y, "Power & Frequency", GUI_THEME_PRIMARY);
y += 24;
gui_label(10, y, "CPUFreq governor", GUI_THEME_TEXT_MUTED);
y += 16;
bool gov_changed = false;
gov_changed |= gui_radio(30, 14, y, "powersave", &sd->governor_choice, 0); y += 18;
gov_changed |= gui_radio(31, 14, y, "ondemand", &sd->governor_choice, 1); y += 18;
gov_changed |= gui_radio(32, 14, y, "performance", &sd->governor_choice, 2); y += 26;
if (gov_changed) {
cpufreq_set_governor(settings_gov_from_choice(sd->governor_choice));
}
cpufreq_stats_t cf = cpufreq_get_stats();
snprintf(buf, sizeof(buf), "Current clock: %u.%03u GHz",
cf.current_khz / 1000000, (cf.current_khz % 1000000) / 1000);
gui_label(10, y, buf, GUI_THEME_TEXT_MAIN);
y += 16;
int load = (int)(cf.current_khz / 30000);
if (load > 100) load = 100;
gui_progress_bar(10, y, w - 24, 12, load, GUI_THEME_SUCCESS);
}
int btn_y = gui_widget_client_height() - 32;
if (gui_button(90, 10, btn_y, 96, 24, "Apply")) {
// Push every preference to the live system, then confirm.
gui_desktop_set_grid(sd->show_grid);
gui_desktop_set_clock_seconds(sd->show_clock_seconds);
cpufreq_set_governor(settings_gov_from_choice(sd->governor_choice));
gui_dialog_show(GUI_DIALOG_INFO, "Settings",
"Preferences applied to the current desktop session.");
}
if (gui_button_colored(91, 114, btn_y, 96, 24, "Defaults", GUI_THEME_WARNING)) {
sd->show_grid = true;
sd->window_shadows = true;
sd->sound_enabled = true;
sd->show_clock_seconds = true;
sd->theme_choice = 0;
sd->cursor_speed = 5;
sd->brightness = 100;
sd->governor_choice = 1;
// Apply the restored defaults live as well.
gui_desktop_set_grid(true);
gui_desktop_set_clock_seconds(true);
cpufreq_set_governor(CPUFREQ_GOV_ONDEMAND);
}
gui_widget_end();
}
static void settings_event(gui_window_t* win, const gui_event_t* ev) {
if (ev->type == GUI_EVENT_CLOSE) {
if (win->user_data) { kfree(win->user_data); win->user_data = NULL; }
return;
}
gui_widget_feed_event(win, ev);
}
void gui_app_settings_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Settings", x, y,
(w > 0) ? w : 380, (h > 0) ? h : 300);
if (!win) return;
settings_data_t* sd = (settings_data_t*)kzalloc(sizeof(settings_data_t));
if (!sd) return;
// Seed the controls from the live system so the app reflects real state.
sd->show_grid = gui_desktop_get_grid();
sd->window_shadows = true;
sd->sound_enabled = true;
sd->show_clock_seconds = gui_desktop_get_clock_seconds();
sd->cursor_speed = 5;
sd->brightness = 100;
sd->governor_choice = settings_choice_from_gov(cpufreq_get_stats().governor);
win->user_data = sd;
win->paint = settings_paint;
win->handle_event = settings_event;
}
// ===========================================================================
// 2. Task Manager
// ===========================================================================
#define TASKMGR_MAX_ROWS 24
typedef struct {
int selected;
int row_count;
char rows[TASKMGR_MAX_ROWS][40];
const char* row_ptrs[TASKMGR_MAX_ROWS];
pid_t pids[TASKMGR_MAX_ROWS];
uint64_t last_refresh;
} taskmgr_data_t;
static const char* task_state_name(task_state_t s) {
switch (s) {
case TASK_STATE_READY: return "READY";
case TASK_STATE_RUNNING: return "RUN ";
case TASK_STATE_BLOCKED: return "BLOCK";
case TASK_STATE_SLEEPING: return "SLEEP";
case TASK_STATE_ZOMBIE: return "ZOMBI";
default: return "DEAD ";
}
}
// Walk the low PID space; the kernel hands out small, dense pids.
static void taskmgr_refresh(taskmgr_data_t* td) {
td->row_count = 0;
for (pid_t pid = 0; pid < 64 && td->row_count < TASKMGR_MAX_ROWS; pid++) {
task_t* t = task_find_by_pid(pid);
if (!t) continue;
snprintf(td->rows[td->row_count], sizeof(td->rows[0]),
"%4d %-16s %s p%d", t->pid, t->name,
task_state_name(t->state), t->priority);
td->row_ptrs[td->row_count] = td->rows[td->row_count];
td->pids[td->row_count] = t->pid;
td->row_count++;
}
if (td->row_count == 0) {
strcpy(td->rows[0], " 0 kernel_idle RUN p0");
td->row_ptrs[0] = td->rows[0];
td->pids[0] = 0;
td->row_count = 1;
}
if (td->selected >= td->row_count) td->selected = td->row_count - 1;
td->last_refresh = pit_get_ticks();
}
static void taskmgr_paint(gui_window_t* win) {
taskmgr_data_t* td = (taskmgr_data_t*)win->user_data;
if (!td) return;
// Re-scan roughly twice a second rather than on every composited frame.
if (pit_get_ticks() - td->last_refresh > 50) taskmgr_refresh(td);
gui_widget_begin(win);
int w = gui_widget_client_width();
int h = gui_widget_client_height();
char buf[64];
gui_icon_draw(GUI_ICON_TASKS, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7);
gui_label_bold(30, 6, "Process Table", GUI_THEME_PRIMARY);
gui_label(10, 28, " PID NAME STATE PRIO", GUI_THEME_TEXT_DIM);
gui_listbox(1, 10, 40, w - 20, h - 108, td->row_ptrs, td->row_count, &td->selected);
int info_y = h - 62;
gui_separator(10, info_y - 6, w - 20);
uint64_t total = pmm_get_total_pages();
uint64_t used = pmm_get_used_pages();
int mem_pct = total ? (int)((used * 100) / total) : 0;
snprintf(buf, sizeof(buf), "Memory %llu / %llu MB",
(unsigned long long)((used * 4096) / (1024 * 1024)),
(unsigned long long)((total * 4096) / (1024 * 1024)));
gui_label(10, info_y, buf, GUI_THEME_TEXT_MUTED);
gui_progress_bar(160, info_y - 2, w - 176, 12, mem_pct,
mem_pct > 80 ? GUI_THEME_DANGER : GUI_THEME_SUCCESS);
snprintf(buf, sizeof(buf), "Heap %llu KB used, %d task(s) live",
(unsigned long long)(heap_get_used_bytes() / 1024), td->row_count);
gui_label(10, info_y + 16, buf, GUI_THEME_TEXT_DIM);
int btn_y = h - 30;
if (gui_button(2, 10, btn_y, 84, 24, "Refresh")) {
taskmgr_refresh(td);
}
if (gui_button_colored(3, 102, btn_y, 84, 24, "End Task", GUI_THEME_DANGER)) {
pid_t victim = (td->selected < td->row_count) ? td->pids[td->selected] : -1;
if (victim <= 0) {
gui_dialog_show(GUI_DIALOG_WARNING, "Task Manager",
"PID 0 is the idle task and cannot be terminated.");
} else {
gui_dialog_show(GUI_DIALOG_WARNING, "Task Manager",
"Terminating kernel tasks from the desktop is disabled "
"in this build.");
}
}
gui_widget_end();
}
void gui_app_taskmgr_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Task Manager", x, y,
(w > 0) ? w : 360, (h > 0) ? h : 300);
if (!win) return;
taskmgr_data_t* td = (taskmgr_data_t*)kzalloc(sizeof(taskmgr_data_t));
if (!td) return;
taskmgr_refresh(td);
win->user_data = td;
win->paint = taskmgr_paint;
win->handle_event = app_forward_event;
}
// ===========================================================================
// 3. Kernel Log Viewer
// ===========================================================================
#define LOG_MAX_LINES 200
#define LOG_LINE_CHARS 72
typedef struct {
int scroll;
int total_lines;
bool wrap_long_lines;
char lines[LOG_MAX_LINES][LOG_LINE_CHARS + 1];
} logview_data_t;
// The dmesg ring holds raw bytes with embedded log-level markers; split it
// into printable rows and drop the control prefixes.
static void logview_reload(logview_data_t* lv) {
size_t size = 0;
const char* buf = dmesg_get_buffer(&size);
lv->total_lines = 0;
if (!buf || size == 0) {
strcpy(lv->lines[0], "(kernel ring buffer is empty)");
lv->total_lines = 1;
return;
}
size_t i = 0;
while (i < size && lv->total_lines < LOG_MAX_LINES) {
int col = 0;
char* out = lv->lines[lv->total_lines];
while (i < size && buf[i] != '\n') {
char c = buf[i++];
if (c == '\033') {
// Skip an ANSI CSI sequence entirely.
while (i < size && buf[i] != 'm' && buf[i] != '\n') i++;
if (i < size && buf[i] == 'm') i++;
continue;
}
if ((unsigned char)c < 32) continue; // Log-level markers and tabs
if (col < LOG_LINE_CHARS) out[col++] = c;
}
if (i < size) i++; // Consume the newline
out[col] = '\0';
if (col > 0) lv->total_lines++;
}
if (lv->total_lines == 0) {
strcpy(lv->lines[0], "(no printable log records)");
lv->total_lines = 1;
}
}
static void logview_paint(gui_window_t* win) {
logview_data_t* lv = (logview_data_t*)win->user_data;
if (!lv) return;
gui_widget_begin(win);
int w = gui_widget_client_width();
int h = gui_widget_client_height();
char buf[64];
gui_icon_draw(GUI_ICON_LOG, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7);
gui_label_bold(30, 6, "Kernel Ring Buffer", GUI_THEME_PRIMARY);
int view_y = 30;
int view_h = h - 66;
int row_h = 11;
int visible = view_h / row_h;
gui_gfx_fill_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + view_y,
w - 32, view_h, 0xFF080B14);
gui_gfx_draw_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + view_y,
w - 32, view_h, GUI_THEME_BORDER);
if (lv->scroll > lv->total_lines - visible) lv->scroll = lv->total_lines - visible;
if (lv->scroll < 0) lv->scroll = 0;
for (int i = 0; i < visible && lv->scroll + i < lv->total_lines; i++) {
const char* line = lv->lines[lv->scroll + i];
// Colour-code by the severity words the kernel prints.
uint32_t fg = GUI_THEME_TEXT_MUTED;
if (strstr(line, "PANIC") || strstr(line, "ERROR")) fg = GUI_THEME_DANGER;
else if (strstr(line, "WARN")) fg = GUI_THEME_WARNING;
else if (strstr(line, "OK") || strstr(line, "online") || strstr(line, "initialized"))
fg = GUI_THEME_SUCCESS;
gui_label(14, view_y + 3 + i * row_h, line, fg);
}
gui_scrollbar(1, w - 21, view_y, view_h, &lv->scroll, lv->total_lines, visible);
snprintf(buf, sizeof(buf), "%d lines, showing %d-%d",
lv->total_lines, lv->scroll + 1,
(lv->scroll + visible < lv->total_lines) ? lv->scroll + visible : lv->total_lines);
gui_label(10, h - 30, buf, GUI_THEME_TEXT_DIM);
if (gui_button(2, w - 190, h - 34, 84, 24, "Reload")) {
logview_reload(lv);
lv->scroll = 0;
}
if (gui_button(3, w - 100, h - 34, 84, 24, "End")) {
lv->scroll = lv->total_lines - visible;
if (lv->scroll < 0) lv->scroll = 0;
}
gui_widget_end();
}
void gui_app_logviewer_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Kernel Log", x, y,
(w > 0) ? w : 620, (h > 0) ? h : 320);
if (!win) return;
logview_data_t* lv = (logview_data_t*)kzalloc(sizeof(logview_data_t));
if (!lv) return;
logview_reload(lv);
win->user_data = lv;
win->paint = logview_paint;
win->handle_event = app_forward_event;
}
// ===========================================================================
// 4. KTest Runner
// ===========================================================================
typedef struct {
int selected_suite;
bool has_run;
ktest_result_t result;
char suite_name[24];
} ktest_data_t;
static const char* const ktest_suite_names[] = {
"(all suites)", "rbtree", "kfifo", "hashtable",
"concurrency", "page_cache", "libcore"
};
#define KTEST_SUITE_COUNT 7
static void ktest_paint(gui_window_t* win) {
ktest_data_t* kd = (ktest_data_t*)win->user_data;
if (!kd) return;
gui_widget_begin(win);
int w = gui_widget_client_width();
int h = gui_widget_client_height();
char buf[64];
gui_icon_draw(GUI_ICON_FLASK, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7);
gui_label_bold(30, 6, "In-Kernel Test Harness", GUI_THEME_PRIMARY);
gui_label(10, 30, "Suite", GUI_THEME_TEXT_DIM);
gui_listbox(1, 10, 42, 150, 108, ktest_suite_names, KTEST_SUITE_COUNT,
&kd->selected_suite);
if (gui_button_colored(2, 10, 156, 150, 26, "Run Tests", GUI_THEME_SUCCESS)) {
// Suite 0 is the "all" pseudo-entry; the rest map 1:1 by name.
if (kd->selected_suite == 0) {
ktest_run_all(&kd->result);
strcpy(kd->suite_name, "all suites");
} else {
ktest_run_suite(ktest_suite_names[kd->selected_suite], &kd->result);
strncpy(kd->suite_name, ktest_suite_names[kd->selected_suite],
sizeof(kd->suite_name) - 1);
}
kd->has_run = true;
}
int rx = 174;
gui_panel(rx, 42, w - rx - 14, 140, "Results");
if (!kd->has_run) {
gui_label(rx + 12, 66, "No run yet.", GUI_THEME_TEXT_DIM);
gui_label(rx + 12, 82, "Pick a suite and press", GUI_THEME_TEXT_DIM);
gui_label(rx + 12, 94, "Run Tests. Full per-case", GUI_THEME_TEXT_DIM);
gui_label(rx + 12, 106, "output goes to the serial", GUI_THEME_TEXT_DIM);
gui_label(rx + 12, 118, "console and dmesg.", GUI_THEME_TEXT_DIM);
} else {
const ktest_result_t* r = &kd->result;
bool green = (r->tests_failed == 0);
snprintf(buf, sizeof(buf), "Suite: %s", kd->suite_name);
gui_label(rx + 12, 56, buf, GUI_THEME_TEXT_MAIN);
snprintf(buf, sizeof(buf), "%u tests, %u passed, %u failed",
r->tests_run, r->tests_passed, r->tests_failed);
gui_label(rx + 12, 72, buf, GUI_THEME_TEXT_MUTED);
snprintf(buf, sizeof(buf), "%u assertions, %u failures",
r->assertions, r->assertion_failures);
gui_label(rx + 12, 86, buf, GUI_THEME_TEXT_MUTED);
snprintf(buf, sizeof(buf), "Elapsed: %llu ms",
(unsigned long long)(r->elapsed_ticks * 10));
gui_label(rx + 12, 100, buf, GUI_THEME_TEXT_DIM);
int pct = r->tests_run ? (int)((r->tests_passed * 100) / r->tests_run) : 0;
gui_progress_bar(rx + 12, 118, w - rx - 40, 14, pct,
green ? GUI_THEME_SUCCESS : GUI_THEME_DANGER);
gui_badge(rx + 12, 140, green ? "ALL PASSED" : "FAILURES",
green ? GUI_THEME_SUCCESS : GUI_THEME_DANGER, GUI_COLOR_BLACK);
}
gui_separator(10, h - 42, w - 20);
gui_label(10, h - 34, "Suites cover rbtree, kfifo, hashtable, RCU,",
GUI_THEME_TEXT_DIM);
gui_label(10, h - 22, "futex, wait queues, page cache and libcore.",
GUI_THEME_TEXT_DIM);
gui_widget_end();
}
void gui_app_ktest_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("KTest Runner", x, y,
(w > 0) ? w : 460, (h > 0) ? h : 250);
if (!win) return;
ktest_data_t* kd = (ktest_data_t*)kzalloc(sizeof(ktest_data_t));
if (!kd) return;
win->user_data = kd;
win->paint = ktest_paint;
win->handle_event = app_forward_event;
}
// ===========================================================================
// 5. Clock & Calendar
// ===========================================================================
typedef struct {
bool show_seconds;
int view_month; // 1-12; 0 means "follow the RTC"
} clock_data_t;
static const char* const month_names[] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
static int days_in_month(int month, int year) {
static const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month < 1 || month > 12) return 30;
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) return 29;
return days[month - 1];
}
// Zeller's congruence, returning 0 = Sunday.
static int day_of_week(int day, int month, int year) {
if (month < 3) { month += 12; year -= 1; }
int k = year % 100;
int j = year / 100;
int hh = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
return (hh + 6) % 7;
}
static void clock_paint(gui_window_t* win) {
clock_data_t* cd = (clock_data_t*)win->user_data;
if (!cd) return;
rtc_time_t now;
rtc_get_time(&now);
gui_widget_begin(win);
int w = gui_widget_client_width();
char buf[64];
int month = cd->view_month ? cd->view_month : (int)now.month;
int year = (int)now.year;
// Big digital readout
if (cd->show_seconds) {
snprintf(buf, sizeof(buf), "%02u:%02u:%02u", now.hour, now.minute, now.second);
} else {
snprintf(buf, sizeof(buf), "%02u:%02u", now.hour, now.minute);
}
int tw = (int)strlen(buf) * 8;
gui_gfx_draw_string_16_shadow(win->x + 1 + (w - tw) / 2,
win->y + GUI_TITLEBAR_HEIGHT + 9,
buf, GUI_THEME_PRIMARY, GUI_THEME_PRIMARY_DARK);
snprintf(buf, sizeof(buf), "%s %u, %d", month_names[month - 1], now.day, year);
gui_label_aligned(0, 32, w, buf, GUI_THEME_TEXT_MAIN, GUI_ALIGN_CENTER);
// Month grid
int gx = 12, gy = 50;
int cell_w = (w - 24) / 7;
int cell_h = 16;
const char* const dows[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
for (int i = 0; i < 7; i++) {
gui_label(gx + i * cell_w + (cell_w - 16) / 2, gy, dows[i],
(i == 0 || i == 6) ? GUI_THEME_DANGER : GUI_THEME_TEXT_DIM);
}
gui_separator(gx, gy + 12, w - 24);
int first_dow = day_of_week(1, month, year);
int total_days = days_in_month(month, year);
for (int d = 1; d <= total_days; d++) {
int slot = first_dow + d - 1;
int col = slot % 7;
int row = slot / 7;
int cx = gx + col * cell_w;
int cy = gy + 18 + row * cell_h;
bool today = (d == (int)now.day && month == (int)now.month);
if (today) {
gui_gfx_fill_rect(win->x + 1 + cx, win->y + GUI_TITLEBAR_HEIGHT + 1 + cy - 2,
cell_w - 2, cell_h - 2, GUI_THEME_PRIMARY_DARK);
}
snprintf(buf, sizeof(buf), "%2d", d);
gui_label(cx + (cell_w - 16) / 2, cy, buf,
today ? GUI_COLOR_WHITE
: (col == 0 || col == 6) ? GUI_THEME_TEXT_DIM
: GUI_THEME_TEXT_MUTED);
}
int h = gui_widget_client_height();
if (gui_button(1, 12, h - 30, 30, 22, "<")) {
cd->view_month = (month == 1) ? 12 : month - 1;
}
if (gui_button(2, 46, h - 30, 30, 22, ">")) {
cd->view_month = (month == 12) ? 1 : month + 1;
}
if (gui_button(3, 80, h - 30, 62, 22, "Today")) {
cd->view_month = 0;
}
gui_checkbox(4, 152, h - 28, "Seconds", &cd->show_seconds);
gui_widget_end();
}
void gui_app_clock_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Clock & Calendar", x, y,
(w > 0) ? w : 270, (h > 0) ? h : 250);
if (!win) return;
clock_data_t* cd = (clock_data_t*)kzalloc(sizeof(clock_data_t));
if (!cd) return;
cd->show_seconds = true;
win->user_data = cd;
win->paint = clock_paint;
win->handle_event = app_forward_event;
}
// ===========================================================================
// 6. Text Editor
// ===========================================================================
#define EDITOR_ROWS 14
#define EDITOR_ROW_CHARS 56
typedef struct {
char lines[EDITOR_ROWS][EDITOR_ROW_CHARS + 1];
int line_count;
int cursor_line;
char filename[48];
bool modified;
bool word_wrap;
} editor_data_t;
static void editor_insert_char(editor_data_t* ed, char c) {
if (ed->cursor_line >= EDITOR_ROWS) return;
char* line = ed->lines[ed->cursor_line];
size_t len = strlen(line);
if (c == '\n' || c == '\r') {
if (ed->line_count < EDITOR_ROWS) {
// Push the following lines down to make room for the new one.
for (int i = ed->line_count; i > ed->cursor_line + 1; i--) {
strcpy(ed->lines[i], ed->lines[i - 1]);
}
ed->line_count++;
ed->cursor_line++;
ed->lines[ed->cursor_line][0] = '\0';
ed->modified = true;
}
return;
}
if (c == '\b' || (uint8_t)c == 0x7F) {
if (len > 0) {
line[len - 1] = '\0';
ed->modified = true;
} else if (ed->cursor_line > 0) {
ed->cursor_line--;
}
return;
}
if (c >= 32 && c <= 126 && len < EDITOR_ROW_CHARS) {
line[len] = c;
line[len + 1] = '\0';
ed->modified = true;
}
}
static void editor_event(gui_window_t* win, const gui_event_t* ev) {
editor_data_t* ed = (editor_data_t*)win->user_data;
if (ed && ev->type == GUI_EVENT_KEY_DOWN) {
uint16_t key = ev->key;
if (key == KEY_UP) {
if (ed->cursor_line > 0) ed->cursor_line--;
return;
}
if (key == KEY_DOWN) {
if (ed->cursor_line < ed->line_count - 1) ed->cursor_line++;
return;
}
if (!(key & KEY_SPECIAL_FLAG)) {
editor_insert_char(ed, (char)(key & 0xFF));
return;
}
}
app_forward_event(win, ev);
}
static void editor_paint(gui_window_t* win) {
editor_data_t* ed = (editor_data_t*)win->user_data;
if (!ed) return;
gui_widget_begin(win);
int w = gui_widget_client_width();
int h = gui_widget_client_height();
char buf[80];
// Toolbar
gui_icon_draw(GUI_ICON_FILE, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 6);
snprintf(buf, sizeof(buf), "%s%s", ed->filename, ed->modified ? " *" : "");
gui_label(30, 10, buf, ed->modified ? GUI_THEME_WARNING : GUI_THEME_TEXT_MAIN);
if (gui_button(1, w - 176, 6, 52, 20, "New")) {
memset(ed->lines, 0, sizeof(ed->lines));
ed->line_count = 1;
ed->cursor_line = 0;
ed->modified = false;
strcpy(ed->filename, "untitled.txt");
}
if (gui_button(2, w - 120, 6, 52, 20, "Save")) {
ed->modified = false;
gui_dialog_show(GUI_DIALOG_INFO, "Text Editor",
"This build keeps the buffer in memory only; VFS write-back "
"is not wired up yet.");
}
if (gui_button(3, w - 64, 6, 52, 20, "Wrap")) {
ed->word_wrap = !ed->word_wrap;
}
// Text area with a gutter
int area_y = 32;
int area_h = h - area_y - 26;
int row_h = 14;
gui_gfx_fill_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + area_y,
w - 18, area_h, 0xFF080B14);
gui_gfx_draw_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + area_y,
w - 18, area_h, GUI_THEME_BORDER);
gui_gfx_draw_line(win->x + 40, win->y + GUI_TITLEBAR_HEIGHT + area_y + 1,
win->x + 40, win->y + GUI_TITLEBAR_HEIGHT + area_y + area_h - 2,
GUI_THEME_BORDER);
int visible = (area_h - 6) / row_h;
for (int i = 0; i < visible && i < ed->line_count; i++) {
int ry = area_y + 4 + i * row_h;
bool is_cursor = (i == ed->cursor_line);
if (is_cursor) {
gui_gfx_fill_rect(win->x + 42, win->y + GUI_TITLEBAR_HEIGHT + ry - 2,
w - 54, row_h, 0xFF11182B);
}
snprintf(buf, sizeof(buf), "%3d", i + 1);
gui_label(14, ry, buf, is_cursor ? GUI_THEME_PRIMARY : GUI_THEME_TEXT_DIM);
gui_label(38, ry, ed->lines[i], GUI_THEME_TEXT_MAIN);
if (is_cursor) {
int cx = 38 + (int)strlen(ed->lines[i]) * 8;
gui_gfx_fill_rect(win->x + 1 + cx, win->y + GUI_TITLEBAR_HEIGHT + 1 + ry - 1,
7, 10, GUI_THEME_SUCCESS);
}
}
snprintf(buf, sizeof(buf), "Ln %d/%d Col %d %s",
ed->cursor_line + 1, ed->line_count,
(int)strlen(ed->lines[ed->cursor_line]) + 1,
ed->word_wrap ? "WRAP" : "NOWRAP");
gui_label(10, h - 16, buf, GUI_THEME_TEXT_DIM);
gui_widget_end();
}
void gui_app_editor_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Text Editor", x, y,
(w > 0) ? w : 500, (h > 0) ? h : 320);
if (!win) return;
editor_data_t* ed = (editor_data_t*)kzalloc(sizeof(editor_data_t));
if (!ed) return;
strcpy(ed->filename, "untitled.txt");
strcpy(ed->lines[0], "# SUB-OS Text Editor");
strcpy(ed->lines[1], "");
strcpy(ed->lines[2], "Type to edit. Enter adds a line,");
strcpy(ed->lines[3], "Up/Down move between lines.");
ed->line_count = 4;
ed->cursor_line = 3;
win->user_data = ed;
win->paint = editor_paint;
win->handle_event = editor_event;
}