<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Plexescor (Abhijot Singh)</title>
    <description>The latest articles on DEV Community by Plexescor (Abhijot Singh) (@plexescor).</description>
    <link>https://dev.to/plexescor</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3932680%2F32a73a5a-85da-4124-a0d9-146ebeac7e93.png</url>
      <title>DEV Community: Plexescor (Abhijot Singh)</title>
      <link>https://dev.to/plexescor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/plexescor"/>
    <language>en</language>
    <item>
      <title>I Built a Native Wayland Activity Tracker in C++23 at 16 — HPR's Journey from 10MB Hobby Project to a Real Tool</title>
      <dc:creator>Plexescor (Abhijot Singh)</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:25:36 +0000</pubDate>
      <link>https://dev.to/plexescor/i-built-a-native-wayland-activity-tracker-in-c23-at-16-hprs-journey-from-10mb-hobby-project-to-3bib</link>
      <guid>https://dev.to/plexescor/i-built-a-native-wayland-activity-tracker-in-c23-at-16-hprs-journey-from-10mb-hobby-project-to-3bib</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Native Wayland Activity Tracker in C++23 at 16 — HPR's Journey from 10MB Hobby Project to a Real Tool
&lt;/h1&gt;

&lt;p&gt;Six weeks ago HPR didn't exist. Today it has an AUR package, a Lua 5.4 extension engine, native backends for Hyprland, GNOME, KDE, Cinnamon, and niri, a real user on Fedora GNOME 50, and its first Ko-fi donation.&lt;/p&gt;

&lt;p&gt;This is the honest story of how that happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I'm Plexescor. I'm 16, self-taught, living in Hanumangarh, India. I wanted to know where my time actually went while coding. Every existing option was either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Python daemon eating 200MB of RAM&lt;/li&gt;
&lt;li&gt;A cloud service requiring an account&lt;/li&gt;
&lt;li&gt;An Electron app&lt;/li&gt;
&lt;li&gt;X11-only with broken Wayland support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run Hyprland. None of them worked properly. So I built my own.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.2 — The thing that actually ran
&lt;/h2&gt;

&lt;p&gt;The first public release was embarrassingly simple. It tracked your active window, accumulated time per app, and wrote everything to SQLite. That's it.&lt;/p&gt;

&lt;p&gt;But it worked. On Hyprland, GNOME, KDE, and Windows. The cross-platform part was the hardest — each compositor exposes window focus completely differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hyprland&lt;/strong&gt;: &lt;code&gt;hyprctl activewindow&lt;/code&gt; IPC call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GNOME Wayland&lt;/strong&gt;: no native API, required a custom shell extension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KDE&lt;/strong&gt;: KWin D-Bus scripting via &lt;code&gt;qdbus6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: &lt;code&gt;GetForegroundWindow()&lt;/code&gt; Win32 API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four completely different implementations for one question: "what window is focused right now?"&lt;/p&gt;

&lt;p&gt;RAM was ~10MB. CPU was 1-3%. Startup was instant. The architecture was already right even if the features weren't there yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tan9h28qxfjz8o00g5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tan9h28qxfjz8o00g5c.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.3 — Foundation work nobody sees
&lt;/h2&gt;

&lt;p&gt;This release was almost entirely backend refactoring. I rewrote the event system into a proper pub/sub &lt;code&gt;EventHub&lt;/code&gt; — a centralized in-process bus with typed &lt;code&gt;std::variant&lt;/code&gt; payloads. Every thread that needed to talk to another thread went through it instead of directly touching shared state.&lt;/p&gt;

&lt;p&gt;The shared state model also got locked down properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="n"&gt;AppState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;AppState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;currentWindow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;timeLog_PerApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;timeLog_PerTab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                 &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;switchHistory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;AppState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt; &lt;span class="n"&gt;stateMutex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lock, copy, release, work on the copy. One rule. Every thread follows it.&lt;/p&gt;

&lt;p&gt;Not glamorous. Nobody cared. But v0.69's extension engine stress testing 1000 simultaneous threads only worked because the foundation was solid.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.4 — First version that felt real
&lt;/h2&gt;

&lt;p&gt;Historical data view via date picker. Insights engine. Custom themes via interpreted &lt;code&gt;.slint&lt;/code&gt; mode.&lt;/p&gt;

&lt;p&gt;The interpreted mode deserves explanation. Slint normally compiles your UI into the binary at build time. I added a second code path that loads &lt;code&gt;.slint&lt;/code&gt; files from disk at runtime instead. Users can now modify HPR's entire visual design without touching C++ or recompiling. Just edit the file, hit RELOAD UI, changes are live.&lt;/p&gt;

