Building HyDE Display Settings in one long feedback loop

By

I wanted display settings on my HyDE laptop to feel like settings, not like a collection of configuration files I happened to remember. Monitor layout, brightness, night light, idle actions, and screen time were all available, but through different commands and different files. The first question was whether there was already a HyDE or Hyprland package that joined them together. The answer led to a small native application and, over the course of one day, a surprisingly useful lesson in testing software against the actual desktop it is meant to control.

The result is HyDE Display Settings, a GTK 4 and libadwaita application for HyDE and Hyprland. It controls brightness, opens a visual monitor editor, writes idle and night-light configuration, keeps private local screen-time totals, and exposes today's total through Waybar.

This note records the implementation and the mistakes, including the changes that looked correct in code and were wrong on the screen.

Starting with the boundaries

The application was initially for my own laptop, but the intention quickly became to release it as open source. That changed what “working” meant. A local script can assume a directory layout, overwrite a file, and depend on the author remembering how to undo it. A public desktop utility cannot.

The first boundary was ownership. HyDE owns files under ~/.local/share/waybar; user customization belongs under ~/.config/waybar. The installer therefore creates a user module at ~/.config/waybar/modules/custom-display-settings.jsonc and a persistent layout at ~/.config/waybar/layouts/display-settings.jsonc. It does not patch the distributed HyDE files.

The same principle applies to Hyprland configuration. The app does not replace hypridle.conf or hyprsunset.conf. It inserts a block between explicit BEGIN HYDE DISPLAY SETTINGS and END HYDE DISPLAY SETTINGS markers, preserves unrelated content, and writes a uniquely timestamped backup before a change. Applying several settings is treated as one operation. If a later write fails, earlier writes are rolled back.

Those choices added more code than simply rendering a configuration template, but they made uninstalling and experimentation safer. The uninstaller removes the application, service, launcher, and Waybar integration while deliberately leaving preferences, backups, and screen-time history for the user to inspect or remove.

The native control surface

The UI has three views: Display, Idle, and Screen Time.

Display reads connected monitors through hyprctl, adjusts brightness through brightnessctl, launches nwg-displays for visual layout editing, and writes a scheduled hyprsunset profile. Idle exposes dim, lock, display-off, and suspend timeouts. Screen Time shows the day's total and the most-used application classes.

I chose libadwaita because this is a system utility, not a branded web surface. The controls should inherit the active desktop theme and use familiar settings patterns. Timeout rows use native Adw.SpinRow inputs, so their minute values can be typed directly or adjusted with the native stepper buttons. Changes are draft values until Save & Apply writes the configuration and reloads the relevant helpers. Brightness is the exception because immediate feedback is the useful behavior for a slider.

The distinction between draft and applied state later caused confusion. I changed a display-off control, closed the application, and saw the old value when I returned. That was the specified behavior, but it was not sufficiently visible behavior. The apply action became Save & Apply, and the Idle page now states that closing without applying discards its draft. A technically consistent interaction can still be unclear.

Screen time without surveillance

Screen-time tracking is opt-in. A user-level service samples the active Hyprland window every five seconds and stores only three things in SQLite: the local date, the application's Wayland class, and accumulated active seconds. It does not store window titles, keyboard input, screenshots, file names, browsing history, or window contents.

The tracker does not count time while the session reports itself locked or idle, or while every monitor has DPMS disabled. An individual sample is capped so a delayed process cannot attribute a large pause to one application. The state directory is mode 0700; the database and its WAL files are 0600.

The systemd service has no network access and uses NoNewPrivileges, a strict filesystem view, an empty capability set, private temporary storage, and kernel protection directives. systemd-analyze security rated the installed unit at exposure level 3.8 OK. This score is not proof of security, but it is a useful check that the declared sandbox matches the intended local-only data model.

Waybar runs a separate small command that reads the database and emits JSON. The bar shows a compact total such as 3h 3m; its tooltip gives a per-app breakdown, and clicking it opens the settings application. Pango markup is escaped before application classes enter the tooltip.

Auditing before calling the tests real

The initial review asked a necessary question: were the test files actual tests, and what vulnerabilities or bugs remained?

The test suite uses Python's unittest, temporary directories, and process mocks. It covers managed-block replacement, preservation of unrelated config, backup creation, rollback on partial failure, default idle ordering, disabled actions, night-light schedules, tracking opt-in, private database permissions, locked and DPMS-off detection, Waybar insertion and removal, idempotency, layout variants, tooltip escaping, and launcher behavior through an installed symlink.

The hardening pass made several implementation details explicit:

  • configuration is written through a temporary file and atomic rename;
  • state and configuration permissions are owner-only;
  • backups include microseconds so rapid consecutive writes cannot collide;
  • app-class output is escaped before entering Waybar markup;
  • Waybar edits are constrained to user-owned configuration;
  • the tracker service is denied IP networking;
  • installation and removal preserve user data by default.

The release checks grew beyond unit tests. Python compilation, warnings as errors, shell syntax, desktop-entry validation, wheel construction, diff whitespace, live tracker status, permissions, systemd hardening, and a GTK launch smoke test all became part of the verification routine. The suite reached 25 tests after the final UI reversals. Earlier intermediate versions reached 29, but four tests belonged to custom controls that were correctly removed with those controls. A smaller suite can represent a better product when it no longer tests the wrong design.

