Repository navigation
Architecture
ScreenGuard is three cooperating processes, three native C helpers, a Python X11 fallback and a database. This page explains how they fit together and why.
graph TD
A[ScreenGuard Flutter GUI] -->|SQLite WAL| DB[(usage.db)]
B[screenguard-daemon] -->|SQLite WAL| DB
B -->|Poll active window every 3s| C{Display server}
C -->|Wayland GNOME| D[GNOME Shell extension]
D -->|D-Bus| B
C -->|X11 desktop| E[X11 EWMH via xprop]
B -->|Enforce limits and focus| F[screenguard-killer]
B -->|Desktop notifications| G[screenguard-notify]
H[screenguard-watchdog] -->|Restarts the daemon with backoff| B
A Flutter application using Material 3. It owns the dashboard, the app-limit dialogs, the focus timer and the settings sidebar. It can start, stop and configure the daemon, including autostart.
The GUI is a client. Closing the window does not stop tracking.
A single-file Dart program compiled to a native binary (screenguard-daemon). The current daemon is written in Dart; the v1.5 roadmap targets a C port that preserves its behavior and database contract. Until then, this section describes the Dart implementation.
It is the only component that:
- samples the foreground window every few seconds,
- writes usage sessions,
- enforces daily app limits and the focus-mode distraction blocker,
- checks for updates and for feedback requests,
- writes a heartbeat file the GUI reads to tell whether the daemon is alive.
It runs as a systemd user service where systemd is available, with an XDG autostart fallback on systems without it.
Single instance. On start the daemon takes an exclusive lock on ~/.local/share/screenguard/daemon.lock. If the lock is already held, the new process exits immediately — two daemons would double-count every session.
Crash safety. A zone error handler catches unhandled exceptions, writes a local crash log and exits non-zero. The watchdog then restarts the daemon. Unclean shutdowns (logout, reboot) are recorded locally but deliberately not reported, because they are normal and would otherwise flood the error stream. Any sessions left orphaned by such a stop are closed on the next start, so they do not inflate the 30-day totals.
GNOME Wayland — Wayland does not let an application ask which window has focus. The ScreenGuard GNOME Shell extension (screenguard@screenguard.app, GJS) runs inside the compositor and exports the active window's title, class and PID over D-Bus:
org.gnome.Shell.Extensions.ScreenGuard
/org/gnome/Shell/Extensions/ScreenGuard
The extension also exposes CloseActiveWindow() and CloseWindowByPid(), and reports the real minimised state so the app can tell whether a window actually went away.
X11 — no extension needed. The daemon reads the active window from the standard EWMH property _NET_ACTIVE_WINDOW using xprop, and minimises or closes it with xdotool. Idle time comes from xprintidle.
Three small C programs in src/, plus a Python fallback in bin/, handle the parts that must not depend on the Dart runtime, on notify-send, or on optional system tools:
| Binary | Purpose |
|---|---|
screenguard-notify |
Sends desktop notifications over D-Bus, with notify-send as a fallback. Doing this natively means alerts behave the same on systemd and on non-systemd systems (OpenRC, runit) |
screenguard-killer |
Closes a window through an escalating, data-safe chain: polite window-close request → SIGTERM → process-tree SIGKILL → Proton-native wineserver -k → guarded pattern-kill |
screenguard-watchdog |
A ~19 KB supervisor that detects a crashed daemon and restarts it with exponential backoff, across all Linux init systems |
bin/active_window_helper.py |
Fallback window probe for unusual X11 setups |
The killer's escalation is what makes Close actually work on Proton games, which ignore ordinary signals.
SQLite at ~/.local/share/screenguard/usage.db, in WAL mode so the GUI and the daemon can write at the same time without blocking each other.
| Table | Holds |
|---|---|
sessions |
One row per tracked window session: app id, app name, window title, PID, start and end timestamps, duration, idle flag |
settings |
Key/value store for every preference and internal flag |
app_limits |
Per-application daily limit, temporary extension and enabled flag |
distracting_apps |
The applications blocked during focus sessions |
focus_sessions |
Focus-session history with completion status |
active_focus_state |
The single running session: end time, duration, paused flag, remaining seconds, cycle counters, break state |
The settings table is also where the daemon publishes state the GUI needs to read — update information, crash counts, daemon ownership, the pending feedback request. It is a tiny message bus over SQLite.
- The daemon samples the foreground window every three seconds.
- It resolves the window to an application id, then to a friendly name. Steam games resolve through your library manifests, so they appear under their real names with their own icons.
- It writes the session row into
sessions. - If that application has a limit and the accumulated time today crosses 100%, the daemon minimises the window and tells the GUI to show the warning screen — it launches the GUI with
--times-up=<app>. - The focus-mode blocker works the same way, with
--focus-block=<app>.
Nothing is closed by default. The default path minimises the window and shows a warning screen with a one-minute save grace, +15 minutes, edit-limit and close options. Only with Strict auto-close enabled does the escalating kill chain run automatically. The per-app grace window is tracked separately, so the warning stops while a grace period is active.
| Data | Where it goes |
|---|---|
| Screen-time sessions | The local SQLite file, and nowhere else |
| Window titles | Stored locally; never transmitted |
| Update check |
GET https://api.github.com/repos/…/releases — no identifiers |
| Crash reports | Opt-out; a random local UUID, the error text, the app version, the desktop environment, the display server and the distribution name. No usernames, no machine identity, no screen-time history |
| Feedback requests |
GET of one public JSON file; the app version in the User-Agent
|
There is no account system, no sync and no analytics endpoint.
Related: Features · Development · Troubleshooting
ScreenGuard — digital wellbeing for Linux. Source code · Issues · Discussions · Releases
Maintained by Aditya Krishnan · GPLv3