Hyprland Screen Time Tracking

I was looking for a screen time tracking solution for Linux, similar to what you see on modern smartphones. I came across ActivityWatch, which seemed to be the most popular open source solution. I’m using Arch Linux with Hyprland, so there was a certain amount of work I had to do to get it working.

TLDR

These are the steps I came up with to get ActivityWatch working with Hyprland on Arch Linux.

Install ActivityWatch

ActivityWatch is available in the AUR. The activitywatch-bin package seems to the most up to date. If you have an AUR helper like yay installed, you can use that, otherwise you can install it manually.

yay -S activitywatch-bin

Install UWSM

ActivityWatch relies on the XDG Autostart specification to start automatically on login. UWSM (Unversal Wayland Session Manager is one solution to supporting this spec. Install the package, then log in with the “Hyprland (UWSM Managed)” session entry.

sudo pacman -S uwsm

Install aw-watcher-window-hyprland

For security reasons, the Wayland specification does not have a native way to retrieve information about the active window, so each desktop environment needs to implement this functionality manually. Hyprland supports this through its hyprctl CLI utility. ActivityWatch supports custom “watchers” (standalone programs that retrieve application info), and one has been created that uses hyprctl to get the active window info. It doesn’t have an Arch package, so it has to be installed manually. This program is developed in the Rust programming language, so you’ll need to install the Rust toolchain if you haven’t already.

pacman -S rustup
rustup update stable
git clone https://github.com/bobvanderlinden/aw-watcher-window-hyprland.git
cd aw-watcher-window-hyprland
cargo install --path .
sudo cp ~/.cargo/bin/aw-watcher-window-hyprland /usr/local/bin/

Configure Autostart

Place a desktop entry file in ~/.config/autostart/ to start the watcher automatically. At the time I’m writing this, there was an issue where if you try to start the watcher at the same time as the ActivityWatch server, it will immediately try to connect to the server, and crash when it can’t connect. So I’ve added a 1 second sleep to the autostart command.

mkdir -p ~/.config/autostart
cat <<EOF > ~/.config/autostart/aw-watcher-window-hyprland.desktop
[Desktop Entry]
Name=aw-watcher-window-hyprland
Comment=ActivityWatch window tracker for Hyprland
Exec=sh -c "sleep 1 && aw-watcher-window-hyprland"
Hidden=false
StartupNotify=true
Terminal=false
Type=Application
X-GNOME-Autostart-enabled=true
Version=1.0
Icon=activitywatch
Categories=Utility;
EOF

Disable the default window watcher

ActivityWatch automatically starts its own window watcher on startup, which we just replaced with aw-watcher-window-hyprland. The default watcher won’t do us any good, so we’ll disable it.

mkdir -p ~/.config/activitywatch
cat <<EOF >~/.config/activitywatch/aw-qt/aw-qt.toml
[aw-qt]
autostart_modules = ["aw-server", "aw-watcher-afk"]

[aw-qt-testing]
autostart_modules = ["aw-server", "aw-watcher-afk"]
EOF

After a reboot, everything should be working as expected. Navigate to http://localhost:5600/#/timeline to view your activity.

How I Got There

The process of getting this all working involved exploration and quit a bit of trial and error.

Autostart

As it happens, autostart was working out of the box for me, but I had just read a GitHub issue stating that XDG Autostart support is not planned for Hyprland. I’ve come to realize that the solution of using UWSM for XDG Autostart support is right there in the Hyprland docs, but I didn’t know that at the time, so for historical interest here’s the trail of breadcrumbs I followed to get there myself.

  1. To make sure autostart is working, I restarted my computer, went to http://locahost:5600 and made sure I could see the ActivityWatch dashboard.
  2. I ran systemctl status and saw ActivityWatch services running under user.slice > user-1000.slice > user@1000.service > app.slice > app-aw\x2dqt@autostart.service.
  3. I Googled “systemd 1000.service” to try to figure out where those service definitions are coming from, and found the freedesktop.org documentation.
  4. In the docs I saw a reference to /usr/lib/systemd, so I went to that folder and search for autostart: grep -ril autostart. I found /usr/lib/systemd/user/xdg-desktop-autostart.target.
  5. I verified that the above file is part of core systemd by finding it in the file listing on the Arch package page
  6. xdg-desktop-autostart.target has a line Documentation=man:uwsm(1) man:systemd.special(7) so I ran man 7 systemd.special and searched for “autostart”. I found the xdg-desktop-autostart.target section, which says “Desktop Environments can opt-in to use this service by adding a Wants= dependency on xdg-desktop-autostart.target.”
  7. I searched the packge file list for hyprland, and saw listings for usr/share/wayland-sessions/hyprland.desktop and usr/share/wayland-sessions/hyprland-uwsm.desktop. I normally use the uwsm (Universal Wayland Session Manager) version, so I have a hunch that uwsm is what adds the autostart support.
  8. I tried logging out and logging in with the non-uwsm managed Hyprland session, and sure enough, ActivityWatch was not started or present in the systemctl status output.
  9. The Arch Wiki page for uwsm confirms that it adds XDG Autostart support.
  10. Just for my own edification, I wanted to see if I could find the Wants= dependency on xdg-desktop-autostart.target somewhere in the uwsm config files, so I searched the package contents and found several “.target” and “.service” files in the /usr/lib/systemd/user folder. I went to this folder and searched grep -ril "Wants=.*autostart", and found wayland-session@.target. That’s one of the files created by uwsm, and a quick search of man uwsm found a “Startup” section which says “UWSM uses a set of units bound to standard user session targets”, mentioning wayland-session@.target as one of the units. I could’ve gone further down the rabbit hole of Wayland and systemd protocols, but at this point I was satisfied.

Application Recognition

When I first installed ActivityWatch, I could see a timeline, but most applications were listed as “unknown”. This issue took several hours to resolve, this is the journey I took to get there.

So far I had observed that Firefox and Foot (a terminal emulator) were showing up as “unknown”, but Minecraft and Discord were showing up correctly. I tried running Ghostty (another terminal emulator) and GIMP, and neither of those were reported correctly either. On the ActivityWatch FAQ I saw a reference to the raw data, which I was able to view interactively by running sqlite3 ~/.local/share/activitywatch/aw-server/peewee-sqlite.v2.db. A .tables command at the interactive shell yielded bucketmodel and eventmodel. I ran select * from eventmodel limit 10; and saw an entry like this:

8|1|2026-01-02 20:45:21.869000+00:00|280.418|{"app": "unknown", "title": "unknown"}

So that wasn’t much help, but at least I knew it was an issue with the raw data, not the frontend. I went back to the FAQ page, and felt a little foolish when I saw a “Why is the active window logged as ‘unknown’ when using Wayland” section, which states that as a security design choice, there is no native way in the Wayland protocol to know the name of the active window. The solutions listed are (1) switch to X11, or (2) try an alternative window watcher that support Wayland. One of the watchers linked was aw-watcher-window-hyprland.

As of the time of writing there’s not an AUR package for aw-watcher-window-hyprland, so I built and ran it manually:

sudo pacman -S rustup
rustup update stable
git clone https://github.com/bobvanderlinden/aw-watcher-window-hyprland.git
cd aw-watcher-window-hyprland
cargo build --release
./target/release/aw-watcher-window-hyprland

And it worked! After a minute or two of switching between different windows, I went back to the timeline view, and when I refreshed the page I saw a aw-watcher-window-hyprland and aw-watcher-workspace-hyprland entry. The window watcher was reporting the correct application names, and the worksapce watcher was reporting the active workspace.

I followed the project’s README instructions and used cargo install --path . to install the watcher to my system. This installed the binary to ~/.cargo/bin. I assumed this would not be enough to autostart the watcher though, and sure enough when I logged out and back in, the new watcher was not active. So I started reading through the ActivityWatch docs again, and found the “Configuration” section. It directed me to the “Settings” page in the GUI, where I found some handy settings but nothing referencing enabling custom watchers, or disabling the default ones.

Looking further, the Directories section of the docs listed the configuration directory for Linux as ~/.config/activitywatch. I browsed the files in there and found aw-qt/aw-qt.toml, with the following contents:

[aw-qt]
#autostart_modules = ["aw-server", "aw-watcher-afk", "aw-watcher-window"]

[aw-qt-testing]
#autostart_modules = ["aw-server", "aw-watcher-afk", "aw-watcher-window"]

This looked promising, so I changed it to disable the default window watcher and enable the Hyprland one:

[aw-qt]
autostart_modules = ["aw-server", "aw-watcher-afk", "aw-watcher-window-hyprland"]

[aw-qt-testing]
autostart_modules = ["aw-server", "aw-watcher-afk", "aw-watcher-window-hyprland"]

This stopped the default window watcher, but it did not start the hyprland one. Does it need to be installed in a specific location, as a “module”?

After searching the docs for “module” to no avail, I tried running aw-qt manually. It failed because it was already running, but I did start seeing logs for the Hyprland window watcher. The ActivityWatch docs list ~/.cache/activitywatch/log as the default log diretory. In that directory there’s a aw-qt subdirectory, with log files for each time the program starts. Here I found the issue: aw-watcher-window-hyprland was not found when run through XDG autostart, but it was found when I tried to run aw-qt manually. I determined this was almost certainly because the watcher is installed in ~/.cargo/bin, which I happen to have in my .zshrc as a PATH entry, but that won’t be respected by XDG autostart programs.

I ran sudo cp ~/.cargo/bin/aw-watcher-window-hyprland /opt/activitywatch/ and restarted Hyprland. This got rid of the error message in the log, and I saw “Staring module aw-watcher-window-hyprland”, but I still didn’t see it running. I modified /etc/xdg/autostart/aw-qt.desktop to enable verbose logging:

Exec=aw-qt -v

No good, I didn’t see any more logs. But I did notice these lines:

2026-01-03 11:47:16 [INFO ]: Found 8 bundled modules  (aw_qt.manager:85)
2026-01-03 11:47:16 [INFO ]: Found 0 system modules  (aw_qt.manager:119)

Maybe we need a system module? I looked in the aw-qt source code and found that it’s searching every directory in os.get_exec_path() for executables starting wtih aw-. Running a quick python -c 'import os; print(os.get_exec_path())' showed me the standard set of search paths for executables, including /usr/local/bin/, but notably not /opt/activitywatch/. So I tried sudo cp ~/.cargo/bin/aw-watcher-window-hyprland /usr/local/bin. Getting closer? At this point I saw “Found 1 system modules”, but still no activity in the Timeline view, and no aw-watcher-window-hyprland in systemctl status output.

At this point I decided to properly run aw-qt manually, so I ran pkill aw-server so I wouldn’t get the port conflict, then ran aw-qt. This time I saw this error:

thread 'tokio-runtime-worker' (59154) panicked at src/window.rs:64:41:
called `Result::unwrap()` on an `Err` value: reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(5600), path: "//api/0/buckets/aw-watcher-window-hyprland_paul-desktop", query: None, fragment: None }, source: hyper::Error(Connect, ConnectError("tcp connect error", Os { code: 111, kind: ConnectionRefused, message: "Connection refused" })) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'tokio-runtime-worker' (59129) panicked at src/workspace.rs:61:41:
called `Result::unwrap()` on an `Err` value: reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(5600), path: "//api/0/buckets/aw-watcher-workspace-hyprland_paul-desktop", query: None, fragment: None }, source: hyper::Error(Connect, ConnectError("tcp connect error", Os { code: 111, kind: ConnectionRefused, message: "Connection refused" })) }

