feat: desktop environment improvements

This commit is contained in:
2026-02-20 09:03:30 +01:00
parent 6eebc64863
commit 359ef3ba53
24 changed files with 2479 additions and 644 deletions
+14
View File
@@ -36,6 +36,20 @@ struct DesktopState {
SvgIcon icon_folder; SvgIcon icon_folder;
SvgIcon icon_file; SvgIcon icon_file;
SvgIcon icon_computer; 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; int screen_w, screen_h;
}; };
+2
View File
@@ -58,6 +58,8 @@ namespace colors {
static constexpr Color MENU_HOVER = {0xE8, 0xF0, 0xFE, 0xFF}; static constexpr Color MENU_HOVER = {0xE8, 0xF0, 0xFE, 0xFF};
static constexpr Color TERM_BG = {0x2D, 0x2D, 0x2D, 0xFF}; static constexpr Color TERM_BG = {0x2D, 0x2D, 0x2D, 0xFF};
static constexpr Color TERM_FG = {0xCC, 0xCC, 0xCC, 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 { struct Point {
+9 -2
View File
@@ -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->cols = cols;
t->rows = rows; t->rows = rows;
t->cursor_x = 0; t->cursor_x = 0;
@@ -91,13 +92,14 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
t->total_rows = rows; t->total_rows = rows;
t->current_fg = colors::TERM_FG; t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG; t->current_bg = colors::TERM_BG;
t->cursor_visible = true; t->cursor_visible = false;
t->alt_screen_active = false; t->alt_screen_active = false;
t->reverse_video = false; t->reverse_video = false;
t->parse_state = TerminalState::STATE_NORMAL; t->parse_state = TerminalState::STATE_NORMAL;
t->csi_private = false; t->csi_private = false;
t->csi_param_count = 0; t->csi_param_count = 0;
t->csi_current_param = 0; t->csi_current_param = 0;
t->child_pid = 0;
int total_cells = cols * rows; int total_cells = cols * rows;
t->cells = (TermCell*)zenith::alloc(total_cells * sizeof(TermCell)); 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->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
t->alt_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"); t->child_pid = zenith::spawn_redir("0:/os/shell.elf");
zenith::childio_settermsz(t->child_pid, cols, rows); zenith::childio_settermsz(t->child_pid, cols, rows);
+2
View File
@@ -25,6 +25,7 @@ using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb);
using WindowMouseCallback = void (*)(Window* win, MouseEvent& ev); using WindowMouseCallback = void (*)(Window* win, MouseEvent& ev);
using WindowKeyCallback = void (*)(Window* win, const Zenith::KeyEvent& key); using WindowKeyCallback = void (*)(Window* win, const Zenith::KeyEvent& key);
using WindowCloseCallback = void (*)(Window* win); using WindowCloseCallback = void (*)(Window* win);
using WindowPollCallback = void (*)(Window* win);
struct Window { struct Window {
char title[MAX_TITLE_LEN]; char title[MAX_TITLE_LEN];
@@ -48,6 +49,7 @@ struct Window {
WindowMouseCallback on_mouse; WindowMouseCallback on_mouse;
WindowKeyCallback on_key; WindowKeyCallback on_key;
WindowCloseCallback on_close; WindowCloseCallback on_close;
WindowPollCallback on_poll;
void* app_data; void* app_data;
Rect titlebar_rect() const { Rect titlebar_rect() const {
+5
View File
@@ -262,6 +262,11 @@ namespace zenith {
syscall2(Zenith::SYS_SETMOUSEBOUNDS, (uint64_t)maxX, (uint64_t)maxY); 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 // I/O redirection
inline int spawn_redir(const char* path, const char* args = nullptr) { inline int spawn_redir(const char* path, const char* args = nullptr) {
return (int)syscall2(Zenith::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args); return (int)syscall2(Zenith::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args);
+1 -1
View File
@@ -63,7 +63,7 @@ LDFLAGS := \
# ---- Source files ---- # ---- 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)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ---- # ---- Target ----
+409
View File
@@ -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;
}
+579
View File
@@ -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;
}
+151
View File
@@ -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;
}
+154
View File
@@ -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;
}
+68
View File
@@ -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;
}
+576
View File
@@ -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;
}
+250
View File
@@ -0,0 +1,250 @@
/*
* apps_common.hpp
* Shared inline utilities and forward declarations for desktop apps
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/framebuffer.hpp>
#include <gui/font.hpp>
#include <gui/draw.hpp>
#include <gui/svg.hpp>
#include <gui/widgets.hpp>
#include <gui/window.hpp>
#include <gui/terminal.hpp>
#include <gui/desktop.hpp>
// Placement new for freestanding environment
inline void* operator new(unsigned long, void* p) { return p; }
using namespace gui;
// ============================================================================
// Minimal snprintf
// ============================================================================
using va_list = __builtin_va_list;
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
struct PfState { char* buf; int pos; int max; };
inline void pf_putc(PfState* st, char c) {
if (st->pos < st->max) st->buf[st->pos] = c;
st->pos++;
}
inline void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) {
char tmp[24]; int i = 0;
const char* digits = "0123456789abcdef";
if (val == 0) { tmp[i++] = '0'; }
else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } }
int total = (neg ? 1 : 0) + i;
if (neg && pad == '0') pf_putc(st, '-');
for (int w = total; w < width; w++) pf_putc(st, pad);
if (neg && pad != '0') pf_putc(st, '-');
while (i > 0) pf_putc(st, tmp[--i]);
}
inline int snprintf(char* buf, int size, const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0;
while (*fmt) {
if (*fmt != '%') { pf_putc(&st, *fmt++); continue; }
fmt++;
char pad = ' ';
if (*fmt == '0') { pad = '0'; fmt++; }
int width = 0;
while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; }
if (*fmt == 'l') fmt++;
switch (*fmt) {
case 'd': case 'i': {
long val = va_arg(ap, int);
int neg = 0; unsigned long uval;
if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val;
pf_putnum(&st, uval, 10, width, pad, neg); break;
}
case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; }
case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; }
case 's': {
const char* s = va_arg(ap, const char*); if (!s) s = "(null)";
int slen = 0; while (s[slen]) slen++;
for (int w = slen; w < width; w++) pf_putc(&st, ' ');
for (int j = 0; j < slen; j++) pf_putc(&st, s[j]);
break;
}
case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; }
case '%': pf_putc(&st, '%'); break;
default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break;
}
if (*fmt) fmt++;
}
if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; }
va_end(ap); return st.pos;
}
// ============================================================================
// String helpers
// ============================================================================
inline void str_append(char* dst, const char* src, int max) {
int len = 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);
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+7
View File
@@ -19,9 +19,16 @@ ICONS=(
"actions/symbolic/window-close-symbolic.svg" "actions/symbolic/window-close-symbolic.svg"
"actions/symbolic/window-maximize-symbolic.svg" "actions/symbolic/window-maximize-symbolic.svg"
"actions/symbolic/window-minimize-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/utilities-terminal-symbolic.svg"
"apps/symbolic/system-file-manager-symbolic.svg" "apps/symbolic/system-file-manager-symbolic.svg"
"apps/symbolic/preferences-desktop-apps-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-symbolic.svg"
"places/symbolic/folder-documents-symbolic.svg" "places/symbolic/folder-documents-symbolic.svg"
"places/symbolic/user-home-symbolic.svg" "places/symbolic/user-home-symbolic.svg"