OpenRiot v7.9.3 — The One Where We Mapped the Home Directory to Oblivion

“The only truly portable home directory accessor is the one you write yourself, because os.UserHomeDir() is a trap. A honey pot. A function that looks innocent until you run go test and realize it cached the wrong home directory like a raccoon with a shiny thing it refuses to let go.” — The OpenRiot Crew, at 4 AM, after go test failed for the fourteenth time


Release Overview

v7.9.3 is the release where we realized that os.UserHomeDir() is the Go equivalent of a static global variable in a C program from 1987. It seems fine. It seems reasonable. It is the standard library. The standard library would not lie to us. But it does. It lies by caching. It lies by being called thirty times across twenty files. It lies by making every path construction look like a first-year computer science homework assignment where the professor said, “Just use getenv("HOME").”

This release is about abstraction. About building a paths package that knows where your home directory is, constructs paths under it without consulting the environment every time, and does not pretend that filepath.Join(homeDir, ".local", "share", "openriot") is an acceptable thing to type in the year 2026. It is not. It was never acceptable. We were young. We were foolish. We have since been to counseling.

The Great os.UserHomeDir() Extinction Event — or, 28 Calls Die So That Millions May Live — We replaced twenty-eight calls to os.UserHomeDir() with paths.HomeDir(), paths.Join(), paths.OpenRiotDir(), paths.ConfigDir(), and paths.IconDir(). The victims — and we use that word deliberately, because these functions did not go quietly — include installer/cmd.go, installer/configs.go, installer/crush.go, installer/games.go, installer/tag.go, installer/release_notes.go, notify/cooldown.go, notify/notify.go, weather/weather.go, wireguard/wireguard.go, screenrec/screenrec.go, nightlight/nightlight.go, resolution/backend.go, windowicon/windowicon.go, screenshot/screenshot.go, update/versions.go, backgrounds/backgrounds.go, crypto/crypto.go, fonts/fonts.go, gurk/gurk.go, assets/assets.go, lock/cache.go, lock/lock.go, network/network.go, polybar/polybar.go, rofi/rofi.go, window/switch.go, settings/settings.go, commands/commands.go, commands/helpers.go, main.go, and config/loader.go.

That is thirty-three files. We counted. We also counted the number of times we typed filepath.Join(homeDir, ".local", "share", "openriot") and discovered it was approaching a statistically significant portion of the codebase. If you printed all those lines and stacked them, they would reach the moon. Not really. But they would reach the top of a desk, which is tall enough to be embarrassing.

The paths package is now the single source of truth for all path construction. paths.HomeDir() caches the result once per process. paths.OpenRiotDir("install", "packages.yaml") returns exactly what you think it returns. paths.Join(".config", "openriot", "games.cfg") does the same. No more filepath.Join(homeDir, ...). No more error handling for a home directory lookup that cannot realistically fail. No more twenty-line boilerplate blocks that exist only to join strings.

paths/paths.go — The Cache War — The original paths.HomeDir() used sync.Once to cache the home directory after the first call. This seemed smart. It is, in fact, the canonical Go pattern for lazy initialization. But sync.Once never forgets. It is the elephant of Go concurrency primitives. Once it executes, it remembers. Forever.

The problem: tests. Tests set HOME to a temporary directory. Then they call paths.HomeDir(). Then sync.Once says, “I already did this. The home directory is /tmp/TestSomething12345. That is the truth now. That is the law.” And every subsequent test that changes HOME gets the old temporary directory back, because sync.Once does not care about your test isolation. It cares about doing something exactly once. That is its entire personality.

We removed the cache. HomeDir() now calls os.UserHomeDir() every time. This is not a performance problem. It is a string lookup. It takes nanoseconds. The sync.Once cache was optimizing a non-problem and creating a real one. This is a classic case of premature optimization causing actual bugs. We are not sorry. We are wiser.

New Wallpapers — 22 Backgrounds, 18 Lock Screens, and the Re-Encoding of Everything That Came Before — We added backgrounds/23.webp through backgrounds/32.webp, and Locked/25.webp through Locked/42.webp. Existing wallpapers were re-encoded at higher quality settings because our previous batch was compressed with the enthusiasm of a JPEG from 1998. The new backgrounds include landscapes that make you want to quit your job, cityscapes that make you want to move to Tokyo, and abstract compositions that make you wonder if the color temperature algorithm in cwebp is sentient.

The lock screens are even more aggressive. They are designed to make you pause before unlocking your laptop. Not because they are distracting, but because they are beautiful enough that you want to look at them for an extra second. This is the OpenRiot security model: make the lock screen so appealing that shoulder surfers get distracted and forget what they were doing.

