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
+76
View File
@@ -0,0 +1,76 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
```bash
make # Build full ISO (kernel + programs + ramdisk)
make kernel # Build kernel only
make -C programs # Build all userspace programs only
make run # Build and run in QEMU (requires sudo for tap networking)
make run-bios # Run in QEMU with legacy BIOS boot
make clean # Remove build artifacts (preserves toolchain/limine)
make distclean # Full clean including downloaded dependencies
make toolchain # Build the cross-compilation toolchain
```
Building individual components:
```bash
make -C programs/src/desktop # Rebuild desktop environment
make -C programs/src/wikipedia # Rebuild Wikipedia standalone client
make -C programs/src/fetch # Rebuild fetch (HTTP client)
make -C programs/src/doom # Rebuild DOOM port
make -C programs/lib/bearssl # Rebuild BearSSL library
make -C programs/lib/libc # Rebuild minimal libc
```
Simple programs (single `main.cpp`) are auto-discovered and built by `programs/GNUmakefile`. Programs with custom build needs (desktop, doom, fetch, wiki, wikipedia) have their own Makefiles in their source directories.
## Build Pipeline
`make` → kernel binary + all programs → `scripts/mkramdisk.sh` tars `programs/bin/` → xorriso assembles ISO with Limine bootloader. The ramdisk (USTAR tar) becomes the root filesystem at boot, mounted as drive `0:`.
## Architecture Overview
**Kernel** (`kernel/src/`): Preemptive multitasking x86_64 kernel booted via Limine. C++20, freestanding, loaded at `0xffffffff80000000`. Key subsystems:
- `Api/` — Syscall dispatch (`Syscall.cpp`) and Window Server (`WinServer.cpp`)
- `Sched/` — Round-robin scheduler, 10ms time slices, max 16 processes
- `Memory/` — Physical page allocator, paging (per-process PML4), HHDM
- `Net/` — Full TCP/IP stack, sockets, DNS resolver
- `Drivers/` — E1000 NIC, Intel GPU, XHCI USB, PS/2 keyboard/mouse
- `Fs/` — VFS layer with ramdisk driver (reads tar archive)
**Userspace** (`programs/`): Programs loaded at `0x400000` (see `link.ld`). Entry point is `extern "C" void _start()`. No libc by default — syscalls via `<zenith/syscall.h>` wrappers. Programs needing libc/BearSSL link against `lib/libc` and `lib/bearssl`.
## Dual Syscall Headers
Syscall numbers are defined in **two mirrored copies** of `Api/Syscall.hpp`:
- `kernel/src/Api/Syscall.hpp` — used by kernel
- `programs/include/Api/Syscall.hpp` — used by userspace
**Both must be kept in sync** when adding or modifying syscalls. The userspace copy also contains shared struct definitions (`WinEvent`, `WinInfo`, `KeyEvent`, `DateTime`, `NetCfg`, etc.) used by both sides.
Userspace wrappers live in `programs/include/zenith/syscall.h` (inline functions using `syscall1`..`syscall6` asm helpers).
## GUI: Two Application Models
**Embedded apps** (compiled into `desktop.elf`): Source in `programs/src/desktop/app_*.cpp`. Render via callbacks (`on_draw`, `on_mouse`, `on_key`). The desktop allocates and manages their pixel buffers. These share a single process.
**External Window Server apps** (separate ELF processes, e.g. Wikipedia): Create a window via `win_create()` syscall, get a shared-memory pixel buffer, render directly, call `win_present()`. The desktop discovers them via `win_enumerate()`, maps their buffers with `win_map()`, and composites them. Events are forwarded via `win_sendevent()`. The kernel's `WinServer` (`kernel/src/Api/WinServer.cpp`) manages the shared-memory slots.
## Key Conventions
- **Compiler**: `x86_64-elf-g++` from `toolchain/local/` (auto-detected, falls back to system g++)
- **Standards**: GNU++20 (C++20 with extensions), freestanding, no exceptions, no RTTI
- **VFS paths**: `driveNum:/path` format, e.g. `0:/os/shell.elf`, `0:/fonts/Roboto-Medium.ttf`
- **Fonts**: TTF files in `programs/gui/fonts/`, copied to `programs/bin/fonts/` by `scripts/copy_fonts.sh`
- **Icons**: SVG files in `programs/gui/icons/` (Flat-Remix set), copied by `scripts/copy_icons.sh`
- **No test framework**: Validation is done by booting in QEMU (`make run`)
## Adding a New Syscall
1. Pick next available number in both `kernel/src/Api/Syscall.hpp` and `programs/include/Api/Syscall.hpp`
2. Implement `static ... Sys_Foo(...)` in `kernel/src/Api/Syscall.cpp`
3. Add `case SYS_FOO:` to the dispatch switch in `SyscallDispatch`
4. Add inline wrapper in `programs/include/zenith/syscall.h`
+11
View File
@@ -833,6 +833,15 @@ namespace Zenith {
return WinServer::SendEvent(windowId, event);
}
static uint64_t Sys_WinResize(int windowId, int newW, int newH) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0;
uint64_t outVa = 0;
int r = WinServer::Resize(windowId, proc->pid, proc->pml4Phys, newW, newH,
proc->heapNext, outVa);
return (r == 0) ? outVa : 0;
}
// ---- Dispatch ----
extern "C" int64_t SyscallDispatch(SyscallFrame* frame) {
@@ -986,6 +995,8 @@ namespace Zenith {
return (int64_t)Sys_WinMap((int)frame->arg1);
case SYS_WINSENDEVENT:
return (int64_t)Sys_WinSendEvent((int)frame->arg1, (const WinEvent*)frame->arg2);
case SYS_WINRESIZE:
return (int64_t)Sys_WinResize((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
case SYS_PROCLIST:
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
case SYS_KILL:
+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;
+39
View File
@@ -162,6 +162,45 @@ namespace WinServer {
return 0;
}
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa) {
if (windowId < 0 || windowId >= MaxWindows) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
if (newW <= 0 || newH <= 0) return -1;
if (newW == slot.width && newH == slot.height) {
outVa = slot.ownerVa;
return 0;
}
uint64_t bufSize = (uint64_t)newW * newH * 4;
int numPages = (int)((bufSize + 0xFFF) / 0x1000);
if (numPages > MaxPixelPages) return -1;
// Allocate new pages and map into owner's address space
uint64_t userVa = heapNext;
for (int i = 0; i < numPages; i++) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return -1;
uint64_t physAddr = Memory::SubHHDM((uint64_t)page);
slot.pixelPhysPages[i] = physAddr;
Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000);
}
slot.width = newW;
slot.height = newH;
slot.pixelNumPages = numPages;
slot.ownerVa = userVa;
heapNext += (uint64_t)numPages * 0x1000;
// Invalidate desktop mapping so it re-maps on next enumerate
slot.desktopVa = 0;
slot.desktopPid = 0;
outVa = userVa;
return 0;
}
void CleanupProcess(int pid) {
for (int i = 0; i < MaxWindows; i++) {
if (g_slots[i].used && g_slots[i].ownerPid == pid) {
+2
View File
@@ -37,6 +37,8 @@ namespace WinServer {
int Enumerate(Zenith::WinInfo* outArray, int maxCount);
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext);
int SendEvent(int windowId, const Zenith::WinEvent* event);
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa);
void CleanupProcess(int pid);
}
+1 -1
View File
@@ -109,7 +109,7 @@ namespace Drivers::Graphics::IntelGPU {
} else {
// Unknown device ID - accept generically but warn
g_gpuInfo.gen = 7; // Assume gen 7 as a safe default
g_gpuInfo.name = "Unknown Intel GPU";
g_gpuInfo.name = "Intel GPU";
KernelLogStream(WARNING, "IntelGPU") << "Unknown Intel display controller "
<< "(device " << base::hex << (uint64_t)found->DeviceId << ")"
+204 -9
View File
@@ -1,6 +1,6 @@
/*
* HidMouse.cpp
* USB HID Boot Protocol Mouse driver
* USB HID Mouse driver (Report Protocol with descriptor parsing)
* Copyright (c) 2025 Daniel Hammer
*/
@@ -18,6 +18,173 @@ namespace Drivers::USB::HidMouse {
// -------------------------------------------------------------------------
static uint8_t g_SlotId = 0;
static MouseReportFormat g_Format = {};
static bool g_FormatValid = false;
// -------------------------------------------------------------------------
// HID Report Descriptor parsing
// -------------------------------------------------------------------------
// HID item size lookup: bSize field (bits 0-1) → byte count
static uint8_t ItemDataSize(uint8_t bSize) {
static const uint8_t sizes[] = {0, 1, 2, 4};
return sizes[bSize & 0x03];
}
// Read an unsigned value from 1-4 bytes (little-endian)
static uint32_t ReadItemData(const uint8_t* p, uint8_t size) {
switch (size) {
case 1: return p[0];
case 2: return (uint32_t)p[0] | ((uint32_t)p[1] << 8);
case 4: return (uint32_t)p[0] | ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
default: return 0;
}
}
// HID usage page constants
static constexpr uint16_t USAGE_PAGE_GENERIC_DESKTOP = 0x01;
static constexpr uint16_t USAGE_PAGE_BUTTON = 0x09;
// HID usage constants (Generic Desktop)
static constexpr uint16_t USAGE_X = 0x30;
static constexpr uint16_t USAGE_Y = 0x31;
static constexpr uint16_t USAGE_WHEEL = 0x38;
// Maximum local usages we track between Input items
static constexpr int MAX_USAGES = 16;
void ParseReportDescriptor(const uint8_t* desc, uint16_t length) {
g_FormatValid = false;
MouseReportFormat fmt = {};
// Global state
uint16_t usagePage = 0;
uint32_t reportSize = 0; // bits per field
uint32_t reportCount = 0; // number of fields
// Local state (reset after each Main item)
uint16_t usages[MAX_USAGES] = {};
int usageCount = 0;
bool hasUsageRange = false;
// Running bit offset within the report (excluding report ID byte)
uint16_t bitOffset = 0;
uint16_t pos = 0;
while (pos < length) {
uint8_t header = desc[pos];
// Long items (0xFE prefix) — skip
if (header == 0xFE) {
if (pos + 1 >= length) break;
uint8_t dataSize = desc[pos + 1];
pos += 3 + dataSize;
continue;
}
uint8_t bSize = header & 0x03;
uint8_t bType = (header >> 2) & 0x03;
uint8_t bTag = (header >> 4) & 0x0F;
uint8_t dataSize = ItemDataSize(bSize);
if (pos + 1 + dataSize > length) break;
uint32_t data = ReadItemData(&desc[pos + 1], dataSize);
pos += 1 + dataSize;
if (bType == 1) {
// Global items
switch (bTag) {
case 0: usagePage = (uint16_t)data; break; // Usage Page
case 7: reportSize = data; break; // Report Size
case 8: fmt.hasReportId = true; // Report ID
fmt.reportId = (uint8_t)data; break;
case 9: reportCount = data; break; // Report Count
}
} else if (bType == 2) {
// Local items
switch (bTag) {
case 0: // Usage
if (usageCount < MAX_USAGES)
usages[usageCount++] = (uint16_t)data;
break;
case 1: hasUsageRange = true; break; // Usage Minimum
case 2: break; // Usage Maximum
}
} else if (bType == 0) {
// Main items
if (bTag == 8 || bTag == 9) {
// Input (tag 8) or Output (tag 9)
bool isConstant = (data & 0x01);
if (bTag == 8 && !isConstant) {
// Data input fields — map usages to bit offsets
if (usagePage == USAGE_PAGE_BUTTON && hasUsageRange) {
fmt.buttonBitOffset = bitOffset;
fmt.buttonCount = (uint8_t)reportCount;
} else if (usagePage == USAGE_PAGE_GENERIC_DESKTOP) {
for (uint32_t i = 0; i < reportCount && i < (uint32_t)usageCount; i++) {
uint16_t u = usages[i];
uint16_t off = bitOffset + (uint16_t)(i * reportSize);
if (u == USAGE_X) {
fmt.xBitOffset = off;
fmt.xBitSize = (uint8_t)reportSize;
} else if (u == USAGE_Y) {
fmt.yBitOffset = off;
fmt.yBitSize = (uint8_t)reportSize;
} else if (u == USAGE_WHEEL) {
fmt.scrollBitOffset = off;
fmt.scrollBitSize = (uint8_t)reportSize;
}
}
}
}
// Advance bit offset for all input fields (data and constant/padding)
bitOffset += (uint16_t)(reportSize * reportCount);
}
// Reset local state after any Main item
usageCount = 0;
hasUsageRange = false;
}
}
// Validate: we need at least X and Y
if (fmt.xBitSize > 0 && fmt.yBitSize > 0) {
g_Format = fmt;
g_FormatValid = true;
KernelLogStream(INFO, "USB/Mouse")
<< "Report format: buttons=" << (uint64_t)fmt.buttonCount
<< " X@" << (uint64_t)fmt.xBitOffset << ":" << (uint64_t)fmt.xBitSize
<< " Y@" << (uint64_t)fmt.yBitOffset << ":" << (uint64_t)fmt.yBitSize
<< " scroll=" << (uint64_t)fmt.scrollBitSize
<< (fmt.hasReportId ? " (has report ID)" : "");
} else {
KernelLogStream(WARNING, "USB/Mouse") << "Could not parse report descriptor, using boot protocol fallback";
}
}
// -------------------------------------------------------------------------
// Bit-field extraction
// -------------------------------------------------------------------------
static int32_t ExtractSigned(const uint8_t* data, uint16_t bitOffset, uint8_t bitSize) {
int32_t value = 0;
for (uint8_t i = 0; i < bitSize; i++) {
uint16_t byteIdx = (bitOffset + i) / 8;
uint8_t bitIdx = (bitOffset + i) % 8;
if (data[byteIdx] & (1 << bitIdx))
value |= (1 << i);
}
// Sign extend
if (bitSize < 32 && (value & (1 << (bitSize - 1)))) {
value |= ~((1 << bitSize) - 1);
}
return value;
}
// -------------------------------------------------------------------------
// Public API
@@ -25,25 +192,53 @@ namespace Drivers::USB::HidMouse {
void RegisterDevice(uint8_t slotId) {
g_SlotId = slotId;
KernelLogStream(OK, "USB/Mouse") << "Registered HID mouse on slot " << (uint64_t)slotId;
}
void ProcessReport(const uint8_t* data, uint16_t length) {
if (length < 3) return;
// Boot protocol mouse report:
// Byte 0: Buttons (bit 0 = left, bit 1 = right, bit 2 = middle)
// Byte 1: X displacement (signed int8_t)
// Byte 2: Y displacement (signed int8_t)
// Byte 3: Scroll wheel (signed int8_t, optional)
if (!g_FormatValid) {
// Fallback: assume boot protocol format
uint8_t buttons = data[0] & 0x07;
int8_t deltaX = (int8_t)data[1];
int8_t deltaY = (int8_t)data[2];
int8_t scroll = (length >= 4) ? (int8_t)data[3] : 0;
Drivers::PS2::Mouse::InjectMouseReport(buttons, deltaX, deltaY, scroll);
return;
}
// Skip report ID byte if present
uint16_t byteOffset = g_Format.hasReportId ? 8 : 0;
// Extract buttons
uint8_t buttons = 0;
for (uint8_t i = 0; i < g_Format.buttonCount && i < 8; i++) {
uint16_t bit = byteOffset + g_Format.buttonBitOffset + i;
if (data[bit / 8] & (1 << (bit % 8)))
buttons |= (1 << i);
}
// Extract X, Y
int32_t rawX = ExtractSigned(data, byteOffset + g_Format.xBitOffset, g_Format.xBitSize);
int32_t rawY = ExtractSigned(data, byteOffset + g_Format.yBitOffset, g_Format.yBitSize);
// Clamp to int8_t range for InjectMouseReport
if (rawX > 127) rawX = 127;
if (rawX < -128) rawX = -128;
if (rawY > 127) rawY = 127;
if (rawY < -128) rawY = -128;
// Extract scroll wheel
int8_t scroll = 0;
if (g_Format.scrollBitSize > 0) {
int32_t rawScroll = ExtractSigned(data, byteOffset + g_Format.scrollBitOffset, g_Format.scrollBitSize);
if (rawScroll > 127) rawScroll = 127;
if (rawScroll < -128) rawScroll = -128;
scroll = (int8_t)rawScroll;
}
Drivers::PS2::Mouse::InjectMouseReport(buttons, (int8_t)rawX, (int8_t)rawY, scroll);
}
}
+20 -2
View File
@@ -1,6 +1,6 @@
/*
* HidMouse.hpp
* USB HID Boot Protocol Mouse driver
* USB HID Mouse driver (Report Protocol with descriptor parsing)
* Copyright (c) 2025 Daniel Hammer
*/
@@ -9,10 +9,28 @@
namespace Drivers::USB::HidMouse {
// Parsed layout of a mouse HID report
struct MouseReportFormat {
bool hasReportId;
uint8_t reportId;
uint8_t buttonBitOffset; // bit offset of first button
uint8_t buttonCount;
uint8_t xBitOffset;
uint8_t xBitSize; // 8 or 16 typically
uint8_t yBitOffset;
uint8_t yBitSize;
uint8_t scrollBitOffset;
uint8_t scrollBitSize; // 0 = no scroll wheel
};
// Register a mouse device by slot ID
void RegisterDevice(uint8_t slotId);
// Process a 3-4 byte boot protocol mouse report
// Parse a HID Report Descriptor to determine the report layout.
// Must be called before ProcessReport for Report Protocol mice.
void ParseReportDescriptor(const uint8_t* desc, uint16_t length);
// Process an incoming mouse report using the parsed format
void ProcessReport(const uint8_t* data, uint16_t length);
};
+28 -2
View File
@@ -339,6 +339,7 @@ namespace Drivers::USB::UsbDevice {
uint16_t offset = 0;
bool foundHid = false;
bool foundEp = false;
uint16_t hidReportDescLen = 0;
while (offset + 2 <= totalLen) {
uint8_t len = cfgBuf[offset];
@@ -360,6 +361,12 @@ namespace Drivers::USB::UsbDevice {
}
}
// HID descriptor (0x21): extract report descriptor length
if (type == DESC_HID && foundHid && !foundEp && len >= 9 && offset + 8 < totalLen) {
hidReportDescLen = (uint16_t)cfgBuf[offset + 7]
| ((uint16_t)cfgBuf[offset + 8] << 8);
}
if (type == DESC_ENDPOINT && foundHid && !foundEp &&
offset + sizeof(EndpointDescriptor) <= totalLen) {
auto* ep = (EndpointDescriptor*)&cfgBuf[offset];
@@ -463,8 +470,9 @@ namespace Drivers::USB::UsbDevice {
// -----------------------------------------------------------------
// Step 10: SET_PROTOCOL -- Boot Protocol for keyboards only
// -----------------------------------------------------------------
// Boot Protocol constrains mice to 3-byte reports (no scroll wheel).
// Keep mice in Report Protocol (the default) so scroll data is included.
// Set Boot Protocol for keyboards only.
// Mice stay in Report Protocol (the default) for scroll wheel support;
// HidMouse parses the HID Report Descriptor to handle variable formats.
if (foundEp && dev->InterfaceProtocol == PROTOCOL_KEYBOARD) {
cc = Xhci::ControlTransfer(slotId, REQTYPE_CLASS_IFACE, REQ_SET_PROTOCOL,
0, 0, 0, nullptr, false);
@@ -474,6 +482,24 @@ namespace Drivers::USB::UsbDevice {
}
}
// -----------------------------------------------------------------
// Step 10b: Fetch HID Report Descriptor for mice
// -----------------------------------------------------------------
if (foundEp && dev->InterfaceProtocol == PROTOCOL_MOUSE && hidReportDescLen > 0) {
uint8_t rdBuf[256] = {};
uint16_t rdLen = hidReportDescLen;
if (rdLen > 256) rdLen = 256;
cc = Xhci::ControlTransfer(slotId, REQTYPE_STD_IFACE_IN, REQ_GET_DESCRIPTOR,
(DESC_HID_REPORT << 8), 0, rdLen,
rdBuf, true);
if (cc == Xhci::CC_SUCCESS || cc == Xhci::CC_SHORT_PACKET) {
HidMouse::ParseReportDescriptor(rdBuf, rdLen);
} else {
KernelLogStream(WARNING, "USB") << "GET_DESCRIPTOR(HID Report) failed, cc=" << (uint64_t)cc;
}
}
// -----------------------------------------------------------------
// Step 11: SET_IDLE(4) -- 16ms idle rate for software typematic
// -----------------------------------------------------------------
+3
View File
@@ -71,6 +71,8 @@ namespace Drivers::USB::UsbDevice {
constexpr uint8_t DESC_CONFIGURATION = 2;
constexpr uint8_t DESC_INTERFACE = 4;
constexpr uint8_t DESC_ENDPOINT = 5;
constexpr uint8_t DESC_HID = 0x21;
constexpr uint8_t DESC_HID_REPORT = 0x22;
// USB class codes
constexpr uint8_t CLASS_HID = 0x03;
@@ -91,6 +93,7 @@ namespace Drivers::USB::UsbDevice {
constexpr uint8_t REQTYPE_DEV_TO_HOST = 0x80;
constexpr uint8_t REQTYPE_HOST_TO_DEV = 0x00;
constexpr uint8_t REQTYPE_CLASS_IFACE = 0x21; // Host-to-device, class, interface
constexpr uint8_t REQTYPE_STD_IFACE_IN = 0x81; // Dev-to-host, standard, interface
// Endpoint direction mask
constexpr uint8_t EP_DIR_IN = 0x80;
+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");
}
+145 -42
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,42 +469,47 @@ 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;
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, sep_y, colors::BORDER);
iy += 1;
fb.put_pixel(sx, iy + row_h / 2, colors::BORDER);
}
Rect item_rect = {menu_x + 4, iy, MENU_W - 8, MENU_ITEM_H};
// 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)) {
@@ -472,15 +518,21 @@ static void desktop_draw_app_menu(DesktopState* ds) {
// 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);
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 + (MENU_ITEM_H - system_font_height()) / 2;
draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR);
int ty = item_rect.y + (row_h - system_font_height()) / 2;
draw_text(fb, tx, ty, row.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);
}
}
return;
}
@@ -998,14 +1074,18 @@ 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) {
// 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;
@@ -1022,6 +1102,10 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
}
ds->app_menu_open = false;
}
break;
}
iy += row_h;
}
return;
} else {
ds->app_menu_open = false;
@@ -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>
BIN
View File
Binary file not shown.
+2
View File
@@ -18,6 +18,8 @@ FONTS=(
"programs/gui/fonts/Roboto/static/Roboto-Medium.ttf"
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf"
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf"
"programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf"
"programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf"
)
copied=0