fix: GUI and kernel fixes, Wikipedia app rewrite, among other things
This commit is contained in:
@@ -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`
|
||||||
@@ -833,6 +833,15 @@ namespace Zenith {
|
|||||||
return WinServer::SendEvent(windowId, event);
|
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 ----
|
// ---- Dispatch ----
|
||||||
|
|
||||||
extern "C" int64_t SyscallDispatch(SyscallFrame* frame) {
|
extern "C" int64_t SyscallDispatch(SyscallFrame* frame) {
|
||||||
@@ -986,6 +995,8 @@ namespace Zenith {
|
|||||||
return (int64_t)Sys_WinMap((int)frame->arg1);
|
return (int64_t)Sys_WinMap((int)frame->arg1);
|
||||||
case SYS_WINSENDEVENT:
|
case SYS_WINSENDEVENT:
|
||||||
return (int64_t)Sys_WinSendEvent((int)frame->arg1, (const WinEvent*)frame->arg2);
|
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:
|
case SYS_PROCLIST:
|
||||||
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
|
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
|
||||||
case SYS_KILL:
|
case SYS_KILL:
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ namespace Zenith {
|
|||||||
static constexpr uint64_t SYS_WINENUM = 58;
|
static constexpr uint64_t SYS_WINENUM = 58;
|
||||||
static constexpr uint64_t SYS_WINMAP = 59;
|
static constexpr uint64_t SYS_WINMAP = 59;
|
||||||
static constexpr uint64_t SYS_WINSENDEVENT = 60;
|
static constexpr uint64_t SYS_WINSENDEVENT = 60;
|
||||||
|
static constexpr uint64_t SYS_WINRESIZE = 64;
|
||||||
|
|
||||||
// Process management syscalls
|
// Process management syscalls
|
||||||
static constexpr uint64_t SYS_PROCLIST = 61;
|
static constexpr uint64_t SYS_PROCLIST = 61;
|
||||||
|
|||||||
@@ -162,6 +162,45 @@ namespace WinServer {
|
|||||||
return 0;
|
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) {
|
void CleanupProcess(int pid) {
|
||||||
for (int i = 0; i < MaxWindows; i++) {
|
for (int i = 0; i < MaxWindows; i++) {
|
||||||
if (g_slots[i].used && g_slots[i].ownerPid == pid) {
|
if (g_slots[i].used && g_slots[i].ownerPid == pid) {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ namespace WinServer {
|
|||||||
int Enumerate(Zenith::WinInfo* outArray, int maxCount);
|
int Enumerate(Zenith::WinInfo* outArray, int maxCount);
|
||||||
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext);
|
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext);
|
||||||
int SendEvent(int windowId, const Zenith::WinEvent* event);
|
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);
|
void CleanupProcess(int pid);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ namespace Drivers::Graphics::IntelGPU {
|
|||||||
} else {
|
} else {
|
||||||
// Unknown device ID - accept generically but warn
|
// Unknown device ID - accept generically but warn
|
||||||
g_gpuInfo.gen = 7; // Assume gen 7 as a safe default
|
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 "
|
KernelLogStream(WARNING, "IntelGPU") << "Unknown Intel display controller "
|
||||||
<< "(device " << base::hex << (uint64_t)found->DeviceId << ")"
|
<< "(device " << base::hex << (uint64_t)found->DeviceId << ")"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* HidMouse.cpp
|
* HidMouse.cpp
|
||||||
* USB HID Boot Protocol Mouse driver
|
* USB HID Mouse driver (Report Protocol with descriptor parsing)
|
||||||
* Copyright (c) 2025 Daniel Hammer
|
* Copyright (c) 2025 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,6 +18,173 @@ namespace Drivers::USB::HidMouse {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
static uint8_t g_SlotId = 0;
|
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
|
// Public API
|
||||||
@@ -25,25 +192,53 @@ namespace Drivers::USB::HidMouse {
|
|||||||
|
|
||||||
void RegisterDevice(uint8_t slotId) {
|
void RegisterDevice(uint8_t slotId) {
|
||||||
g_SlotId = slotId;
|
g_SlotId = slotId;
|
||||||
|
|
||||||
KernelLogStream(OK, "USB/Mouse") << "Registered HID mouse on slot " << (uint64_t)slotId;
|
KernelLogStream(OK, "USB/Mouse") << "Registered HID mouse on slot " << (uint64_t)slotId;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProcessReport(const uint8_t* data, uint16_t length) {
|
void ProcessReport(const uint8_t* data, uint16_t length) {
|
||||||
if (length < 3) return;
|
if (length < 3) return;
|
||||||
|
|
||||||
// Boot protocol mouse report:
|
if (!g_FormatValid) {
|
||||||
// Byte 0: Buttons (bit 0 = left, bit 1 = right, bit 2 = middle)
|
// Fallback: assume boot protocol format
|
||||||
// Byte 1: X displacement (signed int8_t)
|
uint8_t buttons = data[0] & 0x07;
|
||||||
// Byte 2: Y displacement (signed int8_t)
|
int8_t deltaX = (int8_t)data[1];
|
||||||
// Byte 3: Scroll wheel (signed int8_t, optional)
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t buttons = data[0] & 0x07;
|
// Skip report ID byte if present
|
||||||
int8_t deltaX = (int8_t)data[1];
|
uint16_t byteOffset = g_Format.hasReportId ? 8 : 0;
|
||||||
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);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* HidMouse.hpp
|
* HidMouse.hpp
|
||||||
* USB HID Boot Protocol Mouse driver
|
* USB HID Mouse driver (Report Protocol with descriptor parsing)
|
||||||
* Copyright (c) 2025 Daniel Hammer
|
* Copyright (c) 2025 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,10 +9,28 @@
|
|||||||
|
|
||||||
namespace Drivers::USB::HidMouse {
|
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
|
// Register a mouse device by slot ID
|
||||||
void RegisterDevice(uint8_t slotId);
|
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);
|
void ProcessReport(const uint8_t* data, uint16_t length);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
uint16_t offset = 0;
|
uint16_t offset = 0;
|
||||||
bool foundHid = false;
|
bool foundHid = false;
|
||||||
bool foundEp = false;
|
bool foundEp = false;
|
||||||
|
uint16_t hidReportDescLen = 0;
|
||||||
|
|
||||||
while (offset + 2 <= totalLen) {
|
while (offset + 2 <= totalLen) {
|
||||||
uint8_t len = cfgBuf[offset];
|
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 &&
|
if (type == DESC_ENDPOINT && foundHid && !foundEp &&
|
||||||
offset + sizeof(EndpointDescriptor) <= totalLen) {
|
offset + sizeof(EndpointDescriptor) <= totalLen) {
|
||||||
auto* ep = (EndpointDescriptor*)&cfgBuf[offset];
|
auto* ep = (EndpointDescriptor*)&cfgBuf[offset];
|
||||||
@@ -463,8 +470,9 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Step 10: SET_PROTOCOL -- Boot Protocol for keyboards only
|
// Step 10: SET_PROTOCOL -- Boot Protocol for keyboards only
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Boot Protocol constrains mice to 3-byte reports (no scroll wheel).
|
// Set Boot Protocol for keyboards only.
|
||||||
// Keep mice in Report Protocol (the default) so scroll data is included.
|
// 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) {
|
if (foundEp && dev->InterfaceProtocol == PROTOCOL_KEYBOARD) {
|
||||||
cc = Xhci::ControlTransfer(slotId, REQTYPE_CLASS_IFACE, REQ_SET_PROTOCOL,
|
cc = Xhci::ControlTransfer(slotId, REQTYPE_CLASS_IFACE, REQ_SET_PROTOCOL,
|
||||||
0, 0, 0, nullptr, false);
|
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
|
// Step 11: SET_IDLE(4) -- 16ms idle rate for software typematic
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ namespace Drivers::USB::UsbDevice {
|
|||||||
constexpr uint8_t DESC_CONFIGURATION = 2;
|
constexpr uint8_t DESC_CONFIGURATION = 2;
|
||||||
constexpr uint8_t DESC_INTERFACE = 4;
|
constexpr uint8_t DESC_INTERFACE = 4;
|
||||||
constexpr uint8_t DESC_ENDPOINT = 5;
|
constexpr uint8_t DESC_ENDPOINT = 5;
|
||||||
|
constexpr uint8_t DESC_HID = 0x21;
|
||||||
|
constexpr uint8_t DESC_HID_REPORT = 0x22;
|
||||||
|
|
||||||
// USB class codes
|
// USB class codes
|
||||||
constexpr uint8_t CLASS_HID = 0x03;
|
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_DEV_TO_HOST = 0x80;
|
||||||
constexpr uint8_t REQTYPE_HOST_TO_DEV = 0x00;
|
constexpr uint8_t REQTYPE_HOST_TO_DEV = 0x00;
|
||||||
constexpr uint8_t REQTYPE_CLASS_IFACE = 0x21; // Host-to-device, class, interface
|
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
|
// Endpoint direction mask
|
||||||
constexpr uint8_t EP_DIR_IN = 0x80;
|
constexpr uint8_t EP_DIR_IN = 0x80;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# 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))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, games are handled separately.
|
# 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.
|
# Home directory placeholder.
|
||||||
HOMEKEEP := $(BINDIR)/home/.keep
|
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.
|
# Build BearSSL static library.
|
||||||
bearssl:
|
bearssl:
|
||||||
@@ -102,6 +102,10 @@ fetch: bearssl libc
|
|||||||
wiki: bearssl libc
|
wiki: bearssl libc
|
||||||
$(MAKE) -C src/wiki
|
$(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.
|
# Build desktop via its own Makefile.
|
||||||
desktop:
|
desktop:
|
||||||
$(MAKE) -C src/desktop
|
$(MAKE) -C src/desktop
|
||||||
@@ -156,3 +160,4 @@ clean:
|
|||||||
$(MAKE) -C src/fetch clean
|
$(MAKE) -C src/fetch clean
|
||||||
$(MAKE) -C src/doom clean
|
$(MAKE) -C src/doom clean
|
||||||
$(MAKE) -C src/desktop clean
|
$(MAKE) -C src/desktop clean
|
||||||
|
$(MAKE) -C src/wikipedia clean
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||||
@@ -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 aren’t 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.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -74,6 +74,7 @@ namespace Zenith {
|
|||||||
static constexpr uint64_t SYS_WINENUM = 58;
|
static constexpr uint64_t SYS_WINENUM = 58;
|
||||||
static constexpr uint64_t SYS_WINMAP = 59;
|
static constexpr uint64_t SYS_WINMAP = 59;
|
||||||
static constexpr uint64_t SYS_WINSENDEVENT = 60;
|
static constexpr uint64_t SYS_WINSENDEVENT = 60;
|
||||||
|
static constexpr uint64_t SYS_WINRESIZE = 64;
|
||||||
|
|
||||||
// Process management syscalls
|
// Process management syscalls
|
||||||
static constexpr uint64_t SYS_PROCLIST = 61;
|
static constexpr uint64_t SYS_PROCLIST = 61;
|
||||||
|
|||||||
@@ -317,5 +317,8 @@ namespace zenith {
|
|||||||
inline int win_sendevent(int id, const Zenith::WinEvent* event) {
|
inline int win_sendevent(int id, const Zenith::WinEvent* event) {
|
||||||
return (int)syscall2(Zenith::SYS_WINSENDEVENT, (uint64_t)id, (uint64_t)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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ static void devexplorer_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
|
|
||||||
// Scroll
|
// Scroll
|
||||||
if (ev.scroll != 0) {
|
if (ev.scroll != 0) {
|
||||||
de->scroll_y -= ev.scroll;
|
de->scroll_y += ev.scroll;
|
||||||
if (de->scroll_y < 0) de->scroll_y = 0;
|
if (de->scroll_y < 0) de->scroll_y = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,581 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* app_wiki.cpp
|
* app_wiki.cpp
|
||||||
* ZenithOS Desktop - Wikipedia client application
|
* ZenithOS Desktop - Wikipedia launcher
|
||||||
* Spawns wiki.elf -d as a child process for non-blocking network I/O
|
* Spawns wikipedia.elf as a standalone Window Server process
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "apps_common.hpp"
|
#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) {
|
void open_wiki(DesktopState* ds) {
|
||||||
int idx = desktop_create_window(ds, "Wikipedia", 100, 80, 600, 480);
|
(void)ds;
|
||||||
if (idx < 0) return;
|
zenith::spawn("0:/os/wikipedia.elf");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+172
-69
@@ -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_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) {
|
static void desktop_draw_app_menu(DesktopState* ds) {
|
||||||
Framebuffer& fb = ds->fb;
|
Framebuffer& fb = ds->fb;
|
||||||
|
|
||||||
int menu_x = 4;
|
int menu_x = 4;
|
||||||
int menu_y = PANEL_HEIGHT + 2;
|
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
|
// Menu shadow
|
||||||
draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::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);
|
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);
|
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
|
||||||
|
|
||||||
// Menu items
|
// Icon lookup by app_id
|
||||||
struct MenuItem {
|
SvgIcon* icons[13] = {
|
||||||
const char* label;
|
&ds->icon_terminal, // 0
|
||||||
SvgIcon* icon;
|
&ds->icon_filemanager, // 1
|
||||||
};
|
&ds->icon_sysinfo, // 2
|
||||||
MenuItem items[MENU_ITEM_COUNT] = {
|
&ds->icon_calculator, // 3
|
||||||
{ "Terminal", &ds->icon_terminal },
|
&ds->icon_texteditor, // 4
|
||||||
{ "Files", &ds->icon_filemanager },
|
&ds->icon_terminal, // 5 (Kernel Log)
|
||||||
{ "System Info", &ds->icon_sysinfo },
|
&ds->icon_procmgr, // 6
|
||||||
{ "Calculator", &ds->icon_calculator },
|
&ds->icon_mandelbrot, // 7
|
||||||
{ "Text Editor", &ds->icon_texteditor },
|
&ds->icon_devexplorer, // 8
|
||||||
{ "Kernel Log", &ds->icon_terminal },
|
&ds->icon_wikipedia, // 9
|
||||||
{ "Processes", &ds->icon_procmgr },
|
&ds->icon_doom, // 10
|
||||||
{ "Mandelbrot", &ds->icon_mandelbrot },
|
&ds->icon_settings, // 11
|
||||||
{ "Devices", &ds->icon_devexplorer },
|
&ds->icon_reboot, // 12
|
||||||
{ "Wikipedia", &ds->icon_wikipedia },
|
|
||||||
{ "DOOM", &ds->icon_doom },
|
|
||||||
{ "Settings", &ds->icon_settings },
|
|
||||||
{ "Reboot", &ds->icon_reboot },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
int mx = ds->mouse.x;
|
int mx = ds->mouse.x;
|
||||||
int my = ds->mouse.y;
|
int my = ds->mouse.y;
|
||||||
|
int iy = menu_y + 5;
|
||||||
|
|
||||||
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
|
for (int i = 0; i < MENU_ROW_COUNT; i++) {
|
||||||
int iy = menu_y + 4 + i * MENU_ITEM_H;
|
const MenuRow& row = menu_rows[i];
|
||||||
|
int row_h = menu_row_height(row);
|
||||||
|
|
||||||
// Thin separator lines before utility apps and before Settings
|
if (row.is_category) {
|
||||||
if (i == 3 || i == 11) {
|
// Separator line above (except first category)
|
||||||
int sep_y = iy - 1;
|
if (i > 0) {
|
||||||
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
|
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
|
||||||
fb.put_pixel(sx, sep_y, colors::BORDER);
|
fb.put_pixel(sx, iy + row_h / 2, colors::BORDER);
|
||||||
iy += 1;
|
}
|
||||||
|
|
||||||
|
// 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};
|
iy += 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 + (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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,6 +967,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
|
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
|
||||||
zenith::memset(win->content, 0xFF, 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) {
|
} else if (mx >= ds->screen_w - 1) {
|
||||||
win->saved_frame = win->frame;
|
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);
|
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
|
||||||
zenith::memset(win->content, 0xFF, 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);
|
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
|
||||||
zenith::memset(win->content, 0xFF, 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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -998,29 +1074,37 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
if (ds->app_menu_open && left_pressed) {
|
if (ds->app_menu_open && left_pressed) {
|
||||||
int menu_x = 4;
|
int menu_x = 4;
|
||||||
int menu_y = PANEL_HEIGHT + 2;
|
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};
|
Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h};
|
||||||
|
|
||||||
if (menu_rect.contains(mx, my)) {
|
if (menu_rect.contains(mx, my)) {
|
||||||
int rel_y = my - menu_y - 4;
|
// Walk rows to find which one was clicked
|
||||||
int item_idx = rel_y / MENU_ITEM_H;
|
int iy = menu_y + 5;
|
||||||
if (item_idx >= 0 && item_idx < MENU_ITEM_COUNT) {
|
for (int i = 0; i < MENU_ROW_COUNT; i++) {
|
||||||
switch (item_idx) {
|
const MenuRow& row = menu_rows[i];
|
||||||
case 0: open_terminal(ds); break;
|
int row_h = menu_row_height(row);
|
||||||
case 1: open_filemanager(ds); break;
|
if (my >= iy && my < iy + row_h) {
|
||||||
case 2: open_sysinfo(ds); break;
|
if (!row.is_category) {
|
||||||
case 3: open_calculator(ds); break;
|
switch (row.app_id) {
|
||||||
case 4: open_texteditor(ds); break;
|
case 0: open_terminal(ds); break;
|
||||||
case 5: open_klog(ds); break;
|
case 1: open_filemanager(ds); break;
|
||||||
case 6: open_procmgr(ds); break;
|
case 2: open_sysinfo(ds); break;
|
||||||
case 7: open_mandelbrot(ds); break;
|
case 3: open_calculator(ds); break;
|
||||||
case 8: open_devexplorer(ds); break;
|
case 4: open_texteditor(ds); break;
|
||||||
case 9: open_wiki(ds); break;
|
case 5: open_klog(ds); break;
|
||||||
case 10: open_doom(ds); break;
|
case 6: open_procmgr(ds); break;
|
||||||
case 11: open_settings(ds); break;
|
case 7: open_mandelbrot(ds); break;
|
||||||
case 12: open_reboot_dialog(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;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -1136,6 +1220,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
|
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
|
||||||
zenith::memset(win->content, 0xFF, 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);
|
desktop_raise_window(ds, i);
|
||||||
return;
|
return;
|
||||||
@@ -1323,6 +1415,17 @@ void desktop_poll_external_windows(DesktopState* ds) {
|
|||||||
if (extWins[e].dirty) {
|
if (extWins[e].dirty) {
|
||||||
ds->windows[i].dirty = true;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
Binary file not shown.
@@ -18,6 +18,8 @@ FONTS=(
|
|||||||
"programs/gui/fonts/Roboto/static/Roboto-Medium.ttf"
|
"programs/gui/fonts/Roboto/static/Roboto-Medium.ttf"
|
||||||
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf"
|
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf"
|
||||||
"programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.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
|
copied=0
|
||||||
|
|||||||
Reference in New Issue
Block a user