screentime
iPhone-style Screen Time for Hyprland: a tiny daemon that tracks which app you're using, stops the clock when you're idle, locked, the screen is off or the machine sleeps, and gives you today's and this week's numbers.
SCREEN TIME desktop · Today, Wed 4 Nov
4h 20m today
▂▂▅▅▃▃▁▁▆▆▃▃▂▂▁▁▃▃▄▄██▆▆▃▃▂▂▅▅▇▇
00 06 12 18
● Development 1h 52m ● Web 1h 04m ● Social 40m ● Games 21m
MOST USED
1 Alacritty 1h 43m ━━━━━━━━━━━━━━━━━━
2 Firefox 1h 04m ━━━━━━━━━━━
3 Discord 40m ━━━━━━━
4 Counter-Strike 2 21m ━━━━
PICKUPS 23 first at 08:12
first used after pickup: Discord 7, Firefox 5, Alacritty 4
LONGEST SESSION 1h 32m 13:05–14:37, mostly Alacritty
Features
- Tracks the focused app from Hyprland's event socket (
activewindow,workspace), with friendly names and iOS-style categories. - Only counts real use. The clock stops when you're idle (Wayland
ext-idle-notify-v1, so a fullscreen video still counts but a "keep awake" toggle doesn't), when a screen locker runs, when every monitor is off (DPMS), and while the machine is suspended. - iOS numbers: today's total, per-app and per-category time, hourly bars, pickups (wakes, unlocks, screen-on), first pickup, "first used after pickup", longest session, and the week's daily average with the change from last week.
- Private by default: stores the window class only. Window titles are saved only if you turn them on.
--jsonoutput for bars, widgets, dashboards and scripts.- Optional push of each day's summary to a URL with a token, queued in the database while you're offline, so a laptop catches up after sleep or once it's back home.
- No dependencies: Python 3.11+ standard library only, SQLite storage, a systemd user service.
Install
git clone https://github.com/Tjabooo/screentime
cd screentime
./install.sh
This copies the code to ~/.local/share/screentime/lib, puts the screentime command in ~/.local/bin, writes a commented config to ~/.config/screentime/config.toml (if you don't have one), and enables screentime.service for your graphical session. Run ./install.sh again to update and ./install.sh --uninstall to remove it (your data stays).
It also installs with pip or pipx (pipx install .); then add the service yourself from systemd/screentime.service.
Requirements: Hyprland, Python 3.11 or newer, systemd. Your session should export WAYLAND_DISPLAY and HYPRLAND_INSTANCE_SIGNATURE to the systemd user manager (most Hyprland setups do; if not, add exec-once = systemctl --user import-environment WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE).
Usage
screentime today # today, as above
screentime today --date yesterday
screentime week # this week: daily average vs last week, per-day bars, most used
screentime week --date 2026-10-28
screentime today --json # everything as JSON (also: week --json)
screentime push # send unsent days to the push URL now
screentime config # where the config and database live
systemctl --user status screentime
today --json gives total, apps, categories, hourly (24 entries with per-category seconds), pickups, pickups_hourly, first_pickup, after_pickup, longest_session and daemon (whether it's running and what it sees right now). week --json gives average, last_week_average, change_percent, days, apps, categories, pickups and pickups_average.
Configuration
~/.config/screentime/config.toml (or $SCREENTIME_CONFIG). Every key is optional; see config.example.toml.
| Key | Default | |
|---|---|---|
idle_timeout |
180 |
Seconds without input before the clock stops. |
trim_idle |
true |
Also take those idle seconds back off the session. |
track_titles |
false |
Store window titles as well as the app class. |
lockers |
hyprlock, swaylock, … |
Processes that mean "locked". |
ignore |
[] |
Window classes (globs) that never count. |
week_start |
monday |
Or sunday. |
[categories] |
built-in list | "glob" = "Category", checked before the built-in map. |
[names] |
built-in list | "glob" = "Display name". |
[push] url |
off | POST each day's summary here. |
[push] token / token_file |
Sent as Authorization: Bearer … (or set $SCREENTIME_TOKEN). |
|
[push] interval |
600 |
Seconds between pushes of today's running summary. |
[push] catch_up_days |
14 |
How far back unsent days are retried. |
Categories: Social, Games, Entertainment, Web, Development, Productivity, Creativity, Education, Utilities and Other.
Push format
POST <url>
Authorization: Bearer <token>
Content-Type: application/json
{"device": "desktop", "date": "2026-11-04", "final": false, "summary": { …same as `today --json`… }}
Today's summary is re-sent every interval seconds. A day counts as sent once a push succeeds after that day has ended; until then it stays queued in the database and goes out on the next successful push (after a resume, or whenever the URL answers again). Any 2xx answer is success.
How it works
- Focus: a connection to Hyprland's
.socket2.sockdeliversactivewindow>>class,title; the request socket answersj/activewindowandj/monitors(for DPMS). - Idle: a ~100-line pure-Python Wayland client binds
wl_seatandext_idle_notifier_v1and asks for an idle notification afteridle_timeout.idledstops the clock (and, withtrim_idle, ends the session when input stopped);resumedstarts it again. - Lock, screen-off, suspend: every
poll_intervalseconds it looks for a locker process in/procand asks Hyprland whether any monitor is on. A jump betweenCLOCK_BOOTTIMEandCLOCK_MONOTONICmeans the machine was suspended; that time is cut out. - Pickups: coming back from locked, screen-off, suspended or a fresh login is a pickup. Coming back from a short idle with the screen still on isn't.
- Storage: sessions (
app, optionaltitle,start,end) and pickups in SQLite (WAL) at~/.local/share/screentime/screentime.db. The running session is written every 15 seconds, so a crash loses at most that.
Development
python -m unittest discover -s tests -v