diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index 809bf3c..1f28d6e 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -36,6 +36,20 @@ struct DesktopState { SvgIcon icon_folder; SvgIcon icon_file; SvgIcon icon_computer; + SvgIcon icon_network; + SvgIcon icon_calculator; + SvgIcon icon_texteditor; + SvgIcon icon_go_up; + SvgIcon icon_go_back; + SvgIcon icon_go_forward; + SvgIcon icon_save; + SvgIcon icon_home; + SvgIcon icon_exec; + + bool net_popup_open; + Zenith::NetCfg cached_net_cfg; + uint64_t net_cfg_last_poll; + Rect net_icon_rect; int screen_w, screen_h; }; diff --git a/programs/include/gui/gui.hpp b/programs/include/gui/gui.hpp index 492159d..2f178bc 100644 --- a/programs/include/gui/gui.hpp +++ b/programs/include/gui/gui.hpp @@ -58,6 +58,8 @@ namespace colors { static constexpr Color MENU_HOVER = {0xE8, 0xF0, 0xFE, 0xFF}; static constexpr Color TERM_BG = {0x2D, 0x2D, 0x2D, 0xFF}; static constexpr Color TERM_FG = {0xCC, 0xCC, 0xCC, 0xFF}; + static constexpr Color PANEL_INDICATOR_ACTIVE = {0x45, 0x58, 0x6A, 0xFF}; + static constexpr Color PANEL_INDICATOR_INACTIVE = {0x35, 0x48, 0x5A, 0xFF}; } struct Point { diff --git a/programs/include/gui/terminal.hpp b/programs/include/gui/terminal.hpp index 2f30b2a..8f28453 100644 --- a/programs/include/gui/terminal.hpp +++ b/programs/include/gui/terminal.hpp @@ -80,7 +80,8 @@ static inline void terminal_scroll_up(TerminalState* t) { } } -static inline void terminal_init(TerminalState* t, int cols, int rows) { +// Initialize only the cell grid (no child process). Used by viewers like klog. +static inline void terminal_init_cells(TerminalState* t, int cols, int rows) { t->cols = cols; t->rows = rows; t->cursor_x = 0; @@ -91,13 +92,14 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) { t->total_rows = rows; t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; - t->cursor_visible = true; + t->cursor_visible = false; t->alt_screen_active = false; t->reverse_video = false; t->parse_state = TerminalState::STATE_NORMAL; t->csi_private = false; t->csi_param_count = 0; t->csi_current_param = 0; + t->child_pid = 0; int total_cells = cols * rows; t->cells = (TermCell*)zenith::alloc(total_cells * sizeof(TermCell)); @@ -106,6 +108,11 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) { t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; } +} + +static inline void terminal_init(TerminalState* t, int cols, int rows) { + terminal_init_cells(t, cols, rows); + t->cursor_visible = true; t->child_pid = zenith::spawn_redir("0:/os/shell.elf"); zenith::childio_settermsz(t->child_pid, cols, rows); diff --git a/programs/include/gui/window.hpp b/programs/include/gui/window.hpp index 67f42a4..1f58d69 100644 --- a/programs/include/gui/window.hpp +++ b/programs/include/gui/window.hpp @@ -25,6 +25,7 @@ using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb); using WindowMouseCallback = void (*)(Window* win, MouseEvent& ev); using WindowKeyCallback = void (*)(Window* win, const Zenith::KeyEvent& key); using WindowCloseCallback = void (*)(Window* win); +using WindowPollCallback = void (*)(Window* win); struct Window { char title[MAX_TITLE_LEN]; @@ -48,6 +49,7 @@ struct Window { WindowMouseCallback on_mouse; WindowKeyCallback on_key; WindowCloseCallback on_close; + WindowPollCallback on_poll; void* app_data; Rect titlebar_rect() const { diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 9aae5e4..ec45d0e 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -262,6 +262,11 @@ namespace zenith { syscall2(Zenith::SYS_SETMOUSEBOUNDS, (uint64_t)maxX, (uint64_t)maxY); } + // Kernel log + inline int64_t read_klog(char* buf, uint64_t size) { + return syscall2(Zenith::SYS_KLOG, (uint64_t)buf, size); + } + // I/O redirection inline int spawn_redir(const char* path, const char* args = nullptr) { return (int)syscall2(Zenith::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args); diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 9921374..7cb08df 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -63,7 +63,7 @@ LDFLAGS := \ # ---- Source files ---- -SRCS := main.cpp font_data.cpp +SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/desktop/app_calculator.cpp b/programs/src/desktop/app_calculator.cpp new file mode 100644 index 0000000..ab1fe34 --- /dev/null +++ b/programs/src/desktop/app_calculator.cpp @@ -0,0 +1,409 @@ +/* + * app_calculator.cpp + * ZenithOS 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 = zenith::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; + + Rect cr = win->content_rect(); + int cw = cr.w; + int ch = cr.h; + uint32_t* pixels = win->content; + + // Background + uint32_t bg_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); + for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + + // Display area + uint32_t disp_px = Color::from_rgb(0x2D, 0x2D, 0x2D).to_pixel(); + for (int y = 0; y < CALC_DISPLAY_H && y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = disp_px; + + // Display text (right-aligned, 2x scale) + uint32_t disp_text = colors::WHITE.to_pixel(); + int text_len = zenith::slen(cs->display_str); + int text_w = text_len * FONT_WIDTH * 2; + int tx = cw - text_w - 12; + int ty = (CALC_DISPLAY_H - FONT_HEIGHT * 2) / 2; + if (tx < 4) tx = 4; + draw_text_to_pixels_2x(pixels, cw, ch, tx, ty, cs->display_str, disp_text); + + // 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 + uint32_t btn_px; + if (col == 3) { + // Operator column: accent blue + btn_px = colors::ACCENT.to_pixel(); + } else if (row == 0) { + // Function row: gray + btn_px = Color::from_rgb(0xD0, 0xD0, 0xD0).to_pixel(); + } else { + // Digit buttons: light gray + btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel(); + } + + // Draw button + for (int dy = 0; dy < CALC_BTN_H && by + dy < ch; dy++) + for (int dx = 0; dx < bw && bx + dx < cw; dx++) + pixels[(by + dy) * cw + (bx + dx)] = btn_px; + + // Button text + const char* label = calc_labels[row][col]; + uint32_t label_px = (col == 3) ? colors::WHITE.to_pixel() : colors::TEXT_COLOR.to_pixel(); + int label_w = zenith::slen(label) * FONT_WIDTH; + int lx = bx + (bw - label_w) / 2; + int ly = by + (CALC_BTN_H - FONT_HEIGHT) / 2; + draw_text_to_pixels(pixels, cw, ch, lx, ly, label, label_px); + } + } +} + +// ============================================================================ +// 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 Zenith::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) { + zenith::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*)zenith::malloc(sizeof(CalcState)); + zenith::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; +} diff --git a/programs/src/desktop/app_filemanager.cpp b/programs/src/desktop/app_filemanager.cpp new file mode 100644 index 0000000..e5559b7 --- /dev/null +++ b/programs/src/desktop/app_filemanager.cpp @@ -0,0 +1,579 @@ +/* + * app_filemanager.cpp + * ZenithOS 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; +}; + +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; + +// ============================================================================ +// File type detection +// ============================================================================ + +static bool str_ends_with(const char* s, const char* suffix) { + int slen = zenith::slen(s); + int suflen = zenith::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 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 = zenith::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') { + zenith::strcpy(prefix, after_drive); + prefix_len = zenith::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; + } + zenith::strncpy(fm->entry_names[i], raw, 63); + int len = zenith::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]; + zenith::strcpy(fullpath, fm->current_path); + int plen = zenith::slen(fullpath); + if (plen > 0 && fullpath[plen - 1] != '/') { + str_append(fullpath, "/", 512); + } + str_append(fullpath, fm->entry_names[i], 512); + int fd = zenith::open(fullpath); + if (fd >= 0) { + fm->entry_sizes[i] = (int)zenith::getsize(fd); + zenith::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]; + zenith::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; + + zenith::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--; + } + zenith::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->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 (zenith::streq(fm->history[fm->history_pos], fm->current_path)) return; + } + fm->history_pos++; + if (fm->history_pos >= 16) fm->history_pos = 15; + zenith::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 = zenith::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 = zenith::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--; + zenith::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++; + zenith::strcpy(fm->current_path, fm->history[fm->history_pos]); + filemanager_read_dir(fm); +} + +static void filemanager_go_home(FileManagerState* fm) { + zenith::strcpy(fm->current_path, "0:/"); + filemanager_push_history(fm); + filemanager_read_dir(fm); +} + +// ============================================================================ +// Drawing +// ============================================================================ + +static void filemanager_on_draw(Window* win, Framebuffer& fb) { + FileManagerState* fm = (FileManagerState*)win->app_data; + if (!fm) return; + + Rect cr = win->content_rect(); + int cw = cr.w; + int ch = cr.h; + uint32_t* pixels = win->content; + + uint32_t bg_px = colors::WINDOW_BG.to_pixel(); + for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + + uint32_t text_px = colors::TEXT_COLOR.to_pixel(); + uint32_t sep_px = colors::BORDER.to_pixel(); + uint32_t toolbar_px = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel(); + + DesktopState* ds = fm->desktop; + + // ---- Toolbar (32px) ---- + for (int y = 0; y < FM_TOOLBAR_H && y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = toolbar_px; + + // Toolbar buttons: Back, Forward, Up, Home + 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; + // Button background + uint32_t btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel(); + for (int dy = 0; dy < 24 && by + dy < ch; dy++) + for (int dx = 0; dx < 24 && bx + dx < cw; dx++) + pixels[(by + dy) * cw + (bx + dx)] = btn_px; + + if (btns[i].icon) { + int ix = bx + (24 - btns[i].icon->width) / 2; + int iy = by + (24 - btns[i].icon->height) / 2; + blit_icon_to_pixels(pixels, cw, ch, ix, iy, *btns[i].icon); + } + } + + // Toolbar separator + int sep_y = FM_TOOLBAR_H - 1; + if (sep_y < ch) { + for (int x = 0; x < cw; x++) + pixels[sep_y * cw + x] = sep_px; + } + + // ---- Path bar ---- + int pathbar_y = FM_TOOLBAR_H; + uint32_t pathbar_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); + for (int y = pathbar_y; y < pathbar_y + FM_PATHBAR_H && y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = pathbar_px; + + draw_text_to_pixels(pixels, cw, ch, 8, pathbar_y + 4, fm->current_path, text_px); + + // Path bar separator + sep_y = pathbar_y + FM_PATHBAR_H - 1; + if (sep_y < ch) { + for (int x = 0; x < cw; x++) + pixels[sep_y * cw + x] = sep_px; + } + + // ---- Column headers ---- + int header_y = FM_TOOLBAR_H + FM_PATHBAR_H; + uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel(); + for (int y = header_y; y < header_y + FM_HEADER_H && y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = header_px; + + uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel(); + int name_col_x = 8; + int size_col_x = cw - FM_SCROLLBAR_W - 120; + int type_col_x = cw - FM_SCROLLBAR_W - 60; + + draw_text_to_pixels(pixels, cw, ch, name_col_x, header_y + 2, "Name", dim_px); + if (size_col_x > 100) + draw_text_to_pixels(pixels, cw, ch, size_col_x, header_y + 2, "Size", dim_px); + if (type_col_x > 160) + draw_text_to_pixels(pixels, cw, ch, type_col_x, header_y + 2, "Type", dim_px); + + // Header separator + sep_y = header_y + FM_HEADER_H - 1; + if (sep_y < ch) { + for (int x = 0; x < cw; x++) + pixels[sep_y * cw + x] = sep_px; + } + + // Column separator lines + if (size_col_x > 100) { + for (int y = header_y; y < ch; y++) + if (size_col_x - 4 >= 0 && size_col_x - 4 < cw) + pixels[y * cw + (size_col_x - 4)] = sep_px; + } + + // ---- File entries ---- + int list_y = header_y + FM_HEADER_H; + int list_h = ch - list_y; + int visible_items = list_h / FM_ITEM_H; + int content_h = fm->entry_count * FM_ITEM_H; + + // Update scrollbar + fm->scrollbar.bounds = {cw - 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 >= ch) continue; + + // Highlight selected + if (i == fm->selected) { + uint32_t sel_px = colors::MENU_HOVER.to_pixel(); + for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++) + for (int x = 0; x < cw - FM_SCROLLBAR_W; x++) + pixels[y * cw + x] = sel_px; + } + + // Icon + int icon_x = 8; + int icon_y = iy + (FM_ITEM_H - 16) / 2; + if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) { + blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder); + } else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) { + blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec); + } else if (ds && ds->icon_file.pixels) { + blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file); + } else { + uint32_t icon_px = fm->is_dir[i] + ? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel() + : Color::from_rgb(0x90, 0x90, 0x90).to_pixel(); + for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++) + for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++) + pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px; + } + + // Name + int tx = 30; + int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2; + if (ty >= list_y && ty + FONT_HEIGHT <= ch) + draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px); + + // Size + if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= ch) { + char size_str[16]; + format_size(size_str, fm->entry_sizes[i]); + draw_text_to_pixels(pixels, cw, ch, size_col_x, ty, size_str, dim_px); + } + + // Type + if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= ch) { + const char* type_str = "File"; + if (fm->entry_types[i] == 1) type_str = "Dir"; + else if (fm->entry_types[i] == 2) type_str = "Exec"; + draw_text_to_pixels(pixels, cw, ch, type_col_x, ty, type_str, dim_px); + } + } + + // ---- Scrollbar ---- + // Draw scrollbar directly to pixels + if (fm->scrollbar.content_height > fm->scrollbar.view_height) { + uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel(); + uint32_t sb_fg = (fm->scrollbar.hovered || fm->scrollbar.dragging) + ? fm->scrollbar.hover_fg.to_pixel() : fm->scrollbar.fg.to_pixel(); + + int sbx = fm->scrollbar.bounds.x; + int sby = fm->scrollbar.bounds.y; + int sbw = fm->scrollbar.bounds.w; + int sbh = fm->scrollbar.bounds.h; + + for (int y = sby; y < sby + sbh && y < ch; y++) + for (int x = sbx; x < sbx + sbw && x < cw; x++) + pixels[y * cw + x] = sb_bg; + + int th = fm->scrollbar.thumb_height(); + int tty = fm->scrollbar.thumb_y(); + for (int y = tty; y < tty + th && y < sby + sbh && y < ch; y++) + for (int x = sbx + 1; x < sbx + sbw - 1 && x < cw; x++) + pixels[y * cw + x] = sb_fg; + } +} + +// ============================================================================ +// 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); + return; + } + + // File list 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 = zenith::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 text editor + char fullpath[512]; + zenith::strcpy(fullpath, fm->current_path); + int plen = zenith::slen(fullpath); + if (plen > 0 && fullpath[plen - 1] != '/') { + str_append(fullpath, "/", 512); + } + str_append(fullpath, fm->entry_names[clicked_idx], 512); + 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_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H; + if (local_y >= list_y_start) { + fm->scrollbar.scroll_offset -= ev.scroll * FM_ITEM_H; + 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 Zenith::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->selected > 0) fm->selected--; + } else if (key.scancode == 0x50) { + // Down arrow + 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) { + zenith::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*)zenith::malloc(sizeof(FileManagerState)); + zenith::memset(fm, 0, sizeof(FileManagerState)); + zenith::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->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; +} diff --git a/programs/src/desktop/app_klog.cpp b/programs/src/desktop/app_klog.cpp new file mode 100644 index 0000000..bdb2097 --- /dev/null +++ b/programs/src/desktop/app_klog.cpp @@ -0,0 +1,151 @@ +/* + * app_klog.cpp + * ZenithOS 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 the last screenful of log into the terminal +// ============================================================================ + +static void klog_refeed(KlogState* klog, int n) { + // Find start of last `rows` lines from the end of the buffer + int lines_needed = klog->term.rows; + int start = 0; + for (int i = n - 1; i >= 0 && lines_needed > 0; i--) { + if (klog->klog_buf[i] == '\n') { + lines_needed--; + if (lines_needed == 0) { start = i + 1; break; } + } + } + + // Clear terminal + int total = klog->term.cols * klog->term.rows; + for (int i = 0; i < total; i++) + klog->term.cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; + klog->term.cursor_x = 0; + klog->term.cursor_y = 0; + klog->term.current_fg = colors::TERM_FG; + klog->term.current_bg = 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(); + terminal_render(&klog->term, win->content, cr.w, cr.h); +} + +static void klog_on_mouse(Window* win, MouseEvent& ev) { + // Read-only viewer — no mouse interaction needed +} + +static void klog_on_key(Window* win, const Zenith::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 = zenith::get_milliseconds(); + if (now - klog->last_poll_ms < KLOG_POLL_MS) return; + klog->last_poll_ms = now; + + int n = (int)zenith::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) zenith::mfree(klog->term.cells); + if (klog->term.alt_cells) zenith::mfree(klog->term.alt_cells); + if (klog->klog_buf) zenith::mfree(klog->klog_buf); + zenith::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 / FONT_WIDTH; + int rows = cr.h / FONT_HEIGHT; + + KlogState* klog = (KlogState*)zenith::malloc(sizeof(KlogState)); + zenith::memset(klog, 0, sizeof(KlogState)); + + // Initialize the terminal cell grid (reuse terminal infrastructure) + terminal_init_cells(&klog->term, cols, rows); + + // Allocate klog read buffer + klog->klog_buf = (char*)zenith::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)zenith::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; +} diff --git a/programs/src/desktop/app_sysinfo.cpp b/programs/src/desktop/app_sysinfo.cpp new file mode 100644 index 0000000..d817864 --- /dev/null +++ b/programs/src/desktop/app_sysinfo.cpp @@ -0,0 +1,154 @@ +/* + * app_sysinfo.cpp + * ZenithOS Desktop - System Info application + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "apps_common.hpp" + +// ============================================================================ +// System Info state and callbacks +// ============================================================================ + +struct SysInfoState { + Zenith::SysInfo sys_info; + Zenith::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; + + // Refresh uptime + si->uptime_ms = zenith::get_milliseconds(); + + Rect cr = win->content_rect(); + int cw = cr.w; + int ch = cr.h; + uint32_t* pixels = win->content; + + // Fill background + uint32_t bg_px = colors::WINDOW_BG.to_pixel(); + for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + + uint32_t text_px = colors::TEXT_COLOR.to_pixel(); + uint32_t accent_px = colors::ACCENT.to_pixel(); + int y = 16; + int x = 16; + char line[128]; + + // Title + draw_text_to_pixels(pixels, cw, ch, x, y, "System Information", accent_px); + y += FONT_HEIGHT + 12; + + // Separator + uint32_t sep_px = colors::BORDER.to_pixel(); + for (int sx = x; sx < cw - x && y < ch; sx++) + pixels[y * cw + sx] = sep_px; + y += 8; + + // OS Name + snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // OS Version + snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // API Version + snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // Max Processes + snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += 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); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 12; + + // Network section + draw_text_to_pixels(pixels, cw, ch, x, y, "Network", accent_px); + y += FONT_HEIGHT + 8; + + for (int sx = x; sx < cw - x && y < ch; sx++) + pixels[y * cw + sx] = sep_px; + 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)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // 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)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // 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)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // 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)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // 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]); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); +} + +static void sysinfo_on_close(Window* win) { + if (win->app_data) { + zenith::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*)zenith::malloc(sizeof(SysInfoState)); + zenith::memset(si, 0, sizeof(SysInfoState)); + zenith::get_info(&si->sys_info); + zenith::get_netcfg(&si->net_cfg); + si->uptime_ms = zenith::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; +} diff --git a/programs/src/desktop/app_terminal.cpp b/programs/src/desktop/app_terminal.cpp new file mode 100644 index 0000000..8fc8468 --- /dev/null +++ b/programs/src/desktop/app_terminal.cpp @@ -0,0 +1,68 @@ +/* + * app_terminal.cpp + * ZenithOS Desktop - Terminal application + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "apps_common.hpp" + +// ============================================================================ +// Terminal callbacks +// ============================================================================ + +static void terminal_on_draw(Window* win, Framebuffer& fb) { + TerminalState* ts = (TerminalState*)win->app_data; + if (!ts) return; + + Rect cr = win->content_rect(); + terminal_render(ts, win->content, cr.w, cr.h); +} + +static void terminal_on_mouse(Window* win, MouseEvent& ev) { + // Terminal doesn't need mouse handling for now +} + +static void terminal_on_key(Window* win, const Zenith::KeyEvent& key) { + TerminalState* ts = (TerminalState*)win->app_data; + if (!ts) return; + terminal_handle_key(ts, key); +} + +static void terminal_on_close(Window* win) { + TerminalState* ts = (TerminalState*)win->app_data; + if (ts) { + if (ts->cells) zenith::mfree(ts->cells); + zenith::mfree(ts); + win->app_data = nullptr; + } +} + +static void terminal_on_poll(Window* win) { + TerminalState* ts = (TerminalState*)win->app_data; + if (ts) terminal_poll(ts); +} + +// ============================================================================ +// 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 cols = cr.w / FONT_WIDTH; + int rows = cr.h / FONT_HEIGHT; + + TerminalState* ts = (TerminalState*)zenith::malloc(sizeof(TerminalState)); + zenith::memset(ts, 0, sizeof(TerminalState)); + terminal_init(ts, cols, rows); + + win->app_data = ts; + 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; +} diff --git a/programs/src/desktop/app_texteditor.cpp b/programs/src/desktop/app_texteditor.cpp new file mode 100644 index 0000000..08529d4 --- /dev/null +++ b/programs/src/desktop/app_texteditor.cpp @@ -0,0 +1,576 @@ +/* + * app_texteditor.cpp + * ZenithOS 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_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; +}; + +// ============================================================================ +// Line index management +// ============================================================================ + +static void te_recompute_lines(TextEditorState* te) { + if (!te->line_offsets) { + te->line_offsets = (int*)zenith::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*)zenith::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) { + 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 cursor_px = te->cursor_col * FONT_WIDTH; + int view_w = 580 - TE_LINE_NUM_W; // approximate + if (cursor_px - te->scroll_x > view_w - FONT_WIDTH * 2) { + te->scroll_x = cursor_px - view_w + FONT_WIDTH * 4; + } + if (cursor_px < te->scroll_x) { + te->scroll_x = cursor_px - FONT_WIDTH * 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 = zenith::open(path); + if (fd < 0) return; + + uint64_t size = zenith::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*)zenith::realloc(te->buffer, new_cap); + te->buf_cap = new_cap; + } + + zenith::read(fd, (uint8_t*)te->buffer, 0, size); + zenith::close(fd); + + te->buf_len = (int)size; + te->cursor_pos = 0; + te->scroll_y = 0; + te->scroll_x = 0; + te->modified = false; + + zenith::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) { + zenith::strncpy(te->filename, path + last_slash + 1, 63); + } else { + zenith::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 = zenith::fcreate(te->filepath); + if (fd < 0) return; + + zenith::fwrite(fd, (const uint8_t*)te->buffer, 0, te->buf_len); + zenith::close(fd); + + te->modified = false; +} + +// ============================================================================ +// Drawing +// ============================================================================ + +static void texteditor_on_draw(Window* win, Framebuffer& fb) { + TextEditorState* te = (TextEditorState*)win->app_data; + if (!te) return; + + Rect cr = win->content_rect(); + int cw = cr.w; + int ch = cr.h; + uint32_t* pixels = win->content; + + // Background + uint32_t bg_px = colors::WINDOW_BG.to_pixel(); + for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + + int text_area_h = ch - TE_STATUS_H; + int visible_lines = text_area_h / FONT_HEIGHT; + + te_ensure_cursor_visible(te, visible_lines); + + // Line number gutter background + uint32_t gutter_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); + for (int y = 0; y < text_area_h && y < ch; y++) + for (int x = 0; x < TE_LINE_NUM_W && x < cw; x++) + pixels[y * cw + x] = gutter_px; + + // Gutter separator + uint32_t sep_px = colors::BORDER.to_pixel(); + if (TE_LINE_NUM_W < cw) { + for (int y = 0; y < text_area_h && y < ch; y++) + pixels[y * cw + TE_LINE_NUM_W] = sep_px; + } + + // Draw lines + uint32_t text_px = colors::TEXT_COLOR.to_pixel(); + uint32_t linenum_px = Color::from_rgb(0x99, 0x99, 0x99).to_pixel(); + uint32_t cursor_line_px = Color::from_rgb(0xFF, 0xFD, 0xE8).to_pixel(); + uint32_t cursor_px = colors::ACCENT.to_pixel(); + + 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 = vis * FONT_HEIGHT; + if (py >= text_area_h) break; + + // Cursor line highlighting + if (line == te->cursor_line) { + for (int y = py; y < py + FONT_HEIGHT && y < text_area_h; y++) + for (int x = TE_LINE_NUM_W + 1; x < cw; x++) + pixels[y * cw + x] = cursor_line_px; + } + + // Line number + char num_str[8]; + snprintf(num_str, 8, "%4d", line + 1); + draw_text_to_pixels(pixels, cw, ch, 4, py, num_str, linenum_px); + + // Line text + 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 * FONT_WIDTH - te->scroll_x; + if (px + FONT_WIDTH <= TE_LINE_NUM_W + 1) continue; + if (px >= cw) break; + + char c = te->buffer[line_start + ci]; + if (c >= 32 || c < 0) { + const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT]; + for (int fy = 0; fy < FONT_HEIGHT && py + fy < 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 < cw && dy >= 0 && dy < text_area_h) + pixels[dy * cw + dx] = text_px; + } + } + } + } + } + + // Draw cursor + if (line == te->cursor_line) { + int cx = text_start_x + te->cursor_col * FONT_WIDTH - te->scroll_x; + if (cx > TE_LINE_NUM_W && cx + 2 <= cw) { + for (int y = py; y < py + FONT_HEIGHT && y < text_area_h; y++) { + pixels[y * cw + cx] = cursor_px; + if (cx + 1 < cw) + pixels[y * cw + cx + 1] = cursor_px; + } + } + } + } + + // ---- Status bar ---- + int status_y = ch - TE_STATUS_H; + uint32_t status_bg = Color::from_rgb(0x2B, 0x3E, 0x50).to_pixel(); + uint32_t status_fg = colors::PANEL_TEXT.to_pixel(); + + for (int y = status_y; y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = status_bg; + + // Filename + modified flag + 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]" : ""); + } + draw_text_to_pixels(pixels, cw, ch, 4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, + status_left, status_fg); + + // 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 = zenith::slen(status_right) * FONT_WIDTH; + draw_text_to_pixels(pixels, cw, ch, cw - sr_w - 4, + status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, + status_right, status_fg); +} + +// ============================================================================ +// 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 text_area_h = cr.h - TE_STATUS_H; + + if (ev.left_pressed() && local_y < text_area_h && local_x > TE_LINE_NUM_W) { + // Click to position cursor + int clicked_line = te->scroll_y + local_y / FONT_HEIGHT; + 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 + FONT_WIDTH / 2) / FONT_WIDTH; + 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 < 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 / FONT_HEIGHT) + 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 Zenith::KeyEvent& key) { + TextEditorState* te = (TextEditorState*)win->app_data; + if (!te || !key.pressed) return; + + // Ctrl+S: save + if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) { + te_save_file(te); + 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) zenith::mfree(te->buffer); + if (te->line_offsets) zenith::mfree(te->line_offsets); + zenith::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*)zenith::malloc(sizeof(TextEditorState)); + zenith::memset(te, 0, sizeof(TextEditorState)); + + te->buffer = (char*)zenith::malloc(TE_INIT_CAP); + te->buf_cap = TE_INIT_CAP; + te->buf_len = 0; + te->modified = false; + te->desktop = ds; + + 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*)zenith::malloc(sizeof(TextEditorState)); + zenith::memset(te, 0, sizeof(TextEditorState)); + + te->buffer = (char*)zenith::malloc(TE_INIT_CAP); + te->buf_cap = TE_INIT_CAP; + te->buf_len = 0; + te->modified = false; + te->desktop = ds; + + // 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; +} diff --git a/programs/src/desktop/apps_common.hpp b/programs/src/desktop/apps_common.hpp new file mode 100644 index 0000000..04ce1d9 --- /dev/null +++ b/programs/src/desktop/apps_common.hpp @@ -0,0 +1,250 @@ +/* + * apps_common.hpp + * Shared inline utilities and forward declarations for desktop apps + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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 = zenith::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]); +} + +// ============================================================================ +// Text rendering to pixel buffers (for app content areas) +// ============================================================================ + +inline void draw_text_to_pixels(uint32_t* pixels, int pw, int ph, + int tx, int ty, const char* text, uint32_t color_px) { + for (int i = 0; text[i] && tx + (i + 1) * FONT_WIDTH <= pw; i++) { + const uint8_t* glyph = &font_data[(unsigned char)text[i] * FONT_HEIGHT]; + int cx = tx + i * FONT_WIDTH; + for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ph; fy++) { + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = cx + fx; + int dy = ty + fy; + if (dx >= 0 && dx < pw && dy >= 0 && dy < ph) + pixels[dy * pw + dx] = color_px; + } + } + } + } +} + +inline void draw_text_to_pixels_2x(uint32_t* pixels, int pw, int ph, + int tx, int ty, const char* text, uint32_t color_px) { + for (int i = 0; text[i] && tx + (i + 1) * FONT_WIDTH * 2 <= pw; i++) { + const uint8_t* glyph = &font_data[(unsigned char)text[i] * FONT_HEIGHT]; + int cx = tx + i * FONT_WIDTH * 2; + for (int fy = 0; fy < FONT_HEIGHT; fy++) { + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = cx + fx * 2; + int dy = ty + fy * 2; + for (int sy = 0; sy < 2; sy++) + for (int sx = 0; sx < 2; sx++) { + int px = dx + sx; + int py = dy + sy; + if (px >= 0 && px < pw && py >= 0 && py < ph) + pixels[py * pw + px] = color_px; + } + } + } + } + } +} + +// ============================================================================ +// 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); + } + } +} + +// ============================================================================ +// Icon rendering to pixel buffer +// ============================================================================ + +inline void blit_icon_to_pixels(uint32_t* pixels, int pw, int ph, + int ix, int iy, const SvgIcon& icon) { + if (!icon.pixels) return; + for (int row = 0; row < icon.height; row++) { + int dy = iy + row; + if (dy < 0 || dy >= ph) continue; + for (int col = 0; col < icon.width; col++) { + int dx = ix + col; + if (dx < 0 || dx >= pw) continue; + uint32_t src = icon.pixels[row * icon.width + col]; + uint8_t sa = (src >> 24) & 0xFF; + if (sa == 0) continue; + if (sa == 255) { + pixels[dy * pw + dx] = src; + } else { + uint32_t dst = pixels[dy * pw + dx]; + uint8_t sr = (src >> 16) & 0xFF; + uint8_t sg = (src >> 8) & 0xFF; + uint8_t sb = src & 0xFF; + uint8_t dr = (dst >> 16) & 0xFF; + uint8_t dg = (dst >> 8) & 0xFF; + uint8_t db = dst & 0xFF; + uint32_t a = sa, inv_a = 255 - sa; + uint32_t rr = (a * sr + inv_a * dr + 128) / 255; + uint32_t gg = (a * sg + inv_a * dg + 128) / 255; + uint32_t bb = (a * sb + inv_a * db + 128) / 255; + pixels[dy * pw + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } +} + +// ============================================================================ +// 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); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 6ec9f33..43c74c0 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -1,595 +1,20 @@ /* * main.cpp - * ZenithOS Desktop Environment - window manager, compositor, and applications + * ZenithOS Desktop Environment - window manager, compositor, and run loop * Copyright (c) 2026 Daniel Hammer */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Placement new for freestanding environment -inline void* operator new(unsigned long, void* p) { return p; } - -using namespace gui; - -// ============================================================================ -// Minimal snprintf (copied from init/main.cpp pattern) -// ============================================================================ - -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; }; - -static void pf_putc(PfState* st, char c) { - if (st->pos < st->max) st->buf[st->pos] = c; - st->pos++; -} - -static 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]); -} - -static 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 -// ============================================================================ - -static void str_append(char* dst, const char* src, int max) { - int len = zenith::slen(dst); - int i = 0; - while (src[i] && len < max - 1) { - dst[len++] = src[i++]; - } - dst[len] = '\0'; -} - -// ============================================================================ -// File Manager Application -// ============================================================================ - -struct FileManagerState { - char current_path[256]; - char entry_names[64][64]; - int entry_count; - int selected; - int scroll_offset; - bool is_dir[64]; -}; - -static void filemanager_read_dir(FileManagerState* fm) { - const char* names[64]; - fm->entry_count = zenith::readdir(fm->current_path, names, 64); - if (fm->entry_count < 0) fm->entry_count = 0; - for (int i = 0; i < fm->entry_count; i++) { - zenith::strncpy(fm->entry_names[i], names[i], 63); - // Heuristic: entries ending with '/' or without '.' are directories - int len = zenith::slen(fm->entry_names[i]); - if (len > 0 && fm->entry_names[i][len - 1] == '/') { - fm->is_dir[i] = true; - fm->entry_names[i][len - 1] = '\0'; // strip trailing slash - } else { - // Check if entry has an extension (has a dot) - 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->selected = -1; - fm->scroll_offset = 0; -} - -static void filemanager_navigate(FileManagerState* fm, const char* name) { - // Build new path - int path_len = zenith::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_read_dir(fm); -} - -static void filemanager_go_up(FileManagerState* fm) { - int len = zenith::slen(fm->current_path); - if (len <= 3) return; // "0:/" is root - - // Remove trailing slash if present - if (len > 0 && fm->current_path[len - 1] == '/') { - fm->current_path[len - 1] = '\0'; - len--; - } - - // Find last slash - 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_read_dir(fm); -} - -static void filemanager_on_draw(Window* win, Framebuffer& fb) { - FileManagerState* fm = (FileManagerState*)win->app_data; - if (!fm) return; - - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - - // Render to content buffer - uint32_t* pixels = win->content; - uint32_t bg_px = colors::WINDOW_BG.to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; - - // Draw path bar at top (24px tall, light gray background) - uint32_t pathbar_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); - for (int y = 0; y < 24 && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = pathbar_px; - - // Draw path text - { - int tx = 8; - int ty = 4; - uint32_t fg_px = colors::TEXT_COLOR.to_pixel(); - for (int ci = 0; fm->current_path[ci] && tx + FONT_WIDTH <= cw; ci++) { - const uint8_t* glyph = &font_data[(unsigned char)fm->current_path[ci] * FONT_HEIGHT]; - for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ch; fy++) { - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = tx + fx; - int dy = ty + fy; - if (dx < cw && dy < ch) - pixels[dy * cw + dx] = fg_px; - } - } - } - tx += FONT_WIDTH; - } - } - - // Draw separator line - uint32_t sep_px = colors::BORDER.to_pixel(); - if (24 < ch) { - for (int x = 0; x < cw; x++) - pixels[24 * cw + x] = sep_px; - } - - // Draw file entries - int item_height = 24; - int start_y = 26; - int visible_items = (ch - start_y) / item_height; - - for (int i = fm->scroll_offset; i < fm->entry_count && (i - fm->scroll_offset) < visible_items; i++) { - int iy = start_y + (i - fm->scroll_offset) * item_height; - if (iy + item_height > ch) break; - - // Highlight selected - if (i == fm->selected) { - uint32_t sel_px = colors::MENU_HOVER.to_pixel(); - for (int y = iy; y < iy + item_height && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = sel_px; - } - - // Draw icon placeholder (small colored square) - uint32_t icon_px; - if (fm->is_dir[i]) { - icon_px = Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel(); // folder yellow - } else { - icon_px = Color::from_rgb(0x90, 0x90, 0x90).to_pixel(); // file gray - } - int icon_x = 8; - int icon_y = iy + 4; - for (int dy = 0; dy < 16 && icon_y + dy < ch; dy++) - for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++) - pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px; - - // Draw entry name - int tx = 30; - int ty = iy + 4; - uint32_t text_px = colors::TEXT_COLOR.to_pixel(); - for (int ci = 0; fm->entry_names[i][ci] && tx + FONT_WIDTH <= cw; ci++) { - const uint8_t* glyph = &font_data[(unsigned char)fm->entry_names[i][ci] * FONT_HEIGHT]; - for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ch; fy++) { - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = tx + fx; - int dy = ty + fy; - if (dx < cw && dy < ch) - pixels[dy * cw + dx] = text_px; - } - } - } - tx += FONT_WIDTH; - } - } -} - -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_y = ev.y - cr.y; - - if (ev.left_pressed()) { - int item_height = 24; - int start_y = 26; - int clicked_idx = fm->scroll_offset + (local_y - start_y) / item_height; - - if (local_y >= start_y && clicked_idx >= 0 && clicked_idx < fm->entry_count) { - if (fm->selected == clicked_idx) { - // Double-click: navigate into directory - if (fm->is_dir[clicked_idx]) { - filemanager_navigate(fm, fm->entry_names[clicked_idx]); - } - } else { - fm->selected = clicked_idx; - } - } - } - - // Scroll handling - if (ev.scroll != 0) { - fm->scroll_offset -= ev.scroll; - if (fm->scroll_offset < 0) fm->scroll_offset = 0; - int max_off = fm->entry_count - ((cr.h - 26) / 24); - if (max_off < 0) max_off = 0; - if (fm->scroll_offset > max_off) fm->scroll_offset = max_off; - } -} - -static void filemanager_on_key(Window* win, const Zenith::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->selected > 0) fm->selected--; - } else if (key.scancode == 0x50) { - // Down arrow - 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]); - } - } - } -} - -static void filemanager_on_close(Window* win) { - if (win->app_data) { - zenith::mfree(win->app_data); - win->app_data = nullptr; - } -} - -// ============================================================================ -// System Info Application -// ============================================================================ - -struct SysInfoState { - Zenith::SysInfo sys_info; - Zenith::NetCfg net_cfg; - uint64_t uptime_ms; -}; - -static void draw_text_to_pixels(uint32_t* pixels, int pw, int ph, - int tx, int ty, const char* text, uint32_t color_px) { - for (int i = 0; text[i] && tx + (i + 1) * FONT_WIDTH <= pw; i++) { - const uint8_t* glyph = &font_data[(unsigned char)text[i] * FONT_HEIGHT]; - int cx = tx + i * FONT_WIDTH; - for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ph; fy++) { - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = cx + fx; - int dy = ty + fy; - if (dx >= 0 && dx < pw && dy >= 0 && dy < ph) - pixels[dy * pw + dx] = color_px; - } - } - } - } -} - -static void sysinfo_on_draw(Window* win, Framebuffer& fb) { - SysInfoState* si = (SysInfoState*)win->app_data; - if (!si) return; - - // Refresh uptime - si->uptime_ms = zenith::get_milliseconds(); - - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - uint32_t* pixels = win->content; - - // Fill background - uint32_t bg_px = colors::WINDOW_BG.to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; - - uint32_t text_px = colors::TEXT_COLOR.to_pixel(); - uint32_t accent_px = colors::ACCENT.to_pixel(); - int y = 16; - int x = 16; - char line[128]; - - // Title - draw_text_to_pixels(pixels, cw, ch, x, y, "System Information", accent_px); - y += FONT_HEIGHT + 12; - - // Separator - uint32_t sep_px = colors::BORDER.to_pixel(); - for (int sx = x; sx < cw - x && y < ch; sx++) - pixels[y * cw + sx] = sep_px; - y += 8; - - // OS Name - snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // OS Version - snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // API Version - snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // Max Processes - snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += 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); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 12; - - // Network section - draw_text_to_pixels(pixels, cw, ch, x, y, "Network", accent_px); - y += FONT_HEIGHT + 8; - - for (int sx = x; sx < cw - x && y < ch; sx++) - pixels[y * cw + sx] = sep_px; - 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)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // 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)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // 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)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // 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)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; - - // 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]); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); -} - -static void sysinfo_on_close(Window* win) { - if (win->app_data) { - zenith::mfree(win->app_data); - win->app_data = nullptr; - } -} - -// ============================================================================ -// Terminal Application -// ============================================================================ - -static void terminal_on_draw(Window* win, Framebuffer& fb) { - TerminalState* ts = (TerminalState*)win->app_data; - if (!ts) return; - - Rect cr = win->content_rect(); - terminal_render(ts, win->content, cr.w, cr.h); -} - -static void terminal_on_mouse(Window* win, MouseEvent& ev) { - // Terminal doesn't need mouse handling for now -} - -static void terminal_on_key(Window* win, const Zenith::KeyEvent& key) { - TerminalState* ts = (TerminalState*)win->app_data; - if (!ts) return; - terminal_handle_key(ts, key); -} - -static void terminal_on_close(Window* win) { - TerminalState* ts = (TerminalState*)win->app_data; - if (ts) { - if (ts->cells) zenith::mfree(ts->cells); - zenith::mfree(ts); - win->app_data = nullptr; - } -} - -// ============================================================================ -// Application Launchers -// ============================================================================ - -static DesktopState* g_desktop; - -static 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 cols = cr.w / FONT_WIDTH; - int rows = cr.h / FONT_HEIGHT; - - TerminalState* ts = (TerminalState*)zenith::malloc(sizeof(TerminalState)); - zenith::memset(ts, 0, sizeof(TerminalState)); - terminal_init(ts, cols, rows); - - win->app_data = ts; - win->on_draw = terminal_on_draw; - win->on_mouse = terminal_on_mouse; - win->on_key = terminal_on_key; - win->on_close = terminal_on_close; -} - -static void open_filemanager(DesktopState* ds) { - int idx = desktop_create_window(ds, "Files", 150, 120, 500, 400); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - FileManagerState* fm = (FileManagerState*)zenith::malloc(sizeof(FileManagerState)); - zenith::memset(fm, 0, sizeof(FileManagerState)); - zenith::strcpy(fm->current_path, "0:/"); - fm->selected = -1; - 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; -} - -static 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*)zenith::malloc(sizeof(SysInfoState)); - zenith::memset(si, 0, sizeof(SysInfoState)); - zenith::get_info(&si->sys_info); - zenith::get_netcfg(&si->net_cfg); - si->uptime_ms = zenith::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; -} - -// App menu click callbacks -static void on_click_terminal(void* ud) { - DesktopState* ds = (DesktopState*)ud; - open_terminal(ds); - ds->app_menu_open = false; -} - -static void on_click_filemanager(void* ud) { - DesktopState* ds = (DesktopState*)ud; - open_filemanager(ds); - ds->app_menu_open = false; -} - -static void on_click_sysinfo(void* ud) { - DesktopState* ds = (DesktopState*)ud; - open_sysinfo(ds); - ds->app_menu_open = false; -} +#include "apps_common.hpp" // ============================================================================ // Desktop Implementation // ============================================================================ +static const char* month_names[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + void gui::desktop_init(DesktopState* ds) { ds->screen_w = ds->fb.width(); ds->screen_h = ds->fb.height(); @@ -606,7 +31,7 @@ void gui::desktop_init(DesktopState* ds) { zenith::memset(&ds->mouse, 0, sizeof(Zenith::MouseState)); zenith::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1); - // Load SVG icons (filenames match those copied by scripts/copy_icons.sh) + // Load SVG icons ds->icon_terminal = svg_load("0:/icons/utilities-terminal-symbolic.svg", 20, 20, colors::ICON_COLOR); ds->icon_filemanager = svg_load("0:/icons/system-file-manager-symbolic.svg", 20, 20, colors::ICON_COLOR); ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps-symbolic.svg", 20, 20, colors::ICON_COLOR); @@ -614,9 +39,21 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_folder = svg_load("0:/icons/folder-symbolic.svg", 16, 16, Color::from_rgb(0xFF, 0xBD, 0x2E)); ds->icon_file = svg_load("0:/icons/text-x-generic-symbolic.svg", 16, 16, colors::ICON_COLOR); ds->icon_computer = svg_load("0:/icons/computer-symbolic.svg", 20, 20, colors::ICON_COLOR); + ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT); + ds->icon_calculator = svg_load("0:/icons/accessories-calculator-symbolic.svg", 20, 20, colors::ICON_COLOR); + ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor-symbolic.svg", 20, 20, colors::ICON_COLOR); + ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, colors::ICON_COLOR); + ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, colors::ICON_COLOR); + ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, colors::ICON_COLOR); + ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, colors::ICON_COLOR); + ds->icon_home = svg_load("0:/icons/user-home-symbolic.svg", 16, 16, colors::ICON_COLOR); + ds->icon_exec = svg_load("0:/icons/application-x-executable-symbolic.svg", 16, 16, Color::from_rgb(0x4E, 0x9A, 0x06)); + + ds->net_popup_open = false; + zenith::get_netcfg(&ds->cached_net_cfg); + ds->net_cfg_last_poll = zenith::get_milliseconds(); + ds->net_icon_rect = {0, 0, 0, 0}; - // Open initial terminal window - open_terminal(ds); } int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) { @@ -648,6 +85,7 @@ int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y win->on_mouse = nullptr; win->on_key = nullptr; win->on_close = nullptr; + win->on_poll = nullptr; win->app_data = nullptr; // Unfocus previous window @@ -779,8 +217,17 @@ void gui::desktop_draw_panel(DesktopState* ds) { Framebuffer& fb = ds->fb; int sw = ds->screen_w; - // Panel background - fb.fill_rect(0, 0, sw, PANEL_HEIGHT, colors::PANEL_BG); + // Panel gradient background (slightly lighter at top) + for (int y = 0; y < PANEL_HEIGHT; y++) { + int t = y * 255 / PANEL_HEIGHT; + uint8_t r = colors::PANEL_BG.r + (10 - t * 10 / 255); + uint8_t g = colors::PANEL_BG.g + (10 - t * 10 / 255); + uint8_t b = colors::PANEL_BG.b + (10 - t * 10 / 255); + fb.fill_rect(0, y, sw, 1, Color::from_rgb(r, g, b)); + } + + // Bottom highlight line + fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10)); // App menu button (left side) int btn_x = 4; @@ -788,13 +235,11 @@ void gui::desktop_draw_panel(DesktopState* ds) { int btn_w = 28; int btn_h = 28; - // Draw grid icon for app menu (3x3 dots) if (ds->icon_appmenu.pixels) { int ix = btn_x + (btn_w - ds->icon_appmenu.width) / 2; int iy = btn_y + (btn_h - ds->icon_appmenu.height) / 2; fb.blit_alpha(ix, iy, ds->icon_appmenu.width, ds->icon_appmenu.height, ds->icon_appmenu.pixels); } else { - // Fallback: draw 3x3 grid of small squares for (int gr = 0; gr < 3; gr++) { for (int gc = 0; gc < 3; gc++) { int dx = btn_x + 6 + gc * 6; @@ -804,7 +249,7 @@ void gui::desktop_draw_panel(DesktopState* ds) { } } - // Window indicator buttons (center area) + // Window indicator pills (center area) int indicator_x = 40; for (int i = 0; i < ds->window_count; i++) { Window* win = &ds->windows[i]; @@ -815,11 +260,17 @@ void gui::desktop_draw_panel(DesktopState* ds) { int iw = tw + pad * 2; if (iw > 150) iw = 150; - Color btn_bg = (i == ds->focused_window) - ? Color::from_rgba(0xFF, 0xFF, 0xFF, 0x30) - : Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10); + // Pre-blended indicator pill colors (opaque, blended against PANEL_BG) + Color pill_bg = (i == ds->focused_window) + ? colors::PANEL_INDICATOR_ACTIVE + : colors::PANEL_INDICATOR_INACTIVE; - fb.fill_rect_alpha(indicator_x, 4, iw, 24, btn_bg); + fill_rounded_rect(fb, indicator_x, 4, iw, 24, 6, pill_bg); + + // Active window accent underline bar + if (i == ds->focused_window) { + fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, colors::ACCENT); + } // Truncate title if too long char short_title[20]; @@ -832,50 +283,104 @@ void gui::desktop_draw_panel(DesktopState* ds) { indicator_x += iw + 4; } - // Clock (right side) + // Date + Clock (right side) Zenith::DateTime dt; zenith::gettime(&dt); + char clock_str[8]; snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute); int clock_w = text_width(clock_str); int clock_x = sw - clock_w - 12; int clock_y = (PANEL_HEIGHT - FONT_HEIGHT) / 2; draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT); + + // Date before clock + char date_str[12]; + int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0; + snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day); + int date_w = text_width(date_str); + int date_x = clock_x - date_w - 16; + draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT); + + // Network icon (to the left of the date) + uint64_t now = zenith::get_milliseconds(); + if (now - ds->net_cfg_last_poll > 5000) { + zenith::get_netcfg(&ds->cached_net_cfg); + ds->net_cfg_last_poll = now; + } + + int net_icon_x = date_x - 16 - 12; + int net_icon_y = (PANEL_HEIGHT - 16) / 2; + ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16}; + + if (ds->icon_network.pixels) { + if (ds->cached_net_cfg.ipAddress == 0) { + uint32_t* src = ds->icon_network.pixels; + int npx = 16 * 16; + uint32_t tinted[256]; + for (int p = 0; p < npx; p++) { + uint32_t px = src[p]; + uint8_t a = (px >> 24) & 0xFF; + tinted[p] = ((uint32_t)a << 24) | 0x004444CC; + } + fb.blit_alpha(net_icon_x, net_icon_y, 16, 16, tinted); + } else { + fb.blit_alpha(net_icon_x, net_icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels); + } + } } +// ============================================================================ +// App Menu (5 items with separator and rounded corners) +// ============================================================================ + +static constexpr int MENU_ITEM_COUNT = 6; +static constexpr int MENU_W = 220; +static constexpr int MENU_ITEM_H = 40; + static void desktop_draw_app_menu(DesktopState* ds) { Framebuffer& fb = ds->fb; int menu_x = 4; int menu_y = PANEL_HEIGHT + 2; - int menu_w = 200; - int item_h = 36; - int menu_h = item_h * 3 + 8; + int menu_h = MENU_ITEM_H * MENU_ITEM_COUNT + 10; // +10 for padding + separator // Menu shadow - draw_shadow(fb, menu_x, menu_y, menu_w, menu_h, 4, colors::SHADOW); + draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW); - // Menu background - fb.fill_rect(menu_x, menu_y, menu_w, menu_h, colors::MENU_BG); - draw_rect(fb, menu_x, menu_y, menu_w, menu_h, colors::BORDER); + // Menu background with rounded corners + fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG); + draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); // Menu items struct MenuItem { const char* label; SvgIcon* icon; }; - MenuItem items[3] = { + MenuItem items[MENU_ITEM_COUNT] = { { "Terminal", &ds->icon_terminal }, { "Files", &ds->icon_filemanager }, - { "System Info", &ds->icon_sysinfo } + { "System Info", &ds->icon_sysinfo }, + { "Calculator", &ds->icon_calculator }, + { "Text Editor", &ds->icon_texteditor }, + { "Kernel Log", &ds->icon_terminal }, }; int mx = ds->mouse.x; int my = ds->mouse.y; - for (int i = 0; i < 3; i++) { - int iy = menu_y + 4 + i * item_h; - Rect item_rect = {menu_x + 4, iy, menu_w - 8, item_h}; + for (int i = 0; i < MENU_ITEM_COUNT; i++) { + int iy = menu_y + 4 + i * MENU_ITEM_H; + + // Thin separator line after System Info (before utility apps) + if (i == 3) { + int sep_y = iy - 1; + for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) + fb.put_pixel(sx, sep_y, colors::BORDER); + iy += 1; + } + + Rect item_rect = {menu_x + 4, iy, MENU_W - 8, MENU_ITEM_H}; // Hover highlight if (item_rect.contains(mx, my)) { @@ -884,32 +389,106 @@ static void desktop_draw_app_menu(DesktopState* ds) { // Icon int icon_x = item_rect.x + 8; - int icon_y = item_rect.y + (item_h - 20) / 2; + int icon_y = item_rect.y + (MENU_ITEM_H - 20) / 2; if (items[i].icon && items[i].icon->pixels) { fb.blit_alpha(icon_x, icon_y, items[i].icon->width, items[i].icon->height, items[i].icon->pixels); } // Label int tx = icon_x + 28; - int ty = item_rect.y + (item_h - FONT_HEIGHT) / 2; + int ty = item_rect.y + (MENU_ITEM_H - FONT_HEIGHT) / 2; draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR); } } +static void desktop_draw_net_popup(DesktopState* ds) { + Framebuffer& fb = ds->fb; + + int popup_w = 220; + int popup_h = 130; + int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w; + int popup_y = PANEL_HEIGHT + 2; + if (popup_x < 4) popup_x = 4; + + draw_shadow(fb, popup_x, popup_y, popup_w, popup_h, 4, colors::SHADOW); + fb.fill_rect(popup_x, popup_y, popup_w, popup_h, colors::MENU_BG); + draw_rect(fb, popup_x, popup_y, popup_w, popup_h, colors::BORDER); + + int tx = popup_x + 12; + int ty = popup_y + 10; + int line_h = FONT_HEIGHT + 6; + char line[64]; + + Zenith::NetCfg& nc = ds->cached_net_cfg; + + if (nc.ipAddress != 0) { + char ipbuf[20]; + format_ip(ipbuf, nc.ipAddress); + snprintf(line, sizeof(line), "IP: %s", ipbuf); + } else { + snprintf(line, sizeof(line), "IP: Not connected"); + } + draw_text(fb, tx, ty, line, colors::TEXT_COLOR); + ty += line_h; + + char buf[20]; + format_ip(buf, nc.subnetMask); + snprintf(line, sizeof(line), "Subnet: %s", buf); + draw_text(fb, tx, ty, line, colors::TEXT_COLOR); + ty += line_h; + + format_ip(buf, nc.gateway); + snprintf(line, sizeof(line), "Gateway: %s", buf); + draw_text(fb, tx, ty, line, colors::TEXT_COLOR); + ty += line_h; + + format_ip(buf, nc.dnsServer); + snprintf(line, sizeof(line), "DNS: %s", buf); + draw_text(fb, tx, ty, line, colors::TEXT_COLOR); + ty += line_h; + + format_mac(buf, nc.macAddress); + snprintf(line, sizeof(line), "MAC: %s", buf); + draw_text(fb, tx, ty, line, colors::TEXT_COLOR); +} + void gui::desktop_compose(DesktopState* ds) { Framebuffer& fb = ds->fb; - // Desktop background - fb.clear(colors::DESKTOP_BG); + // Desktop background gradient + int sw = ds->screen_w; + int sh = ds->screen_h; + int grad_start = PANEL_HEIGHT; + int grad_range = sh - grad_start; + if (grad_range < 1) grad_range = 1; - // Draw windows from bottom to top (index 0 = bottom) + uint32_t* buf = fb.buffer(); + int pitch = fb.pitch(); + + for (int y = 0; y < sh; y++) { + uint32_t* row = (uint32_t*)((uint8_t*)buf + y * pitch); + if (y < grad_start) { + // Panel area - will be overwritten by panel drawing + uint32_t px = colors::PANEL_BG.to_pixel(); + for (int x = 0; x < sw; x++) row[x] = px; + } else { + int t = y - grad_start; + uint8_t r = 0xD0 - (0xD0 - 0xA0) * t / grad_range; + uint8_t g = 0xD8 - (0xD8 - 0xA8) * t / grad_range; + uint8_t b = 0xE8 - (0xE8 - 0xB8) * t / grad_range; + uint32_t px = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + for (int x = 0; x < sw; x++) row[x] = px; + } + } + + // Draw windows from bottom to top for (int i = 0; i < ds->window_count; i++) { if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) { desktop_draw_window(ds, i); } } - // Draw panel on top of everything + // Draw panel on top desktop_draw_panel(ds); // Draw app menu if open @@ -917,7 +496,12 @@ void gui::desktop_compose(DesktopState* ds) { desktop_draw_app_menu(ds); } - // Draw cursor last (on top of everything) + // Draw network popup if open + if (ds->net_popup_open) { + desktop_draw_net_popup(ds); + } + + // Draw cursor last draw_cursor(fb, ds->mouse.x, ds->mouse.y); } @@ -930,7 +514,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) { bool left_held = (buttons & 0x01); bool left_released = !(buttons & 0x01) && (prev & 0x01); - // Build mouse event MouseEvent ev; ev.x = mx; ev.y = my; @@ -945,7 +528,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (left_held) { win->frame.x = mx - win->drag_offset_x; win->frame.y = my - win->drag_offset_y; - // Clamp to screen if (win->frame.x < -win->frame.w + 50) win->frame.x = -win->frame.w + 50; if (win->frame.y < 0) win->frame.y = 0; if (win->frame.x > ds->screen_w - 50) win->frame.x = ds->screen_w - 50; @@ -962,26 +544,42 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (ds->app_menu_open && left_pressed) { int menu_x = 4; int menu_y = PANEL_HEIGHT + 2; - int menu_w = 200; - int item_h = 36; - int menu_h = item_h * 3 + 8; - Rect menu_rect = {menu_x, menu_y, menu_w, menu_h}; + int menu_h = MENU_ITEM_H * MENU_ITEM_COUNT + 10; + Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h}; if (menu_rect.contains(mx, my)) { - // Which item was clicked? int rel_y = my - menu_y - 4; - int item_idx = rel_y / item_h; - if (item_idx >= 0 && item_idx < 3) { + int item_idx = rel_y / MENU_ITEM_H; + if (item_idx >= 0 && item_idx < MENU_ITEM_COUNT) { switch (item_idx) { - case 0: on_click_terminal(ds); break; - case 1: on_click_filemanager(ds); break; - case 2: on_click_sysinfo(ds); break; + case 0: open_terminal(ds); break; + case 1: open_filemanager(ds); break; + case 2: open_sysinfo(ds); break; + case 3: open_calculator(ds); break; + case 4: open_texteditor(ds); break; + case 5: open_klog(ds); break; } + ds->app_menu_open = false; } return; } else { ds->app_menu_open = false; - // Fall through to handle click on other things + } + } + + // Handle net popup clicks + if (ds->net_popup_open && left_pressed) { + int popup_w = 220; + int popup_h = 130; + int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w; + int popup_y = PANEL_HEIGHT + 2; + if (popup_x < 4) popup_x = 4; + Rect popup_rect = {popup_x, popup_y, popup_w, popup_h}; + + if (popup_rect.contains(mx, my)) { + return; + } else if (!ds->net_icon_rect.contains(mx, my)) { + ds->net_popup_open = false; } } @@ -990,6 +588,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) { // App menu button if (mx < 36) { ds->app_menu_open = !ds->app_menu_open; + ds->net_popup_open = false; + return; + } + + // Network icon + if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) { + ds->net_popup_open = !ds->net_popup_open; + ds->app_menu_open = false; return; } @@ -1036,7 +642,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->state = WIN_MINIMIZED; if (ds->focused_window == i) { ds->focused_window = -1; - // Focus next visible window for (int j = ds->window_count - 1; j >= 0; j--) { if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) { ds->focused_window = j; @@ -1052,16 +657,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) { Rect max_r = win->max_btn_rect(); if (max_r.contains(mx, my)) { if (win->state == WIN_MAXIMIZED) { - // Restore win->frame = win->saved_frame; win->state = WIN_NORMAL; } else { - // Maximize win->saved_frame = win->frame; win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT}; win->state = WIN_MAXIMIZED; } - // Reallocate content buffer for new size Rect cr = win->content_rect(); if (cr.w != win->content_w || cr.h != win->content_h) { if (win->content) zenith::free(win->content); @@ -1080,9 +682,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->dragging = true; win->drag_offset_x = mx - win->frame.x; win->drag_offset_y = my - win->frame.y; - desktop_raise_window(ds, ds->window_count - 1 == i ? i : i); - // After raise, this window is at the end - // Need to update the reference since raise moved it + desktop_raise_window(ds, i); int new_idx = ds->window_count - 1; ds->windows[new_idx].dragging = true; ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x; @@ -1094,7 +694,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) { Rect cr = win->content_rect(); if (cr.contains(mx, my)) { desktop_raise_window(ds, i); - // Dispatch mouse event to app int new_idx = ds->window_count - 1; if (ds->windows[new_idx].on_mouse) { ev.x = mx; @@ -1104,14 +703,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) { return; } - // Check full frame (catches border clicks) + // Check full frame if (win->frame.contains(mx, my)) { desktop_raise_window(ds, i); return; } } - // Clicked on desktop background - close app menu ds->app_menu_open = false; } @@ -1142,6 +740,18 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key) open_sysinfo(ds); return; } + if (key.ascii == 'c' || key.ascii == 'C') { + open_calculator(ds); + return; + } + if (key.ascii == 'e' || key.ascii == 'E') { + open_texteditor(ds); + return; + } + if (key.ascii == 'k' || key.ascii == 'K') { + open_klog(ds); + return; + } } // Dispatch to focused window @@ -1166,13 +776,12 @@ void gui::desktop_run(DesktopState* ds) { desktop_handle_keyboard(ds, key); } - // Poll terminal I/O for all terminal windows + // Poll windows that have a poll callback for (int i = 0; i < ds->window_count; i++) { Window* win = &ds->windows[i]; if (win->state == WIN_CLOSED) continue; - if (win->app_data && win->on_draw == terminal_on_draw) { - TerminalState* ts = (TerminalState*)win->app_data; - terminal_poll(ts); + if (win->on_poll) { + win->on_poll(win); } } @@ -1192,6 +801,8 @@ void gui::desktop_run(DesktopState* ds) { // Entry Point // ============================================================================ +static DesktopState* g_desktop; + extern "C" void _start() { DesktopState* ds = (DesktopState*)zenith::malloc(sizeof(DesktopState)); zenith::memset(ds, 0, sizeof(DesktopState)); diff --git a/programs/src/desktop/obj/app_calculator.o b/programs/src/desktop/obj/app_calculator.o new file mode 100644 index 0000000..f9c3a6d Binary files /dev/null and b/programs/src/desktop/obj/app_calculator.o differ diff --git a/programs/src/desktop/obj/app_filemanager.o b/programs/src/desktop/obj/app_filemanager.o new file mode 100644 index 0000000..c527da9 Binary files /dev/null and b/programs/src/desktop/obj/app_filemanager.o differ diff --git a/programs/src/desktop/obj/app_klog.o b/programs/src/desktop/obj/app_klog.o new file mode 100644 index 0000000..a2bdd2a Binary files /dev/null and b/programs/src/desktop/obj/app_klog.o differ diff --git a/programs/src/desktop/obj/app_sysinfo.o b/programs/src/desktop/obj/app_sysinfo.o new file mode 100644 index 0000000..ecb9f13 Binary files /dev/null and b/programs/src/desktop/obj/app_sysinfo.o differ diff --git a/programs/src/desktop/obj/app_terminal.o b/programs/src/desktop/obj/app_terminal.o new file mode 100644 index 0000000..35b82af Binary files /dev/null and b/programs/src/desktop/obj/app_terminal.o differ diff --git a/programs/src/desktop/obj/app_texteditor.o b/programs/src/desktop/obj/app_texteditor.o new file mode 100644 index 0000000..705108a Binary files /dev/null and b/programs/src/desktop/obj/app_texteditor.o differ diff --git a/programs/src/desktop/obj/font_data.o b/programs/src/desktop/obj/font_data.o index ab24d2b..adf6a6f 100644 Binary files a/programs/src/desktop/obj/font_data.o and b/programs/src/desktop/obj/font_data.o differ diff --git a/programs/src/desktop/obj/main.o b/programs/src/desktop/obj/main.o index 8f199c8..bb23631 100644 Binary files a/programs/src/desktop/obj/main.o and b/programs/src/desktop/obj/main.o differ diff --git a/ramdisk.tar b/ramdisk.tar index 6a5e8fe..32e4751 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index bb35f1d..887ae67 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -19,9 +19,16 @@ ICONS=( "actions/symbolic/window-close-symbolic.svg" "actions/symbolic/window-maximize-symbolic.svg" "actions/symbolic/window-minimize-symbolic.svg" + "actions/symbolic/go-up-symbolic.svg" + "actions/symbolic/go-previous-symbolic.svg" + "actions/symbolic/go-next-symbolic.svg" + "actions/symbolic/go-home-symbolic.svg" + "actions/symbolic/document-save-symbolic.svg" "apps/symbolic/utilities-terminal-symbolic.svg" "apps/symbolic/system-file-manager-symbolic.svg" "apps/symbolic/preferences-desktop-apps-symbolic.svg" + "apps/symbolic/accessories-calculator-symbolic.svg" + "apps/symbolic/accessories-text-editor-symbolic.svg" "places/symbolic/folder-symbolic.svg" "places/symbolic/folder-documents-symbolic.svg" "places/symbolic/user-home-symbolic.svg"