diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..13b38d3 --- /dev/null +++ b/CLAUDE.md @@ -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 `` 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` diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 15012f3..ac63bcb 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -833,6 +833,15 @@ namespace Zenith { return WinServer::SendEvent(windowId, event); } + static uint64_t Sys_WinResize(int windowId, int newW, int newH) { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc == nullptr) return 0; + uint64_t outVa = 0; + int r = WinServer::Resize(windowId, proc->pid, proc->pml4Phys, newW, newH, + proc->heapNext, outVa); + return (r == 0) ? outVa : 0; + } + // ---- Dispatch ---- extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { @@ -986,6 +995,8 @@ namespace Zenith { return (int64_t)Sys_WinMap((int)frame->arg1); case SYS_WINSENDEVENT: return (int64_t)Sys_WinSendEvent((int)frame->arg1, (const WinEvent*)frame->arg2); + case SYS_WINRESIZE: + return (int64_t)Sys_WinResize((int)frame->arg1, (int)frame->arg2, (int)frame->arg3); case SYS_PROCLIST: return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2); case SYS_KILL: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 17eea57..f128d6b 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -74,6 +74,7 @@ namespace Zenith { static constexpr uint64_t SYS_WINENUM = 58; static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINSENDEVENT = 60; + static constexpr uint64_t SYS_WINRESIZE = 64; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index 21800ce..88f9259 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -162,6 +162,45 @@ namespace WinServer { return 0; } + int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, + uint64_t& heapNext, uint64_t& outVa) { + if (windowId < 0 || windowId >= MaxWindows) return -1; + WindowSlot& slot = g_slots[windowId]; + if (!slot.used || slot.ownerPid != callerPid) return -1; + if (newW <= 0 || newH <= 0) return -1; + if (newW == slot.width && newH == slot.height) { + outVa = slot.ownerVa; + return 0; + } + + uint64_t bufSize = (uint64_t)newW * newH * 4; + int numPages = (int)((bufSize + 0xFFF) / 0x1000); + if (numPages > MaxPixelPages) return -1; + + // Allocate new pages and map into owner's address space + uint64_t userVa = heapNext; + for (int i = 0; i < numPages; i++) { + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) return -1; + uint64_t physAddr = Memory::SubHHDM((uint64_t)page); + slot.pixelPhysPages[i] = physAddr; + Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000); + } + + slot.width = newW; + slot.height = newH; + slot.pixelNumPages = numPages; + slot.ownerVa = userVa; + heapNext += (uint64_t)numPages * 0x1000; + + // Invalidate desktop mapping so it re-maps on next enumerate + slot.desktopVa = 0; + slot.desktopPid = 0; + + outVa = userVa; + return 0; + } + void CleanupProcess(int pid) { for (int i = 0; i < MaxWindows; i++) { if (g_slots[i].used && g_slots[i].ownerPid == pid) { diff --git a/kernel/src/Api/WinServer.hpp b/kernel/src/Api/WinServer.hpp index 53fd63f..da8091b 100644 --- a/kernel/src/Api/WinServer.hpp +++ b/kernel/src/Api/WinServer.hpp @@ -37,6 +37,8 @@ namespace WinServer { int Enumerate(Zenith::WinInfo* outArray, int maxCount); uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext); int SendEvent(int windowId, const Zenith::WinEvent* event); + int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, + uint64_t& heapNext, uint64_t& outVa); void CleanupProcess(int pid); } diff --git a/kernel/src/Drivers/Graphics/IntelGPU.cpp b/kernel/src/Drivers/Graphics/IntelGPU.cpp index 8cc0360..77516d4 100644 --- a/kernel/src/Drivers/Graphics/IntelGPU.cpp +++ b/kernel/src/Drivers/Graphics/IntelGPU.cpp @@ -109,7 +109,7 @@ namespace Drivers::Graphics::IntelGPU { } else { // Unknown device ID - accept generically but warn g_gpuInfo.gen = 7; // Assume gen 7 as a safe default - g_gpuInfo.name = "Unknown Intel GPU"; + g_gpuInfo.name = "Intel GPU"; KernelLogStream(WARNING, "IntelGPU") << "Unknown Intel display controller " << "(device " << base::hex << (uint64_t)found->DeviceId << ")" diff --git a/kernel/src/Drivers/USB/HidMouse.cpp b/kernel/src/Drivers/USB/HidMouse.cpp index 6dfcd9b..d7f1209 100644 --- a/kernel/src/Drivers/USB/HidMouse.cpp +++ b/kernel/src/Drivers/USB/HidMouse.cpp @@ -1,6 +1,6 @@ /* * HidMouse.cpp - * USB HID Boot Protocol Mouse driver + * USB HID Mouse driver (Report Protocol with descriptor parsing) * Copyright (c) 2025 Daniel Hammer */ @@ -18,6 +18,173 @@ namespace Drivers::USB::HidMouse { // ------------------------------------------------------------------------- static uint8_t g_SlotId = 0; + static MouseReportFormat g_Format = {}; + static bool g_FormatValid = false; + + // ------------------------------------------------------------------------- + // HID Report Descriptor parsing + // ------------------------------------------------------------------------- + + // HID item size lookup: bSize field (bits 0-1) → byte count + static uint8_t ItemDataSize(uint8_t bSize) { + static const uint8_t sizes[] = {0, 1, 2, 4}; + return sizes[bSize & 0x03]; + } + + // Read an unsigned value from 1-4 bytes (little-endian) + static uint32_t ReadItemData(const uint8_t* p, uint8_t size) { + switch (size) { + case 1: return p[0]; + case 2: return (uint32_t)p[0] | ((uint32_t)p[1] << 8); + case 4: return (uint32_t)p[0] | ((uint32_t)p[1] << 8) + | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); + default: return 0; + } + } + + // HID usage page constants + static constexpr uint16_t USAGE_PAGE_GENERIC_DESKTOP = 0x01; + static constexpr uint16_t USAGE_PAGE_BUTTON = 0x09; + + // HID usage constants (Generic Desktop) + static constexpr uint16_t USAGE_X = 0x30; + static constexpr uint16_t USAGE_Y = 0x31; + static constexpr uint16_t USAGE_WHEEL = 0x38; + + // Maximum local usages we track between Input items + static constexpr int MAX_USAGES = 16; + + void ParseReportDescriptor(const uint8_t* desc, uint16_t length) { + g_FormatValid = false; + + MouseReportFormat fmt = {}; + + // Global state + uint16_t usagePage = 0; + uint32_t reportSize = 0; // bits per field + uint32_t reportCount = 0; // number of fields + + // Local state (reset after each Main item) + uint16_t usages[MAX_USAGES] = {}; + int usageCount = 0; + bool hasUsageRange = false; + + // Running bit offset within the report (excluding report ID byte) + uint16_t bitOffset = 0; + + uint16_t pos = 0; + while (pos < length) { + uint8_t header = desc[pos]; + + // Long items (0xFE prefix) — skip + if (header == 0xFE) { + if (pos + 1 >= length) break; + uint8_t dataSize = desc[pos + 1]; + pos += 3 + dataSize; + continue; + } + + uint8_t bSize = header & 0x03; + uint8_t bType = (header >> 2) & 0x03; + uint8_t bTag = (header >> 4) & 0x0F; + uint8_t dataSize = ItemDataSize(bSize); + + if (pos + 1 + dataSize > length) break; + uint32_t data = ReadItemData(&desc[pos + 1], dataSize); + pos += 1 + dataSize; + + if (bType == 1) { + // Global items + switch (bTag) { + case 0: usagePage = (uint16_t)data; break; // Usage Page + case 7: reportSize = data; break; // Report Size + case 8: fmt.hasReportId = true; // Report ID + fmt.reportId = (uint8_t)data; break; + case 9: reportCount = data; break; // Report Count + } + } else if (bType == 2) { + // Local items + switch (bTag) { + case 0: // Usage + if (usageCount < MAX_USAGES) + usages[usageCount++] = (uint16_t)data; + break; + case 1: hasUsageRange = true; break; // Usage Minimum + case 2: break; // Usage Maximum + } + } else if (bType == 0) { + // Main items + if (bTag == 8 || bTag == 9) { + // Input (tag 8) or Output (tag 9) + bool isConstant = (data & 0x01); + + if (bTag == 8 && !isConstant) { + // Data input fields — map usages to bit offsets + if (usagePage == USAGE_PAGE_BUTTON && hasUsageRange) { + fmt.buttonBitOffset = bitOffset; + fmt.buttonCount = (uint8_t)reportCount; + } else if (usagePage == USAGE_PAGE_GENERIC_DESKTOP) { + for (uint32_t i = 0; i < reportCount && i < (uint32_t)usageCount; i++) { + uint16_t u = usages[i]; + uint16_t off = bitOffset + (uint16_t)(i * reportSize); + if (u == USAGE_X) { + fmt.xBitOffset = off; + fmt.xBitSize = (uint8_t)reportSize; + } else if (u == USAGE_Y) { + fmt.yBitOffset = off; + fmt.yBitSize = (uint8_t)reportSize; + } else if (u == USAGE_WHEEL) { + fmt.scrollBitOffset = off; + fmt.scrollBitSize = (uint8_t)reportSize; + } + } + } + } + + // Advance bit offset for all input fields (data and constant/padding) + bitOffset += (uint16_t)(reportSize * reportCount); + } + + // Reset local state after any Main item + usageCount = 0; + hasUsageRange = false; + } + } + + // Validate: we need at least X and Y + if (fmt.xBitSize > 0 && fmt.yBitSize > 0) { + g_Format = fmt; + g_FormatValid = true; + + KernelLogStream(INFO, "USB/Mouse") + << "Report format: buttons=" << (uint64_t)fmt.buttonCount + << " X@" << (uint64_t)fmt.xBitOffset << ":" << (uint64_t)fmt.xBitSize + << " Y@" << (uint64_t)fmt.yBitOffset << ":" << (uint64_t)fmt.yBitSize + << " scroll=" << (uint64_t)fmt.scrollBitSize + << (fmt.hasReportId ? " (has report ID)" : ""); + } else { + KernelLogStream(WARNING, "USB/Mouse") << "Could not parse report descriptor, using boot protocol fallback"; + } + } + + // ------------------------------------------------------------------------- + // Bit-field extraction + // ------------------------------------------------------------------------- + + static int32_t ExtractSigned(const uint8_t* data, uint16_t bitOffset, uint8_t bitSize) { + int32_t value = 0; + for (uint8_t i = 0; i < bitSize; i++) { + uint16_t byteIdx = (bitOffset + i) / 8; + uint8_t bitIdx = (bitOffset + i) % 8; + if (data[byteIdx] & (1 << bitIdx)) + value |= (1 << i); + } + // Sign extend + if (bitSize < 32 && (value & (1 << (bitSize - 1)))) { + value |= ~((1 << bitSize) - 1); + } + return value; + } // ------------------------------------------------------------------------- // Public API @@ -25,25 +192,53 @@ namespace Drivers::USB::HidMouse { void RegisterDevice(uint8_t slotId) { g_SlotId = slotId; - KernelLogStream(OK, "USB/Mouse") << "Registered HID mouse on slot " << (uint64_t)slotId; } void ProcessReport(const uint8_t* data, uint16_t length) { if (length < 3) return; - // Boot protocol mouse report: - // Byte 0: Buttons (bit 0 = left, bit 1 = right, bit 2 = middle) - // Byte 1: X displacement (signed int8_t) - // Byte 2: Y displacement (signed int8_t) - // Byte 3: Scroll wheel (signed int8_t, optional) + if (!g_FormatValid) { + // Fallback: assume boot protocol format + uint8_t buttons = data[0] & 0x07; + int8_t deltaX = (int8_t)data[1]; + int8_t deltaY = (int8_t)data[2]; + int8_t scroll = (length >= 4) ? (int8_t)data[3] : 0; + Drivers::PS2::Mouse::InjectMouseReport(buttons, deltaX, deltaY, scroll); + return; + } - uint8_t buttons = data[0] & 0x07; - int8_t deltaX = (int8_t)data[1]; - int8_t deltaY = (int8_t)data[2]; - int8_t scroll = (length >= 4) ? (int8_t)data[3] : 0; + // Skip report ID byte if present + uint16_t byteOffset = g_Format.hasReportId ? 8 : 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); } } diff --git a/kernel/src/Drivers/USB/HidMouse.hpp b/kernel/src/Drivers/USB/HidMouse.hpp index 054e40a..a5767b1 100644 --- a/kernel/src/Drivers/USB/HidMouse.hpp +++ b/kernel/src/Drivers/USB/HidMouse.hpp @@ -1,6 +1,6 @@ /* * HidMouse.hpp - * USB HID Boot Protocol Mouse driver + * USB HID Mouse driver (Report Protocol with descriptor parsing) * Copyright (c) 2025 Daniel Hammer */ @@ -9,10 +9,28 @@ namespace Drivers::USB::HidMouse { + // Parsed layout of a mouse HID report + struct MouseReportFormat { + bool hasReportId; + uint8_t reportId; + uint8_t buttonBitOffset; // bit offset of first button + uint8_t buttonCount; + uint8_t xBitOffset; + uint8_t xBitSize; // 8 or 16 typically + uint8_t yBitOffset; + uint8_t yBitSize; + uint8_t scrollBitOffset; + uint8_t scrollBitSize; // 0 = no scroll wheel + }; + // Register a mouse device by slot ID void RegisterDevice(uint8_t slotId); - // Process a 3-4 byte boot protocol mouse report + // Parse a HID Report Descriptor to determine the report layout. + // Must be called before ProcessReport for Report Protocol mice. + void ParseReportDescriptor(const uint8_t* desc, uint16_t length); + + // Process an incoming mouse report using the parsed format void ProcessReport(const uint8_t* data, uint16_t length); }; diff --git a/kernel/src/Drivers/USB/UsbDevice.cpp b/kernel/src/Drivers/USB/UsbDevice.cpp index b811b71..03577c4 100644 --- a/kernel/src/Drivers/USB/UsbDevice.cpp +++ b/kernel/src/Drivers/USB/UsbDevice.cpp @@ -339,6 +339,7 @@ namespace Drivers::USB::UsbDevice { uint16_t offset = 0; bool foundHid = false; bool foundEp = false; + uint16_t hidReportDescLen = 0; while (offset + 2 <= totalLen) { uint8_t len = cfgBuf[offset]; @@ -360,6 +361,12 @@ namespace Drivers::USB::UsbDevice { } } + // HID descriptor (0x21): extract report descriptor length + if (type == DESC_HID && foundHid && !foundEp && len >= 9 && offset + 8 < totalLen) { + hidReportDescLen = (uint16_t)cfgBuf[offset + 7] + | ((uint16_t)cfgBuf[offset + 8] << 8); + } + if (type == DESC_ENDPOINT && foundHid && !foundEp && offset + sizeof(EndpointDescriptor) <= totalLen) { auto* ep = (EndpointDescriptor*)&cfgBuf[offset]; @@ -463,8 +470,9 @@ namespace Drivers::USB::UsbDevice { // ----------------------------------------------------------------- // Step 10: SET_PROTOCOL -- Boot Protocol for keyboards only // ----------------------------------------------------------------- - // Boot Protocol constrains mice to 3-byte reports (no scroll wheel). - // Keep mice in Report Protocol (the default) so scroll data is included. + // Set Boot Protocol for keyboards only. + // Mice stay in Report Protocol (the default) for scroll wheel support; + // HidMouse parses the HID Report Descriptor to handle variable formats. if (foundEp && dev->InterfaceProtocol == PROTOCOL_KEYBOARD) { cc = Xhci::ControlTransfer(slotId, REQTYPE_CLASS_IFACE, REQ_SET_PROTOCOL, 0, 0, 0, nullptr, false); @@ -474,6 +482,24 @@ namespace Drivers::USB::UsbDevice { } } + // ----------------------------------------------------------------- + // Step 10b: Fetch HID Report Descriptor for mice + // ----------------------------------------------------------------- + if (foundEp && dev->InterfaceProtocol == PROTOCOL_MOUSE && hidReportDescLen > 0) { + uint8_t rdBuf[256] = {}; + uint16_t rdLen = hidReportDescLen; + if (rdLen > 256) rdLen = 256; + + cc = Xhci::ControlTransfer(slotId, REQTYPE_STD_IFACE_IN, REQ_GET_DESCRIPTOR, + (DESC_HID_REPORT << 8), 0, rdLen, + rdBuf, true); + if (cc == Xhci::CC_SUCCESS || cc == Xhci::CC_SHORT_PACKET) { + HidMouse::ParseReportDescriptor(rdBuf, rdLen); + } else { + KernelLogStream(WARNING, "USB") << "GET_DESCRIPTOR(HID Report) failed, cc=" << (uint64_t)cc; + } + } + // ----------------------------------------------------------------- // Step 11: SET_IDLE(4) -- 16ms idle rate for software typematic // ----------------------------------------------------------------- diff --git a/kernel/src/Drivers/USB/UsbDevice.hpp b/kernel/src/Drivers/USB/UsbDevice.hpp index 4fffb80..d7a9e0d 100644 --- a/kernel/src/Drivers/USB/UsbDevice.hpp +++ b/kernel/src/Drivers/USB/UsbDevice.hpp @@ -71,6 +71,8 @@ namespace Drivers::USB::UsbDevice { constexpr uint8_t DESC_CONFIGURATION = 2; constexpr uint8_t DESC_INTERFACE = 4; constexpr uint8_t DESC_ENDPOINT = 5; + constexpr uint8_t DESC_HID = 0x21; + constexpr uint8_t DESC_HID_REPORT = 0x22; // USB class codes constexpr uint8_t CLASS_HID = 0x03; @@ -91,6 +93,7 @@ namespace Drivers::USB::UsbDevice { constexpr uint8_t REQTYPE_DEV_TO_HOST = 0x80; constexpr uint8_t REQTYPE_HOST_TO_DEV = 0x00; constexpr uint8_t REQTYPE_CLASS_IFACE = 0x21; // Host-to-device, class, interface + constexpr uint8_t REQTYPE_STD_IFACE_IN = 0x81; // Dev-to-host, standard, interface // Endpoint direction mask constexpr uint8_t EP_DIR_IN = 0x80; diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 278d5b8..35d52b7 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -57,7 +57,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia desktop SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, games are handled separately. @@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Home directory placeholder. HOMEKEEP := $(BINDIR)/home/.keep -.PHONY: all clean doom fetch wiki desktop icons fonts bearssl libc +.PHONY: all clean doom fetch wiki wikipedia desktop icons fonts bearssl libc -all: bearssl libc $(TARGETS) fetch wiki doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc $(TARGETS) fetch wiki wikipedia doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library. bearssl: @@ -102,6 +102,10 @@ fetch: bearssl libc wiki: bearssl libc $(MAKE) -C src/wiki +# Build wikipedia standalone GUI client (depends on bearssl and libc). +wikipedia: bearssl libc + $(MAKE) -C src/wikipedia + # Build desktop via its own Makefile. desktop: $(MAKE) -C src/desktop @@ -156,3 +160,4 @@ clean: $(MAKE) -C src/fetch clean $(MAKE) -C src/doom clean $(MAKE) -C src/desktop clean + $(MAKE) -C src/wikipedia clean diff --git a/programs/gui/fonts/Noto_Serif/NotoSerif-Italic-VariableFont_wdth,wght.ttf b/programs/gui/fonts/Noto_Serif/NotoSerif-Italic-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..e3d344b Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/NotoSerif-Italic-VariableFont_wdth,wght.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/NotoSerif-VariableFont_wdth,wght.ttf b/programs/gui/fonts/Noto_Serif/NotoSerif-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..1a24c3a Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/NotoSerif-VariableFont_wdth,wght.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/OFL.txt b/programs/gui/fonts/Noto_Serif/OFL.txt new file mode 100644 index 0000000..09f020b --- /dev/null +++ b/programs/gui/fonts/Noto_Serif/OFL.txt @@ -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. diff --git a/programs/gui/fonts/Noto_Serif/README.txt b/programs/gui/fonts/Noto_Serif/README.txt new file mode 100644 index 0000000..859477b --- /dev/null +++ b/programs/gui/fonts/Noto_Serif/README.txt @@ -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. diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Black.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Black.ttf new file mode 100644 index 0000000..f40660e Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Black.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-BlackItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-BlackItalic.ttf new file mode 100644 index 0000000..23217d9 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-BlackItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Bold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Bold.ttf new file mode 100644 index 0000000..519d313 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Bold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-BoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-BoldItalic.ttf new file mode 100644 index 0000000..23ea70a Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-BoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraBold.ttf new file mode 100644 index 0000000..49037de Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraBoldItalic.ttf new file mode 100644 index 0000000..77f20d9 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraLight.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraLight.ttf new file mode 100644 index 0000000..0941a12 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraLight.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraLightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraLightItalic.ttf new file mode 100644 index 0000000..22e1eb1 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ExtraLightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Italic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Italic.ttf new file mode 100644 index 0000000..c3ccb5f Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Italic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Light.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Light.ttf new file mode 100644 index 0000000..d019e89 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Light.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-LightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-LightItalic.ttf new file mode 100644 index 0000000..479fd0a Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-LightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Medium.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Medium.ttf new file mode 100644 index 0000000..180c1a9 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Medium.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-MediumItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-MediumItalic.ttf new file mode 100644 index 0000000..91e4d28 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-MediumItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf new file mode 100644 index 0000000..1b13cd7 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf new file mode 100644 index 0000000..a776350 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBoldItalic.ttf new file mode 100644 index 0000000..64a7a1f Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-Thin.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Thin.ttf new file mode 100644 index 0000000..797cbb8 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-Thin.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif-ThinItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ThinItalic.ttf new file mode 100644 index 0000000..c2832d4 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif-ThinItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Black.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Black.ttf new file mode 100644 index 0000000..551a90b Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Black.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-BlackItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-BlackItalic.ttf new file mode 100644 index 0000000..553566d Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-BlackItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Bold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Bold.ttf new file mode 100644 index 0000000..b589479 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Bold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-BoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-BoldItalic.ttf new file mode 100644 index 0000000..4ccb29b Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-BoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraBold.ttf new file mode 100644 index 0000000..9c866a8 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraBoldItalic.ttf new file mode 100644 index 0000000..3dad569 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraLight.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraLight.ttf new file mode 100644 index 0000000..524429f Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraLight.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraLightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraLightItalic.ttf new file mode 100644 index 0000000..b155b59 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ExtraLightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Italic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Italic.ttf new file mode 100644 index 0000000..0c42f43 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Italic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Light.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Light.ttf new file mode 100644 index 0000000..0c7486a Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Light.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-LightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-LightItalic.ttf new file mode 100644 index 0000000..d1e203c Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-LightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Medium.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Medium.ttf new file mode 100644 index 0000000..348f4f8 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Medium.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-MediumItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-MediumItalic.ttf new file mode 100644 index 0000000..0967039 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-MediumItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Regular.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Regular.ttf new file mode 100644 index 0000000..23b4321 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Regular.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-SemiBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-SemiBold.ttf new file mode 100644 index 0000000..0be5aca Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-SemiBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-SemiBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-SemiBoldItalic.ttf new file mode 100644 index 0000000..c3ce387 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-SemiBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Thin.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Thin.ttf new file mode 100644 index 0000000..10c7398 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-Thin.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ThinItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ThinItalic.ttf new file mode 100644 index 0000000..059b7c1 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_Condensed-ThinItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Black.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Black.ttf new file mode 100644 index 0000000..ed7629c Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Black.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-BlackItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-BlackItalic.ttf new file mode 100644 index 0000000..59303c9 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-BlackItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Bold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Bold.ttf new file mode 100644 index 0000000..69d7801 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Bold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-BoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-BoldItalic.ttf new file mode 100644 index 0000000..28c1123 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-BoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraBold.ttf new file mode 100644 index 0000000..45b37ea Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraBoldItalic.ttf new file mode 100644 index 0000000..a21541c Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraLight.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraLight.ttf new file mode 100644 index 0000000..05bae4c Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraLight.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraLightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraLightItalic.ttf new file mode 100644 index 0000000..856b1d3 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ExtraLightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Italic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Italic.ttf new file mode 100644 index 0000000..900fb75 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Italic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Light.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Light.ttf new file mode 100644 index 0000000..7b31903 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Light.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-LightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-LightItalic.ttf new file mode 100644 index 0000000..88a9c81 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-LightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Medium.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Medium.ttf new file mode 100644 index 0000000..875b5cc Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Medium.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-MediumItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-MediumItalic.ttf new file mode 100644 index 0000000..2a516b0 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-MediumItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Regular.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Regular.ttf new file mode 100644 index 0000000..4707ea2 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Regular.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-SemiBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-SemiBold.ttf new file mode 100644 index 0000000..925ac04 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-SemiBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-SemiBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-SemiBoldItalic.ttf new file mode 100644 index 0000000..b47d5d6 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-SemiBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Thin.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Thin.ttf new file mode 100644 index 0000000..da919d0 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-Thin.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ThinItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ThinItalic.ttf new file mode 100644 index 0000000..f79e437 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_ExtraCondensed-ThinItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Black.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Black.ttf new file mode 100644 index 0000000..2c0ff24 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Black.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-BlackItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-BlackItalic.ttf new file mode 100644 index 0000000..e28288a Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-BlackItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Bold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Bold.ttf new file mode 100644 index 0000000..d70a0d0 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Bold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-BoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-BoldItalic.ttf new file mode 100644 index 0000000..5fd4e4e Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-BoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraBold.ttf new file mode 100644 index 0000000..9235da6 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraBoldItalic.ttf new file mode 100644 index 0000000..4218eca Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraLight.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraLight.ttf new file mode 100644 index 0000000..7c60efe Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraLight.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraLightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraLightItalic.ttf new file mode 100644 index 0000000..bbe0de3 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ExtraLightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Italic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Italic.ttf new file mode 100644 index 0000000..9e67658 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Italic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Light.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Light.ttf new file mode 100644 index 0000000..1c3c298 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Light.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-LightItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-LightItalic.ttf new file mode 100644 index 0000000..ac9d28e Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-LightItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Medium.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Medium.ttf new file mode 100644 index 0000000..75f641c Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Medium.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-MediumItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-MediumItalic.ttf new file mode 100644 index 0000000..4896b93 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-MediumItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Regular.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Regular.ttf new file mode 100644 index 0000000..edc232b Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Regular.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-SemiBold.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-SemiBold.ttf new file mode 100644 index 0000000..0ea029d Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-SemiBold.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-SemiBoldItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-SemiBoldItalic.ttf new file mode 100644 index 0000000..73009ae Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-SemiBoldItalic.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Thin.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Thin.ttf new file mode 100644 index 0000000..aac9d53 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-Thin.ttf differ diff --git a/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ThinItalic.ttf b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ThinItalic.ttf new file mode 100644 index 0000000..5735489 Binary files /dev/null and b/programs/gui/fonts/Noto_Serif/static/NotoSerif_SemiCondensed-ThinItalic.ttf differ diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 43f309f..f5520eb 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -74,6 +74,7 @@ namespace Zenith { static constexpr uint64_t SYS_WINENUM = 58; static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINSENDEVENT = 60; + static constexpr uint64_t SYS_WINRESIZE = 64; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 0a9e3da..44ca103 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -317,5 +317,8 @@ namespace zenith { inline int win_sendevent(int id, const Zenith::WinEvent* event) { return (int)syscall2(Zenith::SYS_WINSENDEVENT, (uint64_t)id, (uint64_t)event); } + inline uint64_t win_resize(int id, int w, int h) { + return (uint64_t)syscall3(Zenith::SYS_WINRESIZE, (uint64_t)id, (uint64_t)w, (uint64_t)h); + } } diff --git a/programs/src/desktop/app_devexplorer.cpp b/programs/src/desktop/app_devexplorer.cpp index 71818be..a0016eb 100644 --- a/programs/src/desktop/app_devexplorer.cpp +++ b/programs/src/desktop/app_devexplorer.cpp @@ -315,7 +315,7 @@ static void devexplorer_on_mouse(Window* win, MouseEvent& ev) { // Scroll if (ev.scroll != 0) { - de->scroll_y -= ev.scroll; + de->scroll_y += ev.scroll; if (de->scroll_y < 0) de->scroll_y = 0; return; } diff --git a/programs/src/desktop/app_wiki.cpp b/programs/src/desktop/app_wiki.cpp index 2a568bc..c530942 100644 --- a/programs/src/desktop/app_wiki.cpp +++ b/programs/src/desktop/app_wiki.cpp @@ -1,581 +1,13 @@ /* * app_wiki.cpp - * ZenithOS Desktop - Wikipedia client application - * Spawns wiki.elf -d as a child process for non-blocking network I/O + * ZenithOS Desktop - Wikipedia launcher + * Spawns wikipedia.elf as a standalone Window Server process * Copyright (c) 2026 Daniel Hammer */ #include "apps_common.hpp" -// ============================================================================ -// Constants -// ============================================================================ - -static constexpr int WIKI_RESP_MAX = 131072; // 128 KB -static constexpr int WIKI_MAX_LINES = 4096; -static constexpr int WIKI_TOOLBAR_H = 36; -static constexpr int WIKI_SCROLLBAR_W = 12; - -// ============================================================================ -// Wiki state -// ============================================================================ - -struct WikiDisplayLine { - char text[256]; - Color color; -}; - -struct WikiState { - DesktopState* ds; - - enum Mode { IDLE, FETCHING, DONE, WIKI_ERROR }; - Mode mode; - char searchQuery[256]; - - WikiDisplayLine* lines; - int lineCount; - int lineCap; - int scrollY; // scroll offset in lines - - // Child process for non-blocking fetch - int child_pid; - char* respBuf; // accumulated child output - int respPos; // current position in respBuf - - char statusMsg[128]; -}; - -// ============================================================================ -// JSON string extraction (for parsing child output) -// ============================================================================ - -static int slen(const char* s) { int n = 0; while (s[n]) n++; return n; } - -static bool memeq(const char* a, const char* b, int n) { - for (int i = 0; i < n; i++) if (a[i] != b[i]) return false; - return true; -} - -static int wiki_extract_json_string(const char* buf, int len, const char* key, - char* out, int maxOut) { - int klen = slen(key); - - for (int i = 0; i < len - klen - 3; i++) { - if (buf[i] != '"') continue; - if (!memeq(buf + i + 1, key, klen)) continue; - if (buf[i + 1 + klen] != '"') continue; - if (buf[i + 2 + klen] != ':') continue; - - int p = i + 3 + klen; - while (p < len && (buf[p] == ' ' || buf[p] == '\t')) p++; - if (p >= len || buf[p] != '"') continue; - p++; - - int j = 0; - while (p < len && j < maxOut - 4) { - if (buf[p] == '"') break; - if (buf[p] == '\\' && p + 1 < len) { - p++; - switch (buf[p]) { - case '"': out[j++] = '"'; break; - case '\\': out[j++] = '\\'; break; - case 'n': out[j++] = '\n'; break; - case 'r': break; - case 't': out[j++] = '\t'; break; - case '/': out[j++] = '/'; break; - case 'u': { - if (p + 4 < len) { - unsigned val = 0; - for (int k = 1; k <= 4; k++) { - char h = buf[p + k]; - val <<= 4; - if (h >= '0' && h <= '9') val |= h - '0'; - else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10; - else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10; - } - p += 4; - if (val < 128) out[j++] = (char)val; - else if (val == 0x2013 || val == 0x2014) out[j++] = '-'; - else if (val == 0x2018 || val == 0x2019) out[j++] = '\''; - else if (val == 0x201C || val == 0x201D) out[j++] = '"'; - else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; } - else out[j++] = '?'; - } - break; - } - default: out[j++] = buf[p]; break; - } - } else { - out[j++] = buf[p]; - } - p++; - } - out[j] = '\0'; - return j; - } - out[0] = '\0'; - return 0; -} - -// ============================================================================ -// Display line building (word-wrap adapted for pixel widths) -// ============================================================================ - -static void wiki_add_line(WikiState* ws, const char* text, int len, Color color) { - if (ws->lineCount >= ws->lineCap) return; - WikiDisplayLine* dl = &ws->lines[ws->lineCount]; - int copyLen = len; - if (copyLen > 255) copyLen = 255; - for (int i = 0; i < copyLen; i++) dl->text[i] = text[i]; - dl->text[copyLen] = '\0'; - dl->color = color; - ws->lineCount++; -} - -static void wiki_wrap_text(WikiState* ws, const char* text, int textLen, - int maxChars, Color color) { - if (textLen <= 0 || maxChars <= 0) return; - const char* p = text; - const char* end = text + textLen; - - while (p < end && ws->lineCount < ws->lineCap) { - while (p < end && *p == ' ') p++; - if (p >= end) break; - - const char* lineStart = p; - const char* lastSpace = nullptr; - int col = 0; - - while (p < end && col < maxChars) { - if (*p == ' ') lastSpace = p; - p++; - col++; - } - - if (p >= end) { - wiki_add_line(ws, lineStart, (int)(p - lineStart), color); - } else if (lastSpace && lastSpace > lineStart) { - wiki_add_line(ws, lineStart, (int)(lastSpace - lineStart), color); - p = lastSpace + 1; - } else { - wiki_add_line(ws, lineStart, (int)(p - lineStart), color); - } - } -} - -static void wiki_build_display(WikiState* ws, const char* title, - const char* extract, int extractLen, int contentW) { - ws->lineCount = 0; - ws->scrollY = 0; - - int char_w = mono_cell_width(); - int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / char_w; - if (maxChars < 20) maxChars = 20; - - Color accent_c = colors::ACCENT; - Color green_c = Color::from_rgb(0x2E, 0x7D, 0x32); - Color text_c = colors::TEXT_COLOR; - - // Title - if (title && title[0]) { - wiki_wrap_text(ws, title, slen(title), maxChars, accent_c); - } - - // Blank separator - if (ws->lineCount > 0) - wiki_add_line(ws, "", 0, text_c); - - // Process extract line by line - const char* p = extract; - const char* end = extract + extractLen; - - while (p < end && ws->lineCount < ws->lineCap) { - const char* lineStart = p; - while (p < end && *p != '\n') p++; - int lineLen = (int)(p - lineStart); - if (p < end) p++; - - if (lineLen == 0) { - wiki_add_line(ws, "", 0, text_c); - continue; - } - - // Section header detection (== Title ==) - if (lineLen >= 4 && lineStart[0] == '=' && lineStart[1] == '=') { - int si = 0; - while (si < lineLen && lineStart[si] == '=') si++; - while (si < lineLen && lineStart[si] == ' ') si++; - int ei = lineLen; - while (ei > si && lineStart[ei - 1] == '=') ei--; - while (ei > si && lineStart[ei - 1] == ' ') ei--; - - wiki_add_line(ws, "", 0, text_c); - if (ei > si) { - char secBuf[256]; - int secLen = ei - si; - if (secLen > 255) secLen = 255; - for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i]; - secBuf[secLen] = '\0'; - wiki_add_line(ws, secBuf, secLen, green_c); - } - continue; - } - - // Regular text - wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_c); - } -} - -// ============================================================================ -// Process completed response from child -// ============================================================================ - -static void wiki_process_response(WikiState* ws) { - const char* body = ws->respBuf; - int bodyLen = ws->respPos; - - if (bodyLen <= 0) { - snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: no response from Wikipedia"); - ws->mode = WikiState::WIKI_ERROR; - return; - } - - // Check for error sentinel from child - if (bodyLen >= 1 && body[0] == '\x01') { - snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Article not found: %s", ws->searchQuery); - ws->mode = WikiState::WIKI_ERROR; - return; - } - - static char title[512], extractBuf[131072]; - wiki_extract_json_string(body, bodyLen, "title", title, sizeof(title)); - int extractLen = wiki_extract_json_string(body, bodyLen, "extract", - extractBuf, sizeof(extractBuf) - 1); - - if (extractLen == 0) { - snprintf(ws->statusMsg, sizeof(ws->statusMsg), "No content found for: %s", ws->searchQuery); - ws->mode = WikiState::WIKI_ERROR; - return; - } - - // Find content width from the window - int contentW = 580; - for (int i = 0; i < ws->ds->window_count; i++) { - Window* w = &ws->ds->windows[i]; - if (w->app_data == ws) { - Rect cr = w->content_rect(); - contentW = cr.w; - break; - } - } - - wiki_build_display(ws, title, extractBuf, extractLen, contentW); - ws->mode = WikiState::DONE; -} - -// ============================================================================ -// Callbacks -// ============================================================================ - -static void wiki_on_draw(Window* win, Framebuffer& fb) { - WikiState* ws = (WikiState*)win->app_data; - if (!ws) return; - - Canvas c(win); - c.fill(colors::WINDOW_BG); - - // ---- Toolbar background ---- - c.fill_rect(0, 0, c.w, WIKI_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); - - // Toolbar separator line - c.hline(0, WIKI_TOOLBAR_H, c.w, colors::BORDER); - - // ---- Draw search box outline ---- - int tb_x = 8; - int tb_y = 6; - int tb_w = c.w - 90; - int tb_h = 24; - if (tb_w < 100) tb_w = 100; - - // TextBox background + border - c.fill_rect(tb_x, tb_y, tb_w, tb_h, colors::WHITE); - c.rect(tb_x, tb_y, tb_w, tb_h, colors::BORDER); - - // Search text - c.text(tb_x + 4, tb_y + (tb_h - system_font_height()) / 2, ws->searchQuery, colors::TEXT_COLOR); - - // ---- Search button ---- - int btn_x = tb_x + tb_w + 6; - int btn_y = tb_y; - int btn_w = 66; - int btn_h = tb_h; - c.button(btn_x, btn_y, btn_w, btn_h, "Search", colors::ACCENT, colors::WHITE, 0); - - // ---- Content area ---- - int content_y = WIKI_TOOLBAR_H + 1; - int content_h = c.h - content_y; - int wiki_sfh = system_font_height(); - int visibleLines = content_h / (wiki_sfh + 4); - if (visibleLines < 1) visibleLines = 1; - - if (ws->mode == WikiState::FETCHING) { - c.text(16, content_y + 16, "Loading...", Color::from_rgb(0x88, 0x88, 0x88)); - } else if (ws->mode == WikiState::WIKI_ERROR) { - c.text(16, content_y + 16, ws->statusMsg, colors::CLOSE_BTN); - } else if (ws->mode == WikiState::IDLE) { - c.text(16, content_y + 16, - "Type a topic and press Enter or click Search.", - Color::from_rgb(0x88, 0x88, 0x88)); - } else if (ws->mode == WikiState::DONE && ws->lineCount > 0) { - int y = content_y + 8; - int lineH = wiki_sfh + 4; - for (int i = ws->scrollY; i < ws->lineCount && y + wiki_sfh < c.h; i++) { - WikiDisplayLine* dl = &ws->lines[i]; - if (dl->text[0] != '\0') { - c.text(12, y, dl->text, dl->color); - } - y += lineH; - } - - // ---- Scrollbar ---- - if (ws->lineCount > visibleLines) { - int sb_x = c.w - WIKI_SCROLLBAR_W; - int sb_y = content_y; - int sb_h = content_h; - - c.fill_rect(sb_x, sb_y, WIKI_SCROLLBAR_W, sb_h, colors::SCROLLBAR_BG); - - // Thumb - int maxScroll = ws->lineCount - visibleLines; - if (maxScroll < 1) maxScroll = 1; - int thumbH = (visibleLines * sb_h) / ws->lineCount; - if (thumbH < 20) thumbH = 20; - int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll; - - c.fill_rect(sb_x + 2, thumbY, WIKI_SCROLLBAR_W - 4, thumbH, colors::SCROLLBAR_FG); - } - } -} - -static void wiki_trigger_search(WikiState* ws) { - if (ws->searchQuery[0] == '\0') return; - if (ws->mode == WikiState::FETCHING) return; // already fetching - - ws->lineCount = 0; - ws->scrollY = 0; - ws->respPos = 0; - - // Build args string: "-d " - static char args[512]; - args[0] = '-'; args[1] = 'd'; args[2] = ' '; - int qi = 0; - while (ws->searchQuery[qi] && qi < 500) { - args[3 + qi] = ws->searchQuery[qi]; - qi++; - } - args[3 + qi] = '\0'; - - ws->child_pid = zenith::spawn_redir("0:/os/wiki.elf", args); - if (ws->child_pid <= 0) { - snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: could not start wiki process"); - ws->mode = WikiState::WIKI_ERROR; - return; - } - - ws->mode = WikiState::FETCHING; -} - -static void wiki_on_mouse(Window* win, MouseEvent& ev) { - WikiState* ws = (WikiState*)win->app_data; - if (!ws) return; - - Rect cr = win->content_rect(); - int cw = cr.w; - int local_x = ev.x - cr.x; - int local_y = ev.y - cr.y; - - // Check search button click - int tb_w = cw - 90; - if (tb_w < 100) tb_w = 100; - int btn_x = 8 + tb_w + 6; - int btn_y = 6; - int btn_w = 66; - int btn_h = 24; - - if (ev.left_pressed()) { - if (local_x >= btn_x && local_x < btn_x + btn_w && - local_y >= btn_y && local_y < btn_y + btn_h) { - wiki_trigger_search(ws); - return; - } - } - - // Scroll wheel - if (ev.scroll != 0 && ws->mode == WikiState::DONE && ws->lineCount > 0) { - int ch = cr.h; - int content_h = ch - WIKI_TOOLBAR_H - 1; - int visibleLines = content_h / (system_font_height() + 4); - int maxScroll = ws->lineCount - visibleLines; - if (maxScroll < 0) maxScroll = 0; - - ws->scrollY += ev.scroll * 3; - if (ws->scrollY < 0) ws->scrollY = 0; - if (ws->scrollY > maxScroll) ws->scrollY = maxScroll; - } -} - -static void wiki_on_key(Window* win, const Zenith::KeyEvent& key) { - WikiState* ws = (WikiState*)win->app_data; - if (!ws || !key.pressed) return; - - // Enter key triggers search - if (key.ascii == '\n' || key.ascii == '\r') { - wiki_trigger_search(ws); - return; - } - - // Page Up / Page Down / arrows - if (ws->mode == WikiState::DONE && ws->lineCount > 0) { - Rect cr = {0, 0, 0, 0}; - for (int i = 0; i < ws->ds->window_count; i++) { - Window* w = &ws->ds->windows[i]; - if (w->app_data == ws) { - cr = w->content_rect(); - break; - } - } - int content_h = cr.h - WIKI_TOOLBAR_H - 1; - int visibleLines = content_h / (system_font_height() + 4); - if (visibleLines < 1) visibleLines = 1; - int maxScroll = ws->lineCount - visibleLines; - if (maxScroll < 0) maxScroll = 0; - - if (key.scancode == 0x49) { // Page Up - ws->scrollY -= visibleLines; - if (ws->scrollY < 0) ws->scrollY = 0; - return; - } - if (key.scancode == 0x51) { // Page Down - ws->scrollY += visibleLines; - if (ws->scrollY > maxScroll) ws->scrollY = maxScroll; - return; - } - if (key.scancode == 0x48) { // Up arrow - if (ws->scrollY > 0) ws->scrollY--; - return; - } - if (key.scancode == 0x50) { // Down arrow - if (ws->scrollY < maxScroll) ws->scrollY++; - return; - } - if (key.scancode == 0x47) { // Home - ws->scrollY = 0; - return; - } - if (key.scancode == 0x4F) { // End - ws->scrollY = maxScroll; - return; - } - } - - // Text input for search box - if (key.ascii == '\b' || key.scancode == 0x0E) { - int len = zenith::slen(ws->searchQuery); - if (len > 0) ws->searchQuery[len - 1] = '\0'; - } else if (key.ascii >= 32 && key.ascii < 127) { - int len = zenith::slen(ws->searchQuery); - if (len < 254) { - ws->searchQuery[len] = key.ascii; - ws->searchQuery[len + 1] = '\0'; - } - } -} - -static void wiki_on_poll(Window* win) { - WikiState* ws = (WikiState*)win->app_data; - if (!ws) return; - if (ws->mode != WikiState::FETCHING || ws->child_pid <= 0) return; - - // Non-blocking read from child process - char buf[4096]; - int n = zenith::childio_read(ws->child_pid, buf, sizeof(buf)); - - if (n > 0) { - // Accumulate data, checking for EOT sentinel - for (int i = 0; i < n && ws->respPos < WIKI_RESP_MAX - 1; i++) { - if (buf[i] == '\x04') { - // EOT: child is done, process the response - ws->respBuf[ws->respPos] = '\0'; - ws->child_pid = -1; - wiki_process_response(ws); - return; - } - if (buf[i] == '\x01') { - // Error sentinel from child - ws->child_pid = -1; - snprintf(ws->statusMsg, sizeof(ws->statusMsg), - "Article not found: %s", ws->searchQuery); - ws->mode = WikiState::WIKI_ERROR; - return; - } - ws->respBuf[ws->respPos++] = buf[i]; - } - } else if (n < 0) { - // Child process exited — process whatever we accumulated - ws->child_pid = -1; - if (ws->respPos > 0) { - ws->respBuf[ws->respPos] = '\0'; - wiki_process_response(ws); - } else { - snprintf(ws->statusMsg, sizeof(ws->statusMsg), - "Error: fetch failed for \"%s\"", ws->searchQuery); - ws->mode = WikiState::WIKI_ERROR; - } - } -} - -static void wiki_on_close(Window* win) { - WikiState* ws = (WikiState*)win->app_data; - if (!ws) return; - - if (ws->lines) zenith::mfree(ws->lines); - if (ws->respBuf) zenith::mfree(ws->respBuf); - zenith::mfree(ws); - win->app_data = nullptr; -} - -// ============================================================================ -// Wikipedia launcher -// ============================================================================ - void open_wiki(DesktopState* ds) { - int idx = desktop_create_window(ds, "Wikipedia", 100, 80, 600, 480); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - WikiState* ws = (WikiState*)zenith::malloc(sizeof(WikiState)); - zenith::memset(ws, 0, sizeof(WikiState)); - ws->ds = ds; - ws->mode = WikiState::IDLE; - ws->searchQuery[0] = '\0'; - ws->scrollY = 0; - ws->child_pid = -1; - - // Allocate display lines - ws->lineCap = WIKI_MAX_LINES; - ws->lines = (WikiDisplayLine*)zenith::malloc(ws->lineCap * sizeof(WikiDisplayLine)); - ws->lineCount = 0; - - // Allocate response buffer - ws->respBuf = (char*)zenith::malloc(WIKI_RESP_MAX); - ws->respPos = 0; - - ws->statusMsg[0] = '\0'; - - win->app_data = ws; - win->on_draw = wiki_on_draw; - win->on_mouse = wiki_on_mouse; - win->on_key = wiki_on_key; - win->on_poll = wiki_on_poll; - win->on_close = wiki_on_close; + (void)ds; + zenith::spawn("0:/os/wikipedia.elf"); } diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 6816be1..899aca4 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -407,19 +407,60 @@ void gui::desktop_draw_panel(DesktopState* ds) { } // ============================================================================ -// App Menu (5 items with separator and rounded corners) +// App Menu (categorized) // ============================================================================ -static constexpr int MENU_ITEM_COUNT = 13; static constexpr int MENU_W = 220; -static constexpr int MENU_ITEM_H = 40; +static constexpr int MENU_ITEM_H = 36; +static constexpr int MENU_CAT_H = 28; +static constexpr int MENU_DIV_H = 10; + +struct MenuRow { + bool is_category; + const char* label; // "" for divider-only rows + int app_id; // -1 for category headers / dividers +}; + +static constexpr int MENU_ROW_COUNT = 18; +static const MenuRow menu_rows[MENU_ROW_COUNT] = { + { true, "Applications", -1 }, + { false, "Terminal", 0 }, + { false, "Files", 1 }, + { false, "Text Editor", 4 }, + { false, "Calculator", 3 }, + { true, "Internet", -1 }, + { false, "Wikipedia", 9 }, + { true, "System", -1 }, + { false, "System Info", 2 }, + { false, "Kernel Log", 5 }, + { false, "Processes", 6 }, + { false, "Devices", 8 }, + { true, "Games", -1 }, + { false, "Mandelbrot", 7 }, + { false, "DOOM", 10 }, + { true, "", -1 }, // divider + { false, "Settings", 11 }, + { false, "Reboot", 12 }, +}; + +static int menu_row_height(const MenuRow& row) { + if (!row.is_category) return MENU_ITEM_H; + return row.label[0] ? MENU_CAT_H : MENU_DIV_H; +} + +static int menu_total_height() { + int h = 10; // top + bottom padding + for (int i = 0; i < MENU_ROW_COUNT; i++) + h += menu_row_height(menu_rows[i]); + return h; +} static void desktop_draw_app_menu(DesktopState* ds) { Framebuffer& fb = ds->fb; int menu_x = 4; int menu_y = PANEL_HEIGHT + 2; - int menu_h = MENU_ITEM_H * MENU_ITEM_COUNT + 10; // +10 for padding + separator + int menu_h = menu_total_height(); // Menu shadow draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW); @@ -428,59 +469,70 @@ static void desktop_draw_app_menu(DesktopState* ds) { fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG); draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); - // Menu items - struct MenuItem { - const char* label; - SvgIcon* icon; - }; - MenuItem items[MENU_ITEM_COUNT] = { - { "Terminal", &ds->icon_terminal }, - { "Files", &ds->icon_filemanager }, - { "System Info", &ds->icon_sysinfo }, - { "Calculator", &ds->icon_calculator }, - { "Text Editor", &ds->icon_texteditor }, - { "Kernel Log", &ds->icon_terminal }, - { "Processes", &ds->icon_procmgr }, - { "Mandelbrot", &ds->icon_mandelbrot }, - { "Devices", &ds->icon_devexplorer }, - { "Wikipedia", &ds->icon_wikipedia }, - { "DOOM", &ds->icon_doom }, - { "Settings", &ds->icon_settings }, - { "Reboot", &ds->icon_reboot }, + // Icon lookup by app_id + SvgIcon* icons[13] = { + &ds->icon_terminal, // 0 + &ds->icon_filemanager, // 1 + &ds->icon_sysinfo, // 2 + &ds->icon_calculator, // 3 + &ds->icon_texteditor, // 4 + &ds->icon_terminal, // 5 (Kernel Log) + &ds->icon_procmgr, // 6 + &ds->icon_mandelbrot, // 7 + &ds->icon_devexplorer, // 8 + &ds->icon_wikipedia, // 9 + &ds->icon_doom, // 10 + &ds->icon_settings, // 11 + &ds->icon_reboot, // 12 }; int mx = ds->mouse.x; int my = ds->mouse.y; + int iy = menu_y + 5; - for (int i = 0; i < MENU_ITEM_COUNT; i++) { - int iy = menu_y + 4 + i * MENU_ITEM_H; + for (int i = 0; i < MENU_ROW_COUNT; i++) { + const MenuRow& row = menu_rows[i]; + int row_h = menu_row_height(row); - // Thin separator lines before utility apps and before Settings - if (i == 3 || i == 11) { - int sep_y = iy - 1; - for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) - fb.put_pixel(sx, sep_y, colors::BORDER); - iy += 1; + if (row.is_category) { + // Separator line above (except first category) + if (i > 0) { + for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) + fb.put_pixel(sx, iy + row_h / 2, colors::BORDER); + } + + // Category label (dimmed), skip for divider-only rows + if (row.label[0]) { + int tx = menu_x + 12; + int ty = iy + (row_h - system_font_height()) / 2; + if (i > 0) ty += 2; + draw_text(fb, tx, ty, row.label, Color::from_rgb(0x88, 0x88, 0x88)); + } + } else { + Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; + + // Hover highlight + if (item_rect.contains(mx, my)) { + fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER); + } + + // Icon + int icon_x = item_rect.x + 8; + int icon_y = item_rect.y + (row_h - 20) / 2; + if (row.app_id >= 0 && row.app_id < 13) { + SvgIcon* icon = icons[row.app_id]; + if (icon && icon->pixels) { + fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); + } + } + + // Label + int tx = icon_x + 28; + int ty = item_rect.y + (row_h - system_font_height()) / 2; + draw_text(fb, tx, ty, row.label, colors::TEXT_COLOR); } - Rect item_rect = {menu_x + 4, iy, MENU_W - 8, MENU_ITEM_H}; - - // Hover highlight - if (item_rect.contains(mx, my)) { - fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER); - } - - // Icon - int icon_x = item_rect.x + 8; - int icon_y = item_rect.y + (MENU_ITEM_H - 20) / 2; - if (items[i].icon && items[i].icon->pixels) { - fb.blit_alpha(icon_x, icon_y, items[i].icon->width, items[i].icon->height, items[i].icon->pixels); - } - - // Label - int tx = icon_x + 28; - int ty = item_rect.y + (MENU_ITEM_H - system_font_height()) / 2; - draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR); + iy += row_h; } } @@ -915,6 +967,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); } + } else { + Rect cr = win->content_rect(); + Zenith::WinEvent rev; + zenith::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + zenith::win_sendevent(win->ext_win_id, &rev); } } else if (mx >= ds->screen_w - 1) { win->saved_frame = win->frame; @@ -929,6 +989,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); } + } else { + Rect cr = win->content_rect(); + Zenith::WinEvent rev; + zenith::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + zenith::win_sendevent(win->ext_win_id, &rev); } } } @@ -987,8 +1055,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); } + win->dirty = true; + } else { + Rect cr = win->content_rect(); + Zenith::WinEvent rev; + zenith::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + zenith::win_sendevent(win->ext_win_id, &rev); } - win->dirty = true; } return; } @@ -998,29 +1074,37 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (ds->app_menu_open && left_pressed) { int menu_x = 4; int menu_y = PANEL_HEIGHT + 2; - int menu_h = MENU_ITEM_H * MENU_ITEM_COUNT + 10; + int menu_h = menu_total_height(); Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h}; if (menu_rect.contains(mx, my)) { - int rel_y = my - menu_y - 4; - int item_idx = rel_y / MENU_ITEM_H; - if (item_idx >= 0 && item_idx < MENU_ITEM_COUNT) { - switch (item_idx) { - case 0: open_terminal(ds); break; - case 1: open_filemanager(ds); break; - case 2: open_sysinfo(ds); break; - case 3: open_calculator(ds); break; - case 4: open_texteditor(ds); break; - case 5: open_klog(ds); break; - case 6: open_procmgr(ds); break; - case 7: open_mandelbrot(ds); break; - case 8: open_devexplorer(ds); break; - case 9: open_wiki(ds); break; - case 10: open_doom(ds); break; - case 11: open_settings(ds); break; - case 12: open_reboot_dialog(ds); break; + // Walk rows to find which one was clicked + int iy = menu_y + 5; + for (int i = 0; i < MENU_ROW_COUNT; i++) { + const MenuRow& row = menu_rows[i]; + int row_h = menu_row_height(row); + if (my >= iy && my < iy + row_h) { + if (!row.is_category) { + switch (row.app_id) { + case 0: open_terminal(ds); break; + case 1: open_filemanager(ds); break; + case 2: open_sysinfo(ds); break; + case 3: open_calculator(ds); break; + case 4: open_texteditor(ds); break; + case 5: open_klog(ds); break; + case 6: open_procmgr(ds); break; + case 7: open_mandelbrot(ds); break; + case 8: open_devexplorer(ds); break; + case 9: open_wiki(ds); break; + case 10: open_doom(ds); break; + case 11: open_settings(ds); break; + case 12: open_reboot_dialog(ds); break; + } + ds->app_menu_open = false; + } + break; } - ds->app_menu_open = false; + iy += row_h; } return; } else { @@ -1136,6 +1220,14 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); } + } else { + Rect cr = win->content_rect(); + Zenith::WinEvent rev; + zenith::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + zenith::win_sendevent(win->ext_win_id, &rev); } desktop_raise_window(ds, i); return; @@ -1323,6 +1415,17 @@ void desktop_poll_external_windows(DesktopState* ds) { if (extWins[e].dirty) { ds->windows[i].dirty = true; } + // Re-map if external app resized its buffer + if (extWins[e].width != ds->windows[i].content_w || + extWins[e].height != ds->windows[i].content_h) { + uint64_t va = zenith::win_map(extId); + if (va != 0) { + ds->windows[i].content = (uint32_t*)va; + ds->windows[i].content_w = extWins[e].width; + ds->windows[i].content_h = extWins[e].height; + ds->windows[i].dirty = true; + } + } break; } } diff --git a/programs/src/wikipedia/Makefile b/programs/src/wikipedia/Makefile new file mode 100644 index 0000000..e62b743 --- /dev/null +++ b/programs/src/wikipedia/Makefile @@ -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) diff --git a/programs/src/wikipedia/main.cpp b/programs/src/wikipedia/main.cpp new file mode 100644 index 0000000..d4c9c56 --- /dev/null +++ b/programs/src/wikipedia/main.cpp @@ -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 +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +} + +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); +} diff --git a/programs/src/wikipedia/stb_truetype_impl.cpp b/programs/src/wikipedia/stb_truetype_impl.cpp new file mode 100644 index 0000000..c598111 --- /dev/null +++ b/programs/src/wikipedia/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in ZenithOS freestanding environment + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include +#include +#include + +// 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 diff --git a/ramdisk.tar b/ramdisk.tar index 0d220df..137a34d 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ diff --git a/scripts/copy_fonts.sh b/scripts/copy_fonts.sh index 1531bf7..2ac178c 100755 --- a/scripts/copy_fonts.sh +++ b/scripts/copy_fonts.sh @@ -18,6 +18,8 @@ FONTS=( "programs/gui/fonts/Roboto/static/Roboto-Medium.ttf" "programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf" "programs/gui/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf" + "programs/gui/fonts/Noto_Serif/static/NotoSerif-Regular.ttf" + "programs/gui/fonts/Noto_Serif/static/NotoSerif-SemiBold.ttf" ) copied=0