Based on this error, my guess was that the watcher was started before the server, and it crashed because it couldn’t connect to the server’s address. I modified src/main.rs in the aw-watcher-window-hyprland source to add a 1 second sleep:

    println!("Waiting 1 second to allow server to start...");
    std::thread::sleep(Duration::from_secs(1));
    println!("Connecting...");

Verified it compiled and ran:

cargo run --release

And installed it:

cargo install --path .
sudo cp ~/.cargo/bin/aw-watcher-window-hyprland /usr/local/bin

Now I could see the watcher running, but I still wasn’t getting any data in the Timeline view.

As an aside, it was at this point I realized I could put the aw-watcher-window-hyprland binary in the /opt/activitywatch folder, either in the root or in a aw-watcher-window-hyprland folder. It just gets loaded as a “bundled module” instead of a “system module”. It didn’t seem to make any difference in behavior where I put it.

I then tried running aw-qt manually again, and this time I noticed some error messages:

[aw-watcher-window-hyprland] hyprctl failed: exit status: 1
[aw-watcher-window-hyprland] Failed to get active window info
[aw-watcher-window-hyprland] hyprctl activeworkspace failed: exit status: 1
[aw-watcher-window-hyprland] Failed to get active workspace info
[aw-watcher-window-hyprland] hyprctl failed: exit status: 1

