From e0bfc97f0faa6b2bf2b85620dd68cba72f1d6108 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sat, 21 Feb 2026 13:05:28 +0100 Subject: [PATCH] feat: common TLS library, weather app, UI scaling fix in Window Server, and more --- kernel/src/Api/Syscall.cpp | 14 + kernel/src/Api/Syscall.hpp | 5 +- kernel/src/Api/WinServer.cpp | 23 + kernel/src/Api/WinServer.hpp | 2 + programs/GNUmakefile | 28 +- programs/include/Api/Syscall.hpp | 5 +- programs/include/gui/desktop.hpp | 2 + programs/include/gui/svg.hpp | 138 ++++- programs/include/gui/truetype.hpp | 4 +- programs/include/tls/tls.hpp | 45 ++ programs/include/zenith/syscall.h | 6 + programs/lib/tls/Makefile | 72 +++ programs/lib/tls/libtls.a | Bin 0 -> 105850 bytes programs/lib/tls/obj/tls.o | Bin 0 -> 105456 bytes programs/lib/tls/tls.cpp | 279 ++++++++++ programs/src/desktop/Makefile | 2 +- programs/src/desktop/app_settings.cpp | 3 + programs/src/desktop/app_weather.cpp | 13 + programs/src/desktop/apps_common.hpp | 1 + programs/src/desktop/main.cpp | 12 +- programs/src/fetch/Makefile | 3 +- programs/src/fetch/main.cpp | 458 ++-------------- programs/src/weather/Makefile | 94 ++++ programs/src/weather/main.cpp | 575 +++++++++++++++++++++ programs/src/weather/stb_truetype_impl.cpp | 35 ++ programs/src/wiki/Makefile | 3 +- programs/src/wiki/main.cpp | 357 +------------ programs/src/wiki/obj/main.o | Bin 263688 -> 181080 bytes programs/src/wikipedia/Makefile | 3 +- programs/src/wikipedia/main.cpp | 282 +--------- ramdisk.tar | Bin 11868160 -> 12800000 bytes scripts/copy_icons.sh | 24 + 32 files changed, 1440 insertions(+), 1048 deletions(-) create mode 100644 programs/include/tls/tls.hpp create mode 100644 programs/lib/tls/Makefile create mode 100644 programs/lib/tls/libtls.a create mode 100644 programs/lib/tls/obj/tls.o create mode 100644 programs/lib/tls/tls.cpp create mode 100644 programs/src/desktop/app_weather.cpp create mode 100644 programs/src/weather/Makefile create mode 100644 programs/src/weather/main.cpp create mode 100644 programs/src/weather/stb_truetype_impl.cpp diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index ac63bcb..6cda082 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -789,6 +789,16 @@ namespace Zenith { return count; } + // ---- Window scale syscalls ---- + + static int Sys_WinSetScale(int scale) { + return WinServer::SetScale(scale); + } + + static int Sys_WinGetScale() { + return WinServer::GetScale(); + } + // ---- Window server syscalls ---- static int Sys_WinCreate(const char* title, int w, int h, WinCreateResult* result) { @@ -1003,6 +1013,10 @@ namespace Zenith { return (int64_t)Sys_Kill((int)frame->arg1); case SYS_DEVLIST: return (int64_t)Sys_DevList((DevInfo*)frame->arg1, (int)frame->arg2); + case SYS_WINSETSCALE: + return (int64_t)Sys_WinSetScale((int)frame->arg1); + case SYS_WINGETSCALE: + return (int64_t)Sys_WinGetScale(); default: return -1; } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index f128d6b..33e947b 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -75,6 +75,8 @@ namespace Zenith { static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINSENDEVENT = 60; static constexpr uint64_t SYS_WINRESIZE = 64; + static constexpr uint64_t SYS_WINSETSCALE = 65; + static constexpr uint64_t SYS_WINGETSCALE = 66; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; @@ -135,12 +137,13 @@ namespace Zenith { // Window server shared types struct WinEvent { - uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close + uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; union { KeyEvent key; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t w, h; } resize; + struct { int32_t scale; } scale; }; }; diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index 88f9259..116ecf6 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -14,6 +14,7 @@ namespace WinServer { static WindowSlot g_slots[MaxWindows]; + static int g_uiScale = 1; int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h, uint64_t& heapNext, uint64_t& outVa) { @@ -201,6 +202,28 @@ namespace WinServer { return 0; } + int SetScale(int scale) { + if (scale < 0) scale = 0; + if (scale > 2) scale = 2; + g_uiScale = scale; + + // Broadcast scale event to all active windows + Zenith::WinEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = 4; + ev.scale.scale = scale; + for (int i = 0; i < MaxWindows; i++) { + if (g_slots[i].used) { + SendEvent(i, &ev); + } + } + return 0; + } + + int GetScale() { + return g_uiScale; + } + 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 da8091b..a63ff7c 100644 --- a/kernel/src/Api/WinServer.hpp +++ b/kernel/src/Api/WinServer.hpp @@ -40,5 +40,7 @@ namespace WinServer { int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, uint64_t& heapNext, uint64_t& outVa); void CleanupProcess(int pid); + int SetScale(int scale); + int GetScale(); } diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 35d52b7..5f5b225 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 wikipedia desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia weather 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 wikipedia desktop icons fonts bearssl libc +.PHONY: all clean doom fetch wiki wikipedia weather desktop icons fonts bearssl libc tls -all: bearssl libc $(TARGETS) fetch wiki wikipedia doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc tls $(TARGETS) fetch wiki wikipedia weather doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library. bearssl: @@ -94,18 +94,26 @@ bearssl: libc: $(MAKE) -C lib/libc -# Build fetch via its own Makefile (depends on bearssl and libc). -fetch: bearssl libc +# Build shared TLS helper library. +tls: bearssl libc + $(MAKE) -C lib/tls + +# Build fetch via its own Makefile (depends on bearssl, libc, and tls). +fetch: bearssl libc tls $(MAKE) -C src/fetch -# Build wiki via its own Makefile (depends on bearssl and libc). -wiki: bearssl libc +# Build wiki via its own Makefile (depends on bearssl, libc, and tls). +wiki: bearssl libc tls $(MAKE) -C src/wiki -# Build wikipedia standalone GUI client (depends on bearssl and libc). -wikipedia: bearssl libc +# Build wikipedia standalone GUI client (depends on bearssl, libc, and tls). +wikipedia: bearssl libc tls $(MAKE) -C src/wikipedia +# Build weather standalone GUI client (depends on bearssl, libc, and tls). +weather: bearssl libc tls + $(MAKE) -C src/weather + # Build desktop via its own Makefile. desktop: $(MAKE) -C src/desktop @@ -157,7 +165,9 @@ clean: rm -rf $(BINDIR) obj $(MAKE) -C lib/bearssl clean $(MAKE) -C lib/libc clean + $(MAKE) -C lib/tls clean $(MAKE) -C src/fetch clean $(MAKE) -C src/doom clean $(MAKE) -C src/desktop clean $(MAKE) -C src/wikipedia clean + $(MAKE) -C src/weather clean diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index f5520eb..b96c454 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -75,6 +75,8 @@ namespace Zenith { static constexpr uint64_t SYS_WINMAP = 59; static constexpr uint64_t SYS_WINSENDEVENT = 60; static constexpr uint64_t SYS_WINRESIZE = 64; + static constexpr uint64_t SYS_WINSETSCALE = 65; + static constexpr uint64_t SYS_WINGETSCALE = 66; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; @@ -135,12 +137,13 @@ namespace Zenith { // Window server shared types struct WinEvent { - uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close + uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; union { KeyEvent key; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t w, h; } resize; + struct { int32_t scale; } scale; }; }; diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index d46f693..25b439b 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -73,6 +73,8 @@ struct DesktopState { SvgIcon icon_settings; SvgIcon icon_reboot; + SvgIcon icon_weather; + SvgIcon icon_doom; SvgIcon icon_procmgr; SvgIcon icon_mandelbrot; diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp index 522a8be..1516cee 100644 --- a/programs/include/gui/svg.hpp +++ b/programs/include/gui/svg.hpp @@ -77,6 +77,52 @@ struct SvgGradientTable { } }; +// --------------------------------------------------------------------------- +// CSS class color table (.ClassName { color: #rrggbb; }) +// Used to resolve fill:currentColor via the element's class= attribute. +// --------------------------------------------------------------------------- +static constexpr int SVG_MAX_CSS_CLASSES = 8; + +struct SvgCssClass { + char name[48]; + Color color; +}; + +struct SvgCssTable { + SvgCssClass entries[SVG_MAX_CSS_CLASSES]; + int count; + + void clear() { count = 0; } + + void add(const char* name, Color c) { + if (count >= SVG_MAX_CSS_CLASSES) return; + int i = 0; + while (name[i] && i < 47) { entries[count].name[i] = name[i]; i++; } + entries[count].name[i] = '\0'; + entries[count].color = c; + count++; + } + + // Look up class by name (first token if multiple classes are listed). + // Returns true if found. + bool lookup(const char* cls, Color* out) const { + for (int i = 0; i < count; i++) { + const char* a = entries[i].name; + const char* b = cls; + bool match = true; + while (*a && *b) { + if (*a != *b) { match = false; break; } + a++; b++; + } + if (match && *a == '\0' && (*b == '\0' || *b == ' ')) { + *out = entries[i].color; + return true; + } + } + return false; + } +}; + // --------------------------------------------------------------------------- // Fixed-point number parser (NO floating point) // Parses strings like "3.25", "-0.5", ".1115", "16" @@ -815,6 +861,52 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h, zenith::free(isect); } +// --------------------------------------------------------------------------- +// Parse CSS class color mappings out of a "); + if (se) + svg_parse_css_table(sp, (int)(se - sp), &css); + } + } + // Shared edge list (cleared per element for multi-color support) SvgEdgeList el; el.init(SVG_MAX_EDGES); @@ -1113,7 +1243,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t // Determine fill color for this element Color elem_color = fill_color; - int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); + int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } // fill="none" int alpha = svg_get_element_opacity(elem_start, elem_len); @@ -1146,7 +1276,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; - int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); + int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } int alpha = svg_get_element_opacity(elem_start, elem_len); @@ -1189,7 +1319,7 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t } Color elem_color = fill_color; - int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads); + int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads, &css); if (fillResult == -1) { p = elem_end; continue; } int alpha = svg_get_element_opacity(elem_start, elem_len); diff --git a/programs/include/gui/truetype.hpp b/programs/include/gui/truetype.hpp index ef0192c..6d53ef3 100644 --- a/programs/include/gui/truetype.hpp +++ b/programs/include/gui/truetype.hpp @@ -50,7 +50,7 @@ struct CachedGlyph { }; struct GlyphCache { - CachedGlyph glyphs[128]; + CachedGlyph glyphs[256]; // Latin-1 Supplement (U+0080..U+00FF) included int pixel_size; float scale; int ascent, descent, line_gap; @@ -127,7 +127,7 @@ struct TrueTypeFont { } CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) { - if (codepoint < 0 || codepoint >= 128) return nullptr; + if (codepoint < 0 || codepoint >= 256) return nullptr; CachedGlyph* g = &gc->glyphs[codepoint]; if (g->loaded) return g; diff --git a/programs/include/tls/tls.hpp b/programs/include/tls/tls.hpp new file mode 100644 index 0000000..7281314 --- /dev/null +++ b/programs/include/tls/tls.hpp @@ -0,0 +1,45 @@ +/* + * tls.hpp + * Shared TLS helper library for ZenithOS + * Trust anchor loading, BearSSL time, TLS I/O, HTTPS fetch + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +extern "C" { +#include +} + +#include +#include + +namespace tls { + +struct TrustAnchors { + br_x509_trust_anchor* anchors; + size_t count; + size_t capacity; +}; + +TrustAnchors load_trust_anchors(); +void get_bearssl_time(uint32_t* days, uint32_t* seconds); +int tls_send_all(int fd, const unsigned char* data, size_t len); +int tls_recv_some(int fd, unsigned char* buf, size_t maxlen); + +// Optional abort callback (for Ctrl+Q in terminal apps). nullptr = no abort. +using AbortCheckFn = bool (*)(); + +int tls_exchange(int fd, br_ssl_engine_context* eng, + const char* request, int reqLen, + char* respBuf, int respMax, + AbortCheckFn abort_check = nullptr); + +// High-level: socket -> TLS setup -> exchange -> cleanup, all in one call. +int https_fetch(const char* host, uint32_t ip, uint16_t port, + const char* request, int reqLen, + const TrustAnchors& tas, + char* respBuf, int respMax, + AbortCheckFn abort_check = nullptr); + +} // namespace tls diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 44ca103..5c2b9d3 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -320,5 +320,11 @@ namespace zenith { 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); } + inline int win_setscale(int scale) { + return (int)syscall1(Zenith::SYS_WINSETSCALE, (uint64_t)scale); + } + inline int win_getscale() { + return (int)syscall0(Zenith::SYS_WINGETSCALE); + } } diff --git a/programs/lib/tls/Makefile b/programs/lib/tls/Makefile new file mode 100644 index 0000000..9529120 --- /dev/null +++ b/programs/lib/tls/Makefile @@ -0,0 +1,72 @@ +# Makefile for shared TLS helper library 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++ + AR := $(TOOLCHAIN_PREFIX)ar +else + CXX := g++ + AR := ar +endif + +# ---- Paths ---- + +BEARSSL := ../bearssl +LIBC_INC := ../../include/libc +PROG_INC := ../../include +OBJDIR := obj + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -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 + +# ---- Target ---- + +TARGET := libtls.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJDIR)/tls.o + $(AR) rcs $@ $^ + +$(OBJDIR)/tls.o: tls.cpp Makefile + @mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a new file mode 100644 index 0000000000000000000000000000000000000000..2eb62f0ab1fb4a08c06da2d99d2c817cbbd122f7 GIT binary patch literal 105850 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ã?eC!JjDGm-pFocGQqKPe3{%OY|ucLjMmtI$CJ+=T1gXEiW zee^SVBJMApA=h^4iwfMXk{Dm=+ODeP+OGQK+OC%5+O7`d+OA&Yk#+4jOD11{^U2%D zwOy0Qb-%usT=(l$ zzw0G(y?>?if!1dy=J8z=udlPT!F>*muYv6zN3Q*!LVi8sZzrE-hfgq_d>hVl=8&Jn zdEhhTWAXSk@|Kv_ekX4#4KYgt+@I5SeFbk%{wDV8e&l`7ZzIY7#_Lj3$g3d#J>+jA zemQv^oY$=*P3^YFRkpJ9L0`*|9F5&d5h z=dJ2(P;M*o0ytmqO$aTM-O@6?J2TRGnMt#uM*9mr2(y!9e~ z4coT>#*x;i0FIk2$@O|Kk$e{B&Hm(7u-zAsufjOnNv_9*&&YMV{6-#)`9wcDqV?DJ z^HoQ`s`ta=#^l}5Z`YFFkACPz{sGDzME);~+jR1A@QLKNVqUtJyfe1bGvt^af>+63 zzntVRy&sWGN!rvyp8}rP5^1slo z@5oPL9zQ`|3H#AMNtcQOnlnTJj8xw{GNT(4T|IU&nrtPQCK-+^)4l3b6c@#NQ`Kl_lskN!*{e=x91 z4tZD9dpfxu2j`JbNBlB!eSQ2Qd0RX#gZ|Kd$i)1q*B|OyZexmn4C5h|Twkw$n*3dy zKfFM$$FDcZlOk;Y>?DsvxnGj+E@0z-B=3#wdyZVM*E(Z7YJ0PA-ZGH<8T4Bkx#rI% z*ZjN4hhg6Nid_340tYOukNUOb5tzpYlHZNT>Ex9$P9~H8fblt(T+3bW@n6WbK38JD z*ZOOHs*t~g`MDnXc^qfkl2^bylR&Qh-mNsc9^?Ne^3~Yy zlE^iG2D#>ck$fh$)QT_sQSFc0Wj73FGY;x#mAhuKBOMCT~Bq!2Uj!T>I@2kAFyB9pn5v@(mHTyi??V zqW}LSKZEgEv|-+Q9zlEs@{2gW)F!_H@iFB0qMp6UJEK2`lk5G69CGcSW#l&?&sK7c z-%GBqr=B6#JUtra?T4Lc?_lz1oM)z!Z^!;MfxIZ@$$QAp;re1R`6pM}daNY>6vwZE zjq}zs8v8{gc{+{@HOTM5IBZP5H_GOXB|nLNNF=|rsEr>$z82$ZI{9ljj;|mehWu;E z=i$74Gx-CBY`J^L+ZDI|CHXtWtp7y54E=DPT(6glHnHo;>M|vr!gi@hz76BQF8Kf) z2V0WAiut)Cd2!U|Ci15dpG@8p<8T~#>=m}&caiJ;pU27d`sD?3y?)tE{v-PNAbE@} zHaJdR4BPz=@_5YKMVi_bwV!`QyDE?;<9w?Q`CPQO1-ZVTGM>CDt~dLT>-F_8^0!Oa z`i&)j5cAJ8@@V)xay`FaPX0dX^E~-kd}A*8 z0nEe8$j@Rve~x@WY1_bBtWy-*ArnEsT?L(RMxEF8eTl z)*{#Y9&^dNVZU2JeiHljI`XVYTi#A`JLei-+^t|sq`^Mq#P`(#6# z%0-!GH-vrkL$AfXRWLqp zARmVPs|UHhe_{}MB*yI+@^_FYo4iRWTb~!nhhV?hL|z-m$G6E3z<(pJQ`YAHle{2~ zzZF~O?Vn`K57o&tah?!Megys2fqVweulkd>!hAA>d^C;=cauMcewa(H?|XQJyf*st z8S)i4j;|x1g!BK+4`V(*PhJ|^u~3`5+vR!G zKazX_&c~{eA1Y$kZ$LgD`У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

