Cross-platform idle detection agent. Detects when the user has had no keyboard/mouse input past a threshold, shows a notification, writes structured idle_start / idle_end events to a JSONL log, and — when connected to the companion time-tracker backend — reports them to POST /idle-events, where floor-level employees are automatically clocked out (their open shift is closed, so paid time stops).
The original single-file version had five real problems, all fixed here:
- Blocking alert.
MessageBoxWon the main thread froze the monitoring loop until the user clicked OK — no idle-end detection, no logging, nothing. Notifications now run on a detached thread (Windows) or fire-and-forget OS notifications (macOS/Linux). - Crash on bad input.
std::stoull(argv[1])with a non-numeric argument threw an uncaught exception. Arguments are now validated with clear error messages. - Silent failure.
GetLastInputInfofailure returned idle=0, silently masking a broken agent as "user is active." Failures are now propagated and logged after repeated occurrences. - Windows-only. Platform code is now isolated behind one interface (
idle_time.h) with native implementations for Windows (GetLastInputInfo), macOS (CGEventSourceSecondsSinceLastEventType), and Linux (X11 MIT-SCREEN-SAVER extension). - No observability or configuration. Added a config file, CLI flags, JSONL event log with UTC timestamps and idle durations, graceful SIGINT/SIGTERM shutdown (service-manager friendly), and optional repeat reminders.
idle-tracker/
├── CMakeLists.txt
├── config/idle-tracker.conf # example config
├── src/
│ ├── main.cpp # agent logic, config, logging
│ ├── idle_time.h # platform interface
│ ├── idle_time_windows.cpp
│ ├── idle_time_macos.cpp
│ └── idle_time_linux.cpp
└── installer/
├── windows/installer.iss # Inno Setup → setup.exe
├── macos/build_pkg.sh # → IdleTracker.pkg
└── linux/install.sh # script + systemd user service
Prerequisites (one-time): Visual Studio 2022 Build Tools with "Desktop development with C++", CMake, and Inno Setup 6.
Step 1 — open a "Developer PowerShell for VS 2022" window (Start menu → type "Developer PowerShell"). A plain PowerShell window won't have the compiler on PATH.
Step 2 — build the executable. In that Developer PowerShell window, from the project folder:
cd C:\path\to\idle-tracker
cmake -S . -B build
cmake --build build --config ReleaseThe exe is now at build\Release\idle-tracker.exe.
Step 3 — test it before packaging. Same window:
.\build\Release\idle-tracker.exe --threshold 10 --log events.jsonlDon't touch the keyboard/mouse for 10 seconds → popup appears and an idle_start line is written. Move the mouse → idle_end with the duration. Press Ctrl+C to stop (you'll see agent_stop logged — that's the graceful shutdown working).
Step 4 — build the installer. In a regular PowerShell window:
cd C:\path\to\idle-tracker\installer\windows
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer.issThis produces Output\IdleTrackerSetup-1.0.0.exe. Users run it once; it installs to Program Files, puts the config in C:\ProgramData\IdleTracker\, and registers auto-start at login for all users. Events land in C:\ProgramData\IdleTracker\events.jsonl.
For distribution to an organization, sign
idle-tracker.exeand the setup exe with your code-signing certificate (signtool sign ...) or SmartScreen will warn users.
Prerequisites: Xcode Command Line Tools (xcode-select --install) and CMake (brew install cmake). Run these in Terminal on a Mac:
cd /path/to/idle-tracker
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# Test it
./build/idle-tracker --threshold 10 --log /tmp/events.jsonl
# Build the installer package
cd installer/macos
./build_pkg.shThis produces IdleTracker-1.0.0.pkg. Users double-click it to install: the binary goes to /usr/local/bin, and a launchd agent starts it automatically at every login (and restarts it if it crashes). Events land in /Users/Shared/IdleTracker/events.jsonl.
For distribution outside your own machines, sign and notarize the pkg (commands are printed by the build script) — unsigned pkgs trigger Gatekeeper warnings.
No installer package here by design. The agent must run inside the user's graphical session to read X11 idle time, so the method is a script that builds from source and registers a systemd user service. In a terminal:
cd /path/to/idle-tracker/installer/linux
./install.shThe script installs build dependencies (apt/dnf/pacman), compiles, installs to ~/.local/bin, and enables idle-tracker.service tied to graphical-session.target — it starts at login and restarts on failure.
Useful commands afterwards:
systemctl --user status idle-tracker # is it running?
journalctl --user -u idle-tracker -f # live logs
tail -f ~/.local/share/idle-tracker/events.jsonl
./install.sh remove # uninstallIdle detection uses the X11 MIT-SCREEN-SAVER extension, which also works under XWayland on GNOME/KDE. On pure-Wayland-only setups without XWayland, idle time can't be read by a plain process; the agent logs detection_error rather than reporting fake activity.
idle-tracker --help shows all flags. File-based config (CLI flags override it):
idle_threshold_seconds = 300 # idle after 5 minutes
poll_interval_ms = 1000
remind_every_seconds = 0 # 0 = one alert per idle period
notify = true
log_path = /var/log/idle-tracker/events.jsonl
One JSON object per line — trivial to tail, parse, or bulk-POST to an API:
{"ts":"2026-07-02T08:15:03Z","event":"agent_start","poll_ms":1000,"threshold_ms":300000}
{"ts":"2026-07-02T08:22:10Z","event":"idle_start","idle_ms":300012}
{"ts":"2026-07-02T08:31:44Z","event":"idle_end","idle_duration_ms":874120}
{"ts":"2026-07-02T17:00:01Z","event":"agent_stop"}The same events are also POSTed live to the backend when the integration below is configured — the JSONL file stays as the local audit copy.
The time-tracker backend exposes POST /idle-events. When the agent reports idle_start for a floor-level employee (someone with no privileged role — Team Leads, WFM, Managers, HR, Payroll, and Admins are exempt), the server:
- ends every open activity session on the employee's open shift,
- closes the shift as
AUTO_CLOSEDwithclockOutAt = now— paid time stops here, - records a
ComplianceViolation(IDLE_TIMEOUT) and anAuditLogrow (IDLE_AUTO_LOGOUT), and - pushes live WebSocket toasts to the employee ("no activity detected at your station") and to their Team Lead/Manager console.
If the employee is on an approved break, the report is ignored (break overrun has its own enforcement). If they aren't clocked in, or the idle time is below the server's threshold, nothing happens. idle_end is acknowledged but never reopens a shift — the employee clocks back in normally.
Server side (time-tracker .env):
IDLE_AGENT_KEY="some-long-random-fleet-secret" # unset = ingestion disabled
IDLE_CLOCKOUT_SECONDS="300" # server-side minimum idle time
Agent side — flags or config file:
idle-tracker --threshold 300 --api-url https://tracker.example.com/idle-events `
--api-key some-long-random-fleet-secret --employee EMP-12or in idle-tracker.conf (what the installers deploy):
idle_threshold_seconds = 300 # keep equal to the server's IDLE_CLOCKOUT_SECONDS
api_url = https://tracker.example.com/idle-events
api_key = some-long-random-fleet-secret
employee_code = EMP-12
Uploads run on a detached thread with a 5s timeout, so a down backend never stalls idle detection; failures are logged as upload_error events and the JSONL log remains the source of truth for backfill. The server ignores any report whose idleMs is below its own IDLE_CLOCKOUT_SECONDS, so a mistuned agent can't clock anyone out early.
Hardening note: the fleet key is one shared secret for all machines — fine for a single-site deployment, but any holder of the key can emit events for any employee code. If that matters, move to per-employee agent tokens issued by the backend.
This agent notifies the user visibly and logs locally — it is not hidden. If you deploy it to employee machines, disclose it in policy and follow applicable labor/privacy law (in the Philippines, the Data Privacy Act of 2012 requires informing employees about monitoring and its purpose). Transparent deployment is also simply better for adoption.