In 2017, during a meeting with a client, a government agency, we discussed a project we developed and offered them, (in C++, obviously…), they asked us if it was possible to add log entries about a PC user’s activity level. That gave us the idea to add such a feature and in this post, I am going to show you how we can assign a dedicated thread to measure your level of activity while using your PC. That is, in fact, the final exercise of Chapter 15, Multithreading the Needle.
When you wish to monitor the activity level of the user, you would need a separate thread for
that. In our example, we will start the program and constantly display a gauge as a User
Interface element to help us provide that feedback.

Here is the source code, which is compatible with Windows and Mac OSX.
#include <iostream>
#include <atomic>
#include <chrono>
#ifdef _WIN32
#include <Windows.h>
#elif defined(__APPLE__)
#include <ApplicationServices/ApplicationServices.h>
#else
#error Unsupported platform
#endif
// Constants for activity level gauge
constexpr int MinActivityLevel = 0;
constexpr int MaxActivityLevel = 10;
constexpr int GaugeWidth = 20;
constexpr int InactivityThreshold = 5; // Number of seconds of inactivity before decrementing activity level
// Atomic variable to hold the current activity level
std::atomic<int> activityLevel(0);
std::chrono::time_point<std::chrono::system_clock> lastActivityTime;
// Low-level keyboard hook procedure
#ifdef _WIN32
LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
// Increase the activity level when a key is pressed
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
{
activityLevel = std::max<int>(activityLevel.load() - 0.5, MinActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
}
// Call the next hook procedure in the hook chain
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
#elif defined(__APPLE)
{
CGEventRef KeyboardEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon)
{
if (type == kCGEventKeyDown || type == kCGEventFlagsChanged)
{
activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
else if (type == kCGEventKeyUp)
{
activityLevel = std::max<int>(activityLevel.load() - 0.5, MinActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
return event;
}
}
#endif
// Low-level mouse hook procedure
#ifdef _WIN32
LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
// Increase the activity level when the mouse moves
if (wParam == WM_MOUSEMOVE)
{
activityLevel = std::min<int>(activityLevel.load() + 0.5, MaxActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
else if (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN)
{
activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
}
// Call the next hook procedure in the hook chain
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
#elif defined (__APPLE__)
CGEventRef MouseEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon)
{
if (type == kCGEventMouseMoved || type == kCGEventLeftMouseDown || type == kCGEventRightMouseDown)
{
activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
else if (type == kCGEventLeftMouseUp || type == kCGEventRightMouseUp)
{
activityLevel = std::max<int>(activityLevel.load() - 0.5, MinActivityLevel);
lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
}
return event;
}
#endif
// Function to calculate activity level based on keyboard and mouse events
void calculateActivityLevel()
{
#ifdef _WIN32
// Set low-level keyboard hook
HHOOK keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardHookProc, nullptr, 0);
if (!keyboardHook)
{
std::cerr << "Failed to set keyboard hook." << std::endl;
return;
}
// Set low-level mouse hook
HHOOK mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, nullptr, 0);
if (!mouseHook)
{
std::cerr << "Failed to set mouse hook." << std::endl;
return;
}
// Message loop to keep the hooks active
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Unhook the keyboard
// Unhook the keyboard hook
UnhookWindowsHookEx(keyboardHook);
// Unhook the mouse hook
UnhookWindowsHookEx(mouseHook);
#elif defined( __APPLE__)
// Create the event tap for keyboard events
CGEventMask keyboardEventMask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged);
CFMachPortRef keyboardEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, keyboardEventMask, KeyboardEventCallback, nullptr);
if (!keyboardEventTap)
{
std::cerr << "Failed to create keyboard event tap." << std::endl;
return;
}
// Create a run loop source from the keyboard event tap
CFRunLoopSourceRef keyboardRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, keyboardEventTap, 0);
if (!keyboardRunLoopSource)
{
std::cerr << "Failed to create keyboard run loop source." << std::endl;
CFRelease(keyboardEventTap);
return;
}
// Add the keyboard run loop source to the current run loop
CFRunLoopAddSource(CFRunLoopGetCurrent(), keyboardRunLoopSource, kCFRunLoopDefaultMode);
// Enable the keyboard event tap
CGEventTapEnable(keyboardEventTap, true);
// Create the event tap for mouse events
CGEventMask mouseEventMask = CGEventMaskBit(kCGEventMouseMoved) | CGEventMaskBit(kCGEventLeftMouseDown) | CGEventMaskBit(kCGEventRightMouseDown) | CGEventMaskBit(kCGEventLeftMouseUp) | CGEventMaskBit(kCGEventRightMouseUp);
CFMachPortRef mouseEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, mouseEventMask, MouseEventCallback, nullptr);
if (!mouseEventTap)
{
std::cerr << "Failed to create mouse event tap." << std::endl;
return;
}
// Create a run loop source from the mouse event tap
CFRunLoopSourceRef mouseRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0);
if (!mouseRunLoopSource)
{
std::cerr << "Failed to create mouse run loop source." << std::endl;
CFRelease(mouseEventTap);
return;
}
// Add the mouse run loop source to the current run loop
CFRunLoopAddSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopDefaultMode);
// Enable the mouse event tap
CGEventTapEnable(mouseEventTap, true);
// Run the current run loop
CFRunLoopRun();
#endif
}
// Function to display the color gradient gauge for a given activity level
// Function to display the color gradient gauge for a given activity level
void displayColorGradientGauge(int activityLevel)
{
const int GaugeWidth = 20;
// Define the color codes for gradient display
const std::string ResetColor = "\033[0m";
// Calculate the number of filled and empty cells for the activity level gauge
int filledCells = std::round(static_cast<double>(GaugeWidth * activityLevel) / 10);
int emptyCells = GaugeWidth - filledCells;
// Display the activity level gauge
std::cout << "[";
// Display the filled cells with color gradient
for (int i = 0; i < filledCells; ++i)
{
double percentage = static_cast<double>(i) / (GaugeWidth - 1);
if (percentage <= 0.5)
{
// Gradient from light green to yellow
int greenValue = static_cast<int>(255 - 128 * percentage / 0.5);
int redValue = static_cast<int>(255 * percentage / 0.5);
std::cout << "\033[38;2;" << redValue << ";" << greenValue << ";" << 0 << "m#";
}
else
{
// Gradient from yellow to red
int redValue = 255;
int greenValue = static_cast<int>(255 * (1.0 - percentage) / 0.5);
std::cout << "\033[38;2;" << redValue << ";" << greenValue << ";" << 0 << "m#";
}
}
// Display the empty cells in the default color
std::cout << ResetColor;
for (int i = 0; i < emptyCells; ++i)
{
std::cout << "-";
}
std::cout << "]" << std::endl;
}
// Function to display the activity gauge on the console
void displayActivityGauge()
{
int previousLevel = -1; // Store the previous activity level
lastActivityTime = std::chrono::system_clock::now();
while (true)
{
int currentLevel = activityLevel.load(); // Read the current activity level
// Check for inactivity
std::chrono::time_point<std::chrono::system_clock> currentTime = std::chrono::system_clock::now();
std::chrono::duration<double> elapsedTime = currentTime - lastActivityTime;
if (elapsedTime.count() >= InactivityThreshold)
{
activityLevel = std::max<int>(activityLevel.load() - 1, MinActivityLevel);
lastActivityTime = currentTime; // Update last activity time
}
// Clear the console
#ifdef _WIN32
system("cls");
#elif defined(__APPLE__)
system("clear");
#endif
// Display the activity level gauge
std::cout << "System Activity Level Gauge" << std::endl;
displayColorGradientGauge(currentLevel);
previousLevel = currentLevel; // Update the previous activity level
// Sleep
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
// Start the activity level calculation thread
std::thread calculationThread(calculateActivityLevel);
// Start the activity gauge display thread
std::thread displayThread(displayActivityGauge);
// Wait for the threads to finish
calculationThread.join();
displayThread.join();
return 0;
}