HPR Extension Documentation
HPR ships with a built-in Lua scripting engine that lets you extend its behavior without touching a single line of C++. Write a script, drop it in a folder, and HPR loads it automatically.
What are HPR Extensions?
HPR extensions are plain .lua script files that you place in a specific folder on your system.
HPR automatically discovers and loads every .lua file it finds there — including files inside
subfolders — every time it starts up. No installation steps, no package managers, no compilers.
Inside your script, you have access to a global object called HPR. This object exposes a set of
functions that let your script communicate with HPR's internals — reading the currently active window, running
shell commands, and registering entirely new window-tracking backends for compositors or desktop environments
that HPR doesn't natively support.
Extensions are designed to be small and focused. The most powerful extension currently possible — registering a full custom window detection backend — takes fewer than 30 lines of Lua. You do not need to be an experienced programmer to write one. If you know what terminal command gets the active window on your system, you already know 90% of what you need.
Drop a file, done
Extensions are just .lua files. Place them in your HPR extensions folder and they load
automatically on the next launch. No configuration, no registration, no installation steps. HPR scans the
folder recursively so you can organize extensions into subfolders however you like.
Each extension runs on its own thread
HPR spawns a dedicated background thread for every loaded extension. This means a slow or misbehaving extension cannot block HPR's tracking loop, freeze the UI, or affect any other extension. Each extension is completely isolated from the others and runs independently at its own pace.
Sandboxed Lua environment
Extensions only have access to the Lua standard libraries that HPR explicitly opens — base,
string, and table. File I/O, OS execution, and package loading are not available
directly. Any system interaction goes through HPR's own controlled API functions, keeping the extension
environment predictable and safe.
Live memory access, no HTTP overhead
When your extension calls HPR.getCurrentWindow_E(), it reads directly from HPR's in-memory state
through a mutex-protected C++ lambda. There is no serialization, no network stack, no JSON parsing. The
call is nanoseconds. Compare this to ActivityWatch where every data read is a full HTTP round-trip to a
localhost server.
Where to Put Your Extensions
HPR looks for extensions in a specific folder depending on your operating system. The folder is created automatically the first time HPR launches — you don't need to create it yourself.
🐧 Linux
~/.config/HPR/extensions/
🪟 Windows
%APPDATA%\HPR\HPR_Config\extensions\
You can organize extensions into subfolders. HPR uses recursive directory scanning so a structure like this works perfectly:
extensions/
sway-backend/
sway.lua
niri-backend/
niri.lua
my-custom-thing.lua
All three of those extensions will be discovered and loaded. Each gets its own isolated Lua VM and its own background thread.
Extension Lifecycle Hooks
HPR calls specific Lua functions in your script at specific points during its lifecycle. You don't have to define all of them — HPR checks whether each function exists before calling it, so you only implement
init()
Called exactly once when HPR starts and loads your extension, before the tick loop begins. This is where
you should do any one-time setup — registering a backend, establishing connections, or printing messages.
Optionally, you can return an integer from init() to set a custom tick rate (in milliseconds) for your extension thread (defaults to 1000ms).
onTick(delta)
Called periodically on your extension's isolated background thread (at the rate defined in init()).
It receives a single parameter delta (a float representing actual elapsed milliseconds since the last tick).
This is where you put recurring logic — reading active windows, logging info, or firing notifications.
onExit()
Called exactly once when HPR shuts down or when your extension is stopped. Use this hook to perform safety flushes of database caches, clean up system hooks, and disconnect Event Hub subscriptions to prevent memory leaks.
A minimal extension that just prints the active app every tick looks like this:
function onTick(delta) print(HPR.getCurrentWindow_E()) end
Native Extensions (.dll / .so)
🛑 WARNING: EXPERTS ONLY
This feature is strictly intended for advanced C++ programmers or developers who fully accept the risk of system instability. If you do not have extensive experience compiling low-level systems, or if you care about the absolute security and stability of your HPR tracking app, DO NOT USE NATIVE EXTENSIONS. Stick to standard Lua extensions.
⚠️ Critical Security & Sandbox Warning
Unlike standard Lua extensions which execute safely inside a restricted, sandboxed virtual machine, native extensions run as raw compiled code directly on your host operating system.
What this means: Native extensions completely bypass ALL Lua sandboxing, dangerous command filtering, system hook protections, and VM isolation. They have absolute read/write access to your system memory, files, and network. **Only enable this feature and load native binaries if you fully trust their origin and have reviewed their source code.**
Enabling Native Extension Support
By default, HPR blocks native extensions. If a binary is detected but the feature is disabled, HPR will log a warning to the console on startup and skip loading it:
[HPR] Native extension found but allow-dynamic-library-extensions is false. Skipping: /path/to/extension.so
To enable native extension loading, modify your config.csv (located in the HPR configuration directory) and add or modify the following line:
allow-dynamic-library-extensions, true
The Exported C Interface
Because there is no official pre-built documentation or static library for the C++ extension API, native extensions must be written to export traditional extern "C" symbols that HPR resolves dynamically at runtime.
To build a native extension, your code must export the following three main lifecycle functions:
extern "C" int init()
Called exactly once on startup when the library is loaded.
Must return an integer representing the desired **tick interval/sleep time** in milliseconds.
If omitted or not exported, HPR defaults the thread sleep interval to 1000ms.
extern "C" void onTick(float delta)
Called repeatedly inside a dedicated background thread.
Receives a single float delta representing the actual elapsed time in milliseconds since the previous tick loop.
extern "C" void onExit()
Called exactly once when HPR shuts down or stops the extension. Use this hook to free allocated memory, release system resources, flush file streams, or gracefully close network connections to avoid memory leaks.
Below is a minimal, fully-conforming C++ template for a native extension. You can build this into a shared object (.so / .dylib) or dynamic-link library (.dll):
#include <iostream> extern "C" { // Called once on initialization. Sets the tick interval to 500ms. int init() { std::cout << "[Native] Extension loaded!" << std::endl; return 500; } // Called every 500ms. Receives actual delta time in milliseconds. void onTick(float delta) { std::cout << "[Native] Tick delta: " << delta << "ms" << std::endl; } // Called on shutdown. Clean up resources here. void onExit() { std::cout << "[Native] Shutting down..." << std::endl; } }
Linking & Source Code Reference
Since HPR does not distribute a pre-compiled SDK or stable public headers for native development, there is no formal linkage protocol.
To write and link a stable native extension, developers must read the general HPR Source Code (not just the extension manager files) to explore how internal structures, database handlers, and APIs are managed, and link against the application's targets.
To understand the basic dynamic loader lifecycle and thread execution details, see the src/extension/extensionManager.cpp implementation directly.
Tutorials & Reference
Comprehensive guides and references for writing custom HPR extensions.
Building a Custom Window Backend
Learn how to use HPR.registerBackend_E() to add tracking support for any compositor or desktop
environment. Covers all 6 required functions with a real working example using Hyprland.
Active Media Monitoring & Spotify Tracker
Construct a rich media logging extension from the ground up. Track Spotify playback state through window details, structure high-performance SQL databases, and build a beautiful, interactive Slint tracks listing panel.
Read the tutorial →Event Hub & Burnout Detector
Learn how to use HPR's Event Hub to make isolated extensions talk to each other. Build a Focus Shield with a watcher that monitors your work and an enforcer that triggers native OS notifications.
Read the tutorial →AW Parasite Extension Tutorial
Learn how to utilize HPR's native network clients and JSON parser functions. Build a parasitic browser activity tracker that imports full active URLs and displays them in Slint.
Read the tutorial →Raw Extension API Reference
Detailed reference for the global HPR table functions, including alias lookup, reverse-alias lookup, and lifecycle tick configuration.