&lt;p&gt;This was also the release where I made the architectural decision that still holds: one &lt;code&gt;.db&lt;/code&gt; SQLite file per day, one folder per month.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.local/share/HPR/HPR_DB/

05-26/
01-05-26.db
02-05-26.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A normal day is 19KB. A full year is under 50MB. You can open any specific day in DB Browser for SQLite without touching HPR. Delete last month by deleting the folder. No export step, no proprietary format.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.5 — Going open source
&lt;/h2&gt;

&lt;p&gt;HPR originally had a free tier and a premium closed-source tier. I killed that entirely in v0.5.&lt;/p&gt;

&lt;p&gt;Everything, current and future, is free forever. The premium version's source got merged into the main repo.&lt;/p&gt;

&lt;p&gt;This release also added browser tab tracking without any browser extension — HPR just reads the window title. When Chrome is focused, the title contains the tab name. Parse it. Done. Works on every supported platform because they all already had window title getters.&lt;/p&gt;

&lt;p&gt;KDE Fedora users also stopped being second-class citizens. I added auto-detection for &lt;code&gt;qdbus6&lt;/code&gt; vs &lt;code&gt;qdbus-qt6&lt;/code&gt; vs &lt;code&gt;qdbus&lt;/code&gt; at startup so it just works regardless of distro.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.6 — The release that made HPR actually useful
&lt;/h2&gt;

&lt;p&gt;Three things shipped together that changed what HPR was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VS Code project tracking.&lt;/strong&gt; No extension. No plugin. VS Code puts the project name in its window title: &lt;code&gt;filename - projectname - Visual Studio Code&lt;/code&gt;. Strip the suffix, find the last separator, extract the project name. Works everywhere already because the window title getter existed on every platform. Per-project time breakdowns with zero setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cinnamon support.&lt;/strong&gt; &lt;code&gt;org.Cinnamon.Eval&lt;/code&gt; D-Bus lets you evaluate JavaScript inside the live Cinnamon process. I use it to query &lt;code&gt;global.display.focus_window&lt;/code&gt; directly. Works on both X11 and Cinnamon's experimental Wayland session with no code branching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native system tray on Linux.&lt;/strong&gt; No GTK. No Qt. Pure &lt;code&gt;libdbus-1&lt;/code&gt;. HPR registers as &lt;code&gt;org.kde.StatusNotifierItem&lt;/code&gt; — the same protocol Discord and Steam use. Works with Waybar on Hyprland, KDE's tray, and Cinnamon's panel out of the box.&lt;/p&gt;

&lt;p&gt;This was also the release that landed on the AUR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yay &lt;span class="nt"&gt;-S&lt;/span&gt; hpr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wjelpr6w0xhhmd50lz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wjelpr6w0xhhmd50lz6.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwihmmigcnmjitxygnf2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwihmmigcnmjitxygnf2e.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.69 — The extension engine
&lt;/h2&gt;

&lt;p&gt;This is the release I'm most proud of technically.&lt;/p&gt;

&lt;p&gt;HPR now ships a sandboxed Lua 5.4 extension engine. Drop a &lt;code&gt;.lua&lt;/code&gt; file into &lt;code&gt;~/.config/HPR/extensions/&lt;/code&gt; and HPR loads it automatically. Each extension runs in its own isolated VM on a dedicated OS thread. A broken extension cannot affect HPR's tracking loop, UI, or any other extension.&lt;/p&gt;

&lt;p&gt;The minimal extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HPR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getCurrentWindow_E&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's two lines. It runs.&lt;/p&gt;

&lt;p&gt;What you can actually do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the active window in real time&lt;/li&gt;
&lt;li&gt;Query live time logs per app, per tab, per VS Code project&lt;/li&gt;
&lt;li&gt;Run shell commands (dangerous ones are blocked at the API level with an explicit blocklist)&lt;/li&gt;
&lt;li&gt;Full HTTP GET/POST with custom headers&lt;/li&gt;
&lt;li&gt;Spin up an embedded HTTP server inside your extension&lt;/li&gt;
&lt;li&gt;Direct SQLite access — write your own tables, query any past day by date&lt;/li&gt;
&lt;li&gt;Subscribe to HPR's internal EventHub — window switches, midnight rollover, UI ready, custom events between extensions&lt;/li&gt;
&lt;li&gt;Push data into HPR's Slint UI and register callbacks for UI interactions&lt;/li&gt;
&lt;li&gt;Register a fully custom window detection backend for any compositor HPR doesn't natively support&lt;/li&gt;
&lt;li&gt;Override 26+ core C++ functions directly from Lua via the Function Overriding API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stress test: 1000 simultaneous extensions. HPR started in under 300ms, settled at 5.6% CPU, database came up clean after a hard kill.&lt;/p&gt;