4^7u1qwP$MQFDY?~USbwKQ?>U8bNnPqOE( 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 literal 0 HcmV?d00001 diff --git a/programs/lib/tls/tls.cpp b/programs/lib/tls/tls.cpp new file mode 100644 index 0000000..ffa1552 --- /dev/null +++ b/programs/lib/tls/tls.cpp @@ -0,0 +1,279 @@ +/* + * tls.cpp + * Shared TLS helper library for ZenithOS + * Extracted from fetch, wiki, wikipedia, and weather apps + * Copyright (c) 2025-2026 Daniel Hammer + */ + +#include +#include + +extern "C" { +#include +#include +} + +// ============================================================================ +// File-local: PEM/DER certificate parsing helpers +// ============================================================================ + +namespace { + +struct DerAccum { unsigned char* data; size_t len, cap; }; +struct DnAccum { unsigned char* data; size_t len, cap; }; + +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; +} + +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; +} + +void ta_add(tls::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; +} + +bool process_cert_der(tls::TrustAnchors* tas, const unsigned char* der, size_t der_len) { + static br_x509_decoder_context dc; // ~2KB+, keep off stack + 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; +} + +} // anonymous namespace + +// ============================================================================ +// Exported functions +// ============================================================================ + +namespace tls { + +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; // keep off stack + 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; +} + +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); +} + +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; +} + +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); + } +} + +int tls_exchange(int fd, br_ssl_engine_context* eng, + const char* request, int reqLen, + char* respBuf, int respMax, + AbortCheckFn abort_check) { + 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 (abort_check && abort_check()) { + br_ssl_engine_close(eng); + return respLen > 0 ? respLen : -1; + } + 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); + } +} + +int https_fetch(const char* host, uint32_t ip, uint16_t port, + const char* request, int reqLen, + const TrustAnchors& tas, + char* respBuf, int respMax, + AbortCheckFn abort_check) { + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) return -1; + if (zenith::connect(fd, ip, port) < 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) { + free(cc); free(xc); free(iobuf); + zenith::closesocket(fd); return -1; + } + + br_ssl_client_init_full(cc, xc, tas.anchors, 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, host, 0)) { + zenith::closesocket(fd); free(cc); free(xc); free(iobuf); return -1; + } + + int respLen = tls_exchange(fd, &cc->eng, request, reqLen, respBuf, respMax, abort_check); + zenith::closesocket(fd); + free(cc); free(xc); free(iobuf); + return respLen; +} + +} // namespace tls diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 926befb..6a3b409 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -61,7 +61,7 @@ LDFLAGS := \ # ---- C++ source files ---- -SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp +SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_weather.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/desktop/app_settings.cpp b/programs/src/desktop/app_settings.cpp index 0cddcf7..ce79825 100644 --- a/programs/src/desktop/app_settings.cpp +++ b/programs/src/desktop/app_settings.cpp @@ -458,18 +458,21 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) { if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) { s.ui_scale = 0; apply_ui_scale(0); + zenith::win_setscale(0); return; } // Default if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) { s.ui_scale = 1; apply_ui_scale(1); + zenith::win_setscale(1); return; } // Large if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) { s.ui_scale = 2; apply_ui_scale(2); + zenith::win_setscale(2); return; } } diff --git a/programs/src/desktop/app_weather.cpp b/programs/src/desktop/app_weather.cpp new file mode 100644 index 0000000..0d3aa6c --- /dev/null +++ b/programs/src/desktop/app_weather.cpp @@ -0,0 +1,13 @@ +/* + * app_weather.cpp + * ZenithOS Desktop - Weather app launcher + * Spawns weather.elf as a standalone Window Server process + * Copyright (c) 2026 Daniel Hammer + */ + +#include "apps_common.hpp" + +void open_weather(DesktopState* ds) { + (void)ds; + zenith::spawn("0:/os/weather.elf"); +} diff --git a/programs/src/desktop/apps_common.hpp b/programs/src/desktop/apps_common.hpp index 4b7cc77..ef493c9 100644 --- a/programs/src/desktop/apps_common.hpp +++ b/programs/src/desktop/apps_common.hpp @@ -167,6 +167,7 @@ void open_texteditor(DesktopState* ds); void open_texteditor_with_file(DesktopState* ds, const char* path); void open_klog(DesktopState* ds); void open_wiki(DesktopState* ds); +void open_weather(DesktopState* ds); void open_procmgr(DesktopState* ds); void open_mandelbrot(DesktopState* ds); void open_devexplorer(DesktopState* ds); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 899aca4..6e8b78a 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -61,6 +61,8 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor); ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor); + ds->icon_weather = svg_load("0:/icons/weather-widget.svg", 20, 20, defColor); + ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor); ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor); ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor); @@ -76,6 +78,7 @@ void gui::desktop_init(DesktopState* ds) { ds->settings.show_shadows = true; ds->settings.clock_24h = true; ds->settings.ui_scale = 1; + zenith::win_setscale(1); ds->ctx_menu_open = false; ds->ctx_menu_x = 0; @@ -421,7 +424,7 @@ struct MenuRow { int app_id; // -1 for category headers / dividers }; -static constexpr int MENU_ROW_COUNT = 18; +static constexpr int MENU_ROW_COUNT = 19; static const MenuRow menu_rows[MENU_ROW_COUNT] = { { true, "Applications", -1 }, { false, "Terminal", 0 }, @@ -430,6 +433,7 @@ static const MenuRow menu_rows[MENU_ROW_COUNT] = { { false, "Calculator", 3 }, { true, "Internet", -1 }, { false, "Wikipedia", 9 }, + { false, "Weather", 13 }, { true, "System", -1 }, { false, "System Info", 2 }, { false, "Kernel Log", 5 }, @@ -470,7 +474,7 @@ static void desktop_draw_app_menu(DesktopState* ds) { draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); // Icon lookup by app_id - SvgIcon* icons[13] = { + SvgIcon* icons[14] = { &ds->icon_terminal, // 0 &ds->icon_filemanager, // 1 &ds->icon_sysinfo, // 2 @@ -484,6 +488,7 @@ static void desktop_draw_app_menu(DesktopState* ds) { &ds->icon_doom, // 10 &ds->icon_settings, // 11 &ds->icon_reboot, // 12 + &ds->icon_weather, // 13 }; int mx = ds->mouse.x; @@ -519,7 +524,7 @@ static void desktop_draw_app_menu(DesktopState* ds) { // 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) { + if (row.app_id >= 0 && row.app_id < 14) { SvgIcon* icon = icons[row.app_id]; if (icon && icon->pixels) { fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); @@ -1099,6 +1104,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { case 10: open_doom(ds); break; case 11: open_settings(ds); break; case 12: open_reboot_dialog(ds); break; + case 13: open_weather(ds); break; } ds->app_menu_open = false; } diff --git a/programs/src/fetch/Makefile b/programs/src/fetch/Makefile index 901874d..2a2f7b7 100644 --- a/programs/src/fetch/Makefile +++ b/programs/src/fetch/Makefile @@ -16,6 +16,7 @@ endif # ---- Paths ---- BEARSSL := ../../lib/bearssl +TLS_LIB := ../../lib/tls LIBC_LIB := ../../lib/libc LIBC_INC := ../../include/libc PROG_INC := ../../include @@ -67,7 +68,7 @@ LDFLAGS := \ # ---- Libraries ---- -LIBS := $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a +LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a # ---- Target ---- diff --git a/programs/src/fetch/main.cpp b/programs/src/fetch/main.cpp index c587f19..ba19e08 100644 --- a/programs/src/fetch/main.cpp +++ b/programs/src/fetch/main.cpp @@ -8,9 +8,9 @@ #include #include +#include extern "C" { -#include #include #include #include @@ -156,366 +156,15 @@ static void parse_status_text(const char* buf, int len, char* out, int outMax) { out[j] = '\0'; } -// ---- Trust anchor loading ---- +// ---- Keyboard abort check for TLS ---- -struct TrustAnchors { - br_x509_trust_anchor* anchors; - size_t count; - size_t capacity; -}; - -// Accumulate DER-decoded certificate data -struct DerAccum { - unsigned char* data; - size_t len; - size_t 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 newcap = a->cap * 2; - if (newcap < a->len + len) newcap = a->len + len + 4096; - unsigned char* nb = (unsigned char*)malloc(newcap); - if (!nb) return; - if (a->data) { - memcpy(nb, a->data, a->len); - free(a->data); - } - a->data = nb; - a->cap = newcap; - } - memcpy(a->data + a->len, buf, len); - a->len += len; -} - -// Accumulate DN data from X.509 decoder -struct DnAccum { - unsigned char* data; - size_t len; - size_t cap; -}; - -static void dn_append(void* ctx, const void* buf, size_t len) { - DnAccum* a = (DnAccum*)ctx; - if (a->len + len > a->cap) { - size_t newcap = a->cap * 2; - if (newcap < a->len + len) newcap = a->len + len + 256; - unsigned char* nb = (unsigned char*)malloc(newcap); - if (!nb) return; - if (a->data) { - memcpy(nb, a->data, a->len); - free(a->data); - } - a->data = nb; - a->cap = newcap; - } - 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 newcap = tas->capacity == 0 ? 64 : tas->capacity * 2; - br_x509_trust_anchor* na = (br_x509_trust_anchor*)malloc( - newcap * sizeof(br_x509_trust_anchor)); - if (!na) return; - if (tas->anchors) { - memcpy(na, tas->anchors, tas->count * sizeof(br_x509_trust_anchor)); - free(tas->anchors); - } - tas->anchors = na; - tas->capacity = newcap; - } - tas->anchors[tas->count++] = *ta; -} - -// Process a single DER certificate into a trust anchor -static bool process_cert_der(TrustAnchors* tas, const unsigned char* der, size_t der_len) { - 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)); - - // Copy DN - 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; - - // Deep-copy public key data - 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(bool verbose) { - TrustAnchors tas = {nullptr, 0, 0}; - - int fh = zenith::open("0:/etc/ca-certificates.crt"); - if (fh < 0) { - printf("Warning: could not open CA certificate bundle\n"); - return tas; - } - - uint64_t fsize = zenith::getsize(fh); - if (fsize == 0 || fsize > 512 * 1024) { - zenith::close(fh); - printf("Warning: CA cert file invalid size\n"); - return tas; - } - - unsigned char* pem = (unsigned char*)malloc(fsize + 1); - if (!pem) { - zenith::close(fh); - printf("Warning: out of memory loading CA certs\n"); - return tas; - } - - zenith::read(fh, pem, 0, fsize); - zenith::close(fh); - pem[fsize] = 0; - - // Parse PEM -> DER certificates -> trust anchors - 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 event = br_pem_decoder_event(&pc); - if (event == BR_PEM_BEGIN_OBJ) { - const char* name = br_pem_decoder_name(&pc); - inCert = (strcmp(name, "CERTIFICATE") == 0); - if (inCert) { - der.len = 0; - br_pem_decoder_setdest(&pc, der_append, &der); - } else { - br_pem_decoder_setdest(&pc, nullptr, nullptr); - } - } else if (event == BR_PEM_END_OBJ) { - if (inCert && der.len > 0) { - process_cert_der(&tas, der.data, der.len); - } - inCert = false; - } else if (event == BR_PEM_ERROR) { - break; - } - } - - if (der.data) free(der.data); - free(pem); - - if (verbose) { - char msg[64]; - snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count); - zenith::print(msg); - } - - return tas; -} - -// ---- Time conversion for certificate validation ---- - -// Returns days since January 1, 0 AD (Gregorian) and seconds within the day. -// This is the format BearSSL's br_x509_minimal_set_time() expects. -static void get_bearssl_time(uint32_t* days, uint32_t* seconds) { - Zenith::DateTime dt; - zenith::gettime(&dt); - - int y = dt.Year; - int m = dt.Month; - int d = dt.Day; - - // Days from year 0 to start of year y (Gregorian proleptic calendar) - uint32_t total_days = 365 * (uint32_t)y - + (uint32_t)(y / 4) - - (uint32_t)(y / 100) - + (uint32_t)(y / 400); - - // Add days for completed months in this year - const int mdays[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - for (int mo = 1; mo < m && mo <= 12; mo++) { - total_days += mdays[mo]; - } - // Leap day for this year if we're past February - bool leap = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)); - if (leap && m > 2) total_days++; - - total_days += d - 1; - - *days = total_days; - *seconds = (uint32_t)(dt.Hour * 3600 + dt.Minute * 60 + dt.Second); -} - -// ---- TLS I/O loop ---- - -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); - } -} - -// Run BearSSL I/O loop until handshake + app data exchange is done -// Returns: number of response bytes in respBuf, or -1 on error -static int tls_exchange(int fd, br_ssl_engine_context* eng, - const char* request, int reqLen, - char* respBuf, int respMax, bool verbose) { - 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) { - char msg[64]; - snprintf(msg, sizeof(msg), "TLS error: %d\n", err); - zenith::print(msg); - if (respLen == 0) return -1; - } - return respLen; - } - - // Check for keyboard abort - if (zenith::is_key_available()) { - Zenith::KeyEvent ev; - zenith::getkey(&ev); - if (ev.pressed && ev.ctrl && ev.ascii == 'q') { - br_ssl_engine_close(eng); - return respLen > 0 ? respLen : -1; - } - } - - // Send record data to network - 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; - } - - // Read application data from TLS - 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; - } - - // Send application data (HTTP request) into TLS - 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; - } - - // Receive record data from network - 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; - } - - // Nothing actionable — wait - if (zenith::get_milliseconds() >= deadline) { - return respLen > 0 ? respLen : -1; - } - zenith::sleep_ms(1); +static bool check_keyboard_abort() { + if (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (ev.pressed && ev.ctrl && ev.ascii == 'q') return true; } + return false; } // ---- Plain HTTP exchange (no TLS) ---- @@ -706,19 +355,6 @@ extern "C" void _start() { zenith::print(msg); } - // Create and connect socket - int fd = zenith::socket(Zenith::SOCK_TCP); - if (fd < 0) { - zenith::print("Error: failed to create socket\n"); - zenith::exit(1); - } - - if (zenith::connect(fd, serverIp, port) < 0) { - zenith::print("Error: connection failed\n"); - zenith::closesocket(fd); - zenith::exit(1); - } - // Build HTTP request char request[1024]; int reqLen = snprintf(request, sizeof(request), @@ -740,7 +376,6 @@ extern "C" void _start() { char* respBuf = (char*)malloc(RESP_MAX); if (!respBuf) { zenith::print("Error: out of memory\n"); - zenith::closesocket(fd); zenith::exit(1); } @@ -748,35 +383,21 @@ extern "C" void _start() { if (useHttps) { // ---- TLS handshake and exchange ---- - - // Load trust anchors - TrustAnchors tas = load_trust_anchors(verbose); + tls::TrustAnchors tas = tls::load_trust_anchors(); + if (verbose) { + char msg[64]; + snprintf(msg, sizeof(msg), "Loaded %u trust anchors\n", (unsigned)tas.count); + zenith::print(msg); + } if (tas.count == 0) { zenith::print("Error: no trust anchors loaded\n"); free(respBuf); - zenith::closesocket(fd); zenith::exit(1); } - // Initialize BearSSL client - // Allocate contexts on heap to avoid stack overflow - br_ssl_client_context* cc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context)); - br_x509_minimal_context* xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context)); - if (!cc || !xc) { - zenith::print("Error: out of memory for TLS context\n"); - free(respBuf); - zenith::closesocket(fd); - zenith::exit(1); - } - - br_ssl_client_init_full(cc, xc, tas.anchors, tas.count); - - // Set time for certificate validation - uint32_t days, secs; - get_bearssl_time(&days, &secs); - br_x509_minimal_set_time(xc, days, secs); - if (verbose) { + uint32_t days, secs; + tls::get_bearssl_time(&days, &secs); Zenith::DateTime dt; zenith::gettime(&dt); char tmsg[128]; @@ -785,54 +406,39 @@ extern "C" void _start() { (unsigned)dt.Hour, (unsigned)dt.Minute, (unsigned)dt.Second, (unsigned)days, (unsigned)secs); zenith::print(tmsg); - } - - // Seed the PRNG with RDRAND entropy - unsigned char seed[32]; - zenith::getrandom(seed, sizeof(seed)); - br_ssl_engine_set_buffer(&cc->eng, malloc(BR_SSL_BUFSIZE_BIDI), - BR_SSL_BUFSIZE_BIDI, 1); - - // Inject entropy - br_ssl_engine_inject_entropy(&cc->eng, seed, sizeof(seed)); - - // Reset client with server name for SNI - if (!br_ssl_client_reset(cc, hostStr, 0)) { - int err = br_ssl_engine_last_error(&cc->eng); - char msg[64]; - snprintf(msg, sizeof(msg), "Error: TLS reset failed (err=%d)\n", err); - zenith::print(msg); - free(respBuf); - zenith::closesocket(fd); - zenith::exit(1); - } - - if (verbose) { zenith::print("TLS handshake...\n"); } - // Run TLS I/O loop - respLen = tls_exchange(fd, &cc->eng, request, reqLen, - respBuf, RESP_MAX, verbose); + respLen = tls::https_fetch(hostStr, serverIp, port, + request, reqLen, tas, + respBuf, RESP_MAX, check_keyboard_abort); if (verbose && respLen > 0) { zenith::print("TLS connection established\n"); } - - // Cleanup (we don't bother freeing everything since we're exiting) } else { // ---- Plain HTTP ---- + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) { + zenith::print("Error: failed to create socket\n"); + zenith::exit(1); + } + + if (zenith::connect(fd, serverIp, port) < 0) { + zenith::print("Error: connection failed\n"); + zenith::closesocket(fd); + zenith::exit(1); + } + respLen = plain_http_exchange(fd, request, reqLen, respBuf, RESP_MAX); + zenith::closesocket(fd); if (respLen == -2) { zenith::print("\nAborted.\n"); - zenith::closesocket(fd); zenith::exit(0); } } - zenith::closesocket(fd); - if (respLen <= 0) { zenith::print("Error: no response received\n"); zenith::exit(1); diff --git a/programs/src/weather/Makefile b/programs/src/weather/Makefile new file mode 100644 index 0000000..6b98f19 --- /dev/null +++ b/programs/src/weather/Makefile @@ -0,0 +1,94 @@ +# Makefile for weather (standalone Weather 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 +TLS_LIB := ../../lib/tls +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 := $(TLS_LIB)/libtls.a $(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/weather.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/weather/main.cpp b/programs/src/weather/main.cpp new file mode 100644 index 0000000..60ce7bf --- /dev/null +++ b/programs/src/weather/main.cpp @@ -0,0 +1,575 @@ +/* + * main.cpp + * ZenithOS Weather app - standalone Window Server process + * Fetches current weather from wttr.in via HTTPS (BearSSL) + * Displays temperature, description, feels like, and location + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +using namespace gui; +using namespace gui::colors; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int INIT_W = 380; +static constexpr int INIT_H = 280; +static constexpr int HEADER_H = 160; +static constexpr int FOOTER_H = 50; +static constexpr int ICON_SIZE = 80; +static constexpr int ICON_X = 28; +static constexpr int ICON_Y = 40; +static constexpr int INFO_X = ICON_X + ICON_SIZE + 20; // 128 +static constexpr int TEMP_Y = 40; +static int TEMP_SIZE = 40; +static constexpr int DESC_Y = 92; +static int DESC_SIZE = 17; +static constexpr int FEELS_Y = 116; +static int LABEL_SIZE = 15; +static constexpr int RESP_MAX = 65536; + +static const char WTTR_HOST[] = "wttr.in"; + +// ============================================================================ +// App state +// ============================================================================ + +enum class AppPhase { IDLE, LOADING, DONE, ERR }; + +static AppPhase g_phase = AppPhase::IDLE; +static char g_temp[32] = {}; +static char g_desc[128] = {}; +static char g_feels[48] = {}; +static char g_location[128] = {}; +static char g_status[256] = {}; +static int g_win_w = INIT_W; +static int g_win_h = INIT_H; + +static char* g_resp_buf = nullptr; + +// Fonts +static TrueTypeFont* g_font = nullptr; // Roboto Medium +static TrueTypeFont* g_font_bold = nullptr; // Roboto Bold + +// Weather icon (loaded per-fetch based on weather code) +static SvgIcon g_icon = {nullptr, 0, 0}; +static char g_icon_name[64] = {}; + +// TLS state (lazy-init on first fetch) +static bool g_tls_ready = false; +static uint32_t g_server_ip = 0; +static tls::TrustAnchors g_tas = {nullptr, 0, 0}; + +// ============================================================================ +// Pixel buffer helpers +// ============================================================================ + +static void px_fill(uint32_t* px, int bw, int bh, + 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 > bw ? bw : x + w; + int y1 = y + h > bh ? bh : y + h; + 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 bh, int x, int y, int len, Color c) { + if (y < 0 || y >= bh) return; + uint32_t v = c.to_pixel(); + int x1 = x + len > bw ? bw : x + len; + for (int col = x < 0 ? 0 : x; col < x1; col++) + px[y * bw + col] = v; +} + +// Integer square root (Newton's method). +static int isqrt(int n) { + if (n <= 0) return 0; + int x = n, y = (x + 1) / 2; + while (y < x) { x = y; y = (x + n / x) / 2; } + return x; +} + +// Fill a rounded rectangle directly onto the pixel buffer. +static void px_fill_rounded(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, int r, Color c) { + if (r <= 0) { px_fill(px, bw, bh, x, y, w, h, c); return; } + if (r > w / 2) r = w / 2; + if (r > h / 2) r = h / 2; + + uint32_t v = c.to_pixel(); + // Corner arc centers + int cx_l = x + r; + int cx_r = x + w - 1 - r; + + for (int row = y; row < y + h; row++) { + if (row < 0 || row >= bh) continue; + + int left, right; + if (row < y + r) { + int dy = y + r - row; // 1..r + int dx = isqrt(r * r - dy * dy); + left = cx_l - dx; + right = cx_r + dx + 1; + } else if (row >= y + h - r) { + int dy = row - (y + h - 1 - r); // 1..r + int dx = isqrt(r * r - dy * dy); + left = cx_l - dx; + right = cx_r + dx + 1; + } else { + left = x; + right = x + w; + } + + if (left < 0) left = 0; + if (right > bw) right = bw; + for (int col = left; col < right; col++) + px[row * bw + col] = v; + } +} + +// Alpha-composite an SvgIcon onto the pixel buffer. +static void draw_icon(uint32_t* px, int bw, int bh, + int dx, int dy, const SvgIcon& icon) { + if (!icon.pixels) return; + for (int row = 0; row < icon.height; row++) { + int ty = dy + row; + if (ty < 0 || ty >= bh) continue; + for (int col = 0; col < icon.width; col++) { + int tx = dx + col; + if (tx < 0 || tx >= bw) continue; + uint32_t s = icon.pixels[row * icon.width + col]; + uint8_t sa = (s >> 24) & 0xFF; + if (sa == 0) continue; + if (sa == 255) { px[ty * bw + tx] = s; continue; } + uint8_t sr = (s >> 16) & 0xFF, sg = (s >> 8) & 0xFF, sb = s & 0xFF; + uint32_t d = px[ty * bw + tx]; + uint8_t dr = (d >> 16) & 0xFF, dg = (d >> 8) & 0xFF, db = d & 0xFF; + uint32_t inv = 255 - sa; + px[ty * bw + tx] = 0xFF000000u + | (((sr * sa + dr * inv) / 255u) << 16) + | (((sg * sa + dg * inv) / 255u) << 8) + | ((sb * sa + db * inv) / 255u); + } + } +} + +// ============================================================================ +// 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'); +} + +// ============================================================================ +// JSON parsing +// ============================================================================ + +// Extract value of a simple JSON string field: "key":"value" +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 - 1 && buf[p] != '"') { + if (buf[p] == '\\' && p + 1 < len) { p++; out[j++] = buf[p]; } + else out[j++] = buf[p]; + p++; + } + out[j] = '\0'; + return j; + } + out[0] = '\0'; + return 0; +} + +// Find the first occurrence of needle in buf[0..len-1]. Returns offset or -1. +static int find_substr(const char* buf, int len, const char* needle) { + int nlen = (int)strlen(needle); + if (nlen > len) return -1; + for (int i = 0; i <= len - nlen; i++) { + if (memcmp(buf + i, needle, nlen) == 0) return i; + } + return -1; +} + +// Extract the "value" string within a named JSON array-of-objects field. +// e.g., "weatherDesc":[{"value":"Partly cloudy"}] → "Partly cloudy" +// Works by finding section_key first, then the first "value" after it. +static int extract_array_value(const char* buf, int len, const char* section_key, + char* out, int maxOut) { + int pos = find_substr(buf, len, section_key); + if (pos < 0) { out[0] = '\0'; return 0; } + return extract_json_string(buf + pos, len - pos, "value", out, maxOut); +} + +// Parse a signed integer from a decimal string. +static int parse_int(const char* s) { + int sign = 1, val = 0, i = 0; + if (s[0] == '-') { sign = -1; i = 1; } + while (s[i] >= '0' && s[i] <= '9') val = val * 10 + (s[i++] - '0'); + return sign * val; +} + +// ============================================================================ +// Weather code → icon filename +// Maps wttr.in WMO-style weather codes to Flat-Remix panel icon names. +// ============================================================================ + +static const char* weather_code_to_icon(int code) { + switch (code) { + case 113: return "weather-clear.svg"; + case 116: return "weather-few-clouds.svg"; + case 119: return "weather-clouds.svg"; + case 122: return "weather-overcast.svg"; + case 143: return "weather-mist.svg"; + case 248: case 260: return "weather-fog.svg"; + case 176: case 263: case 266: case 353: return "weather-showers-scattered.svg"; + case 293: case 296: case 299: case 302: + case 305: case 308: case 356: case 359: return "weather-showers.svg"; + case 179: case 362: case 365: case 368: return "weather-snow-scattered.svg"; + case 323: case 326: case 329: case 332: + case 335: case 338: case 371: case 374: return "weather-snow.svg"; + case 227: case 230: return "weather-snow.svg"; + case 182: case 311: case 314: case 317: case 320: return "weather-snow-rain.svg"; + case 185: case 281: case 284: return "weather-freezing-rain.svg"; + case 350: case 377: return "weather-hail.svg"; + case 200: case 386: case 389: case 392: case 395: return "weather-storm.svg"; + default: return "weather-none-available.svg"; + } +} + +// Load the weather icon for the given icon filename (caches the last result). +static void load_weather_icon(const char* icon_name) { + if (strcmp(g_icon_name, icon_name) == 0 && g_icon.pixels) return; + + if (g_icon.pixels) svg_free(g_icon); + + static char path[80]; + snprintf(path, sizeof(path), "0:/icons/%s", icon_name); + // ColorScheme-Text paths (main shape) use this fill_color; accent classes + // (ColorScheme-Highlight=blue, ColorScheme-NeutralText=yellow, etc.) use their + // CSS-defined colors as parsed from the SVG's