← Blog

Night Mode in sigilOS: Time-of-Day Theming Without a Network Call

June 22, 2026 · sigil-os · Sigil-Docs
night-mode theming os 0.6.0

sigilOS's Lumen window manager now switches between a dark and light theme based on the time of day. The implementation — nightmode.sg in sigil-os — does this without network access, without NTP, and without geolocation. The user sets their UTC offset once. The OS applies it every time nm_apply() is called by the theme scheduler.

Night mode active in Lumen
Night mode active — Lumen-dark theme applied at sunset via time-of-day scheduler
THEME_DARK
Background: (16, 16, 32) · Active after sunset
THEME_LIGHT
Background: (250, 250, 253) · Active after sunrise

Why no network?

Most operating systems derive the local time from a combination of NTP (for UTC accuracy) and timezone database lookups (for the local offset). Both require either a network connection or a bundled timezone database. sigilOS's Pi 3 floor has neither on a fresh boot — no NTP daemon, no /usr/share/zoneinfo. The tz-coarse tier solves this by pushing the lookup to the user: you tell the OS your UTC offset once, and the scheduler uses it forever. Coarse, but correct. A network-aware tz tier (NTP + automatic timezone detection) is planned for later milestones when the network stack is fully integrated.


The config cell

The night-mode configuration lives at a fixed address — 0xA20000, a 64-byte cell in kernel-reserved memory:

OffsetFieldDefaultNotes
0tz_offset0UTC hours, clamped to −12..+14
8sunrise7Hour (local time) to switch to THEME_LIGHT
16sunset20Hour (local time) to switch to THEME_DARK

The cell is readable and writable via the OS config API. nm_set_tz(cell, offset) clamps the supplied value to [−12, +14] and stores it. nm_set_times(cell, rise, set) stores the sunrise and sunset hours. Both calls validate their inputs — a tz_offset of 99 gets clamped to +14, not silently accepted.


The logic

nm_apply_hour(cell, h) is the core decision function. It takes a local hour (0–23) and returns THEME_DARK or THEME_LIGHT:

if h >= sunrise and h < sunset:
    return THEME_LIGHT
else:
    return THEME_DARK

The window is half-open — [sunrise, sunset). At exactly sunrise, light mode activates. At exactly sunset, dark mode activates. This matches the intuitive behavior: you want light mode to turn on at dawn, not one hour after it.

nm_apply(cell) wraps this with the RTC read:

utc_seconds = nm_rtc_seconds()           # loopback=0 on QEMU; Kernel RTC at integration
local_hour  = (utc_seconds / 3600 + tz_offset) mod 24
return nm_apply_hour(cell, local_hour)

On QEMU, nm_rtc_seconds() returns 0 — midnight UTC. That means the integration test always starts in dark mode (local hour = tz_offset mod 24; with tz_offset=0, local_hour=0, which is before sunrise=7). The QEMU PASS pixel for THEME_DARK is (16, 16, 32). The Kernel RTC integration will bind the real wall-clock value when the RTC driver lands.


Test coverage

The nightmodetest.sg covers nine logic assertions:

#What it checks
1Default sunrise=7, sunset=20
2tz_offset clamps to −12 for −99 input
3tz_offset clamps to +14 for +99 input
4Hour 12 (noon) → THEME_LIGHT
5Hour 23 (late night) → THEME_DARK
6Hour sunrise−1 → THEME_DARK (boundary: one hour before)
7Hour sunrise → THEME_LIGHT (boundary: exactly at dawn)
8Hour sunset → THEME_DARK (boundary: exactly at dusk)
9UTC hour 17 + tz_offset +5 → local hour 22 → THEME_DARK

The test also renders a split-screen: left half painted in THEME_DARK (bg=(16,16,32)), right half in THEME_LIGHT (bg=(250,250,253)). Both halves are verified with pixel samples. QEMU raspi3b: PASS (74,200,160).


What's next

The RTC integration: when the Kernel RTC driver lands, nm_rtc_seconds() will return real wall-clock UTC seconds instead of the loopback zero. The night-mode scheduler will immediately reflect the real time with no other changes — the logic is already correct, only the time source is a stub.

The tz-precise tier (network time + automatic timezone) is planned for the 1.3 networking milestone, when the TCP stack is fully integrated with the OS session layer. For now, tz-coarse with a manual UTC offset is the right floor — it works on a Pi 3 with no network, which is exactly what the Four Pillars ask for.