feat: Fix USB mouse support, GUI wkipedia app, icon refresh, file manager improvements, more

This commit is contained in:
2026-02-20 11:33:06 +01:00
parent 359ef3ba53
commit 4c6b783832
21 changed files with 1513 additions and 160 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ LDFLAGS := \
# ---- Source files ----
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.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 app_wiki.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
+281 -111
View File
@@ -26,6 +26,7 @@ struct FileManagerState {
uint64_t last_click_time;
Scrollbar scrollbar;
DesktopState* desktop;
bool grid_view;
};
static constexpr int FM_TOOLBAR_H = 32;
@@ -33,6 +34,10 @@ static constexpr int FM_PATHBAR_H = 24;
static constexpr int FM_HEADER_H = 20;
static constexpr int FM_ITEM_H = 24;
static constexpr int FM_SCROLLBAR_W = 12;
static constexpr int FM_GRID_CELL_W = 80;
static constexpr int FM_GRID_CELL_H = 80;
static constexpr int FM_GRID_ICON = 48;
static constexpr int FM_GRID_PAD = 4;
// ============================================================================
// File type detection
@@ -287,6 +292,32 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
}
}
// Toggle view button (5th toolbar button)
{
int bx = 120, by = 4;
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;
uint32_t glyph_px = colors::TEXT_COLOR.to_pixel();
if (fm->grid_view) {
// Draw 4 small squares to indicate grid mode
for (int r = 0; r < 2; r++)
for (int c = 0; c < 2; c++)
for (int dy = 0; dy < 6; dy++)
for (int dx = 0; dx < 6; dx++)
if (by + 5 + r * 8 + dy < ch && bx + 5 + c * 8 + dx < cw)
pixels[(by + 5 + r * 8 + dy) * cw + (bx + 5 + c * 8 + dx)] = glyph_px;
} else {
// Draw 3 horizontal lines to indicate list mode
for (int r = 0; r < 3; r++)
for (int dx = 0; dx < 14; dx++)
for (int dy = 0; dy < 2; dy++)
if (by + 5 + r * 5 + dy < ch && bx + 5 + dx < cw)
pixels[(by + 5 + r * 5 + dy) * cw + (bx + 5 + dx)] = glyph_px;
}
}
// Toolbar separator
int sep_y = FM_TOOLBAR_H - 1;
if (sep_y < ch) {
@@ -310,100 +341,171 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
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;
if (fm->grid_view) {
// ---- Grid View ----
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
int list_h = ch - list_y;
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
int rows = (fm->entry_count + cols - 1) / cols;
int content_h = rows * FM_GRID_CELL_H;
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;
// 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;
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);
for (int i = 0; i < fm->entry_count; i++) {
int col = i % cols;
int row = i / cols;
int cell_x = col * FM_GRID_CELL_W;
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
// 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;
}
// Skip if entirely off-screen
if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= ch) continue;
// 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;
}
// Selection highlight
if (i == fm->selected) {
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
for (int y = gui_max(cell_y, list_y); y < gui_min(cell_y + FM_GRID_CELL_H, ch); y++)
for (int x = cell_x; x < gui_min(cell_x + FM_GRID_CELL_W, cw - FM_SCROLLBAR_W); x++)
pixels[y * cw + x] = sel_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;
// Large icon centered horizontally
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
int icon_y = cell_y + FM_GRID_PAD;
if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder_lg);
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec_lg);
} else if (ds && ds->icon_file_lg.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file_lg);
} 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 < FM_GRID_ICON && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
for (int dx = 0; dx < FM_GRID_ICON && icon_x + dx < cw; dx++)
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
}
// 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;
// Filename centered below icon, truncated if needed
char label[16];
int nlen = zenith::slen(fm->entry_names[i]);
if (nlen > 9) {
for (int k = 0; k < 9; k++) label[k] = fm->entry_names[i][k];
label[9] = '.';
label[10] = '.';
label[11] = '\0';
} else {
zenith::strncpy(label, fm->entry_names[i], 15);
}
int tw = text_width(label);
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
if (tx < cell_x) tx = cell_x;
int ty = icon_y + FM_GRID_ICON + 2;
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
draw_text_to_pixels(pixels, cw, ch, tx, ty, label, text_px);
}
} else {
// ---- List View ----
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
// ---- 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;
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;
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;
// 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;
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;
}
// 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;
// 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;
}
// 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);
// ---- 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;
// 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);
}
// 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;
// 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);
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);
}
}
}
@@ -457,42 +559,86 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
else if (local_x >= 32 && local_x < 56) filemanager_go_forward(fm);
else if (local_x >= 60 && local_x < 84) filemanager_go_up(fm);
else if (local_x >= 88 && local_x < 112) filemanager_go_home(fm);
else if (local_x >= 120 && local_x < 144) {
fm->grid_view = !fm->grid_view;
fm->scrollbar.scroll_offset = 0;
}
return;
}
// File 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;
// File clicks (grid vs list)
if (fm->grid_view) {
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
int col = local_x / FM_GRID_CELL_W;
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
int clicked_idx = row * cols + col;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = zenith::get_milliseconds();
if (clicked_idx >= 0 && clicked_idx < fm->entry_count && col < cols) {
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]);
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
char fullpath[512];
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 {
// 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->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
}
}
} else {
// List view clicks
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
int clicked_idx = rel_y / FM_ITEM_H;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = 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;
}
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;
}
}
}
@@ -500,9 +646,12 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
// Scroll handling
if (ev.scroll != 0) {
int list_y_start = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int list_y_start = fm->grid_view
? FM_TOOLBAR_H + FM_PATHBAR_H
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
if (local_y >= list_y_start) {
fm->scrollbar.scroll_offset -= ev.scroll * FM_ITEM_H;
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
int ms = fm->scrollbar.max_scroll();
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
if (fm->scrollbar.scroll_offset > ms) fm->scrollbar.scroll_offset = ms;
@@ -522,9 +671,29 @@ static void filemanager_on_key(Window* win, const Zenith::KeyEvent& key) {
filemanager_go_up(fm);
} else if (key.scancode == 0x48) {
// Up arrow
if (fm->selected > 0) fm->selected--;
if (fm->grid_view) {
Rect cr = win->content_rect();
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected >= cols) fm->selected -= cols;
} else {
if (fm->selected > 0) fm->selected--;
}
} else if (key.scancode == 0x50) {
// Down arrow
if (fm->grid_view) {
Rect cr = win->content_rect();
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
} else {
if (fm->selected < fm->entry_count - 1) fm->selected++;
}
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
// Left arrow (grid view only)
if (fm->selected > 0) fm->selected--;
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
// Right arrow (grid view only)
if (fm->selected < fm->entry_count - 1) fm->selected++;
} else if (key.ascii == '\n' || key.ascii == '\r') {
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
@@ -565,6 +734,7 @@ void open_filemanager(DesktopState* ds) {
fm->history_pos = -1;
fm->history_count = 0;
fm->desktop = ds;
fm->grid_view = true;
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
+625
View File
@@ -0,0 +1,625 @@
/*
* app_wiki.cpp
* ZenithOS Desktop - Wikipedia client application
* Spawns wiki.elf -d as a child process for non-blocking network I/O
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Constants
// ============================================================================
static constexpr int WIKI_RESP_MAX = 131072; // 128 KB
static constexpr int WIKI_MAX_LINES = 4096;
static constexpr int WIKI_TOOLBAR_H = 36;
static constexpr int WIKI_SCROLLBAR_W = 12;
// ============================================================================
// Wiki state
// ============================================================================
struct WikiDisplayLine {
char text[256];
uint32_t color;
};
struct WikiState {
DesktopState* ds;
enum Mode { IDLE, FETCHING, DONE, WIKI_ERROR };
Mode mode;
char searchQuery[256];
WikiDisplayLine* lines;
int lineCount;
int lineCap;
int scrollY; // scroll offset in lines
// Child process for non-blocking fetch
int child_pid;
char* respBuf; // accumulated child output
int respPos; // current position in respBuf
char statusMsg[128];
};
// ============================================================================
// JSON string extraction (for parsing child output)
// ============================================================================
static int slen(const char* s) { int n = 0; while (s[n]) n++; return n; }
static bool memeq(const char* a, const char* b, int n) {
for (int i = 0; i < n; i++) if (a[i] != b[i]) return false;
return true;
}
static int wiki_extract_json_string(const char* buf, int len, const char* key,
char* out, int maxOut) {
int klen = slen(key);
for (int i = 0; i < len - klen - 3; i++) {
if (buf[i] != '"') continue;
if (!memeq(buf + i + 1, key, klen)) continue;
if (buf[i + 1 + klen] != '"') continue;
if (buf[i + 2 + klen] != ':') continue;
int p = i + 3 + klen;
while (p < len && (buf[p] == ' ' || buf[p] == '\t')) p++;
if (p >= len || buf[p] != '"') continue;
p++;
int j = 0;
while (p < len && j < maxOut - 4) {
if (buf[p] == '"') break;
if (buf[p] == '\\' && p + 1 < len) {
p++;
switch (buf[p]) {
case '"': out[j++] = '"'; break;
case '\\': out[j++] = '\\'; break;
case 'n': out[j++] = '\n'; break;
case 'r': break;
case 't': out[j++] = '\t'; break;
case '/': out[j++] = '/'; break;
case 'u': {
if (p + 4 < len) {
unsigned val = 0;
for (int k = 1; k <= 4; k++) {
char h = buf[p + k];
val <<= 4;
if (h >= '0' && h <= '9') val |= h - '0';
else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
}
p += 4;
if (val < 128) out[j++] = (char)val;
else if (val == 0x2013 || val == 0x2014) out[j++] = '-';
else if (val == 0x2018 || val == 0x2019) out[j++] = '\'';
else if (val == 0x201C || val == 0x201D) out[j++] = '"';
else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; }
else out[j++] = '?';
}
break;
}
default: out[j++] = buf[p]; break;
}
} else {
out[j++] = buf[p];
}
p++;
}
out[j] = '\0';
return j;
}
out[0] = '\0';
return 0;
}
// ============================================================================
// Display line building (word-wrap adapted for pixel widths)
// ============================================================================
static void wiki_add_line(WikiState* ws, const char* text, int len, uint32_t color) {
if (ws->lineCount >= ws->lineCap) return;
WikiDisplayLine* dl = &ws->lines[ws->lineCount];
int copyLen = len;
if (copyLen > 255) copyLen = 255;
for (int i = 0; i < copyLen; i++) dl->text[i] = text[i];
dl->text[copyLen] = '\0';
dl->color = color;
ws->lineCount++;
}
static void wiki_wrap_text(WikiState* ws, const char* text, int textLen,
int maxChars, uint32_t color) {
if (textLen <= 0 || maxChars <= 0) return;
const char* p = text;
const char* end = text + textLen;
while (p < end && ws->lineCount < ws->lineCap) {
while (p < end && *p == ' ') p++;
if (p >= end) break;
const char* lineStart = p;
const char* lastSpace = nullptr;
int col = 0;
while (p < end && col < maxChars) {
if (*p == ' ') lastSpace = p;
p++;
col++;
}
if (p >= end) {
wiki_add_line(ws, lineStart, (int)(p - lineStart), color);
} else if (lastSpace && lastSpace > lineStart) {
wiki_add_line(ws, lineStart, (int)(lastSpace - lineStart), color);
p = lastSpace + 1;
} else {
wiki_add_line(ws, lineStart, (int)(p - lineStart), color);
}
}
}
static void wiki_build_display(WikiState* ws, const char* title,
const char* extract, int extractLen, int contentW) {
ws->lineCount = 0;
ws->scrollY = 0;
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / FONT_WIDTH;
if (maxChars < 20) maxChars = 20;
uint32_t accent_px = colors::ACCENT.to_pixel();
uint32_t green_px = Color::from_rgb(0x2E, 0x7D, 0x32).to_pixel();
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
// Title
if (title && title[0]) {
wiki_wrap_text(ws, title, slen(title), maxChars, accent_px);
}
// Blank separator
if (ws->lineCount > 0)
wiki_add_line(ws, "", 0, text_px);
// Process extract line by line
const char* p = extract;
const char* end = extract + extractLen;
while (p < end && ws->lineCount < ws->lineCap) {
const char* lineStart = p;
while (p < end && *p != '\n') p++;
int lineLen = (int)(p - lineStart);
if (p < end) p++;
if (lineLen == 0) {
wiki_add_line(ws, "", 0, text_px);
continue;
}
// Section header detection (== Title ==)
if (lineLen >= 4 && lineStart[0] == '=' && lineStart[1] == '=') {
int si = 0;
while (si < lineLen && lineStart[si] == '=') si++;
while (si < lineLen && lineStart[si] == ' ') si++;
int ei = lineLen;
while (ei > si && lineStart[ei - 1] == '=') ei--;
while (ei > si && lineStart[ei - 1] == ' ') ei--;
wiki_add_line(ws, "", 0, text_px);
if (ei > si) {
char secBuf[256];
int secLen = ei - si;
if (secLen > 255) secLen = 255;
for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i];
secBuf[secLen] = '\0';
wiki_add_line(ws, secBuf, secLen, green_px);
}
continue;
}
// Regular text
wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_px);
}
}
// ============================================================================
// Process completed response from child
// ============================================================================
static void wiki_process_response(WikiState* ws) {
const char* body = ws->respBuf;
int bodyLen = ws->respPos;
if (bodyLen <= 0) {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: no response from Wikipedia");
ws->mode = WikiState::WIKI_ERROR;
return;
}
// Check for error sentinel from child
if (bodyLen >= 1 && body[0] == '\x01') {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Article not found: %s", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
return;
}
static char title[512], extractBuf[131072];
wiki_extract_json_string(body, bodyLen, "title", title, sizeof(title));
int extractLen = wiki_extract_json_string(body, bodyLen, "extract",
extractBuf, sizeof(extractBuf) - 1);
if (extractLen == 0) {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "No content found for: %s", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
return;
}
// Find content width from the window
int contentW = 580;
for (int i = 0; i < ws->ds->window_count; i++) {
Window* w = &ws->ds->windows[i];
if (w->app_data == ws) {
Rect cr = w->content_rect();
contentW = cr.w;
break;
}
}
wiki_build_display(ws, title, extractBuf, extractLen, contentW);
ws->mode = WikiState::DONE;
}
// ============================================================================
// Callbacks
// ============================================================================
static void wiki_on_draw(Window* win, Framebuffer& fb) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
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;
// ---- Toolbar background ----
uint32_t toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel();
for (int y = 0; y < WIKI_TOOLBAR_H && y < ch; y++)
for (int x = 0; x < cw; x++)
pixels[y * cw + x] = toolbar_bg;
// Toolbar separator line
uint32_t sep_px = colors::BORDER.to_pixel();
if (WIKI_TOOLBAR_H < ch)
for (int x = 0; x < cw; x++)
pixels[WIKI_TOOLBAR_H * cw + x] = sep_px;
// ---- Draw search box outline ----
int tb_x = 8;
int tb_y = 6;
int tb_w = cw - 90;
int tb_h = 24;
if (tb_w < 100) tb_w = 100;
// TextBox background
uint32_t white_px = colors::WHITE.to_pixel();
uint32_t border_px = colors::BORDER.to_pixel();
for (int y = tb_y; y < tb_y + tb_h && y < ch; y++)
for (int x = tb_x; x < tb_x + tb_w && x < cw; x++)
pixels[y * cw + x] = white_px;
// Border
for (int x = tb_x; x < tb_x + tb_w && x < cw; x++) {
if (tb_y < ch) pixels[tb_y * cw + x] = border_px;
if (tb_y + tb_h - 1 < ch) pixels[(tb_y + tb_h - 1) * cw + x] = border_px;
}
for (int y = tb_y; y < tb_y + tb_h && y < ch; y++) {
if (tb_x < cw) pixels[y * cw + tb_x] = border_px;
if (tb_x + tb_w - 1 < cw) pixels[y * cw + (tb_x + tb_w - 1)] = border_px;
}
// Search text
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
draw_text_to_pixels(pixels, cw, ch, tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2,
ws->searchQuery, text_px);
// ---- Search button ----
int btn_x = tb_x + tb_w + 6;
int btn_y = tb_y;
int btn_w = 66;
int btn_h = tb_h;
uint32_t accent_px = colors::ACCENT.to_pixel();
for (int y = btn_y; y < btn_y + btn_h && y < ch; y++)
for (int x = btn_x; x < btn_x + btn_w && x < cw; x++)
pixels[y * cw + x] = accent_px;
// Button text
uint32_t white_text = colors::WHITE.to_pixel();
const char* btnLabel = "Search";
int labelW = zenith::slen(btnLabel) * FONT_WIDTH;
draw_text_to_pixels(pixels, cw, ch,
btn_x + (btn_w - labelW) / 2,
btn_y + (btn_h - FONT_HEIGHT) / 2,
btnLabel, white_text);
// ---- Content area ----
int content_y = WIKI_TOOLBAR_H + 1;
int content_h = ch - content_y;
int visibleLines = content_h / (FONT_HEIGHT + 4);
if (visibleLines < 1) visibleLines = 1;
if (ws->mode == WikiState::FETCHING) {
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
"Loading...", Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
} else if (ws->mode == WikiState::WIKI_ERROR) {
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
ws->statusMsg, colors::CLOSE_BTN.to_pixel());
} else if (ws->mode == WikiState::IDLE) {
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
"Type a topic and press Enter or click Search.",
Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
int y = content_y + 8;
int lineH = FONT_HEIGHT + 4;
for (int i = ws->scrollY; i < ws->lineCount && y + FONT_HEIGHT < ch; i++) {
WikiDisplayLine* dl = &ws->lines[i];
if (dl->text[0] != '\0') {
draw_text_to_pixels(pixels, cw, ch, 12, y, dl->text, dl->color);
}
y += lineH;
}
// ---- Scrollbar ----
if (ws->lineCount > visibleLines) {
int sb_x = cw - WIKI_SCROLLBAR_W;
int sb_y = content_y;
int sb_h = content_h;
// Scrollbar track
uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel();
for (int y2 = sb_y; y2 < sb_y + sb_h && y2 < ch; y2++)
for (int x = sb_x; x < cw; x++)
pixels[y2 * cw + x] = sb_bg;
// Thumb
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 1) maxScroll = 1;
int thumbH = (visibleLines * sb_h) / ws->lineCount;
if (thumbH < 20) thumbH = 20;
int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll;
uint32_t sb_fg = colors::SCROLLBAR_FG.to_pixel();
for (int y2 = thumbY; y2 < thumbY + thumbH && y2 < ch; y2++)
for (int x = sb_x + 2; x < cw - 2; x++)
pixels[y2 * cw + x] = sb_fg;
}
}
}
static void wiki_trigger_search(WikiState* ws) {
if (ws->searchQuery[0] == '\0') return;
if (ws->mode == WikiState::FETCHING) return; // already fetching
ws->lineCount = 0;
ws->scrollY = 0;
ws->respPos = 0;
// Build args string: "-d <query>"
static char args[512];
args[0] = '-'; args[1] = 'd'; args[2] = ' ';
int qi = 0;
while (ws->searchQuery[qi] && qi < 500) {
args[3 + qi] = ws->searchQuery[qi];
qi++;
}
args[3 + qi] = '\0';
ws->child_pid = zenith::spawn_redir("0:/os/wiki.elf", args);
if (ws->child_pid <= 0) {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: could not start wiki process");
ws->mode = WikiState::WIKI_ERROR;
return;
}
ws->mode = WikiState::FETCHING;
}
static void wiki_on_mouse(Window* win, MouseEvent& ev) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
Rect cr = win->content_rect();
int cw = cr.w;
int local_x = ev.x - cr.x;
int local_y = ev.y - cr.y;
// Check search button click
int tb_w = cw - 90;
if (tb_w < 100) tb_w = 100;
int btn_x = 8 + tb_w + 6;
int btn_y = 6;
int btn_w = 66;
int btn_h = 24;
if (ev.left_pressed()) {
if (local_x >= btn_x && local_x < btn_x + btn_w &&
local_y >= btn_y && local_y < btn_y + btn_h) {
wiki_trigger_search(ws);
return;
}
}
// Scroll wheel
if (ev.scroll != 0 && ws->mode == WikiState::DONE && ws->lineCount > 0) {
int ch = cr.h;
int content_h = ch - WIKI_TOOLBAR_H - 1;
int visibleLines = content_h / (FONT_HEIGHT + 4);
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 0) maxScroll = 0;
ws->scrollY += ev.scroll * 3;
if (ws->scrollY < 0) ws->scrollY = 0;
if (ws->scrollY > maxScroll) ws->scrollY = maxScroll;
}
}
static void wiki_on_key(Window* win, const Zenith::KeyEvent& key) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws || !key.pressed) return;
// Enter key triggers search
if (key.ascii == '\n' || key.ascii == '\r') {
wiki_trigger_search(ws);
return;
}
// Page Up / Page Down / arrows
if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
Rect cr = {0, 0, 0, 0};
for (int i = 0; i < ws->ds->window_count; i++) {
Window* w = &ws->ds->windows[i];
if (w->app_data == ws) {
cr = w->content_rect();
break;
}
}
int content_h = cr.h - WIKI_TOOLBAR_H - 1;
int visibleLines = content_h / (FONT_HEIGHT + 4);
if (visibleLines < 1) visibleLines = 1;
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 0) maxScroll = 0;
if (key.scancode == 0x49) { // Page Up
ws->scrollY -= visibleLines;
if (ws->scrollY < 0) ws->scrollY = 0;
return;
}
if (key.scancode == 0x51) { // Page Down
ws->scrollY += visibleLines;
if (ws->scrollY > maxScroll) ws->scrollY = maxScroll;
return;
}
if (key.scancode == 0x48) { // Up arrow
if (ws->scrollY > 0) ws->scrollY--;
return;
}
if (key.scancode == 0x50) { // Down arrow
if (ws->scrollY < maxScroll) ws->scrollY++;
return;
}
if (key.scancode == 0x47) { // Home
ws->scrollY = 0;
return;
}
if (key.scancode == 0x4F) { // End
ws->scrollY = maxScroll;
return;
}
}
// Text input for search box
if (key.ascii == '\b' || key.scancode == 0x0E) {
int len = zenith::slen(ws->searchQuery);
if (len > 0) ws->searchQuery[len - 1] = '\0';
} else if (key.ascii >= 32 && key.ascii < 127) {
int len = zenith::slen(ws->searchQuery);
if (len < 254) {
ws->searchQuery[len] = key.ascii;
ws->searchQuery[len + 1] = '\0';
}
}
}
static void wiki_on_poll(Window* win) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
if (ws->mode != WikiState::FETCHING || ws->child_pid <= 0) return;
// Non-blocking read from child process
char buf[4096];
int n = zenith::childio_read(ws->child_pid, buf, sizeof(buf));
if (n > 0) {
// Accumulate data, checking for EOT sentinel
for (int i = 0; i < n && ws->respPos < WIKI_RESP_MAX - 1; i++) {
if (buf[i] == '\x04') {
// EOT: child is done, process the response
ws->respBuf[ws->respPos] = '\0';
ws->child_pid = -1;
wiki_process_response(ws);
return;
}
if (buf[i] == '\x01') {
// Error sentinel from child
ws->child_pid = -1;
snprintf(ws->statusMsg, sizeof(ws->statusMsg),
"Article not found: %s", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
return;
}
ws->respBuf[ws->respPos++] = buf[i];
}
} else if (n < 0) {
// Child process exited — process whatever we accumulated
ws->child_pid = -1;
if (ws->respPos > 0) {
ws->respBuf[ws->respPos] = '\0';
wiki_process_response(ws);
} else {
snprintf(ws->statusMsg, sizeof(ws->statusMsg),
"Error: fetch failed for \"%s\"", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
}
}
}
static void wiki_on_close(Window* win) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
if (ws->lines) zenith::mfree(ws->lines);
if (ws->respBuf) zenith::mfree(ws->respBuf);
zenith::mfree(ws);
win->app_data = nullptr;
}
// ============================================================================
// Wikipedia launcher
// ============================================================================
void open_wiki(DesktopState* ds) {
int idx = desktop_create_window(ds, "Wikipedia", 100, 80, 600, 480);
if (idx < 0) return;
Window* win = &ds->windows[idx];
WikiState* ws = (WikiState*)zenith::malloc(sizeof(WikiState));
zenith::memset(ws, 0, sizeof(WikiState));
ws->ds = ds;
ws->mode = WikiState::IDLE;
ws->searchQuery[0] = '\0';
ws->scrollY = 0;
ws->child_pid = -1;
// Allocate display lines
ws->lineCap = WIKI_MAX_LINES;
ws->lines = (WikiDisplayLine*)zenith::malloc(ws->lineCap * sizeof(WikiDisplayLine));
ws->lineCount = 0;
// Allocate response buffer
ws->respBuf = (char*)zenith::malloc(WIKI_RESP_MAX);
ws->respPos = 0;
ws->statusMsg[0] = '\0';
win->app_data = ws;
win->on_draw = wiki_on_draw;
win->on_mouse = wiki_on_mouse;
win->on_key = wiki_on_key;
win->on_poll = wiki_on_poll;
win->on_close = wiki_on_close;
}
+1
View File
@@ -248,3 +248,4 @@ void open_calculator(DesktopState* ds);
void open_texteditor(DesktopState* ds);
void open_texteditor_with_file(DesktopState* ds, const char* path);
void open_klog(DesktopState* ds);
void open_wiki(DesktopState* ds);
+26 -19
View File
@@ -31,23 +31,29 @@ 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
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);
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
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));
// Load SVG icons — scalable (colorful) for app menu, symbolic for toolbar/panel
Color defColor = colors::ICON_COLOR;
ds->icon_terminal = svg_load("0:/icons/utilities-terminal.svg", 20, 20, defColor);
ds->icon_filemanager = svg_load("0:/icons/system-file-manager.svg", 20, 20, defColor);
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps.svg", 20, 20, defColor);
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor);
ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor);
ds->icon_computer = svg_load("0:/icons/computer.svg", 20, 20, defColor);
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.svg", 20, 20, defColor);
ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor.svg", 20, 20, defColor);
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor);
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor);
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor);
ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, defColor);
ds->icon_home = svg_load("0:/icons/user-home.svg", 16, 16, defColor);
ds->icon_exec = svg_load("0:/icons/utilities-terminal.svg", 16, 16, defColor);
ds->icon_wikipedia = svg_load("0:/icons/web-browser.svg", 20, 20, defColor);
ds->icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, defColor);
ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor);
ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor);
ds->net_popup_open = false;
zenith::get_netcfg(&ds->cached_net_cfg);
@@ -334,7 +340,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
// App Menu (5 items with separator and rounded corners)
// ============================================================================
static constexpr int MENU_ITEM_COUNT = 6;
static constexpr int MENU_ITEM_COUNT = 7;
static constexpr int MENU_W = 220;
static constexpr int MENU_ITEM_H = 40;
@@ -364,6 +370,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
{ "Calculator", &ds->icon_calculator },
{ "Text Editor", &ds->icon_texteditor },
{ "Kernel Log", &ds->icon_terminal },
{ "Wikipedia", &ds->icon_wikipedia },
};
int mx = ds->mouse.x;
@@ -558,6 +565,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_wiki(ds); break;
}
ds->app_menu_open = false;
}
@@ -598,7 +606,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false;
return;
}
// Window indicator buttons
int indicator_x = 40;
for (int i = 0; i < ds->window_count; i++) {
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.
+58 -3
View File
@@ -25,7 +25,7 @@ static const char WIKI_HOST[] = "en.wikipedia.org";
static constexpr int MAX_LINES = 4096;
static constexpr int MAX_SEARCH_RESULTS = 10;
enum Mode { MODE_SUMMARY, MODE_FULL, MODE_SEARCH };
enum Mode { MODE_SUMMARY, MODE_FULL, MODE_SEARCH, MODE_DUMP };
enum LineType { LINE_BLANK, LINE_TITLE, LINE_DESC, LINE_SECTION, LINE_BODY };
struct WikiLine {
@@ -1002,6 +1002,9 @@ extern "C" void _start() {
} else if (arg[0] == '-' && arg[1] == 's' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_SEARCH;
arg = skip_spaces(arg + 2);
} else if (arg[0] == '-' && arg[1] == 'd' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_DUMP;
arg = skip_spaces(arg + 2);
}
if (*arg == '\0') {
@@ -1017,16 +1020,19 @@ extern "C" void _start() {
query[qlen] = '\0';
// Initialize: resolve DNS and load certs
zenith::print("\033[1;33mConnecting to Wikipedia...\033[0m\n");
if (mode != MODE_DUMP)
zenith::print("\033[1;33mConnecting to Wikipedia...\033[0m\n");
g_serverIp = zenith::resolve(WIKI_HOST);
if (g_serverIp == 0) {
if (mode == MODE_DUMP) { zenith::print("\x01"); zenith::sleep_ms(100); zenith::exit(1); }
zenith::print("\033[1;31mError:\033[0m could not resolve en.wikipedia.org\n");
zenith::exit(1);
}
g_tas = load_trust_anchors();
if (g_tas.count == 0) {
if (mode == MODE_DUMP) { zenith::print("\x01"); zenith::sleep_ms(100); zenith::exit(1); }
zenith::print("\033[1;31mError:\033[0m no CA certificates loaded\n");
zenith::exit(1);
}
@@ -1040,7 +1046,56 @@ extern "C" void _start() {
zenith::exit(1);
}
if (mode == MODE_SEARCH) {
if (mode == MODE_DUMP) {
// ---- Dump mode: output raw JSON body for desktop GUI ----
static char path[2048], encoded[1024];
url_encode_title(query, encoded, sizeof(encoded));
snprintf(path, sizeof(path),
"/w/api.php?action=query&format=json&formatversion=2"
"&prop=extracts&explaintext=1&titles=%s", encoded);
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
zenith::print("\x01"); // error sentinel
zenith::sleep_ms(100);
zenith::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
zenith::print("\x01");
zenith::sleep_ms(100);
zenith::exit(1);
}
int statusCode = parse_status_code(respBuf, headerEnd);
if (statusCode == 404) {
zenith::print("\x01");
zenith::sleep_ms(100);
zenith::exit(1);
}
// Output raw JSON body in chunks to avoid overflowing
// the 4KB kernel ring buffer (parent polls at ~60fps)
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
static char chunk[2049];
int sent = 0;
while (sent < bodyLen) {
int n = bodyLen - sent;
if (n > 2048) n = 2048;
for (int ci = 0; ci < n; ci++) chunk[ci] = body[sent + ci];
chunk[n] = '\0';
zenith::print(chunk);
sent += n;
if (sent < bodyLen) zenith::sleep_ms(20);
}
zenith::putchar('\x04'); // EOT sentinel
// Brief delay so parent can drain the ring buffer before we exit
zenith::sleep_ms(100);
zenith::exit(0);
} else if (mode == MODE_SEARCH) {
// ---- Search mode ----
static char path[2048], encoded[1024];
url_encode_query(query, encoded, sizeof(encoded));
Binary file not shown.