Discord and SolveSpace 3D — The Social Network and the CAD Software Walk Into a Bar — Two new .desktop files: config/applications/discord.desktop (abaddon, ) and config/applications/solvespace.desktop (󰐫). The rofi app menu (Super+D) now includes both. The workspace icon table (config/window/icons.toml) maps abaddon and solvespace to their respective nerd font icons. The README App Launcher table now lists them alongside the existing entries, because a desktop that does not acknowledge Discord and CAD software is a desktop that does not acknowledge reality.

The 3D Printing Section — or, README.md Gets a Manufacturing Arm — We added a new ## 🖨️ 3D Printing section to the README, complete with a SolveSpace documentation link and a 7-printer compatibility table: Prusa CORE One+, Bambu Lab P2S, Elegoo Centauri Carbon, Prusa SL1S SPEED, Anycubic Photon Mono M7 Max, Elegoo Saturn 4 Ultra, and Bambu Lab A1 Mini. The table lists printer type, build volume, and compatibility status. The README TOC was updated to include [🖨️ 3D Printing](#3d-printing).

This exists because OpenRiot users do not just code. They also design parts in SolveSpace, slice them in Cura, and watch their printers create physical objects from nothing while make release runs in the background. We respect this. We document this. We do not judge the person who has a Prusa and a ThinkPad running OpenBSD. We celebrate them.

NZBGet Polybar Module — The SabNZBD Replacement That Does Not Eat Thirty Percent of Your CPU — The previous approach used SabNZBD, a Python-based usenet downloader that launches multiple processes, spawns threads like a mushroom cloud, and generally behaves like a cryptocurrency miner that happens to download Linux ISOs. We replaced it with NZBGet, a C binary that uses pgrep -x for process detection instead of pgrep -f (which self-matched the polybar exec command).

source/polybar/polybar.go now has RunNzbget() with icons 󱑤 (running) and 󱑥 (installed, not running). source/commands/commands.go registers --polybar-nzbget and --nzbget-open. The config/polybar/config.ini has a new [module/nzbget] between crypto and proton-drive. source/settings/settings.go adds a conditional “NZBGet” stop entry in the settings rofi menu, visible only when NZBGet is running.

--nzbget-open logic: if running, opens http://127.0.0.1:6789 via Firefox with a notification. If installed but not running, launches /usr/local/bin/nzbget -D (daemon mode, capital D) with a notification and a fixNzbgetPerms() helper that chowns /etc/nzbget.conf and /var/nzbget to the current user if they exist and are not already owned. Otherwise, warns that NZBGet is not installed.

