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
+8 -3
View File
@@ -57,7 +57,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately).
CUSTOM_BUILDS := doom fetch wiki desktop
CUSTOM_BUILDS := doom fetch wiki wikipedia desktop
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, games are handled separately.
@@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom fetch wiki desktop icons fonts bearssl libc
.PHONY: all clean doom fetch wiki wikipedia desktop icons fonts bearssl libc
all: bearssl libc $(TARGETS) fetch wiki doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
all: bearssl libc $(TARGETS) fetch wiki wikipedia doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
# Build BearSSL static library.
bearssl:
@@ -102,6 +102,10 @@ fetch: bearssl libc
wiki: bearssl libc
$(MAKE) -C src/wiki
# Build wikipedia standalone GUI client (depends on bearssl and libc).
wikipedia: bearssl libc
$(MAKE) -C src/wikipedia
# Build desktop via its own Makefile.
desktop:
$(MAKE) -C src/desktop
@@ -156,3 +160,4 @@ clean:
$(MAKE) -C src/fetch clean
$(MAKE) -C src/doom clean
$(MAKE) -C src/desktop clean
$(MAKE) -C src/wikipedia clean
+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+136
View File
@@ -0,0 +1,136 @@
Noto Serif Variable Font
========================
This download contains Noto Serif as both variable fonts and static fonts.
Noto Serif is a variable font with these axes:
wdth
wght
This means all the styles are contained in these files:
NotoSerif-VariableFont_wdth,wght.ttf
NotoSerif-Italic-VariableFont_wdth,wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for Noto Serif:
static/NotoSerif_ExtraCondensed-Thin.ttf
static/NotoSerif_ExtraCondensed-ExtraLight.ttf
static/NotoSerif_ExtraCondensed-Light.ttf
static/NotoSerif_ExtraCondensed-Regular.ttf
static/NotoSerif_ExtraCondensed-Medium.ttf
static/NotoSerif_ExtraCondensed-SemiBold.ttf
static/NotoSerif_ExtraCondensed-Bold.ttf
static/NotoSerif_ExtraCondensed-ExtraBold.ttf
static/NotoSerif_ExtraCondensed-Black.ttf
static/NotoSerif_Condensed-Thin.ttf
static/NotoSerif_Condensed-ExtraLight.ttf
static/NotoSerif_Condensed-Light.ttf
static/NotoSerif_Condensed-Regular.ttf
static/NotoSerif_Condensed-Medium.ttf
static/NotoSerif_Condensed-SemiBold.ttf
static/NotoSerif_Condensed-Bold.ttf
static/NotoSerif_Condensed-ExtraBold.ttf
static/NotoSerif_Condensed-Black.ttf
static/NotoSerif_SemiCondensed-Thin.ttf
static/NotoSerif_SemiCondensed-ExtraLight.ttf
static/NotoSerif_SemiCondensed-Light.ttf
static/NotoSerif_SemiCondensed-Regular.ttf
static/NotoSerif_SemiCondensed-Medium.ttf
static/NotoSerif_SemiCondensed-SemiBold.ttf
static/NotoSerif_SemiCondensed-Bold.ttf
static/NotoSerif_SemiCondensed-ExtraBold.ttf
static/NotoSerif_SemiCondensed-Black.ttf
static/NotoSerif-Thin.ttf
static/NotoSerif-ExtraLight.ttf
static/NotoSerif-Light.ttf
static/NotoSerif-Regular.ttf
static/NotoSerif-Medium.ttf
static/NotoSerif-SemiBold.ttf
static/NotoSerif-Bold.ttf
static/NotoSerif-ExtraBold.ttf
static/NotoSerif-Black.ttf
static/NotoSerif_ExtraCondensed-ThinItalic.ttf
static/NotoSerif_ExtraCondensed-ExtraLightItalic.ttf
static/NotoSerif_ExtraCondensed-LightItalic.ttf
static/NotoSerif_ExtraCondensed-Italic.ttf
static/NotoSerif_ExtraCondensed-MediumItalic.ttf
static/NotoSerif_ExtraCondensed-SemiBoldItalic.ttf
static/NotoSerif_ExtraCondensed-BoldItalic.ttf
static/NotoSerif_ExtraCondensed-ExtraBoldItalic.ttf
static/NotoSerif_ExtraCondensed-BlackItalic.ttf
static/NotoSerif_Condensed-ThinItalic.ttf
static/NotoSerif_Condensed-ExtraLightItalic.ttf
static/NotoSerif_Condensed-LightItalic.ttf
static/NotoSerif_Condensed-Italic.ttf
static/NotoSerif_Condensed-MediumItalic.ttf
static/NotoSerif_Condensed-SemiBoldItalic.ttf
static/NotoSerif_Condensed-BoldItalic.ttf
static/NotoSerif_Condensed-ExtraBoldItalic.ttf
static/NotoSerif_Condensed-BlackItalic.ttf
static/NotoSerif_SemiCondensed-ThinItalic.ttf
static/NotoSerif_SemiCondensed-ExtraLightItalic.ttf
static/NotoSerif_SemiCondensed-LightItalic.ttf
static/NotoSerif_SemiCondensed-Italic.ttf
static/NotoSerif_SemiCondensed-MediumItalic.ttf
static/NotoSerif_SemiCondensed-SemiBoldItalic.ttf
static/NotoSerif_SemiCondensed-BoldItalic.ttf
static/NotoSerif_SemiCondensed-ExtraBoldItalic.ttf
static/NotoSerif_SemiCondensed-BlackItalic.ttf
static/NotoSerif-ThinItalic.ttf
static/NotoSerif-ExtraLightItalic.ttf
static/NotoSerif-LightItalic.ttf
static/NotoSerif-Italic.ttf
static/NotoSerif-MediumItalic.ttf
static/NotoSerif-SemiBoldItalic.ttf
static/NotoSerif-BoldItalic.ttf
static/NotoSerif-ExtraBoldItalic.ttf
static/NotoSerif-BlackItalic.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
License
-------
Please read the full license text (OFL.txt) to understand the permissions,
restrictions and requirements for usage, redistribution, and modification.
You can use them in your products & projects print or digital,
commercial or otherwise.
This isn't legal advice, please consider consulting a lawyer and see the full
license for all details.
+1
View File
@@ -74,6 +74,7 @@ namespace Zenith {
static constexpr uint64_t SYS_WINENUM = 58;
static constexpr uint64_t SYS_WINMAP = 59;
static constexpr uint64_t SYS_WINSENDEVENT = 60;
static constexpr uint64_t SYS_WINRESIZE = 64;
// Process management syscalls
static constexpr uint64_t SYS_PROCLIST = 61;
+3
View File
@@ -317,5 +317,8 @@ namespace zenith {
inline int win_sendevent(int id, const Zenith::WinEvent* event) {
return (int)syscall2(Zenith::SYS_WINSENDEVENT, (uint64_t)id, (uint64_t)event);
}
inline uint64_t win_resize(int id, int w, int h) {
return (uint64_t)syscall3(Zenith::SYS_WINRESIZE, (uint64_t)id, (uint64_t)w, (uint64_t)h);
}
}
+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>