fix: GUI and kernel fixes, Wikipedia app rewrite, among other things

This commit is contained in:
2026-02-21 11:08:11 +01:00
parent 596be25eaf
commit 9d5e7eac8d
97 changed files with 1844 additions and 662 deletions
+1 -1
View File
@@ -315,7 +315,7 @@ static void devexplorer_on_mouse(Window* win, MouseEvent& ev) {
// Scroll
if (ev.scroll != 0) {
de->scroll_y -= ev.scroll;
de->scroll_y += ev.scroll;
if (de->scroll_y < 0) de->scroll_y = 0;
return;
}
+4 -572
View File
@@ -1,581 +1,13 @@
/*
* app_wiki.cpp
* ZenithOS Desktop - Wikipedia client application
* Spawns wiki.elf -d as a child process for non-blocking network I/O
* ZenithOS Desktop - Wikipedia launcher
* Spawns wikipedia.elf as a standalone Window Server process
* 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];
Color 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, Color 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, Color 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 char_w = mono_cell_width();
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / char_w;
if (maxChars < 20) maxChars = 20;
Color accent_c = colors::ACCENT;
Color green_c = Color::from_rgb(0x2E, 0x7D, 0x32);
Color text_c = colors::TEXT_COLOR;
// Title
if (title && title[0]) {
wiki_wrap_text(ws, title, slen(title), maxChars, accent_c);
}
// Blank separator
if (ws->lineCount > 0)
wiki_add_line(ws, "", 0, text_c);
// 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_c);
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_c);
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_c);
}
continue;
}
// Regular text
wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_c);
}
}
// ============================================================================
// 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;
Canvas c(win);
c.fill(colors::WINDOW_BG);
// ---- Toolbar background ----
c.fill_rect(0, 0, c.w, WIKI_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
// Toolbar separator line
c.hline(0, WIKI_TOOLBAR_H, c.w, colors::BORDER);
// ---- Draw search box outline ----
int tb_x = 8;
int tb_y = 6;
int tb_w = c.w - 90;
int tb_h = 24;
if (tb_w < 100) tb_w = 100;
// TextBox background + border
c.fill_rect(tb_x, tb_y, tb_w, tb_h, colors::WHITE);
c.rect(tb_x, tb_y, tb_w, tb_h, colors::BORDER);
// Search text
c.text(tb_x + 4, tb_y + (tb_h - system_font_height()) / 2, ws->searchQuery, colors::TEXT_COLOR);
// ---- Search button ----
int btn_x = tb_x + tb_w + 6;
int btn_y = tb_y;
int btn_w = 66;
int btn_h = tb_h;
c.button(btn_x, btn_y, btn_w, btn_h, "Search", colors::ACCENT, colors::WHITE, 0);
// ---- Content area ----
int content_y = WIKI_TOOLBAR_H + 1;
int content_h = c.h - content_y;
int wiki_sfh = system_font_height();
int visibleLines = content_h / (wiki_sfh + 4);
if (visibleLines < 1) visibleLines = 1;
if (ws->mode == WikiState::FETCHING) {
c.text(16, content_y + 16, "Loading...", Color::from_rgb(0x88, 0x88, 0x88));
} else if (ws->mode == WikiState::WIKI_ERROR) {
c.text(16, content_y + 16, ws->statusMsg, colors::CLOSE_BTN);
} else if (ws->mode == WikiState::IDLE) {
c.text(16, content_y + 16,
"Type a topic and press Enter or click Search.",
Color::from_rgb(0x88, 0x88, 0x88));
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
int y = content_y + 8;
int lineH = wiki_sfh + 4;
for (int i = ws->scrollY; i < ws->lineCount && y + wiki_sfh < c.h; i++) {
WikiDisplayLine* dl = &ws->lines[i];
if (dl->text[0] != '\0') {
c.text(12, y, dl->text, dl->color);
}
y += lineH;
}
// ---- Scrollbar ----
if (ws->lineCount > visibleLines) {
int sb_x = c.w - WIKI_SCROLLBAR_W;
int sb_y = content_y;
int sb_h = content_h;
c.fill_rect(sb_x, sb_y, WIKI_SCROLLBAR_W, sb_h, colors::SCROLLBAR_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;
c.fill_rect(sb_x + 2, thumbY, WIKI_SCROLLBAR_W - 4, thumbH, colors::SCROLLBAR_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 / (system_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 / (system_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;
(void)ds;
zenith::spawn("0:/os/wikipedia.elf");
}
+172 -69
View File
@@ -407,19 +407,60 @@ void gui::desktop_draw_panel(DesktopState* ds) {
}
// ============================================================================
// App Menu (5 items with separator and rounded corners)
// App Menu (categorized)
// ============================================================================
static constexpr int MENU_ITEM_COUNT = 13;
static constexpr int MENU_W = 220;
static constexpr int MENU_ITEM_H = 40;
static constexpr int MENU_ITEM_H = 36;
static constexpr int MENU_CAT_H = 28;
static constexpr int MENU_DIV_H = 10;
struct MenuRow {
bool is_category;
const char* label; // "" for divider-only rows
int app_id; // -1 for category headers / dividers
};
static constexpr int MENU_ROW_COUNT = 18;
static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ true, "Applications", -1 },
{ false, "Terminal", 0 },
{ false, "Files", 1 },
{ false, "Text Editor", 4 },
{ false, "Calculator", 3 },
{ true, "Internet", -1 },
{ false, "Wikipedia", 9 },
{ true, "System", -1 },
{ false, "System Info", 2 },
{ false, "Kernel Log", 5 },
{ false, "Processes", 6 },
{ false, "Devices", 8 },
{ true, "Games", -1 },
{ false, "Mandelbrot", 7 },
{ false, "DOOM", 10 },
{ true, "", -1 }, // divider
{ false, "Settings", 11 },
{ false, "Reboot", 12 },
};
static int menu_row_height(const MenuRow& row) {
if (!row.is_category) return MENU_ITEM_H;
return row.label[0] ? MENU_CAT_H : MENU_DIV_H;
}
static int menu_total_height() {
int h = 10; // top + bottom padding
for (int i = 0; i < MENU_ROW_COUNT; i++)
h += menu_row_height(menu_rows[i]);
return h;
}
static void desktop_draw_app_menu(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int menu_x = 4;
int menu_y = PANEL_HEIGHT + 2;
int menu_h = MENU_ITEM_H * MENU_ITEM_COUNT + 10; // +10 for padding + separator
int menu_h = menu_total_height();
// Menu shadow
draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW);
@@ -428,59 +469,70 @@ static void desktop_draw_app_menu(DesktopState* ds) {
fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG);
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
// Menu items
struct MenuItem {
const char* label;
SvgIcon* icon;
};
MenuItem items[MENU_ITEM_COUNT] = {
{ "Terminal", &ds->icon_terminal },
{ "Files", &ds->icon_filemanager },
{ "System Info", &ds->icon_sysinfo },
{ "Calculator", &ds->icon_calculator },
{ "Text Editor", &ds->icon_texteditor },
{ "Kernel Log", &ds->icon_terminal },
{ "Processes", &ds->icon_procmgr },
{ "Mandelbrot", &ds->icon_mandelbrot },
{ "Devices", &ds->icon_devexplorer },
{ "Wikipedia", &ds->icon_wikipedia },
{ "DOOM", &ds->icon_doom },
{ "Settings", &ds->icon_settings },
{ "Reboot", &ds->icon_reboot },
// Icon lookup by app_id
SvgIcon* icons[13] = {
&ds->icon_terminal, // 0
&ds->icon_filemanager, // 1
&ds->icon_sysinfo, // 2
&ds->icon_calculator, // 3
&ds->icon_texteditor, // 4
&ds->icon_terminal, // 5 (Kernel Log)
&ds->icon_procmgr, // 6
&ds->icon_mandelbrot, // 7
&ds->icon_devexplorer, // 8
&ds->icon_wikipedia, // 9
&ds->icon_doom, // 10
&ds->icon_settings, // 11
&ds->icon_reboot, // 12
};
int mx = ds->mouse.x;
int my = ds->mouse.y;
int iy = menu_y + 5;
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
int iy = menu_y + 4 + i * MENU_ITEM_H;
for (int i = 0; i < MENU_ROW_COUNT; i++) {
const MenuRow& row = menu_rows[i];
int row_h = menu_row_height(row);
// Thin separator lines before utility apps and before Settings
if (i == 3 || i == 11) {
int sep_y = iy - 1;
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
fb.put_pixel(sx, sep_y, colors::BORDER);
iy += 1;
if (row.is_category) {
// Separator line above (except first category)
if (i > 0) {
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
fb.put_pixel(sx, iy + row_h / 2, colors::BORDER);
}
// Category label (dimmed), skip for divider-only rows
if (row.label[0]) {
int tx = menu_x + 12;
int ty = iy + (row_h - system_font_height()) / 2;
if (i > 0) ty += 2;
draw_text(fb, tx, ty, row.label, Color::from_rgb(0x88, 0x88, 0x88));
}
} else {
Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h};
// Hover highlight
if (item_rect.contains(mx, my)) {
fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER);
}
// Icon
int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (row_h - 20) / 2;
if (row.app_id >= 0 && row.app_id < 13) {
SvgIcon* icon = icons[row.app_id];
if (icon && icon->pixels) {
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels);
}
}
// Label
int tx = icon_x + 28;
int ty = item_rect.y + (row_h - system_font_height()) / 2;
draw_text(fb, tx, ty, row.label, colors::TEXT_COLOR);
}
Rect item_rect = {menu_x + 4, iy, MENU_W - 8, MENU_ITEM_H};
// Hover highlight
if (item_rect.contains(mx, my)) {
fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER);
}
// Icon
int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (MENU_ITEM_H - 20) / 2;
if (items[i].icon && items[i].icon->pixels) {
fb.blit_alpha(icon_x, icon_y, items[i].icon->width, items[i].icon->height, items[i].icon->pixels);
}
// Label
int tx = icon_x + 28;
int ty = item_rect.y + (MENU_ITEM_H - system_font_height()) / 2;
draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR);
iy += row_h;
}
}
@@ -915,6 +967,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
} else if (mx >= ds->screen_w - 1) {
win->saved_frame = win->frame;
@@ -929,6 +989,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
}
}
@@ -987,8 +1055,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
win->dirty = true;
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
win->dirty = true;
}
return;
}
@@ -998,29 +1074,37 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (ds->app_menu_open && left_pressed) {
int menu_x = 4;
int menu_y = PANEL_HEIGHT + 2;
int menu_h = MENU_ITEM_H * MENU_ITEM_COUNT + 10;
int menu_h = menu_total_height();
Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h};
if (menu_rect.contains(mx, my)) {
int rel_y = my - menu_y - 4;
int item_idx = rel_y / MENU_ITEM_H;
if (item_idx >= 0 && item_idx < MENU_ITEM_COUNT) {
switch (item_idx) {
case 0: open_terminal(ds); break;
case 1: open_filemanager(ds); break;
case 2: open_sysinfo(ds); break;
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_procmgr(ds); break;
case 7: open_mandelbrot(ds); break;
case 8: open_devexplorer(ds); break;
case 9: open_wiki(ds); break;
case 10: open_doom(ds); break;
case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break;
// Walk rows to find which one was clicked
int iy = menu_y + 5;
for (int i = 0; i < MENU_ROW_COUNT; i++) {
const MenuRow& row = menu_rows[i];
int row_h = menu_row_height(row);
if (my >= iy && my < iy + row_h) {
if (!row.is_category) {
switch (row.app_id) {
case 0: open_terminal(ds); break;
case 1: open_filemanager(ds); break;
case 2: open_sysinfo(ds); break;
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_procmgr(ds); break;
case 7: open_mandelbrot(ds); break;
case 8: open_devexplorer(ds); break;
case 9: open_wiki(ds); break;
case 10: open_doom(ds); break;
case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break;
}
ds->app_menu_open = false;
}
break;
}
ds->app_menu_open = false;
iy += row_h;
}
return;
} else {
@@ -1136,6 +1220,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
desktop_raise_window(ds, i);
return;
@@ -1323,6 +1415,17 @@ void desktop_poll_external_windows(DesktopState* ds) {
if (extWins[e].dirty) {
ds->windows[i].dirty = true;
}
// Re-map if external app resized its buffer
if (extWins[e].width != ds->windows[i].content_w ||
extWins[e].height != ds->windows[i].content_h) {
uint64_t va = zenith::win_map(extId);
if (va != 0) {
ds->windows[i].content = (uint32_t*)va;
ds->windows[i].content_w = extWins[e].width;
ds->windows[i].content_h = extWins[e].height;
ds->windows[i].dirty = true;
}
}
break;
}
}
+93
View File
@@ -0,0 +1,93 @@
# Makefile for wikipedia (standalone Wikipedia GUI client) on ZenithOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
# ---- Paths ----
BEARSSL := ../../lib/bearssl
LIBC_LIB := ../../lib/libc
LIBC_INC := ../../include/libc
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(LIBC_INC) \
-I $(BEARSSL)/inc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Libraries ----
LIBS := $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
# ---- Source files ----
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/os/wikipedia.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+908
View File
@@ -0,0 +1,908 @@
/*
* main.cpp
* ZenithOS Wikipedia GUI client - standalone Window Server process
* Fetches articles via TLS (BearSSL), renders with Roboto TTF
* Copyright (c) 2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <bearssl.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
}
using namespace gui;
using namespace gui::colors;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 820;
static constexpr int INIT_H = 580;
static constexpr int TOOLBAR_H = 42;
static constexpr int SCROLLBAR_W = 14;
static constexpr int FONT_SIZE = 18;
static constexpr int TITLE_SIZE = 32;
static constexpr int SECTION_SIZE = 24;
static constexpr int TEXT_PAD = 16;
static constexpr int RESP_MAX = 131072;
static constexpr int MAX_LINES = 2000;
static const char WIKI_HOST[] = "en.wikipedia.org";
// ============================================================================
// Display line
// ============================================================================
struct WikiLine {
char text[256];
Color color;
int font_size;
TrueTypeFont* font; // which font to render with
};
// ============================================================================
// App state
// ============================================================================
enum class AppPhase { IDLE, LOADING, DONE, ERR };
static AppPhase g_phase = AppPhase::IDLE;
static char g_query[256] = {};
static char g_status[256] = {};
static int g_scroll_y = 0;
static int g_line_count = 0;
static int g_line_h = 24; // updated after font load
static int g_win_w = INIT_W;
static int g_win_h = INIT_H;
static char g_title[512] = {};
static int g_extract_len = 0;
// Large buffers — heap allocated in _start
static WikiLine* g_lines = nullptr;
static char* g_resp_buf = nullptr;
static char* g_extract_buf = nullptr;
// Fonts
static TrueTypeFont* g_font = nullptr; // Roboto Medium
static TrueTypeFont* g_font_bold = nullptr; // Roboto Bold
static TrueTypeFont* g_font_serif = nullptr; // NotoSerif SemiBold (headings)
// TLS state (lazy-init on first search)
static bool g_tls_ready = false;
static uint32_t g_server_ip = 0;
struct TrustAnchors {
br_x509_trust_anchor* anchors;
size_t count;
size_t capacity;
};
static TrustAnchors g_tas = {nullptr, 0, 0};
// ============================================================================
// Pixel buffer helpers
// ============================================================================
static void px_fill(uint32_t* px, int bw, int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w, y1 = y + h;
if (x1 > bw) x1 = bw;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
px[row * bw + col] = v;
}
static void px_hline(uint32_t* px, int bw, int x, int y, int len, Color c) {
uint32_t v = c.to_pixel();
int x1 = x + len;
if (x < 0) x = 0;
if (x1 > bw) x1 = bw;
for (int col = x; col < x1; col++)
px[y * bw + col] = v;
}
static void px_vline(uint32_t* px, int bw, int x, int y, int len, Color c) {
uint32_t v = c.to_pixel();
for (int row = y; row < y + len; row++)
px[row * bw + x] = v;
}
static void px_rect_outline(uint32_t* px, int bw, int x, int y, int w, int h, Color c) {
px_hline(px, bw, x, y, w, c);
px_hline(px, bw, x, y + h - 1, w, c);
px_vline(px, bw, x, y, h, c);
px_vline(px, bw, x + w - 1, y, h, c);
}
// ============================================================================
// Trust anchor loading
// ============================================================================
struct DerAccum { unsigned char* data; size_t len, cap; };
struct DnAccum { unsigned char* data; size_t len, cap; };
static void der_append(void* ctx, const void* buf, size_t len) {
DerAccum* a = (DerAccum*)ctx;
if (a->len + len > a->cap) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 4096;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
static void dn_append(void* ctx, const void* buf, size_t len) {
DnAccum* a = (DnAccum*)ctx;
if (a->len + len > a->cap) {
size_t nc = a->cap * 2;
if (nc < a->len + len) nc = a->len + len + 256;
unsigned char* nb = (unsigned char*)malloc(nc);
if (!nb) return;
if (a->data) { memcpy(nb, a->data, a->len); free(a->data); }
a->data = nb; a->cap = nc;
}
memcpy(a->data + a->len, buf, len);
a->len += len;
}
static void ta_add(TrustAnchors* tas, const br_x509_trust_anchor* ta) {
if (tas->count >= tas->capacity) {
size_t nc = tas->capacity == 0 ? 64 : tas->capacity * 2;
br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc(nc * sizeof(*na));
if (!na) return;
if (tas->anchors) { memcpy(na, tas->anchors, tas->count * sizeof(*na)); free(tas->anchors); }
tas->anchors = na; tas->capacity = nc;
}
tas->anchors[tas->count++] = *ta;
}
static bool process_cert_der(TrustAnchors* tas, const unsigned char* der, size_t der_len) {
static br_x509_decoder_context dc;
DnAccum dn = {nullptr, 0, 0};
br_x509_decoder_init(&dc, dn_append, &dn);
br_x509_decoder_push(&dc, der, der_len);
br_x509_pkey* pk = br_x509_decoder_get_pkey(&dc);
if (!pk) { if (dn.data) free(dn.data); return false; }
br_x509_trust_anchor ta;
memset(&ta, 0, sizeof(ta));
ta.dn.data = dn.data; ta.dn.len = dn.len; ta.flags = 0;
if (br_x509_decoder_isCA(&dc)) ta.flags |= BR_X509_TA_CA;
switch (pk->key_type) {
case BR_KEYTYPE_RSA:
ta.pkey.key_type = BR_KEYTYPE_RSA;
ta.pkey.key.rsa.nlen = pk->key.rsa.nlen;
ta.pkey.key.rsa.n = (unsigned char*)malloc(pk->key.rsa.nlen);
if (ta.pkey.key.rsa.n) memcpy(ta.pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
ta.pkey.key.rsa.elen = pk->key.rsa.elen;
ta.pkey.key.rsa.e = (unsigned char*)malloc(pk->key.rsa.elen);
if (ta.pkey.key.rsa.e) memcpy(ta.pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
break;
case BR_KEYTYPE_EC:
ta.pkey.key_type = BR_KEYTYPE_EC;
ta.pkey.key.ec.curve = pk->key.ec.curve;
ta.pkey.key.ec.qlen = pk->key.ec.qlen;
ta.pkey.key.ec.q = (unsigned char*)malloc(pk->key.ec.qlen);
if (ta.pkey.key.ec.q) memcpy(ta.pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
break;
default:
if (dn.data) free(dn.data);
return false;
}
ta_add(tas, &ta);
return true;
}
static TrustAnchors load_trust_anchors() {
TrustAnchors tas = {nullptr, 0, 0};
int fh = zenith::open("0:/etc/ca-certificates.crt");
if (fh < 0) return tas;
uint64_t fsize = zenith::getsize(fh);
if (fsize == 0 || fsize > 512 * 1024) { zenith::close(fh); return tas; }
unsigned char* pem = (unsigned char*)malloc(fsize + 1);
if (!pem) { zenith::close(fh); return tas; }
zenith::read(fh, pem, 0, fsize);
zenith::close(fh);
pem[fsize] = 0;
static br_pem_decoder_context pc;
br_pem_decoder_init(&pc);
DerAccum der = {nullptr, 0, 0};
bool inCert = false;
size_t offset = 0;
while (offset < fsize) {
size_t pushed = br_pem_decoder_push(&pc, pem + offset, fsize - offset);
offset += pushed;
int ev = br_pem_decoder_event(&pc);
if (ev == BR_PEM_BEGIN_OBJ) {
inCert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0);
br_pem_decoder_setdest(&pc, inCert ? der_append : nullptr, inCert ? &der : nullptr);
if (inCert) der.len = 0;
} else if (ev == BR_PEM_END_OBJ) {
if (inCert && der.len > 0) process_cert_der(&tas, der.data, der.len);
inCert = false;
} else if (ev == BR_PEM_ERROR) {
break;
}
}
if (der.data) free(der.data);
free(pem);
return tas;
}
// ============================================================================
// BearSSL time
// ============================================================================
static void get_bearssl_time(uint32_t* days, uint32_t* seconds) {
Zenith::DateTime dt;
zenith::gettime(&dt);
int y = dt.Year, m = dt.Month, d = dt.Day;
uint32_t total = 365u * (uint32_t)y
+ (uint32_t)(y/4) - (uint32_t)(y/100) + (uint32_t)(y/400);
const int md[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
for (int mo = 1; mo < m && mo <= 12; mo++) total += md[mo];
if (y%4==0 && (y%100!=0 || y%400==0) && m > 2) total++;
total += d - 1;
*days = total;
*seconds = (uint32_t)(dt.Hour*3600 + dt.Minute*60 + dt.Second);
}
// ============================================================================
// TLS I/O
// ============================================================================
static int tls_send_all(int fd, const unsigned char* data, size_t len) {
size_t sent = 0;
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (sent < len) {
int r = zenith::send(fd, data + sent, (uint32_t)(len - sent));
if (r > 0) { sent += r; deadline = zenith::get_milliseconds() + 15000; }
else if (r < 0) return -1;
else { if (zenith::get_milliseconds() >= deadline) return -1; zenith::sleep_ms(1); }
}
return (int)sent;
}
static int tls_recv_some(int fd, unsigned char* buf, size_t maxlen) {
uint64_t deadline = zenith::get_milliseconds() + 15000;
while (true) {
int r = zenith::recv(fd, buf, (uint32_t)maxlen);
if (r > 0) return r;
if (r < 0) return -1;
if (zenith::get_milliseconds() >= deadline) return -1;
zenith::sleep_ms(1);
}
}
static int tls_exchange(int fd, br_ssl_engine_context* eng,
const char* request, int reqLen,
char* respBuf, int respMax) {
bool requestSent = false;
int respLen = 0;
uint64_t deadline = zenith::get_milliseconds() + 30000;
while (true) {
unsigned state = br_ssl_engine_current_state(eng);
if (state & BR_SSL_CLOSED) {
int err = br_ssl_engine_last_error(eng);
if (err != BR_ERR_OK && err != BR_ERR_IO && respLen == 0) return -1;
return respLen;
}
if (state & BR_SSL_SENDREC) {
size_t len; unsigned char* buf = br_ssl_engine_sendrec_buf(eng, &len);
int sent = tls_send_all(fd, buf, len);
if (sent < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_sendrec_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVAPP) {
size_t len; unsigned char* buf = br_ssl_engine_recvapp_buf(eng, &len);
size_t toCopy = len;
if (respLen + (int)toCopy > respMax - 1) toCopy = respMax - 1 - respLen;
if (toCopy > 0) { memcpy(respBuf + respLen, buf, toCopy); respLen += toCopy; }
br_ssl_engine_recvapp_ack(eng, len);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if ((state & BR_SSL_SENDAPP) && !requestSent) {
size_t len; unsigned char* buf = br_ssl_engine_sendapp_buf(eng, &len);
size_t toWrite = (size_t)reqLen;
if (toWrite > len) toWrite = len;
memcpy(buf, request, toWrite);
br_ssl_engine_sendapp_ack(eng, toWrite);
br_ssl_engine_flush(eng, 0);
requestSent = true;
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (state & BR_SSL_RECVREC) {
size_t len; unsigned char* buf = br_ssl_engine_recvrec_buf(eng, &len);
int got = tls_recv_some(fd, buf, len);
if (got < 0) { br_ssl_engine_close(eng); return respLen > 0 ? respLen : -1; }
br_ssl_engine_recvrec_ack(eng, got);
deadline = zenith::get_milliseconds() + 30000; continue;
}
if (zenith::get_milliseconds() >= deadline) return respLen > 0 ? respLen : -1;
zenith::sleep_ms(1);
}
}
static int wiki_fetch(const char* path, char* respBuf, int respMax) {
int fd = zenith::socket(Zenith::SOCK_TCP);
if (fd < 0) return -1;
if (zenith::connect(fd, g_server_ip, 443) < 0) { zenith::closesocket(fd); return -1; }
br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(*cc));
br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(*xc));
void* iobuf = malloc(BR_SSL_BUFSIZE_BIDI);
if (!cc || !xc || !iobuf) {
if (cc) free(cc); if (xc) free(xc); if (iobuf) free(iobuf);
zenith::closesocket(fd); return -1;
}
br_ssl_client_init_full(cc, xc, g_tas.anchors, g_tas.count);
uint32_t days, secs;
get_bearssl_time(&days, &secs);
br_x509_minimal_set_time(xc, days, secs);
unsigned char seed[32];
zenith::getrandom(seed, sizeof(seed));
br_ssl_engine_set_buffer(&cc->eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed));
if (!br_ssl_client_reset(cc, WIKI_HOST, 0)) {
zenith::closesocket(fd); free(cc); free(xc); free(iobuf); return -1;
}
static char request[2560];
int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"User-Agent: ZenithOS/1.0 wikipedia\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n",
path, WIKI_HOST);
int respLen = tls_exchange(fd, &cc->eng, request, reqLen, respBuf, respMax);
zenith::closesocket(fd);
free(cc); free(xc); free(iobuf);
return respLen;
}
// ============================================================================
// HTTP parsing
// ============================================================================
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++)
if (buf[i]=='\r' && buf[i+1]=='\n' && buf[i+2]=='\r' && buf[i+3]=='\n')
return i + 4;
return -1;
}
static int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len || i + 3 >= len) return -1;
i++;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i]-'0')*100 + (buf[i+1]-'0')*10 + (buf[i+2]-'0');
}
// ============================================================================
// URL encoding
// ============================================================================
static int url_encode_title(const char* in, char* out, int maxLen) {
const char hex[] = "0123456789ABCDEF";
int j = 0;
for (int i = 0; in[i] && j < maxLen - 4; i++) {
char c = in[i];
if (c == ' ') {
out[j++] = '_';
} else if ((c>='A'&&c<='Z')||(c>='a'&&c<='z')||(c>='0'&&c<='9')||
c=='-'||c=='_'||c=='.'||c=='~'||c=='('||c==')'||c==',') {
out[j++] = c;
} else {
out[j++] = '%';
out[j++] = hex[(unsigned char)c >> 4];
out[j++] = hex[(unsigned char)c & 0x0F];
}
}
out[j] = '\0';
return j;
}
// ============================================================================
// JSON string extraction
// ============================================================================
static int extract_json_string(const char* buf, int len, const char* key,
char* out, int maxOut) {
int klen = (int)strlen(key);
for (int i = 0; i < len - klen - 3; i++) {
if (buf[i] != '"') continue;
if (memcmp(buf + i + 1, key, klen) != 0) 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
// ============================================================================
static void add_line(const char* text, int len, Color color, int size, TrueTypeFont* font) {
if (g_line_count >= MAX_LINES) return;
WikiLine* l = &g_lines[g_line_count++];
int copy = len < 255 ? len : 255;
memcpy(l->text, text, copy);
l->text[copy] = '\0';
l->color = color;
l->font_size = size;
l->font = font;
}
static void add_empty_line() {
if (g_line_count >= MAX_LINES) return;
WikiLine* l = &g_lines[g_line_count++];
l->text[0] = '\0';
l->color = TEXT_COLOR;
l->font_size = FONT_SIZE;
l->font = g_font;
}
// Word-wrap a text segment into display lines using pixel-width measurement.
static void wrap_text(TrueTypeFont* font, int size, const char* text, int textLen,
int max_px, Color color) {
char cur[256];
int cur_len = 0;
const char* p = text;
const char* end = text + textLen;
while (p < end) {
while (p < end && *p == ' ') p++;
if (p >= end) break;
const char* word_start = p;
while (p < end && *p != ' ') p++;
int word_len = (int)(p - word_start);
if (word_len <= 0) continue;
// Build candidate: current line + space + word
char test[260];
int test_len = cur_len;
memcpy(test, cur, cur_len);
if (cur_len > 0) test[test_len++] = ' ';
int copy = word_len < (int)(sizeof(test) - test_len - 1)
? word_len : (int)(sizeof(test) - test_len - 1);
memcpy(test + test_len, word_start, copy);
test_len += copy;
test[test_len] = '\0';
int test_w = font->measure_text(test, size);
if (test_w <= max_px || cur_len == 0) {
// Fits — append to current line
memcpy(cur, test, test_len + 1);
cur_len = test_len;
} else {
// Emit current line and start fresh
if (cur_len > 0) add_line(cur, cur_len, color, size, font);
int wl = word_len < 254 ? word_len : 254;
memcpy(cur, word_start, wl);
cur_len = wl;
cur[cur_len] = '\0';
}
}
if (cur_len > 0) add_line(cur, cur_len, color, size, font);
}
static void build_display_lines(const char* title, const char* extract, int extractLen) {
g_line_count = 0;
g_scroll_y = 0;
// Max pixel width for text (accounting for left pad, right pad, scrollbar)
int max_px = g_win_w - TEXT_PAD - SCROLLBAR_W - TEXT_PAD;
// Title — large, serif, black
if (title && title[0]) {
TrueTypeFont* tf = g_font_serif ? g_font_serif : g_font;
wrap_text(tf, TITLE_SIZE, title, (int)strlen(title), max_px, BLACK);
add_empty_line();
}
// Process extract line-by-line
const char* p = extract;
const char* end = extract + extractLen;
while (p < end && g_line_count < MAX_LINES) {
const char* line_start = p;
while (p < end && *p != '\n') p++;
int line_len = (int)(p - line_start);
if (p < end) p++; // consume '\n'
if (line_len == 0) {
add_empty_line();
continue;
}
// Section header: == Title ==
if (line_len >= 4 && line_start[0] == '=' && line_start[1] == '=') {
int si = 0, ei = line_len;
while (si < line_len && line_start[si] == '=') si++;
while (si < line_len && line_start[si] == ' ') si++;
while (ei > si && line_start[ei-1] == '=') ei--;
while (ei > si && line_start[ei-1] == ' ') ei--;
if (ei > si) {
add_empty_line();
TrueTypeFont* tf = g_font_serif ? g_font_serif : g_font;
wrap_text(tf, SECTION_SIZE, line_start + si, ei - si, max_px, BLACK);
}
continue;
}
// Regular body text
wrap_text(g_font, FONT_SIZE, line_start, line_len, max_px, TEXT_COLOR);
}
}
// ============================================================================
// Network search (blocking)
// ============================================================================
static void do_search(const char* query) {
// Lazy TLS/DNS init
if (!g_tls_ready) {
g_server_ip = zenith::resolve(WIKI_HOST);
if (g_server_ip == 0) {
snprintf(g_status, sizeof(g_status),
"Error: could not resolve en.wikipedia.org");
g_phase = AppPhase::ERR; return;
}
g_tas = load_trust_anchors();
if (g_tas.count == 0) {
snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded");
g_phase = AppPhase::ERR; return;
}
g_tls_ready = true;
}
static char encoded[1024];
url_encode_title(query, encoded, sizeof(encoded));
static char path[2048];
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, g_resp_buf, RESP_MAX);
if (respLen <= 0) {
snprintf(g_status, sizeof(g_status), "Error: no response from Wikipedia");
g_phase = AppPhase::ERR; return;
}
g_resp_buf[respLen] = '\0';
int headerEnd = find_header_end(g_resp_buf, respLen);
if (headerEnd < 0) {
snprintf(g_status, sizeof(g_status), "Error: malformed HTTP response");
g_phase = AppPhase::ERR; return;
}
int status = parse_status_code(g_resp_buf, headerEnd);
const char* body = g_resp_buf + headerEnd;
int bodyLen = respLen - headerEnd;
if (status == 404) {
snprintf(g_status, sizeof(g_status), "Article not found: %s", query);
g_phase = AppPhase::ERR; return;
}
extract_json_string(body, bodyLen, "title", g_title, sizeof(g_title));
g_extract_len = extract_json_string(body, bodyLen, "extract",
g_extract_buf, RESP_MAX - 1);
if (g_extract_len == 0) {
snprintf(g_status, sizeof(g_status), "No content found for: %s", query);
g_phase = AppPhase::ERR; return;
}
build_display_lines(g_title, g_extract_buf, g_extract_len);
g_phase = AppPhase::DONE;
}
// ============================================================================
// Rendering
// ============================================================================
static void render(uint32_t* pixels) {
static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
static constexpr Color HINT_COLOR = Color::from_rgb(0x99, 0x99, 0x99);
// Background
px_fill(pixels, g_win_w, 0, 0, g_win_w, g_win_h, WINDOW_BG);
// ---- Toolbar ----
px_fill(pixels, g_win_w, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG);
px_hline(pixels, g_win_w, 0, TOOLBAR_H, g_win_w, BORDER);
// Search box geometry
int sb_y = 8, sb_h = TOOLBAR_H - 16;
int btn_w = 80, btn_gap = 8;
int sb_x = 8;
int sb_w = g_win_w - sb_x - btn_gap - btn_w - 8;
if (sb_w < 80) sb_w = 80;
px_fill(pixels, g_win_w, sb_x, sb_y, sb_w, sb_h, WHITE);
px_rect_outline(pixels, g_win_w, sb_x, sb_y, sb_w, sb_h, BORDER);
// Search box text + cursor
if (g_font) {
int ty = sb_y + (sb_h - FONT_SIZE) / 2;
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
sb_x + 6, ty, g_query, TEXT_COLOR, FONT_SIZE);
int qw = g_font->measure_text(g_query, FONT_SIZE);
int cx = sb_x + 6 + qw + 1;
if (cx < sb_x + sb_w - 4)
px_vline(pixels, g_win_w, cx, ty + 1, FONT_SIZE - 2, TEXT_COLOR);
}
// Search button
int btn_x = sb_x + sb_w + btn_gap;
px_fill(pixels, g_win_w, btn_x, sb_y, btn_w, sb_h, ACCENT);
if (g_font) {
int stw = g_font->measure_text("Search", FONT_SIZE);
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
btn_x + (btn_w - stw) / 2,
sb_y + (sb_h - FONT_SIZE) / 2,
"Search", WHITE, FONT_SIZE);
}
// ---- Content area ----
int cy = TOOLBAR_H + 1;
int ch = g_win_h - cy;
if (!g_font) return;
if (g_phase == AppPhase::IDLE) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
TEXT_PAD, cy + 16,
"Type a topic and press Enter or click Search.",
HINT_COLOR, FONT_SIZE);
} else if (g_phase == AppPhase::LOADING) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
TEXT_PAD, cy + 16,
"Searching Wikipedia...", HINT_COLOR, FONT_SIZE);
} else if (g_phase == AppPhase::ERR) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
TEXT_PAD, cy + 16, g_status, CLOSE_BTN, FONT_SIZE);
} else if (g_phase == AppPhase::DONE && g_line_count > 0) {
int visible = ch / g_line_h; // approximate using body line height
int y = cy + 8;
for (int i = g_scroll_y; i < g_line_count && y < g_win_h; i++) {
WikiLine& l = g_lines[i];
int lh = g_font->get_line_height(l.font_size) + 4;
if (y + lh > g_win_h) break;
if (l.text[0] != '\0') {
l.font->draw_to_buffer(pixels, g_win_w, g_win_h,
TEXT_PAD, y, l.text, l.color, l.font_size);
}
y += lh;
}
// Scrollbar
if (g_line_count > visible) {
int sbx = g_win_w - SCROLLBAR_W;
px_fill(pixels, g_win_w, sbx, cy, SCROLLBAR_W, ch, SCROLLBAR_BG);
int max_sc = g_line_count - visible;
int thumb_h = (visible * ch) / g_line_count;
if (thumb_h < 20) thumb_h = 20;
int thumb_y = cy + (g_scroll_y * (ch - thumb_h)) / (max_sc > 0 ? max_sc : 1);
px_fill(pixels, g_win_w, sbx + 2, thumb_y,
SCROLLBAR_W - 4, thumb_h, SCROLLBAR_FG);
}
}
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
// Allocate large buffers from heap
g_lines = (WikiLine*)zenith::malloc(MAX_LINES * sizeof(WikiLine));
g_resp_buf = (char*)malloc(RESP_MAX + 1);
g_extract_buf = (char*)malloc(RESP_MAX + 1);
if (!g_lines || !g_resp_buf || !g_extract_buf) zenith::exit(1);
// Load fonts
auto load_font = [](const char* path) -> TrueTypeFont* {
TrueTypeFont* f = (TrueTypeFont*)zenith::malloc(sizeof(TrueTypeFont));
if (!f) return nullptr;
zenith::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init(path)) { zenith::mfree(f); return nullptr; }
return f;
};
g_font = load_font("0:/fonts/Roboto-Medium.ttf");
g_font_bold = load_font("0:/fonts/Roboto-Bold.ttf");
g_font_serif = load_font("0:/fonts/NotoSerif-SemiBold.ttf");
if (!g_font) zenith::exit(1);
g_line_h = g_font->get_line_height(FONT_SIZE) + 4;
// Create window
Zenith::WinCreateResult wres;
if (zenith::win_create("Wikipedia", INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
zenith::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
render(pixels);
zenith::win_present(win_id);
bool search_pending = false;
while (true) {
Zenith::WinEvent ev;
int r = zenith::win_poll(win_id, &ev);
if (r < 0) break; // window closed / error
if (r == 0) {
// No event — idle at ~60 fps
zenith::sleep_ms(16);
render(pixels);
zenith::win_present(win_id);
continue;
}
// ---- Handle event ----
if (ev.type == 3) break; // close
if (ev.type == 2) {
int new_w = ev.resize.w;
int new_h = ev.resize.h;
if (new_w > 0 && new_h > 0 && (new_w != g_win_w || new_h != g_win_h)) {
uint64_t new_va = zenith::win_resize(win_id, new_w, new_h);
if (new_va != 0) {
pixels = (uint32_t*)(uintptr_t)new_va;
g_win_w = new_w;
g_win_h = new_h;
if (g_phase == AppPhase::DONE && g_line_count > 0) {
build_display_lines(g_title, g_extract_buf, g_extract_len);
}
}
}
} else if (ev.type == 0 && ev.key.pressed) {
uint8_t ascii = ev.key.ascii;
uint8_t scan = ev.key.scancode;
if (ascii == '\n' || ascii == '\r') {
search_pending = true;
} else if (ascii == '\b' || scan == 0x0E) {
int len = (int)strlen(g_query);
if (len > 0) g_query[len - 1] = '\0';
} else if (ascii >= 32 && ascii < 127) {
int len = (int)strlen(g_query);
if (len < 254) { g_query[len] = ascii; g_query[len + 1] = '\0'; }
} else if (g_phase == AppPhase::DONE) {
// Navigation keys
int visible = (g_win_h - TOOLBAR_H - 1) / g_line_h;
int max_sc = g_line_count - visible;
if (max_sc < 0) max_sc = 0;
if (scan == 0x48) { if (g_scroll_y > 0) g_scroll_y--; }
else if (scan == 0x50) { if (g_scroll_y < max_sc) g_scroll_y++; }
else if (scan == 0x49) { g_scroll_y -= visible; if (g_scroll_y < 0) g_scroll_y = 0; }
else if (scan == 0x51) { g_scroll_y += visible; if (g_scroll_y > max_sc) g_scroll_y = max_sc; }
else if (scan == 0x47) { g_scroll_y = 0; }
else if (scan == 0x4F) { g_scroll_y = max_sc; }
}
} else if (ev.type == 1) {
// Mouse
int mx = ev.mouse.x, my = ev.mouse.y;
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
// Search button hit-test
int sb_h = TOOLBAR_H - 16;
int btn_w = 80;
int sb_w = g_win_w - 8 - 8 - btn_w - 8;
if (sb_w < 80) sb_w = 80;
int btn_x = 8 + sb_w + 8;
if (just_clicked && mx >= btn_x && mx < btn_x + btn_w &&
my >= 8 && my < 8 + sb_h) {
search_pending = true;
}
// Scroll wheel
if (ev.mouse.scroll != 0 && g_phase == AppPhase::DONE) {
int visible = (g_win_h - TOOLBAR_H - 1) / g_line_h;
int max_sc = g_line_count - visible;
if (max_sc < 0) max_sc = 0;
g_scroll_y += ev.mouse.scroll * 3;
if (g_scroll_y < 0) g_scroll_y = 0;
if (g_scroll_y > max_sc) g_scroll_y = max_sc;
}
}
// Trigger search if requested and query is non-empty
if (search_pending && g_query[0] != '\0') {
search_pending = false;
g_phase = AppPhase::LOADING;
render(pixels);
zenith::win_present(win_id);
do_search(g_query); // blocking
}
render(pixels);
zenith::win_present(win_id);
}
zenith::win_destroy(win_id);
zenith::exit(0);
}
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in ZenithOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <zenith/heap.h>
#include <zenith/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), zenith::malloc(x))
#define STBTT_free(x,u) ((void)(u), zenith::mfree(x))
#define STBTT_memcpy(d,s,n) zenith::memcpy(d,s,n)
#define STBTT_memset(d,v,n) zenith::memset(d,v,n)
#define STBTT_strlen(x) zenith::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>