File: README.md
Twinkle Tray
Twinkle Tray enables brightness control on external displays in Windows 10 & 11. Even though Windows is capable of adjusting the backlight on most monitors, it doesn't support external monitors natively. Windows also lacks any options to manage the brightness of multiple displays. This app inserts a new icon into your system tray, where you can click to have instant access to the brightness levels of all compatible displays.
Features
- Adds brightness sliders to the system tray, similar to the built-in Windows volume flyout.
- Seamlessly blends in with Windows 10 and Windows 11. Uses your Personalization settings to match your taskbar.
- Can automatically change monitor brightness depending on the time of day or when idle.
- Bind hotkeys to adjust the brightness of specific or all displays.
- Normalize backlight across different monitors.
- Control DDC/CI features such as contrast.
- Starts up with Windows.
Design & Personalization
Twinkle Tray will automatically adjust the look and feel to match your Windows version and preferences. Additional options are available to select the Windows version and theme of your choice.
Download
Download the lastest version from twinkletray.com or the Releases page.
Install via Package Manager
Windows Package Manager
winget install xanderfrangos.twinkletrayChocolatey (unofficial)
Chocolatey users can download and install Twinkle Tray from Chocolatey's Community Repository by installing the twinkle-tray package:
choco install twinkle-trayTo upgrade to the latest approved package version, run the following command:
choco upgrade twinkle-trayThis package is not maintained at this repository. Please do not create issues relating to the package here. Instead, go to the package page and follow the Package Triage Process.
Scoop (unofficial)
Scoop users can download and install Twinkle Tray from Scoop's Extras bucket by installing the twinkle-tray package:
scoop bucket add extras
scoop install extras/twinkle-trayTo upgrade to the latest approved package version, run the following command:
scoop update twinkle-trayThis package is not maintained at this repository. Please do not create issues relating to the package here. Instead, go to ScoopInstallers/Extras and search for an existing issue or discussion and create a new issue or discussion if one does not already exist.
Usage
- Download from the Releases page and run the installer EXE.
- Once installation has finished, you should see the Twinkle Tray icon in your system tray.
- Click the icon to bring up the Adjust Brightness flyout.
- Click away to hide the flyout.
- Right-click the system tray icon to quit.
Compatibility
Twinkle Tray uses DDC/CI and WMI to communicate with your monitors. Most monitors offer DDC/CI compatibility, but it may be off by default. Make sure you have the appropriate option(s) enabled on your monitor so that it can work with Twinkle Tray. Refer to your monitor's user manual for more information.
Known issues:
- The AMD Radeon Control Panel can interfere with Twinkle Tray. Ensure "Custom Colors" is not enabled.
- VGA/DVI may not be compatible.
- USB/Thunderbolt/Surface docks with HDMI or DisplayPort may not be compatible.
- DDC/CI features such as brightness control and power state may cause certain models of monitors to behave poorly. This applies to any DDC/CI software, not just Twinkle Tray.
If some of your monitors are not being detected, please see this page for troubleshooting steps.
Command Line Arguments
Twinkle Tray (v1.13.0+) supports requesting brightness changes from the command line. Twinkle Tray must already be running. One monitor argument and one brightness argument are required. Multiple arguments will override each other.
For example: "%LocalAppData%\Programs\twinkle-tray\Twinkle Tray.exe" --MonitorNum=1 --Offset=-30 will adjust monitor number 1 by -30 brightness.
Supported args:
--ListList all displays. (available in v1.14.0+)--MonitorNumSelect monitor by number. Starts at 1. Example:--MonitorNum=2--MonitorIDSelect monitor by internal ID. Partial or whole matches accepted. Example:--MonitorID="UID2353"--AllFlag to select all monitors.--SetSet brightness percentage. Example:--Set=95--OffsetAdjust brightness percentage. Example:--Offset=-20--VCPSend a specific DDC/CI VCP code and value instead of brightness. The first part is the VCP code (decimal or hexadecimal), and the second is the value. Example:--VCP="0xD6:5"(available in v1.14.4+)--OverlayFlag to show new brightness levels in the overlay Example:--Overlay--PanelFlag to show new brightness levels in the panel Example:--Panel
If you are using the Microsoft Store version of Twinkle Tray, you can access Twinkle Tray using the alias Twinkle-Tray.exe (v1.17.1+).
Localization
Thanks to several contributors, Twinkle Tray is localized for multiple languages. If you'd like to create or update a localization, see this page for details. Special thanks to Weblate for allowing free use of their service.
Localization progress
Build Instructions
If you wish to run a development build of Twinkly Tray:
- Download or clone.
- Install the build tools for
node-gyp, if not already installed. You may already have these from installing NodeJS. - Run
npm install. - Run
npm run buildto build an executable ornpm startto run a development build.
Note: Twinkle Tray must be built on Windows.
Special Thanks
Twinkle Tray was built using frameworks & libraries such as Electron, Node.js, node-ddcci, and win32-displayconfig. Thanks to Weblate for allowing free use of their service, along with the many contributors to the localizations of Twinkle Tray. The app would not be nearly as popular without all of your help. And thank you for the many donations, small and large, over the years.
License
Copyright ยฉ 2020 Xander Frangos
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: src/modules/win32-displayconfig/README.md
win32-displayconfig
Simplified Node bindings to the Win32 Display Device Reference API.
Supported Windows Versions
This module has currently only received testing on Windows 10 version 2004.
Additional testing on other versions of Windows 10 and Windows 8.1 would be appreciated.
We are not planning on supporting Windows 7 or Windows 8, as they are generally unsupported
by Microsoft. However, patches to support Windows 7 or Windows 8 are welcome.
Supported Node Versions
This module requires N-API Version 4,
bounding the minimum supported Node versions
to 8.16.0 (without any support for Node 9), Node 10.16.0, and Node 11.8.0. We are only actively testing
on Node 10.21 and above. We'll note incompatibilities and accept patches for Node 8 if the need arises.
Functionality
- Querying display devices, at a low and higher
- Observing display device layout and output changes
- Querying the vertical refresh rate at a given display point
- Saving and restoring layouts via the Display Device Reference API
Examples
Querying Low-Level Display Device Information
The queryDisplayConfig function returns data somewhat similar to the outputs of theQueryDisplayConfig function
via the pathArray and modeArray properties, and naming and manufacturer info via itsnameArray properties.
From scripts/dumpquery.js:
const w32disp = require("win32-displayconfig");
const util = require("util");
w32disp.queryDisplayConfig().then((config) => {
// The outputs of `pathArray` and `modeArray` refer to
// both parsed values and raw buffer values. The raw buffers
// are the exact structs returned by the Win32 API, and are
// used to save and restore display positioning configuration.
const pathArray = config.pathArray.map((pa) => pa.value);
const modeArray = config.modeArray.map((ma) => ma.value);
console.log(
util.inspect(
// However, the `nameArray` only refers to parsed values.
{ pathArray, modeArray, nameArray: config.nameArray },
{ depth: 10 }
)
);
});Which outputs:
{ pathArray:
[ { flags: 1,
sourceInfo:
{ adapterId: { LowPart: 945385802, HighPart: 0 },
id: 0,
statusFlags: 1,
modeInfoIdx: 1 },
targetInfo:
{ adapterId: { LowPart: 945385802, HighPart: 0 },
id: 114948,
statusFlags: 1,
outputTechnology: 'dvi',
rotation: 0,
scaling: 'identity',
refreshRate: { Numerator: 60000, Denominator: 1000 },
scanLineOrdering: 'progressive',
targetAvailable: 1,
modeInfoIdx: 0 } },
...
],
modeArray:
[ { infoType: 'target',
id: 114948,
adapterId: { LowPart: 945385802, HighPart: 0 },
targetMode:
{ targetVideoSignalInfo:
{ pixelRate: { lowPart: 148500000, highPart: 0 },
hSyncFreq: { Numerator: 148500000, Denominator: 2200 },
vSyncFreq: { Numerator: 60000, Denominator: 1000 },
activeSize: { cx: 1920, cy: 1080 },
totalSize: { cx: 2200, cy: 1125 },
videoStandard: 255,
scanlineOrdering: 'progressive' } } },
...
],
nameArray:
[ { adapterId: { LowPart: 945385802, HighPart: 0 },
id: 114948,
deviceFlags: 5,
outputTechnology: 'dvi',
edidManufactureId: 29188,
edidProductCodeId: 147,
connectorInstance: 2,
monitorFriendlyDeviceName: 'Acer X233H',
monitorDevicePath:
'\\\\?\\DISPLAY#ACR0093#5&326e05e0&0&UID114948#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}' },
...
] }This is generally lower-level output than you want. Consider using extractDisplayConfig instead.
Querying Higher-Level Display Device Information
The extractDisplayConfig function reshapes the output of queryDisplayConfig to be more useful
to application programmers. Information from the pathArray, modeArray, and nameArray properties
of the output of queryDisplayConfig are all joined according to their IDs.
From scripts/dumpextract.js:
const w32disp = require("win32-displayconfig");
const util = require("util");
w32disp.extractDisplayConfig().then((output) => {
console.log(util.inspect(output, { depth: 10 }));
});Which outputs:
[ { displayName: 'Acer X233H',
devicePath:
'\\\\?\\DISPLAY#ACR0093#5&326e05e0&0&UID114948#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}',
sourceConfigId: { adapterId: { LowPart: 945385802, HighPart: 0 }, id: 1 },
targetConfigId:
{ adapterId: { LowPart: 945385802, HighPart: 0 }, id: 114948 },
inUse: false,
outputTechnology: 'dvi',
rotation: 0,
scaling: 'preferred',
sourceMode:
{ width: 1920,
height: 1080,
pixelFormat: 32,
position: { x: 0, y: 0 } },
pathBuffer:
,
sourceModeBuffer:
,
targetVideoSignalInfo:
{ pixelRate: { lowPart: 148500000, highPart: 0 },
hSyncFreq: { Numerator: 148500000, Denominator: 2200 },
vSyncFreq: { Numerator: 60000, Denominator: 1000 },
activeSize: { cx: 1920, cy: 1080 },
totalSize: { cx: 2200, cy: 1125 },
videoStandard: 255,
scanlineOrdering: 'progressive' },
targetModeBuffer:
},
...
]Note that the low-level buffers are still present, but sourceMode, targetVideoSignalInfo, devicePath,
and displayName have all been combined from multiple sources. Also note the inUse boolean, which is
derived from comparing against low-level flags in the Display Device Reference API.
You will likely see multiple outputs for a single display device in this output, with only one of them
having inUse: true. The inUse: false entries correspond to alternative output modes available on the device.
Observing Display Device Layout and Output Changes
The display geometry can change at any time during the execution of your program, but sometimes you
depend on knowledge of this geometry for the correct behavior. You can observe changes as they
occur using the addDisplayChangeListener function:
const w32disp = require("win32-displayconfig");
const listener1 = (err, conf) => {
if (err !== null) {
// Report error
console.error(err);
} else {
// conf is the same as the output of extractDisplayConfig
// ...
}
};
w32disp.addDisplayChangeListener(listener1);
// Or equivalently
const listener2 = w32disp.addDisplayChangeListener((err, conf) => {
if (err !== null) {
console.error(err);
} else {
// ...
}
});Adding these change listeners keeps the event loop active, even when all other
activities have been stopped, so for situations where graceful shutdown is required,
remove all of the listeners using the removeDisplayChangeListener function:
w32disp.removeDisplayChangeListener(listener1);
w32disp.removeDisplayChangeListener(listener2);
// No more display config activities are present on the event loop,
// so Node can now exit cleanly.Querying the Vertical Refresh Rate at a Display Point
Some operations need to only occur as quickly as the vertical refresh rate
for smooth performance. For example, many Windows positioning APIs cannot
perform well if they are invoked any faster than the frame rate of the GPU.
This module provides a VerticalRefreshRateContext class to determine the
refresh rate of the displays servicing a given display point. Note that not
all displays will necessarily have the same refresh rate, and many displays
can be servicing the same display point. When multiple displays are servicing
the same display point, the result is the minimum vertical refresh rate.
const { VerticalRefreshRateContext } = require("win32-displayconfig");
const ctx = new VerticalRefreshRateContext();
async function refreshRateForPoint(x, y) {
// Here, x is the vertical offset and y is the horizontal offset.
// These offsets are relative to the upper left hand corner of the
// primary display, so they are allowed and expected to become negative.
//
// The result is either a number indicating the vertical refresh rate in Hz,
// or undefined if the given point is not present in the display bounds of
// the current display configuration.
return await ctx.findVerticalRefreshRateForDisplayPoint(x, y);
}
// ...
// This context reacts to underlying display changes, as a trade-off between
// performance and accuracy. It does so using the `addDisplayChangeListener`
// capabilities, so it needs to be disposed of properly to clear the event
// loop for graceful shutdown.
//
// Call this when you are finished with the context. A good place to do so
// is in the code that handles gracefully exiting your program.
ctx.close();See scripts/watchmouse.js for a working example, polling
for the refresh rate based on the mouse cursor position.
Saving and Restoring Device Layouts
This module can save and restore display device layouts (although it cannot directly modify
them yet; patches welcome!). You can use this to implement a display configuration profile
system.
Saving a profile is achieved with the displayConfigForRestoration function:
const w32disp = require("win32-displayconfig");
async function saveDisplayConfig() {
const conf = await w32disp.displayConfigForRestoration();
// The type of conf is JSON-safe: you can store this on the disk,
// transmit it over a network, etc.
return JSON.stringify(conf);
}And restoring a profile is achieved with the restoreDisplayConfig function:
const w32disp = require("win32-displayconfig");
async function restoreDisplayConfig(serialized, persistent) {
// If "persistent", then this display configuration will be saved in Windows
// as the default display configuration. This means that restoring to defaults
// or rebooting will result in this exact configuration.
persistent = !!persistent;
const conf = JSON.parse(serialized);
// If portions of the configuration refer to displays that are not actually
// present, we make a best effort to enable all of the displays that are both
// present and enabled in this configuration, and disable all other displays.
//
// Otherwise, the display configuration is set exactly.
await w32disp.restoreDisplayConfig(conf);
}Copyright
This module is available under the terms of the MIT license. See the COPYRIGHT file
for more information.
File: src/modules/node-ddcci/README.md
node-ddcci
Installation
node-ddcci is currently only supported on Windows.
npm install @hensm/ddcciUsage
const ddcci = require("@hensm/ddcci");
for (const monitor of ddcci.getMonitorList()) {
console.log(`${monitor} current brightness: ${ddcci.getBrightness(monitor)}`);
ddcci.setBrightness(monitor, 25);
}Docs
`getMonitorList()`
Gets a list of the current connected monitors.
Return value
An array ofStringcontaining the monitor IDs.
`getBrightness(monitorId)`
Queries a monitor's brightness level.
Parameters
monitorIdString. ID of monitor for which to query the brightness.
Return value
Aninteger, typically between 0-100, representing the current brightness.
`getMaxBrightness(monitorId)`
Queries a monitor's maximum brightness level.
Parameters
monitorIdString. ID of monitor for which to query the brightness.
Return value
Aninteger, typically between 0-100, representing the maximum brightness.
`setBrightness(monitorId, level)`
Sets a monitor's brightness level.
Parameters
monitorIdString. ID of monitor for which to set the brightness.levelinteger. Between 0-100 representing the new brightness level.
`getContrast(monitorId)`
Queries a monitor's contrast level.
Parameters
monitorIdString. ID of monitor for which to query the contrast.
Return value
Aninteger, typically between 0-100, representing the current contrast.
`getMaxContrast(monitorId)`
Queries a monitor's maximum contrast level.
Parameters
monitorIdString. ID of monitor for which to query the contrast.
Return value
Aninteger, typically between 0-100, representing the maximum contrast.
`setContrast(monitorId, level)`
Sets a monitor's contrast level.
Parameters
monitorIdString. ID of monitor for which to set the contrast.levelinteger. Between 0-100 representing the new contrast level.
`_getVCP(monitorId, vcpCode)`
Queries a monitor for a VCP code value.
Parameters
monitorIdString. ID of monitor for which to query the VCP feature.vcpCodeinteger. VCP code to query
Return value
Anarrayof twointegervalues in the format of[currentValue, maxValue].
`_setVCP(monitorId, vcpCode, value)`
Sets the value of a VCP code for a monitor.
Parameters
monitorIdString. ID of monitor for which to set the VCP feature.vcpCodeinteger. VCP code to set.valueinteger. Value of the VCP code.
`_refresh()`
Refreshes the monitor list.
File: src/modules/node-active-window/README.md
Node Active Window
NodeJS library using native modules to get the active window and some metadata (including the application icon) on Windows, MacOS and Linux.
Table of Contents
Getting started
Installation
npm install --save @paymoapp/active-windowNative addon
This project uses NodeJS Native Addons to function, so you can use this library in any NodeJS or Electron project, there won't be any problem with bundling and code signing.
The project uses prebuild to supply prebuilt libraries.
The project uses Node-API version 6, you can check this table to see which node versions are supported.
If there's a compliant prebuilt binary, it will be downloaded during installation, or it will be built. You can also rebuild it anytime by running npm run build:gyp.
The library has native addons for all the three major operating systems: Windows, MacOS and Linux. For Linux, only the X11 windowing system is supported.
Example
You can run a demo application by calling npm run demo. You can browse it's source code for a detailed example using the watch API in demo/index.ts.
import ActiveWindow from '@paymoapp/active-window';
ActiveWindow.initialize();
if (!ActiveWindow.requestPermissions()) {
console.log('Error: You need to grant screen recording permission in System Preferences > Security & Privacy > Privacy > Screen Recording');
process.exit(0);
}
const activeWin = ActiveWindow.getActiveWindow();
console.log('Window title:', activeWin.title);
console.log('Application:', activeWin.application);
console.log('Application path:', activeWin.path);
console.log('Application PID:', activeWin.pid);
console.log('Application icon:', activeWin.icon);API
Data structures
๐ WindowInfo
interface WindowInfo {
title: string;
application: string;
path: string;
pid: number;
icon: string;
windows?: {
isUWPApp: boolean;
uwpPackage: string;
};
}This is the only object you will receive when interacting with this library. It contains information about the currently active window:
title- The title of the current windowapplication- The name of the application. On Windows you should use theuwpPackageparameter instead ifisUWPAppis set to truepath- The path to the application's executablepid- Process identifier of the applicationicon- Base64 encoded string representing the application iconwindows- Object containing Windows platform specific information, undefined on other platformswindows.isUWPApp- Set totrueif the active window is owned by an Universal Windows Platform applicationwindows.uwpPackage- Contains the package family name of the UWP application
None of the parameters are nullable, even if their value couldn't be fetched, they will be either be set to an empty string (for the string values), -1 (for the numeric values) or false (for the boolean values).
Functions
๐ getActiveWindow
interface IActiveWindow {
getActiveWindow(): WindowInfo
// ...
}Requests the current foreground window in a synchronous way. It will throw an error if the current window couldn't be fetched (for example there're no focused windows at the moment).
๐ subscribe
interface IActiveWindow {
subscribe(callback: (windowInfo: WindowInfo | null) => void): number;
// ...
}Subscribe to changes of the active window. The supplied callback will be called with null if there're no focused windows at the moment.
The function returns a number representing the ID of the watch. You should store this value to remove the event listener later on.
๐ unsubscribe
interface IActiveWindow {
unsubscribe(watchId: number): void;
// ...
}Remove the event listener associated with the supplied watch ID. Use this to unsubscribe from the active window changed events.
๐ initialize
interface IActiveWindow {
initialize(opts?: { osxRunLoop: boolean }): void;
// ...
}On some platforms (Linux) the library needs some initialization to be done. You must call this function before doing anything with the library regardless of the current platform.
If you're not using this library in a GUI application there might be no runloop running for the main thread. In this case you should set the osxRunLoop property to true if you want to use subscriptions.
๐ requestPermissions
interface IActiveWindow {
requestPermissions(): boolean;
// ...
}On the MacOS platform you need to request screen recording permission to fetch the title of the current window.
The function will return true if the permission is granted and false if the permission is denied. This is a non-blocking function, so you will only get the momentary status.
You can call this function regardless of the current platform. On unsupported platforms it will simply return true.
When the function is called, the user will be presented with a system modal with instructions to grant the permission. You should include these instructions in your application as well, since this is a one-time modal. After the user grants the permission, it is required to relaunch the application for the changes to take effect.
If the user fails to grant the required permissions, the title property of the returned WindowInfo will be an empty string.
Native libraries
You can import the each platform dependent library as a standalone C++ / Objective-C++ library. You can find the library itself in the module's src directory. The napi directory contains the Node-API bindings and the demo directory contains a small demo program that can be built using a Makefile.
You can build the demo by navigating to the module//demo folder and executing make. You can run the demo using make run and clean the build artifacts using make clean.
The demo has 4 running modes:
- default:
make run- in this mode the library is used to fetch the current window details, then there's a 3 second delay after which the current window is fetched again - loop:
make run MODE=loop- in this mode the library is used the poll the current window in every 3 seconds until SIGINT (Ctrl+C) is received - watch:
make run MODE=watch- in this mode the library is used to watch the current window and it's title. There's no polling involved in this mode - benchmark:
make run MODE=benchmark- in this mode the library will fetch the current window details 100.000 times (or 10.000 times on windows) and it will print the total of the consumed CPU seconds while doing so
Linux (`module/linux`)
Data structures
๐ WindowInfo
struct WindowInfo {
std::string title; // UTF8 encoded string of window title. Empty string if couldn't fetch
std::string application; // UTF8 encoded string of application name. Empty string if couldn't fetch
std::string path; // UTF8 encoded string of application path. Empty string if couldn't fetch
int pid; // PID of process. -1 if couldn't fetch
std::string icon; // base64 encoded PNG icon. Empty string if couldn't fetch
}๐ watch_t
typedef unsigned int watch_t;๐ watch_callback
typedef std::function<void(WindowInfo*)> watch_callback;Public functions
๐ Constructor
ActiveWindow(unsigned int iconCacheSize = 0);If you pass iconCacheSize > 0, then an LRU (least recently used) cache will be instantiated which will cache the fetched icons. This results in about 90% less CPU seconds consumed. You can use the benchmark mode of the demo to test it yourself.
You should pass a cache size suitable for your application. A bigger cache results in a greater memory consumption, but it also reduces the CPU utilization if your user switches across a large set of applications.
๐ getActiveWindow
WindowInfo* getActiveWindow();Returns pointer to WindowInfo containing the gathered information about the current window. The pointer can be NULL in the case of an error or if there is no active window (ex: all the windows are minified). You should free up the allocated WindowInfo object using delete.
๐ buildAppCache
void buildAppCache();Gathers all the .desktop entries available on the system (starting from ~/.local/share/applications through each $XDG_DATA_DIRS/applications) and resolves the icon path for them. This cache is used to get the icon for a given window. If this function is not called, then no icons will be resolved.
๐ watchActiveWindow
watch_t watchActiveWindow(watch_callback cb);Sets up a watch for the active window. If there's a change in the current active window, or the title of the active window, the callback will be fired with the current active window. You don't need to call getActiveWindow() in the callback, you can use the supplied parameter.
This method will also start a background watch thread if it's not already running. Please note that the callbacks will be executed on this thread, so you should assure thread safety.
You MUST NOT free up the WindowInfo object received in the parameter. If you need to store the active window you SHOULD make a copy of it, since the WindowInfo object will be freed after calling all the callbacks.
You should save the returned watch ID to unsubscribe later.
๐ unwatchActiveWindow
void unwatchActiveWindow(watch_t watch);Removes the watch associated with the supplied watch ID.
The background watch thread will not be closed, even if there're no more watches left. It will only be closed when the class's destructor is called.
Example
See module/linux/demo/main.cpp for an example.
#include
#include "ActiveWindow.h"
using namespace std;
using namespace PaymoActiveWindow;
int main() {
ActiveWindow* activeWindow = new ActiveWindow(10);
WindowInfo* windowInfo = activeWindow->getActiveWindow();
if (windowInfo == NULL) {
cout<<"Could not get active window\n";
}
else {
cout<<"Title: "<<windowInfo->title<<"\n";
cout<<"Application: "<<windowInfo->application<<"\n";
cout<<"Executable path: "<<windowInfo->path<<"\n";
cout<<"PID: "<<windowInfo->pid<<"\n";
cout<<"Icon: "<<windowInfo->icon<<"\n";
}
delete windowInfo;
delete activeWindow;
return 0;
}Building
See module/linux/demo/Makefile for a sample makefile. You need to check the lib target.
You should use C++17 for building and you need to link the following libraries:
- X11 (
-lX11) - libx11-dev - PThread (
-lpthread) - libpthread-stubs0-dev
Windows (`module/windows`)
Data structures
๐ WindowInfo
struct WindowInfo {
std::wstring title = L""; // UTF16 encoded string of window title. Empty string if couldn't fetch
std::wstring application = L""; // UTF16 encoded string of application. Empty string if couldn't fetch
std::wstring path = L""; // UTF16 encoded string of executable path. Empty string if couldn't fetch
unsigned int pid = 0; // Process PID
bool isUWPApp = false; // if application is detected to be an Universal Windows Platform application
std::wstring uwpPackage = L""; // UTF16 encoded string of the UWP package name. Empty string if couldn't fetch
std::string icon = ""; // base64 encoded icon. Empty string if couldn't fetch
};๐ watch_t
typedef unsigned int watch_t;๐ watch_callback
typedef std::function<void(WindowInfo*)> watch_callback;Public functions
๐ Constructor
ActiveWindow(unsigned int iconCacheSize = 0);If you pass iconCacheSize > 0, then an LRU (least recently used) cache will be instantiated which will cache the fetched icons. This results in about 90% less CPU seconds consumed. You can use the benchmark mode of the demo to test it yourself.
You should pass a cache size suitable for your application. A bigger cache results in a greater memory consumption, but it also reduces the CPU utilization if your user switches across a large set of applications.
๐ getActiveWindow
WindowInfo* getActiveWindow();Returns pointer to WindowInfo containing the gathered information about the current window. The pointer can be NULL in the case of an error or if there is no active window (ex: all the windows are minified or the desktop is selected). You should free up the allocated WindowInfo object using delete.
๐ watchActiveWindow
watch_t watchActiveWindow(watch_callback cb);Sets up a watch for the active window. If there's a change in the current active window, or the title of the active window, the callback will be fired with the current active window. You don't need to call getActiveWindow() in the callback, you can use the supplied parameter.
This method will also start a background watch thread if it's not already running. Please note that the callbacks will be executed on this thread, so you should assure thread safety.
You MUST NOT free up the WindowInfo object received in the parameter. If you need to store the active window you SHOULD make a copy of it, since the WindowInfo object will be freed after calling all the callbacks.
You should save the returned watch ID to unsubscribe later.
๐ unwatchActiveWindow
void unwatchActiveWindow(watch_t watch);Removes the watch associated with the supplied watch ID.
The background watch thread will not be closed, even if there're no more watches left. It will only be closed when the class's destructor is called.
Example
See module/windows/demo/main.cpp for an example.
#include
#include "ActiveWindow.h"
using namespace std;
using namespace PaymoActiveWindow;
int main() {
ActiveWindow* activeWindow = new ActiveWindow(10);
WindowInfo* windowInfo = activeWindow->getActiveWindow();
if (windowInfo == NULL) {
cout<<"Could not get active window\n";
}
else {
wcout<<L"Title: "<<windowInfo->title<<"\n";
wcout<<L"Application: "<<windowInfo->application<<"\n";
wcout<<L"Executable path: "<<windowInfo->path<<"\n";
cout<<"PID: "<<windowInfo->pid<<"\n";
cout<<"Is UWP application: "<<(windowInfo->isUWPApp ? "Yes" : "No")<<"\n";
if (windowInfo->isUWPApp) {
wcout<<"UWP package name: "<<windowInfo->uwpPackage<<"\n";
}
cout<<"Icon: "<<windowInfo->icon<<"\n";
}
delete windowInfo;
delete activeWindow;
return 0;
}Building
See module/windows/demo/Makefile for a sample makefile. You need to check the lib target. This is not a GNU makefile, it should be used with Microsoft's NMAKE.
To prepare your environment, you need run the vcvarsall.bat batch script. If you checked the install windows build tools box during the installation of NodeJS, you should find this file in the following location: C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat. So to gain access to the nmake, cl and link commands, execute this:
"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64You should use C++17 for building and you need to link the following libraries:
- User32.lib
- Shell32.lib
- Version.lib
- Shlwapi.lib
- Gdi32.lib
- Gdiplus.lib
- Windowsapp.lib
MacOS (`module/macos`)
Data structures
๐ WindowInfo
struct WindowInfo {
std::string title; // UTF8 encoded string of window title. Empty string if couldn't fetch
std::string application; // UTF8 encoded string of application name
std::string path; // UTF8 encoded string of application path
int pid; // PID of process
std::string icon; // base64 encoded PNG icon
}๐ watch_t
typedef unsigned int watch_t;๐ watch_callback
typedef std::function<void(WindowInfo*)> watch_callback;Public functions
๐ Constructor
ActiveWindow(unsigned int iconCacheSize = 0);If you pass iconCacheSize > 0, then an LRU (least recently used) cache will be instantiated which will cache the fetched icons. This results in about 40% less CPU seconds consumed. You can use the benchmark mode of the demo to test it yourself.
You should pass a cache size suitable for your application. A bigger cache results in a greater memory consumption, but it also reduces the CPU utilization if your user switches across a large set of applications.
๐ getActiveWindow
WindowInfo* getActiveWindow();Returns pointer to WindowInfo containing the gathered information about the current window. The pointer can be NULL in the case of an error or if there is no active window (ex: all the windows are minified). You should free up the allocated WindowInfo object using delete.
๐ requestScreenCaptureAccess
bool requestScreenCaptureAccess();To access the title of the window the process requires the screen capture permission. To check it and request it you need to call this function. This function is non-blocking and will immediately return with the current status (false - permission denied, true - permission granted).
The first time this function is called there will be a system popup instructing the user how he can grant this permission, but you should also include this information in your application, since it's a one-time popup that closes when you click outside of it.
The application needs to be relaunched after granting the permission.
๐ watchActiveWindow
watch_t watchActiveWindow(watch_callback cb);Sets up a watch for the active window. If there's a change in the current active window, the callback will be fired with the current active window. You don't need to call getActiveWindow() in the callback, you can use the supplied parameter.
This method will use the observer set up on the main thread to listen to the events, so the main thread has to have a running NSRunLoop. If you integrate this library into a desktop application, then this should already be resolved. Otherwise (for example when using a console application), you have to manually run the RunLoop or call the runLoop() helper function which will block for 0.1ms.
If you want to use the watch function, you MUST use this library on the main thread, since the NSWorkspace notifications are only serviced on that thread.
The callbacks are also executed on the main thread, so you shouldn't do anything blocking in them.
You should save the returned watch ID to unsubscribe later.
๐ unwatchActiveWindow
void unwatchActiveWindow(watch_t watch);Removes the watch associated with the supplied watch ID.
๐ runLoop
void runLoop();A helper function which will run the thread's RunLoop for 0.1ms or until the first event is handled. This function should be called on the main thread.
You should only use this function only if you don't have a running RunLoop on your main thread.
Example
See module/macos/demo/main.mm for an example.
#include
#include "ActiveWindow.h"
using namespace std;
using namespace PaymoActiveWindow;
int main() {
ActiveWindow* activeWindow = new ActiveWindow();
WindowInfo* windowInfo = activeWindow->getActiveWindow();
if (windowInfo == NULL) {
cout<<"Could not get active window\n";
}
else {
cout<<"Title: "<<windowInfo->title<<"\n";
cout<<"Application: "<<windowInfo->application<<"\n";
cout<<"Executable path: "<<windowInfo->path<<"\n";
cout<<"PID: "<<windowInfo->pid<<"\n";
cout<<"Icon: "<<windowInfo->icon<<"\n";
}
delete windowInfo;
delete activeWindow;
return 0;
}Building
See module/macos/demo/Makefile for a sample makefile. You need to check the lib target.
You should use C++17 for building and you need to link the following libraries:
-lc++-framework Foundation-framework AppKit-framework ApplicationServices