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 0000000..2eb62f0
Binary files /dev/null and b/programs/lib/tls/libtls.a differ
diff --git a/programs/lib/tls/obj/tls.o b/programs/lib/tls/obj/tls.o
new file mode 100644
index 0000000..89b2fb8
Binary files /dev/null and b/programs/lib/tls/obj/tls.o differ
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