Communication Between Extensions
HPR runs every single extension inside its own isolated Lua Virtual Machine and on its own background OS thread. This design keeps HPR fast and extremely crash-resilient—if one extension errors out or gets stuck in a loop, the rest of your tracker runs completely unaffected.
But isolation doesn't mean silence. HPR provides a powerful built-in Event Hub that allows extensions to talk to each other safely. In this comprehensive tutorial, we are going to build a Focus Shield & Burnout Detector. By splitting the logic across two different extensions, you'll learn how to decouple your tracking logic from your system actions.
Think of the Event Hub like a network of local radio stations.
One extension acts as the Transmitter (Publisher). It tunes into a named frequency (like "BURNOUT_WARNING") and broadcasts data packets into the ether. It does not know or care who is listening. It just transmits.
Another extension acts as the Receiver (Subscriber). It hooks up an antenna to the exact same named frequency. Whenever a signal is received, it runs a function to handle the data. It has no idea who sent it. It just reacts.
What We Are Building
To prove how powerful this concept is, we are going to build an automated burnout prevention system, split into two completely separate scripts:
- The Watcher (
burnout-monitor.lua): This script will quietly run in the background, querying the database and watching your active window. If it detects you've been coding for way too long without a break, it will broadcast a signal to the Event Hub. - The Enforcer (
wellness-officer.lua): This script will listen for that specific signal. When it hears the warning, it will take action—popping up a native OS notification on your desktop.
Part 1: Building the Watcher
Let's start with the watcher. Create a new file called burnout-monitor.lua in your extensions directory.
Step 1: Setting up our state
First, we need some basic variables to keep track of how long we've been focused, and a list of apps we consider "productive". By using a Lua table with boolean values, we can easily check if the current active app is in our list. We also set up a debug_timer so we can test the extension without waiting hours.
local focused_time = 0 local debug_timer = 0 -- Apps we consider "deep work" local productive_apps = { ["code"] = true, -- VS Code ["kitty"] = true, -- Terminal ["terminal"] = true, ["neovim"] = true }
Step 2: Restoring history from the Database
What happens if you restart HPR in the middle of a 3-hour coding session? Your extension would reload, focused_time would reset to 0, and you'd lose your tracked burnout state.
To fix this, we use the init() function to query HPR's main database. We can ask SQLite exactly how much time has been logged for our productive apps today, and add that to our initial state!
function init() -- Query the built-in app_usage table local rows = HPR.dbQuery_E("SELECT name, duration FROM app_usage;") local restored_time = 0 -- Loop through the results from the database for _, row in ipairs(rows) do local appName = string.lower(row.name or "") -- If it's a productive app, add its duration (in milliseconds) if productive_apps[appName] then restored_time = restored_time + (tonumber(row.duration) or 0) end end -- Set our starting time focused_time = restored_time print("[Burnout Monitor] Active work session loaded") return 1000 -- Tell HPR to run our onTick function once every second (1000ms) end
Step 3: Tracking focus in real-time
Now we need the actual tracker. In our onTick(delta) function, we'll check the active window. The delta variable gives us the exact milliseconds since the last tick.
If you are looking at a productive app, we increase the timer. If you are looking at something else (like Spotify or a web browser on Twitter), we slowly decay the timer, simulating a break.
function onTick(delta) local activeWindow = HPR.getCurrentWindow_E() if activeWindow then local appName = string.lower(activeWindow) if productive_apps[appName] then -- Add elapsed milliseconds to our focus timer focused_time = focused_time + delta else -- If we aren't working, slowly lower the burnout level -- math.max prevents the timer from going below zero! focused_time = math.max(0, focused_time - (delta * 0.5)) end end -- Step 4 logic goes here... end
Step 4: Emitting the Signal
Here is where the magic happens. Still inside onTick, we check if focused_time has crossed our threshold. For this tutorial, let's say 2 hours straight (which is 7,200,000 milliseconds).
If it has, we use HPR.emit_E. The first argument is our custom frequency name (a string). The second argument is a Lua table containing the payload we want to send over the airwaves.
We also add a debug signal that broadcasts every 5 seconds, so you don't have to wait 2 hours to see if your extensions are talking to each other!
-- 2 hours = 1000ms * 60s * 60m * 2 if focused_time >= 7200000 then -- Broadcast the event! HPR.emit_E("BURNOUT_WARNING", { active_app = activeWindow or "Unknown", duration_seconds = math.floor(focused_time / 1000), intensity = "HIGH" }) end -- Send a test signal every 5 seconds to verify communication is working debug_timer = debug_timer + delta if debug_timer >= 5000 then HPR.emit_E("TEST_COMMUNICATION", { message = "Hello from the Watcher!", current_focus_seconds = math.floor(focused_time / 1000) }) debug_timer = 0 end
The Complete Watcher Code
Put it all together, and your burnout-monitor.lua looks like this:
local focused_time = 0 local debug_timer = 0 local productive_apps = { ["code"] = true, ["kitty"] = true, ["terminal"] = true, ["neovim"] = true } function init() HPR.authorName = "Plexescor" HPR.extensionName = "Burnout Monitor HPR Extension" local rows = HPR.dbQuery_E("SELECT name, duration FROM app_usage;") local restored_time = 0 for _, row in ipairs(rows) do local appName = string.lower(row.name or "") if productive_apps[appName] then restored_time = restored_time + (tonumber(row.duration) or 0) end end focused_time = restored_time print("[Burnout Monitor] Active work session loaded") return 1000 end function onTick(delta) local activeWindow = HPR.getCurrentWindow_E() if activeWindow then local appName = string.lower(activeWindow) if productive_apps[appName] then focused_time = focused_time + delta else focused_time = math.max(0, focused_time - (delta * 0.5)) end end if focused_time >= 7200000 then HPR.emit_E("BURNOUT_WARNING", { active_app = activeWindow or "Unknown", duration_seconds = math.floor(focused_time / 1000), intensity = "HIGH" }) end -- Send a test signal every 5 seconds to verify communication is working debug_timer = debug_timer + delta if debug_timer >= 5000 then HPR.emit_E("TEST_COMMUNICATION", { message = "Hello from the Watcher!", current_focus_seconds = math.floor(focused_time / 1000) }) debug_timer = 0 end end
Part 2: Building the Enforcer
Now we need the extension that listens for that warning and does something about it. Create a new file called wellness-officer.lua.
Step 5: Tuning in to the Broadcast
To receive events, we use HPR.connect_E inside our init() function.
This function takes two arguments: the name of the event to listen for, and an anonymous function (a callback) that will run whenever the event is received. The callback receives the exact data payload we emitted from the Watcher! We will connect to both the burnout warning and our new debug ping.
Crucially, HPR.connect_E returns an ID number. We must store this ID in a variable at the top of our script so we can disconnect later.
-- Store our subscription IDs here local burnoutListenerId = 0 local testListenerId = 0 function init() -- Tune the antenna to our frequency burnoutListenerId = HPR.connect_E("BURNOUT_WARNING", function(data) local duration_mins = math.floor(data.duration_seconds / 60) -- Step 6 logic goes here... end) -- Listen for the 5-second debug test ping to verify communication is working testListenerId = HPR.connect_E("TEST_COMMUNICATION", function(data) print("[Wellness Officer] Received ping! Message: " .. data.message .. " | Focus: " .. data.current_focus_seconds .. "s") end) return 2000 -- This script is event-driven, so it can tick very slowly end
Step 6: Executing System Actions
Inside our callback function, we'll actually annoy the user into taking a break. We'll use HPR.runSystemCommand_E to pop a desktop notification. If you have custom UI files, this is also where you would call HPR.setUiProperty_E to push values to your Slint interface.
In the code below, we run the system utility notify-send. This is a standard command on Linux and BSD systems. It won't do anything on Windows or macOS out of the box.
But that's the beauty of this architecture! If you are on Windows, you can simply change the system command string to a PowerShell toast notification inside this one file, without ever needing to touch or break the database logic in the Watcher.
-- Inside the connect_E callback: -- Trigger a native OS notification (Linux) local notify_cmd = string.format( "notify-send -u critical 'Take a Break!' 'You have been working on %s for %d minutes straight.'", data.active_app, duration_mins ) HPR.runSystemCommand_E(notify_cmd) -- Do UI stuff here if you have relevant Slint files -- HPR.setUiProperty_E("myWarningBanner", "Take a break!")
Step 7: The Critical Teardown
We aren't done yet. This last step is the most important part of the entire tutorial.
When you call HPR.connect_E(), the underlying C++ engine saves a pointer to your Lua callback function in memory.
If your extension exits, errors out, or is reloaded, and you did not explicitly disconnect, the C++ engine will still have that old pointer. The next time the event fires, C++ will try to execute a Lua function that no longer exists, causing massive memory leaks or an instant hard crash.
To prevent this, you must always provide an onExit() function in any script that subscribes to events. HPR calls onExit() automatically right before shutting down the script. Here, you pass the IDs we saved earlier to HPR.disconnect_E().
function onExit() -- CRITICAL: Disconnect our listener hooks! if burnoutListenerId ~= 0 then HPR.disconnect_E("BURNOUT_WARNING", burnoutListenerId) end if testListenerId ~= 0 then HPR.disconnect_E("TEST_COMMUNICATION", testListenerId) end end
The Complete Enforcer Code
The final wellness-officer.lua is incredibly short, but incredibly powerful:
local burnoutListenerId = 0 local testListenerId = 0 function init() HPR.authorName = "Plexescor" HPR.extensionName = "Wellness Officer HPR Extension" burnoutListenerId = HPR.connect_E("BURNOUT_WARNING", function(data) local duration_mins = math.floor(data.duration_seconds / 60) local notify_cmd = string.format( "notify-send -u critical 'Take a Break!' 'You have been working on %s for %d minutes straight.'", data.active_app, duration_mins ) HPR.runSystemCommand_E(notify_cmd) -- Do UI stuff here if you have relevant Slint files -- HPR.setUiProperty_E("myWarningBanner", "Take a break!") end) -- Listen for the 5-second debug test ping to verify communication is working testListenerId = HPR.connect_E("TEST_COMMUNICATION", function(data) print("[Wellness Officer] Received ping! Message: " .. data.message .. " | Focus: " .. data.current_focus_seconds .. "s") end) return 2000 end function onTick(delta) -- Empty, because everything is event-driven! end function onExit() if burnoutListenerId ~= 0 then HPR.disconnect_E("BURNOUT_WARNING", burnoutListenerId) end if testListenerId ~= 0 then HPR.disconnect_E("TEST_COMMUNICATION", testListenerId) end end
Why This Architecture Matters
You might be wondering: "Why didn't we just put the notify-send command straight into the watcher script?"
By routing communication through the Event Hub, you achieve a professional software engineering practice called Separation of Concerns:
Pure Data Tracking
The burnout-monitor only worries about state logic, database queries, and math. It has absolutely zero knowledge of OS notifications, sound effects, or Slint UI components. It's clean and platform-agnostic.
Hot-Swappable Features
If you decide to build a custom smart-lights extension tomorrow that turns your room red when you overwork, you just have it subscribe to "BURNOUT_WARNING" as well! You do not need to edit a single line of your database watcher.
Explore the full global table functions inside the Raw Extension API Reference to discover more system variables and custom hook callbacks!