← Back to Docs

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.

The "Radio Station" Analogy

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:

burnout-monitor.lua
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.

Notice on notify-send

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.

Dangling Callbacks & Memory Safety

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:

wellness-officer.lua
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.

Next Steps

Explore the full global table functions inside the Raw Extension API Reference to discover more system variables and custom hook callbacks!