So I modified the watcher source code again to print some verbose error output:

// src/window.rs
eprintln!("[aw-watcher-window-hyprland] hyprctl failed: {} {} {}", output.status, String::from_utf8(output.stdout).unwrap(), String::from_utf8(output.stderr).unwrap());
cargo install --path .
sudo cp ~/.cargo/bin/aw-watcher-window-hyprland /opt/activitywatch/

And I found a list of errors that looked something like this:

[aw-watcher-window-hyprland] hyprctl failed: exit status: 1  hyprctl: /opt/activitywatch/libstdc++.so.6: version `GLIBCXX_3.4.32' not found (required by hyprctl)
hyprctl: /opt/activitywatch/libstdc++.so.6: version `GLIBCXX_3.4.31' not found (required by hyprctl)
hyprctl: /opt/activitywatch/libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by hyprctl)
hyprctl: /opt/activitywatch/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by hyprctl)
...

It seems that since the watcher was being executed with /opt/activitywatcher as the working directory, it was trying to use the bundled libstdc++ library. To work around this, I removed the watcher module from aw-qt.toml and created a new XDG Autostart entry to start it independently.

~/.config/activitywatch/aw-qt/aw-qt.toml

[aw-qt]
autostart_modules = ["aw-server", "aw-watcher-afk"]

[aw-qt-testing]
autostart_modules = ["aw-server", "aw-watcher-afk"]

~/.config/autostart/aw-watcher-window-hyprland.desktop

[Desktop Entry]
Name=aw-watcher-window-hyprland
Comment=ActivityWatch window tracker for Hyprland
Exec=aw-watcher-window-hyprland
Hidden=false
StartupNotify=true
Terminal=false
Type=Application
X-GNOME-Autostart-enabled=true
Version=1.0
Icon=activitywatch
Categories=Utility;

I wanted to see if I could implement the 1 second sleep without modifying the source code of aw-watcher-window-hyprland, so I reverted the change and rebuilt to make sure I could still reproduce the issue. Sure enough I stopped seeing data come in, so I modified the autostart to add a sleep:

...
Exec=sh -c "sleep 1 && aw-watcher-window-hyprland"

After another reboot this worked just fine.

Related
Linux