The display-off failure was a stale process

The most important bug did not appear in the isolated tests. The generated hypridle.conf contained the correct listener:

listener {
    timeout = 900
    on-timeout = hyprctl dispatch dpms off
    on-resume = hyprctl dispatch dpms on
}

Yet changing the setting appeared to do nothing. The live machine showed why. hypridle was running as a process launched directly by HyDE, while its optional user systemd unit was inactive. The application tried systemctl --user try-restart hypridle.service. For an inactive unit, try-restart returned success without starting anything. Because the return code looked successful, the fallback never killed or replaced the unmanaged HyDE process. The process had been alive since August 8 and continued reading the old configuration.

The fix stops the existing process, asks systemd to start a fresh instance, and falls back to hyprctl dispatch exec when the user service cannot start. Tests now cover both the unmanaged-process replacement and the failed-systemd fallback.

The end-to-end test briefly dispatched dpms off and then dpms on. The screen actually turned off. That established two things at once: the Hyprland command was valid on the real monitor, and my intended meaning of “display off” needed to be precise. I wanted the panel off while builds, downloads, and other processes continued. DPMS does that. Suspend does not. The saved setup therefore keeps display-off enabled and suspend disabled. The UI copy now says that display-off leaves apps and background work running, while suspend pauses the computer and its processes.

A theme bug, followed by several design mistakes

The most visible failures came from trying to correct a theme interaction too broadly.

On my active theme, the selected tab's text and background were difficult to distinguish. The first attempted fix forced libadwaita's semantic accent background and foreground onto the selected Adw.ViewSwitcher button. That looked defensible in CSS and bad in the actual theme. It made the active tab muddy and dim.

The next attempt removed the fill and used bold text with an accent underline. It solved the contrast problem by inventing a different tab design. It was still wrong because the original native tab treatment was preferable. The correct response was to remove the application-level tab styling entirely and return control to the GTK theme.

The idle steppers went through a similar cycle. Their plus and minus images appeared absent, so I first tried a foreground-color override. When that did not work, I replaced the native spin rows with a custom linked group containing minus, a minute label, and plus. The buttons worked, but the result looked like a generic generated control and removed direct numeric editing. It solved the symptom by discarding the better component.

I reverted the custom controls and restored Adw.SpinRow. Inspecting the live widget tree showed that libadwaita had created the correct native buttons and assigned value-decrease-symbolic and value-increase-symbolic. Inspecting the resolved assets then found the concrete issue: the Tela Circle Dracula minus SVG hard-coded #565656 instead of inheriting the button foreground. On the dark surface it nearly disappeared.

The final repair keeps the native spin row, its editable input, its spacing, and its button behavior. It replaces only the two broken image children inside the native buttons with foreground-inheriting − and + labels. A live widget inspection verified that the down button contained −, the up button contained +, and the editable GtkSpinButton remained intact.

The sequence left a useful rule: when a native component is almost right, inspect the smallest failing layer before replacing the component. A broken theme asset did not justify a new control system. A poor selected color did not justify a new navigation style.

Making it public

The repository was initialized with small development commits rather than one large final dump: the native app, Waybar integration, privacy hardening, documentation, UI corrections, daemon reload fix, reversals, and final native symbol repair each have their own history.

Before publishing, I compared the README structure with active HyDE projects. The resulting documentation covers requirements, install and update commands, the backend behind every control, privacy boundaries, managed paths, backups, uninstallation, development commands, compatibility, contribution expectations, and the fact that this is an independent community project, not an official HyDE component.

The public repository was created with GitHub CLI, the full history was pushed to main, discovery topics were added, and the generic checkout instructions were replaced with the canonical clone URL. The installed copy on my laptop is the same code recorded at commit c2f7712.

One Waybar edge remains. The installer creates the user module and a persistent display-settings layout, then asks HyDE to regenerate its includes. On this machine, a reinstall could regenerate the previously selected layout into the active config, so I explicitly selected display-settings again with HyDE's waybar.py --set command after each reinstall. The layout survives and is available to HyDE, but selecting it automatically without overriding a user's intent still needs a cleaner installer rule.

What is verified, and what is not

On the target laptop, the following paths were exercised against the live desktop:

  • monitor discovery through hyprctl and launch of nwg-displays;
  • brightness read and write through brightnessctl;
  • DPMS off and on through Hyprland;
  • fresh hypridle and hyprsunset processes after apply;
  • managed configuration and backup creation;
  • screen-time collection, permissions, and Waybar JSON;
  • user service activation and security analysis;
  • GTK launch under the active HyDE theme;
  • installation, repeated installation, and explicit Waybar layout selection.

Lock and suspend were not triggered end to end because doing so would disrupt the active session. Their generated commands, ordering, inclusion, omission, and apply behavior are tested in isolation. Night-light schedule generation and daemon configuration loading were tested, but waiting through a real scheduled transition was not. Multi-monitor behavior is implemented through hyprctl and nwg-displays, but the development machine had one internal eDP-1 panel during verification.

Those limits matter. “All tests pass” means the claims covered by those tests pass. The stale hypridle process and the invisible theme asset both survived the earlier automated suite because each depended on the actual session. For desktop software, the live environment is not merely a place to demonstrate the result. It is another test harness, and sometimes the only one capable of showing the bug.