&lt;p&gt;The first real extension I shipped alongside it was an ActivityWatch URL Parasite — it impersonates an ActivityWatch client and feeds HPR's tracking data into ActivityWatch's API, letting HPR users show up in AW dashboards without running AW's own watcher.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyvghwrrkmw52zsei8xy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyvghwrrkmw52zsei8xy.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.7 — Hot reload everything
&lt;/h2&gt;

&lt;p&gt;No more restarting HPR to pick up changes.&lt;/p&gt;

&lt;p&gt;Each loaded extension now has RELOAD, DISABLE buttons. A top-level RESCAN button picks up new &lt;code&gt;.lua&lt;/code&gt; files dropped into the folder without restarting. The RELOAD UI button reloads the entire Slint interface from disk without losing tracking state.&lt;/p&gt;

&lt;p&gt;Iterate on your theme in real time. Fix an extension bug and reload it without losing your day's tracking data.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.8 — niri and the full feature set
&lt;/h2&gt;

&lt;p&gt;niri support landed. Uses &lt;code&gt;niri msg&lt;/code&gt; IPC. Works out of the box.&lt;/p&gt;

&lt;p&gt;App Limits and Goals shipped as the first genuinely productivity-focused feature. Set a daily cap on any app — HPR sends a notification when you hit it and optionally force-quits the application automatically. Goals work the other way — set a minimum daily target and track progress live.&lt;/p&gt;

&lt;p&gt;Advanced users can intercept the limit-reached event via the Function Overriding API and run custom Lua logic — suppress the notification, send a webhook, log it somewhere, anything.&lt;/p&gt;

&lt;p&gt;Multi-day historical queries replaced the single date picker. Three modes: single day, last N days, custom date range. Multiple daily &lt;code&gt;.db&lt;/code&gt; files merge on a background thread without pausing live tracking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuuh1p8866ratvbgjqkju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuuh1p8866ratvbgjqkju.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbfxoeiqgwoyuff31pxdx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbfxoeiqgwoyuff31pxdx.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.9 — Pattern analysis and the timeline
&lt;/h2&gt;

&lt;p&gt;The Day Construction Timeline maps your entire day onto a zoomable, scrollable canvas. Not just numbers — a visual record of exactly what you were doing and when.&lt;/p&gt;

&lt;p&gt;Gap detection handles reboots and closed sessions properly. If HPR was closed for 5 hours, the naive approach stretches your last active app across the entire gap. HPR's reconstruction engine detects focus gaps and caps the preceding segment to 1 minute before transitioning to Unknown. Your logs stay accurate.&lt;/p&gt;

&lt;p&gt;Advanced Pattern Analysis added 9 cross-day metrics. The one I find most useful personally: &lt;strong&gt;Focus Dip Hour&lt;/strong&gt; — the specific hour where your concentration crashes and application switches spike most consistently across all your historical data. Mine is 3PM. Knowing that is useful.&lt;/p&gt;

&lt;p&gt;The full metric list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Escape Pattern — average daily switches from work app to browser&lt;/li&gt;
&lt;li&gt;Return Rate — how often you immediately bounce back after a browser escape&lt;/li&gt;
&lt;li&gt;Average Focus Session duration&lt;/li&gt;
&lt;li&gt;Most Distracted Day of the week&lt;/li&gt;
&lt;li&gt;Productive Days this week&lt;/li&gt;
&lt;li&gt;Screen Time vs your N-day average&lt;/li&gt;
&lt;li&gt;Focus Dip Hour&lt;/li&gt;
&lt;li&gt;Deep Work Before Noon percentage&lt;/li&gt;
&lt;li&gt;Weekend vs Weekday work app usage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F75qd3uygwllk4bguls19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F75qd3uygwllk4bguls19.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl8w5leos1ts0vcegdllm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl8w5leos1ts0vcegdllm.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpspm6vovt9yumxp8g0je.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpspm6vovt9yumxp8g0je.png" alt=" " width="799" height="734"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.9.1 — Headless mode
&lt;/h2&gt;

&lt;p&gt;HPR can now run without a UI entirely. Tray-only mode. Useful for servers, minimal setups, or anyone who just wants the tracking running in the background without a window.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Platform support:&lt;/strong&gt; Hyprland, GNOME, KDE Plasma 6+, Cinnamon, niri, Windows 10/11.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
yay &lt;span class="nt"&gt;-S&lt;/span&gt; hpr

&lt;span class="c"&gt;# Windows&lt;/span&gt;
&lt;span class="c"&gt;# Inno Setup installer on GitHub releases&lt;/span&gt;