The settings rofi menu also got a violet border (border-color: #997de1) because a settings dialog without a border color is like a terminal without a font: technically functional, spiritually empty.

rmpc Workspace Icon Fix — The Music Player That Was Invisibleconfig/window/icons.toml now maps rmpc = "󰽴". The music player was using the rmpc window class (set by the i3 keybinding that launches the terminal with --class rmpc) but the icon table only had ncmpcpp, mpd, and lxmusic. rmpc got the fallback icon, which is the polybar equivalent of being sent to the penalty box. It is now properly labeled. It is now visible. It now has dignity.

Miscellaneous Cleanups — The Things You Notice at 3 AMsource/commands/helpers.go now uses pre-compiled regexes for bind-address-ipv4 and bind-address-ipv6 replacement in Transmission settings, because strings.Replace in a loop is a war crime. strconv.Atoi replaces manual string-to-int parsing where appropriate. syscall imports were added for signal handling in process detection. source/polybar/polybar.go uses pgrep -x instead of pgrep -f to avoid self-matching polybar exec commands. source/lock/lock.go gained filterStealth() and isProcessRunning() helpers to deduplicate process detection logic.


🧾 Files Changed

File Nature of Change
source/paths/paths.go Removed sync.Once; HomeDir() uncached
source/installer/cmd.go paths refactor
source/installer/configs.go paths refactor
source/installer/crush.go paths refactor
source/installer/games.go paths refactor
source/installer/tag.go paths refactor
source/installer/release_notes.go paths refactor
source/notify/cooldown.go paths refactor
source/notify/notify.go paths refactor
source/weather/weather.go paths refactor
source/wireguard/wireguard.go paths refactor
source/screenrec/screenrec.go paths refactor
source/nightlight/nightlight.go paths refactor
source/resolution/backend.go paths refactor
source/windowicon/windowicon.go paths refactor
source/screenshot/screenshot.go paths refactor
source/update/versions.go paths refactor
source/backgrounds/backgrounds.go paths refactor
source/crypto/crypto.go paths refactor
source/fonts/fonts.go paths refactor
source/gurk/gurk.go paths refactor
source/assets/assets.go paths refactor
source/lock/cache.go paths refactor
source/lock/lock.go paths refactor; filterStealth() helper
source/network/network.go paths refactor
source/polybar/polybar.go paths refactor; pgrep -x fix
source/rofi/rofi.go paths refactor; strconv.Atoi
source/window/switch.go paths refactor; strconv.Atoi
source/settings/settings.go paths refactor; NZBGet stop entry
source/commands/commands.go paths refactor; NZBGet commands
source/commands/helpers.go paths refactor; pre-compiled regexes
source/main.go paths refactor
source/config/loader.go paths refactor
config/window/icons.toml Added rmpc, abaddon, solvespace icons
config/polybar/config.ini New [module/nzbget]; modules-right updated
config/rofi/apps.txt Added Discord and SolveSpace entries
config/applications/discord.desktop New file
config/applications/solvespace.desktop New file
backgrounds/*.webp 22 new backgrounds + re-encoded existing
Locked/*.webp 18 new lock screens + re-encoded existing
install/packages.yaml Added nzbget package
README.md Discord/SolveSpace apps, 3D Printing section, TOC
install/openriot Binary rebuilt

🎵 What We’re Listening To

Same playlist. Same questionable Theremin solo. But now your codebase does not contain twenty-eight copies of filepath.Join(homeDir, ".local", "share", "openriot"). Your paths package is a single point of truth. Your tests do not fail because sync.Once decided that the first temporary directory it saw is the One True Home. Your wallpapers are numerous enough that you could use a different one every day for a month and still have leftovers. Your lock screens are so beautiful that you sometimes forget your password because you were staring at the gradient. Your app menu has Discord and a 3D CAD program. Your polybar has an NZBGet module that does not consume an entire core just to check if a process exists.

The paths refactor is the kind of change that makes you wonder why you did not do it six months ago. It is also the kind of change that makes you realize how many times you copy-pasted the same five lines of path construction. Twenty-eight times. That is not a number. That is a cry for help.

We answered the cry. We built the abstraction. We eliminated the duplication. We made the code shorter, cleaner, and less likely to break when someone moves their home directory to a ZFS pool on a network-attached toaster. This is what optimization looks like when you optimize for maintainability instead of microseconds.


🗣️ Final Words

“The best home directory accessor is the one you only write once.” — The OpenRiot Crew, after deleting the fourteenth filepath.Join(homeDir, ".local", "share", "openriot") and feeling a spiritual release usually reserved for finishing a four-hour ambient album

v7.9.3 is the release where we looked at os.UserHomeDir() and said, “You are not wrong. You are just everywhere. And being everywhere is worse than being wrong, because wrong can be fixed. Everywhere requires a systematic extinction event.”

Then we looked at sync.Once and said, “You are a brilliant primitive that solves a real problem. But you are not the right primitive for a home directory lookup. That problem does not need solving. It needs letting go.”

Then we looked at the wallpapers and said, “There should be more of these.”

Then we looked at the lock screens and said, “These should also be more numerous.”

Then we looked at the app menu and said, “Discord and CAD software belong here.”

Then we looked at the polybar and said, “NZBGet should be here, and it should not be written in Python.”

Then we looked at the README and said, “3D printing is a thing that happens. We should document it.”

Then we looked at rmpc and said, “You deserve an icon. Not a question mark. An icon.”

None of these are revolutionary. All of them are correct. The paths are abstracted. The cache is gone. The wallpapers are many. The lock screens are beautiful. The app menu is complete. The polybar is streamlined. The README has a manufacturing section. The music player has an identity. The code is shorter. The tests pass. The build is green.

Your go test does not fail because sync.Once cached the wrong home. Your openriot --polybar-nzbget does not match itself. Your Super+D menu has Discord. Your Super+Shift+K music player has an icon. Your desktop knows what 3D printing is.

This is maintenance. This is care. This is what it looks like when people who build the installer also have to look at their own code and admit that filepath.Join(homeDir, ".local", "share", "openriot") was never a good idea, no matter how many times we typed it.

— The OpenRiot Crew

“Your home directory should be a constant, not a variable. Your paths should be constructed by functions that know what they are doing. Your cache should solve a real problem, not create one. Your wallpapers should be numerous. Your lock screens should be art. Your app menu should be complete. Your polybar should not self-match. And your music player should never be reduced to a question mark.”

← Back to README