&lt;span class="c"&gt;# Linux manual&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x installHPRConfigAndUi.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./installHPRConfigAndUi.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./HPR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;First donation:&lt;/strong&gt; $20 from Jesse Kramer. Completely voluntary. For a free open source tool built by a 16-year-old. I stared at the Ko-fi notification for a while.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/plexescor/HPR" rel="noopener noreferrer"&gt;github.com/plexescor/HPR&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The hardest part wasn't the code.&lt;/strong&gt; The hardest part was the Wayland ecosystem. Every compositor does everything differently and documentation is either nonexistent or buried in mailing list threads from 2019. The KDE &lt;code&gt;js::&lt;/code&gt; filter in the window name normalizer exists because KWin's own JavaScript runtime briefly appears as the active window during injection and silently accumulates time in your logs if you don't catch it. I found that at midnight staring at weird data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing bugs are subtle.&lt;/strong&gt; Using &lt;code&gt;system_clock&lt;/code&gt; for duration measurement is a classic mistake — NTP corrections and DST transitions corrupt your accumulated totals. HPR uses &lt;code&gt;steady_clock&lt;/code&gt; for measurement and &lt;code&gt;system_clock&lt;/code&gt; only for display timestamps. Different clocks, different jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship early.&lt;/strong&gt; v0.2 was embarrassingly minimal. I shipped it anyway. The feedback shaped everything that came after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architecture matters more than the features.&lt;/strong&gt; The EventHub, the single mutex over shared state, the one-file-per-day database layout — these decisions made every subsequent feature straightforward to add. Bad early architecture would have made the extension engine impossible.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The foundation is done. What comes next is polish, more compositor backends if people ask for them, and wherever the extension ecosystem goes.&lt;/p&gt;

&lt;p&gt;If HPR is useful to you, a &lt;a href="//ko-fi.com/plexescor"&gt;Ko-fi&lt;/a&gt; helps keep it going. If you hit a bug, open an issue — I respond to every one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;HPR is free and open source. Built solo. C++23, Slint 1.16.1, SQLite3, Lua 5.4.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://github.com/plexescor/HPR" rel="noopener noreferrer"&gt;github.com/plexescor/HPR&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Best Activity Trackers for Linux Wayland in 2026</title>
      <dc:creator>Plexescor (Abhijot Singh)</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:13:50 +0000</pubDate>
      <link>https://dev.to/plexescor/the-best-activity-trackers-for-linux-wayland-in-2026-2gld</link>
      <guid>https://dev.to/plexescor/the-best-activity-trackers-for-linux-wayland-in-2026-2gld</guid>
      <description>&lt;h1&gt;
  
  
  The Best Activity Trackers for Linux Wayland in 2026
&lt;/h1&gt;

&lt;p&gt;If you're on Linux Wayland and want to know where your time actually goes, your options are surprisingly limited. Most activity trackers were built for X11, assume a web dashboard, or drag a Python runtime and 200MB of RAM into your session. Here's an honest breakdown of what's actually worth using in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. HPR — Human Pattern Recorder
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/plexescor/HPR" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | Free | Open Source | C++23&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full disclosure: I built this. I'm a 16-year-old solo developer in India and HPR is my main project. I'm including it first because it's genuinely the best option for Wayland-native Linux tracking — not because I made it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually does
&lt;/h3&gt;

&lt;p&gt;HPR polls your active window every 50ms, all day, and builds a complete log of where your time went. No extensions. No accounts. No server. Just a compiled C++23 binary that starts instantly and writes to local SQLite.&lt;/p&gt;

&lt;p&gt;You get three things live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What you're in right now&lt;/li&gt;
&lt;li&gt;Total time per app today&lt;/li&gt;
&lt;li&gt;Your complete switch history, timestamped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Historical queries work across any date range — pick a single day, last N days, or a custom range. Multiple daily &lt;code&gt;.db&lt;/code&gt; files get merged on a background thread without ever pausing live tracking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it's different on Wayland
&lt;/h3&gt;

&lt;p&gt;Most trackers bolt on Wayland support as an afterthought. HPR was built with native Wayland backends from day one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hyprland&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hyprctl&lt;/code&gt; IPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GNOME (Wayland)&lt;/td&gt;
&lt;td&gt;Custom GNOME Shell extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KDE Plasma 6+&lt;/td&gt;
&lt;td&gt;KWin D-Bus scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cinnamon&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;org.Cinnamon.Eval&lt;/code&gt; D-Bus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;niri&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;niri msg&lt;/code&gt; IPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 10/11&lt;/td&gt;
&lt;td&gt;Win32 API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No hacks. No XWayland fallback. Each compositor gets its own proper backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features that nothing else has
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Browser tab tracking without extensions&lt;/strong&gt; — HPR reads the window title directly. When Chrome or Firefox is focused, it parses the tab name automatically. No browser extension to install, no permissions to grant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VS Code project tracking&lt;/strong&gt; — HPR parses VS Code's window title to extract the active project name. You get per-project time breakdowns with zero setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Limits and Goals&lt;/strong&gt; — set a daily cap on any app. Hit the limit and HPR sends a notification. Optionally it force-quits the app. Goals work the other way — set a minimum daily target and track progress live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day Construction Timeline&lt;/strong&gt; — a zoomable, scrollable visual timeline of your entire day. Preset zoom levels from 1h to 24h. Gap detection handles reboots and closed sessions cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced Pattern Analysis&lt;/strong&gt; — 9 cross-day metrics including escape pattern (how often you jump from work to browser), return rate, peak focus hour, deep work before noon percentage, and weekend vs weekday comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lua 5.4 Extension Engine&lt;/strong&gt; — drop a &lt;code&gt;.lua&lt;/code&gt; file into your extensions folder and HPR loads it automatically. Each extension runs in an isolated VM on its own thread. You can read the active window in real time, query the SQLite database, subscribe to internal events, build custom UI panels, and override 26+ core C++ functions from Lua. Stress tested to 1000 simultaneous extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~8MB RSS on Windows&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~22MB private footprint on Linux&lt;/strong&gt; (htop shows ~47MB but ~25MB of that is shared Mesa/LLVM GPU pages — not HPR's actual cost)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1-3% CPU during normal use&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~19KB per day of tracking data&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instant startup&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Privacy
&lt;/h3&gt;

&lt;p&gt;No telemetry by default. No accounts ever. The networking libraries are compiled in to power the Lua HTTP API and optional anonymous analytics — but HPR itself never phones home unless you explicitly enable it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
yay &lt;span class="nt"&gt;-S&lt;/span&gt; hpr

&lt;span class="c"&gt;# Windows&lt;/span&gt;
&lt;span class="c"&gt;# Download the Inno Setup installer from GitHub releases&lt;/span&gt;

&lt;span class="c"&gt;# Linux manual&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x installHPRConfigAndUi.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./installHPRConfigAndUi.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./HPR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The honest part
&lt;/h3&gt;

&lt;p&gt;HPR is v0.9 and I'm the only developer. It's not as mature as ActivityWatch. If you need a battle-tested tool with years of production use and a full web dashboard, read the next section. If you're on Wayland, care about memory footprint, and want Lua scripting, give HPR a try.&lt;/p&gt;

&lt;p&gt;Ko-fi link is in the README if it's useful to you.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. ActivityWatch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://activitywatch.net" rel="noopener noreferrer"&gt;activitywatch.net&lt;/a&gt; | Free | Open Source | Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ActivityWatch is the most mature open source activity tracker available. It has been in active development for years, has a proper web dashboard, a plugin ecosystem, browser extensions, and a large community. If you want something that just works and has been battle-tested by thousands of users, this is the honest recommendation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does well
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Full web dashboard with detailed visualizations&lt;/li&gt;
&lt;li&gt;Browser extensions for Chrome and Firefox&lt;/li&gt;
&lt;li&gt;Plugin ecosystem — community has built integrations for VS Code, AFK detection, and more&lt;/li&gt;
&lt;li&gt;Cross-platform (Linux, Windows, macOS)&lt;/li&gt;
&lt;li&gt;REST API for building your own integrations&lt;/li&gt;
&lt;li&gt;Active community and regular releases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wayland situation
&lt;/h3&gt;

&lt;p&gt;ActivityWatch's Wayland support has historically been its weak point. The core watcher uses &lt;code&gt;wnck&lt;/code&gt; which is X11-based. Community-maintained Wayland watchers exist but require separate installation and vary in reliability per compositor. On Hyprland and niri especially, you'll need to find and configure a third-party watcher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource usage
&lt;/h3&gt;

&lt;p&gt;ActivityWatch runs a local web server, a Python daemon, and several watcher processes. Expect 200MB+ RAM in real use. It also takes several seconds to start. For a background daemon that runs all day this matters on lower-spec hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bottom line
&lt;/h3&gt;

&lt;p&gt;ActivityWatch is the right choice if you want maturity, community, and a full dashboard. It's what HPR is measured against. The gap is Wayland support and resource footprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Wakatime (self-hosted: Wakapi)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://wakapi.dev" rel="noopener noreferrer"&gt;wakapi.dev&lt;/a&gt; | Free tier | Open Source self-hosted option&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wakatime is a different category — it tracks coding time specifically, not general window focus. If your main goal is understanding how much time you spend in your editor and across projects, it's worth knowing about.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;Editor plugins (VS Code, Neovim, JetBrains, etc.) send heartbeats to a Wakatime-compatible server every time you type. You get per-project, per-language, and per-file breakdowns of coding time.&lt;/p&gt;

&lt;p&gt;Wakapi is the self-hosted open source server that's compatible with the Wakatime API. Run it locally, point your editor plugins at it, and all your data stays on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it doesn't do
&lt;/h3&gt;

&lt;p&gt;Wakatime only knows about time in editors. It has no awareness of browser usage, application switching, or anything outside your coding environment. It's a coding time tracker, not a general activity tracker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wayland situation
&lt;/h3&gt;

&lt;p&gt;Irrelevant — it runs entirely through editor plugins, no window system access needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bottom line
&lt;/h3&gt;

&lt;p&gt;If your specific question is "how much time did I actually spend coding vs everything else" and you don't care about the rest, Wakapi is elegant and lightweight. For general activity tracking it's out of scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HPR&lt;/th&gt;
&lt;th&gt;ActivityWatch&lt;/th&gt;
&lt;th&gt;Wakapi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native Wayland&lt;/td&gt;
&lt;td&gt;✅ All major compositors&lt;/td&gt;
&lt;td&gt;⚠️ Community watchers&lt;/td&gt;
&lt;td&gt;✅ N/A (editor plugins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM usage&lt;/td&gt;
&lt;td&gt;~22MB private&lt;/td&gt;
&lt;td&gt;200MB+&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web dashboard&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser tab tracking&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;✅ Extension required&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code projects&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;✅ Plugin required&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App limits/goals&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripting/extensions&lt;/td&gt;
&lt;td&gt;✅ Lua 5.4&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maturity&lt;/td&gt;
&lt;td&gt;v0.9, solo dev&lt;/td&gt;
&lt;td&gt;Mature, team&lt;/td&gt;
&lt;td&gt;Mature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup time&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;Several seconds&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding-specific&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Which one should you use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You're on Hyprland, niri, or GNOME Wayland and want something lightweight&lt;/strong&gt; → HPR. It has native backends for all of them, starts instantly, and uses almost no RAM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want something mature with a web dashboard and don't mind Python overhead&lt;/strong&gt; → ActivityWatch. It's been around longer and has a larger ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You specifically want to track coding time by project and language&lt;/strong&gt; → Wakapi with the VS Code/Neovim plugin.&lt;/p&gt;

&lt;p&gt;All three are free and open source. There's no wrong answer — it depends on what you're optimizing for.``&lt;/p&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I built a scriptable Lua extension engine inside my C++ activity tracker (and almost lost my mind to deadlocks)</title>
      <dc:creator>Plexescor (Abhijot Singh)</dc:creator>
      <pubDate>Mon, 01 Jun 2026 11:46:27 +0000</pubDate>
      <link>https://dev.to/plexescor/how-i-built-a-scriptable-lua-extension-engine-inside-my-c-activity-tracker-and-almost-lost-my-4cne</link>
      <guid>https://dev.to/plexescor/how-i-built-a-scriptable-lua-extension-engine-inside-my-c-activity-tracker-and-almost-lost-my-4cne</guid>
      <description>&lt;h1&gt;
  
  
  Hey!
&lt;/h1&gt;

&lt;p&gt;I've been building HPR, an open source activity tracker and window tracker written in C++23. It watches your active window, builds a history of where your time went, and runs locally with zero accounts and zero telemetry. Think of it as a lightweight ActivityWatch alternative with native Wayland support.&lt;/p&gt;

&lt;p&gt;At some point I decided it wasn't enough to just track windows. I wanted people to be able to extend it. So I built a Lua extension engine into it. Here's how that went.&lt;/p&gt;




&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;.lua&lt;/code&gt; file you drop into the extensions folder gets picked up automatically. HPR scans the directory recursively, loads each file into its own &lt;code&gt;sol::state&lt;/code&gt;, registers the API, and spins up a dedicated thread for it. Each extension runs completely isolated from everything else. A slow or broken extension can't freeze the UI or corrupt the tracking loop.&lt;/p&gt;

&lt;p&gt;The lifecycle is simple. You define three functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="c1"&gt;-- tick interval in ms&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;-- called every 500ms&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onExit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;-- cleanup&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; returns how often you want &lt;code&gt;onTick&lt;/code&gt; to fire. That's it. Everything else is opt-in through the API.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;This is where it gets actually useful. From Lua, an extension can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the currently focused window with &lt;code&gt;HPR.getCurrentWindow_E()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Query and write to the SQLite database directly with &lt;code&gt;HPR.dbQuery_E()&lt;/code&gt; and &lt;code&gt;HPR.dbExecute_E()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Subscribe to internal events like &lt;code&gt;WINDOW_CHANGED&lt;/code&gt;, &lt;code&gt;MIDNIGHT_ROLLOVER&lt;/code&gt;, &lt;code&gt;HISTORY_LOADED_SINGULAR&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Register completely custom window detection backends for compositors HPR doesn't natively support&lt;/li&gt;
&lt;li&gt;Set UI properties and register callbacks on the Slint UI from Lua&lt;/li&gt;
&lt;li&gt;Make HTTP requests, start an HTTP server, parse JSON, read and write CSV files&lt;/li&gt;
&lt;li&gt;Stop and resume the window tracking loop entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one sounds weird but its how the AFK detection works in one of the extensions I built, more on that in a second.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hardest parts
&lt;/h2&gt;

&lt;p&gt;Honestly the code itself wasnt the worst part. The worst part was deadlocks.&lt;/p&gt;

&lt;p&gt;The Lua state is not thread safe. The Slint UI is also not thread safe and must only be touched from its event loop thread. The shared app state has its own mutex. So you have extension threads, the Slint event loop thread, the window poller thread, and the database writer thread all needing to talk to each other without stepping on each other.&lt;/p&gt;

&lt;p&gt;The solution was a &lt;code&gt;recursive_mutex&lt;/code&gt; per extension that wraps every Lua call, and &lt;code&gt;slint::invoke_from_event_loop&lt;/code&gt; for anything that touches the UI. Callbacks registered from Lua that get fired by Slint acquire the Lua mutex before calling back into Lua. It took a while to get right.&lt;/p&gt;

&lt;p&gt;The other fun one was cyclic table detection. If a Lua extension pushes a self referential table into a UI property, without protection you get a stack overflow and a segfault. I added a visited set that tracks table pointers during the conversion and bails out with an error message if it sees a cycle instead of recursing forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  A real example: impersonating ActivityWatch
&lt;/h2&gt;

&lt;p&gt;One of the extensions I wrote is called AW Parasite. It starts an HTTP server on port 5600, which is the default ActivityWatch port. &lt;code&gt;aw-watcher-web&lt;/code&gt; and &lt;code&gt;aw-watcher-afk&lt;/code&gt; both think they're talking to a real ActivityWatch instance. They send heartbeats, the extension processes them, accumulates URL time and AFK status, writes everything into the HPR SQLite database, and updates the UI through &lt;code&gt;HPR.setUiProperty_E&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the user goes AFK, the extension calls &lt;code&gt;HPR.stopTracking_E()&lt;/code&gt; to pause the main activity window tracker. When they come back it calls &lt;code&gt;HPR.startTracking_E()&lt;/code&gt;. The whole thing is about 200 lines of Lua and it works on both Wayland and Windows because its just HTTP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hot reload
&lt;/h2&gt;

&lt;p&gt;You can load, unload and reload individual extensions from the UI without restarting HPR. There's also a rescan button that picks up new &lt;code&gt;.lua&lt;/code&gt; files you dropped in after launch. Unloading sets &lt;code&gt;running = false&lt;/code&gt; on the extension's thread and gives it 450ms to exit cleanly. If it doesnt, the thread gets detached and a warning is logged. Native extensions (&lt;code&gt;.dll&lt;/code&gt; / &lt;code&gt;.so&lt;/code&gt;) are also supported for cases where Lua isnt enough, though those run outside the sandbox.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkyllptoif8ek66ryr13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkyllptoif8ek66ryr13.png" alt="Extension UI" width="800" height="749"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;HPR is fully free and open source. If any of this sounds interesting the repo is at &lt;a href="https://github.com/plexescor/HPR" rel="noopener noreferrer"&gt;Github&lt;/a&gt;. Would appreciate a star if you find it useful.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>opensource</category>
      <category>programming</category>
      <category>extensions</category>
    </item>
    <item>
      <title>How I built native Wayland window tracking across Hyprland, GNOME and KDE in C++23</title>
      <dc:creator>Plexescor (Abhijot Singh)</dc:creator>
      <pubDate>Sat, 16 May 2026 11:24:22 +0000</pubDate>
      <link>https://dev.to/plexescor/how-i-built-native-wayland-window-tracking-across-hyprland-gnome-and-kde-in-c23-2lk8</link>
      <guid>https://dev.to/plexescor/how-i-built-native-wayland-window-tracking-across-hyprland-gnome-and-kde-in-c23-2lk8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnpmpc4lj1h1y3hl0kia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnpmpc4lj1h1y3hl0kia.png" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever tried to get the currently focused window on Wayland, you know it's a mess. There's no unified API. Every compositor does it differently. X11 had &lt;code&gt;_NET_ACTIVE_WINDOW&lt;/code&gt; and everyone just used that. Wayland deliberately doesn't have an equivalent for security reasons, which means every tracker, every productivity tool, every anything that needs to know what you're looking at has to implement a different solution per compositor.&lt;/p&gt;

&lt;p&gt;I ran into this while building HPR, an activity tracker that watches your active window every 50ms and logs time per app. Here's how I solved it for the three most common Wayland compositors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hyprland
&lt;/h2&gt;

&lt;p&gt;Easiest of the three. Hyprland exposes an IPC socket and a CLI tool called &lt;code&gt;hyprctl&lt;/code&gt;. One command gives you the active window as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hyprctl activewindow &lt;span class="nt"&gt;-j&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.class'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No extension, no setup, no workarounds. Just works on first launch. The &lt;code&gt;.class&lt;/code&gt; field gives you the application class which is consistent and clean. I call this every 50ms from a background thread and it holds up fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  GNOME
&lt;/h2&gt;

&lt;p&gt;GNOME on Wayland is the painful one. There's no official API for getting the active window from outside the compositor. The only working solution I found is a shell extension called &lt;code&gt;window-calls-extended&lt;/code&gt; which exposes a DBus interface you can query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdbus call &lt;span class="nt"&gt;--session&lt;/span&gt; &lt;span class="nt"&gt;--dest&lt;/span&gt; org.gnome.Shell &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-path&lt;/span&gt; /org/gnome/Shell/Extensions/WindowsExt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--method&lt;/span&gt; org.gnome.Shell.Extensions.WindowsExt.FocusClass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns the window class wrapped in some dirty output you have to parse. HPR checks on startup whether the extension is active. If it isn't, it tells the user exactly what to do instead of silently returning garbage. Because GNOME can't hot-reload shell extensions you log out and back in once after installing. Every launch after that is automatic.&lt;/p&gt;

&lt;p&gt;One thing worth knowing: GNOME shell extensions are sandboxed differently across GNOME versions and some distributions patch the shell in ways that break extension APIs. If you're supporting GNOME you need to test across versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  KDE Plasma
&lt;/h2&gt;

&lt;p&gt;KDE was the one I expected to be easy and wasn't. KDE exposes KWin scripting via &lt;code&gt;qdbus6&lt;/code&gt; which lets you inject JavaScript into the compositor and read the output. The active window class comes from &lt;code&gt;workspace.activeWindow.resourceClass&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem is KWin doesn't give you a clean return value. The script runs, prints output to the system journal with a &lt;code&gt;js:&lt;/code&gt; prefix, and you have to scrape it back out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'print(workspace.activeWindow.resourceClass);'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/kwin_active.js
&lt;span class="nv"&gt;S&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript /tmp/kwin_active.js kwin_tmp_&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;T&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
qdbus6 org.kde.KWin /Scripting/Script&lt;span class="nv"&gt;$S&lt;/span&gt; org.kde.kwin.Script.run &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;span class="nb"&gt;sleep &lt;/span&gt;0.1
journalctl &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$T&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^js:'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^js: //'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes this is a hack. It forks a shell, injects JS, scrapes the journal, and cleans up after itself every 50ms. Somehow it lands at around 1% CPU. I tested every other approach I could find. None of them worked reliably across KDE configurations. This one does.&lt;/p&gt;

&lt;p&gt;One side effect: during the JS injection, KWin's own runtime briefly appears as the active window. If you don't filter it out, strings like &lt;code&gt;js::kwin_tmp_1234&lt;/code&gt; silently accumulate time in your logs. HPR filters anything containing &lt;code&gt;js::&lt;/code&gt; before it ever touches the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The normalization layer
&lt;/h2&gt;

&lt;p&gt;Each backend returns a raw string. Raw strings from OS window APIs are inconsistent and noisy. HPR runs every return value through a shared normalization function before doing anything with it. This filters out compositor artifacts, plasma shell entries, null strings, and KWin JS runtime noise. New backends inherit this filtering automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform detection
&lt;/h2&gt;

&lt;p&gt;HPR reads &lt;code&gt;$XDG_CURRENT_DESKTOP&lt;/code&gt; and matches substrings. Simple, works for 99% of setups. Non-standard session variables or nested compositors can confuse it but that's an edge case worth documenting rather than engineering around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project
&lt;/h2&gt;

&lt;p&gt;HPR is open source, C++23, Slint UI, SQLite3 bundled. If you're on Wayland and want an activity tracker that actually works without a Python runtime or an embedded web server, give it a try.&lt;/p&gt;

&lt;p&gt;GitHub: github.com/plexescor/HPR&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlz7bb33h9kal4i9awd3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlz7bb33h9kal4i9awd3.png" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've solved Wayland window detection in a cleaner way especially on KDE, I'd genuinely like to know.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>linux</category>
      <category>wayland</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
