├── .npmrc ├── .gitattributes ├── .gitignore ├── .editorconfig ├── Package.swift ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── Sources ├── GetWindowsCLI │ ├── Utilities.swift │ └── main.swift └── windows │ └── main.cc ├── lib ├── windows.js ├── macos.js └── linux.js ├── test.js ├── binding.gyp ├── license ├── index.test-d.ts ├── index.js ├── package.json ├── index.d.ts └── readme.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | /.build 4 | /*.xcodeproj 5 | /main 6 | /build 7 | lib/binding 8 | /build-tmp-napi-v9 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "GetWindows", 6 | products: [ 7 | .executable( 8 | name: "get-windows", 9 | targets: [ 10 | "GetWindowsCLI" 11 | ] 12 | ) 13 | ], 14 | targets: [ 15 | .executableTarget( 16 | name: "GetWindowsCLI" 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'releases/**' 6 | pull_request: 7 | branches-ignore: 8 | - 'releases/**' 9 | jobs: 10 | test: 11 | name: Node.js ${{ matrix.node-version }} 12 | runs-on: macos-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node-version: 17 | - 18 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install --ignore-scripts 24 | - run: npm run build:macos 25 | - run: npm run test-ci 26 | -------------------------------------------------------------------------------- /Sources/GetWindowsCLI/Utilities.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | 4 | @discardableResult 5 | func runAppleScript(source: String) -> String? { 6 | NSAppleScript(source: source)?.executeAndReturnError(nil).stringValue 7 | } 8 | 9 | 10 | func toJson(_ data: T) throws -> String { 11 | let json = try JSONSerialization.data(withJSONObject: data) 12 | return String(data: json, encoding: .utf8)! 13 | } 14 | 15 | 16 | // Show the system prompt if there's no permission. 17 | func hasScreenRecordingPermission() -> Bool { 18 | CGDisplayStream( 19 | dispatchQueueDisplay: CGMainDisplayID(), 20 | outputWidth: 1, 21 | outputHeight: 1, 22 | pixelFormat: Int32(kCVPixelFormatType_32BGRA), 23 | properties: nil, 24 | queue: DispatchQueue.global(), 25 | handler: { _, _, _, _ in } 26 | ) != nil 27 | } 28 | -------------------------------------------------------------------------------- /lib/windows.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import {fileURLToPath} from 'node:url'; 4 | import {createRequire} from 'node:module'; 5 | import preGyp from '@mapbox/node-pre-gyp'; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | const getAddon = () => { 10 | const require = createRequire(import.meta.url); 11 | 12 | const bindingPath = preGyp.find(path.resolve(path.join(__dirname, '../package.json'))); 13 | 14 | return (fs.existsSync(bindingPath)) ? require(bindingPath) : { 15 | getActiveWindow() {}, 16 | getOpenWindows() {}, 17 | }; 18 | }; 19 | 20 | export async function activeWindow() { 21 | return getAddon().getActiveWindow(); 22 | } 23 | 24 | export function activeWindowSync() { 25 | return getAddon().getActiveWindow(); 26 | } 27 | 28 | export function openWindows() { 29 | return getAddon().getOpenWindows(); 30 | } 31 | 32 | export function openWindowsSync() { 33 | return getAddon().getOpenWindows(); 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import {inspect} from 'node:util'; 2 | import test from 'ava'; 3 | import { 4 | activeWindow, 5 | activeWindowSync, 6 | openWindows, 7 | openWindowsSync, 8 | } from './index.js'; 9 | 10 | function asserter(t, result) { 11 | t.log(inspect(result)); 12 | t.is(typeof result, 'object'); 13 | t.is(typeof result.title, 'string'); 14 | t.is(typeof result.id, 'number'); 15 | t.is(typeof result.owner, 'object'); 16 | t.is(typeof result.owner.name, 'string'); 17 | } 18 | 19 | function asserterOpenWindows(t, result) { 20 | t.log(inspect(result)); 21 | t.is(typeof result, 'object'); 22 | t.is(typeof result.length, 'number'); 23 | asserter(t, result[0]); 24 | } 25 | 26 | test('activeWindow', async t => { 27 | asserter(t, await activeWindow()); 28 | }); 29 | 30 | test('activeWindowSync', t => { 31 | asserter(t, activeWindowSync()); 32 | }); 33 | 34 | test('openWindows', async t => { 35 | asserterOpenWindows(t, await openWindows()); 36 | }); 37 | 38 | test('openWindowsSync', t => { 39 | asserterOpenWindows(t, openWindowsSync()); 40 | }); 41 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "<(module_name)", 5 | "cflags!": [ 6 | "-fno-exceptions" 7 | ], 8 | "cflags_cc!": [ 9 | "-fno-exceptions" 10 | ], 11 | "conditions":[ 12 | [ 13 | "OS=='win'", 14 | { 15 | "sources": [ 16 | "sources/windows/main.cc", 17 | ], 18 | 'libraries': [ 19 | 'version.lib', 20 | 'Dwmapi.lib', 21 | ], 22 | }, 23 | ], 24 | ], 25 | "include_dirs": [ 26 | " (https://sindresorhus.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType, expectError} from 'tsd'; 2 | import { 3 | activeWindow, 4 | activeWindowSync, 5 | openWindows, 6 | openWindowsSync, 7 | type Result, 8 | type LinuxResult, 9 | type MacOSResult, 10 | type WindowsResult, 11 | BaseOwner, 12 | } from './index.js'; 13 | 14 | expectType>(activeWindow()); 15 | 16 | const result = activeWindowSync({ 17 | screenRecordingPermission: false, 18 | accessibilityPermission: false, 19 | }); 20 | 21 | expectType(result); 22 | 23 | if (result) { 24 | expectType<'macos' | 'linux' | 'windows'>(result.platform); 25 | expectType(result.title); 26 | expectType(result.id); 27 | expectType(result.bounds.x); 28 | expectType(result.bounds.y); 29 | expectType(result.bounds.width); 30 | expectType(result.bounds.height); 31 | expectType(result.owner.name); 32 | expectType(result.owner.processId); 33 | expectType(result.owner.path); 34 | expectType(result.memoryUsage); 35 | 36 | if (result.platform === 'macos') { 37 | expectType(result); 38 | expectType(result.owner.bundleId); 39 | expectType(result.url); 40 | } else if (result.platform === 'linux') { 41 | expectType(result); 42 | } else { 43 | expectType(result); 44 | expectType(result.contentBounds.x); 45 | expectType(result.contentBounds.y); 46 | expectType(result.contentBounds.width); 47 | expectType(result.contentBounds.height); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/macos.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import {promisify} from 'node:util'; 3 | import childProcess from 'node:child_process'; 4 | import {fileURLToPath} from 'node:url'; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | const execFile = promisify(childProcess.execFile); 9 | const binary = path.join(__dirname, '../main'); 10 | 11 | const parseMac = stdout => { 12 | try { 13 | return JSON.parse(stdout); 14 | } catch (error) { 15 | console.error(error); 16 | throw new Error('Error parsing window data'); 17 | } 18 | }; 19 | 20 | const getArguments = options => { 21 | if (!options) { 22 | return []; 23 | } 24 | 25 | const arguments_ = []; 26 | if (options.accessibilityPermission === false) { 27 | arguments_.push('--no-accessibility-permission'); 28 | } 29 | 30 | if (options.screenRecordingPermission === false) { 31 | arguments_.push('--no-screen-recording-permission'); 32 | } 33 | 34 | return arguments_; 35 | }; 36 | 37 | export async function activeWindow(options) { 38 | const {stdout} = await execFile(binary, getArguments(options)); 39 | return parseMac(stdout); 40 | } 41 | 42 | export function activeWindowSync(options) { 43 | const stdout = childProcess.execFileSync(binary, getArguments(options), {encoding: 'utf8'}); 44 | return parseMac(stdout); 45 | } 46 | 47 | export async function openWindows(options) { 48 | const {stdout} = await execFile(binary, [...getArguments(options), '--open-windows-list']); 49 | return parseMac(stdout); 50 | } 51 | 52 | export function openWindowsSync(options) { 53 | const stdout = childProcess.execFileSync(binary, [...getArguments(options), '--open-windows-list'], {encoding: 'utf8'}); 54 | return parseMac(stdout); 55 | } 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { 3 | activeWindowSync as activeWindowSyncMacOS, 4 | openWindowsSync as openWindowsSyncMacOS, 5 | } from './lib/macos.js'; 6 | import { 7 | activeWindowSync as activeWindowSyncLinux, 8 | openWindowsSync as openWindowsSyncLinux, 9 | } from './lib/linux.js'; 10 | import { 11 | activeWindowSync as activeWindowSyncWindows, 12 | openWindowsSync as openWindowsSyncWindows, 13 | } from './lib/windows.js'; 14 | 15 | export async function activeWindow(options) { 16 | if (process.platform === 'darwin') { 17 | const {activeWindow} = await import('./lib/macos.js'); 18 | return activeWindow(options); 19 | } 20 | 21 | if (process.platform === 'linux') { 22 | const {activeWindow} = await import('./lib/linux.js'); 23 | return activeWindow(options); 24 | } 25 | 26 | if (process.platform === 'win32') { 27 | const {activeWindow} = await import('./lib/windows.js'); 28 | return activeWindow(options); 29 | } 30 | 31 | throw new Error('macOS, Linux, and Windows only'); 32 | } 33 | 34 | export function activeWindowSync(options) { 35 | if (process.platform === 'darwin') { 36 | return activeWindowSyncMacOS(options); 37 | } 38 | 39 | if (process.platform === 'linux') { 40 | return activeWindowSyncLinux(options); 41 | } 42 | 43 | if (process.platform === 'win32') { 44 | return activeWindowSyncWindows(options); 45 | } 46 | 47 | throw new Error('macOS, Linux, and Windows only'); 48 | } 49 | 50 | export async function openWindows(options) { 51 | if (process.platform === 'darwin') { 52 | const {openWindows} = await import('./lib/macos.js'); 53 | return openWindows(options); 54 | } 55 | 56 | if (process.platform === 'linux') { 57 | const {openWindows} = await import('./lib/linux.js'); 58 | return openWindows(options); 59 | } 60 | 61 | if (process.platform === 'win32') { 62 | const {openWindows} = await import('./lib/windows.js'); 63 | return openWindows(options); 64 | } 65 | 66 | throw new Error('macOS, Linux, and Windows only'); 67 | } 68 | 69 | export function openWindowsSync(options) { 70 | if (process.platform === 'darwin') { 71 | return openWindowsSyncMacOS(options); 72 | } 73 | 74 | if (process.platform === 'linux') { 75 | return openWindowsSyncLinux(options); 76 | } 77 | 78 | if (process.platform === 'win32') { 79 | return openWindowsSyncWindows(options); 80 | } 81 | 82 | throw new Error('macOS, Linux, and Windows only'); 83 | } 84 | 85 | // Note to self: The `main` field in package.json is requried for pre-gyp. 86 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build & publish binary node 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | concurrency: 8 | group: ${{ github.head_ref || github.run_id }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: 18 | - windows-latest 19 | host: 20 | - x64 21 | target: 22 | - x64 23 | node: 24 | - 18 25 | include: 26 | - os: windows-latest 27 | node: 18 28 | host: x86 29 | target: x86 30 | 31 | name: ${{ matrix.os }} (node=${{ matrix.node }}, host=${{ matrix.host }}, target=${{ matrix.target }}) 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Add msbuild to PATH 36 | uses: microsoft/setup-msbuild@v1.1 37 | with: 38 | msbuild-architecture: ${{ matrix.target }} 39 | 40 | - name: Setup node (node=${{ matrix.node }}, host=${{ matrix.host }}) 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: ${{ matrix.node }} 44 | architecture: ${{ matrix.host }} 45 | - name: Install dependencies 46 | run: npm install --ignore-scripts 47 | 48 | - name: Add env vars 49 | shell: bash 50 | run: | 51 | echo "V=1" >> $GITHUB_ENV 52 | if [ "${{ matrix.target }}" = "x86" ]; then 53 | echo "TARGET=ia32" >> $GITHUB_ENV 54 | else 55 | echo "TARGET=${{ matrix.target }}" >> $GITHUB_ENV 56 | fi 57 | - name: Configure build 58 | run: ./node_modules/.bin/node-pre-gyp configure --target_arch=${{ env.TARGET }} 59 | 60 | - name: Build binaries 61 | run: ./node_modules/.bin/node-pre-gyp build --target_arch=${{ env.TARGET }} 62 | 63 | - name: Package prebuilt binaries 64 | run: ./node_modules/.bin/node-pre-gyp package --target_arch=${{ env.TARGET }} 65 | 66 | # Run only for matrix.node 18 67 | - name: Upload binaries to commit artifacts 68 | uses: actions/upload-artifact@v3 69 | if: matrix.node == 18 70 | with: 71 | name: prebuilt-binaries 72 | path: build/stage/*/* 73 | retention-days: 7 74 | 75 | # Run only for matrix.node 18 76 | - name: Upload binaries to GitHub Release 77 | run: ./node_modules/.bin/node-pre-gyp-github publish 78 | if: matrix.node == 18 && startsWith(github.ref, 'refs/tags/') 79 | env: 80 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ github.token }} 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-windows", 3 | "version": "9.2.0", 4 | "description": "Get metadata about the active window and open windows (title, id, bounds, owner, URL, etc)", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/sindresorhus/get-windows.git" 9 | }, 10 | "funding": "https://github.com/sponsors/sindresorhus", 11 | "author": { 12 | "name": "Sindre Sorhus", 13 | "email": "sindresorhus@gmail.com", 14 | "url": "https://sindresorhus.com" 15 | }, 16 | "type": "module", 17 | "exports": { 18 | "types": "./index.d.ts", 19 | "default": "./index.js" 20 | }, 21 | "main": "./index.js", 22 | "sideEffects": false, 23 | "engines": { 24 | "node": ">=18.18" 25 | }, 26 | "binary": { 27 | "module_name": "node-get-windows", 28 | "module_path": "./lib/binding/napi-{napi_build_version}-{platform}-{libc}-{arch}", 29 | "host": "https://github.com/sindresorhus/get-windows/releases/download/", 30 | "remote_path": "v{version}", 31 | "package_name": "napi-{napi_build_version}-{platform}-{libc}-{arch}.tar.gz", 32 | "napi_versions": [ 33 | 9 34 | ] 35 | }, 36 | "scripts": { 37 | "test": "xo && npm run build:macos && ava && tsd", 38 | "test-ci": "xo && tsd", 39 | "build:windows:install": "node-pre-gyp install --fallback-to-build", 40 | "build:windows": "node-pre-gyp build", 41 | "build:windows:debug": "node-pre-gyp build --debug", 42 | "build:macos": "swift build --configuration=release --arch arm64 --arch x86_64 && mv .build/apple/Products/Release/get-windows main", 43 | "install": "node-pre-gyp install --fallback-to-build" 44 | }, 45 | "files": [ 46 | "index.js", 47 | "index.d.ts", 48 | "lib", 49 | "main", 50 | "Sources/windows/main.cc", 51 | "binding.gyp" 52 | ], 53 | "keywords": [ 54 | "macos", 55 | "linux", 56 | "windows", 57 | "app", 58 | "application", 59 | "window", 60 | "windows", 61 | "active", 62 | "focused", 63 | "current", 64 | "open", 65 | "title", 66 | "name", 67 | "id", 68 | "pid", 69 | "screenshot", 70 | "capture", 71 | "metadata", 72 | "bounds", 73 | "memory", 74 | "usage", 75 | "bundleid", 76 | "browser", 77 | "url", 78 | "chrome", 79 | "safari", 80 | "edge", 81 | "brave" 82 | ], 83 | "devDependencies": { 84 | "ava": "^6.1.3", 85 | "tsd": "^0.31.1", 86 | "xo": "^0.59.3", 87 | "node-pre-gyp-github": "^2.0.0" 88 | }, 89 | "optionalDependencies": { 90 | "@mapbox/node-pre-gyp": "^1.0.11", 91 | "node-addon-api": "^8.1.0", 92 | "node-gyp": "^10.2.0" 93 | }, 94 | "peerDependencies": { 95 | "node-gyp": "^10.1.0" 96 | }, 97 | "peerDependenciesMeta": { 98 | "node-gyp": { 99 | "optional": true 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | Enable the accessibility permission check. _(macOS)_ 4 | 5 | Setting this to `false` will prevent the accessibility permission prompt on macOS versions 10.15 and newer. The `url` property won't be retrieved. 6 | 7 | @default true 8 | */ 9 | readonly accessibilityPermission: boolean; 10 | 11 | /** 12 | Enable the screen recording permission check. _(macOS)_ 13 | 14 | Setting this to `false` will prevent the screen recording permission prompt on macOS versions 10.15 and newer. The `title` property in the result will always be set to an empty string. 15 | 16 | @default true 17 | */ 18 | readonly screenRecordingPermission: boolean; 19 | }; 20 | 21 | export type BaseOwner = { 22 | /** 23 | Name of the app. 24 | */ 25 | name: string; 26 | 27 | /** 28 | Process identifier 29 | */ 30 | processId: number; 31 | 32 | /** 33 | Path to the app. 34 | */ 35 | path: string; 36 | }; 37 | 38 | export type BaseResult = { 39 | /** 40 | Window title. 41 | */ 42 | title: string; 43 | 44 | /** 45 | Window identifier. 46 | 47 | On Windows, there isn't a clear notion of a "Window ID". Instead it returns the memory address of the window "handle" in the `id` property. That "handle" is unique per window, so it can be used to identify them. [Read more…](https://msdn.microsoft.com/en-us/library/windows/desktop/ms632597(v=vs.85).aspx#window_handle). 48 | */ 49 | id: number; 50 | 51 | /** 52 | Window position and size. 53 | */ 54 | bounds: { 55 | x: number; 56 | y: number; 57 | width: number; 58 | height: number; 59 | }; 60 | 61 | /** 62 | App that owns the window. 63 | */ 64 | owner: BaseOwner; 65 | 66 | /** 67 | Memory usage by the window. 68 | */ 69 | memoryUsage: number; 70 | }; 71 | 72 | // eslint-disable-next-line @typescript-eslint/naming-convention 73 | export type MacOSOwner = { 74 | /** 75 | Bundle identifier. 76 | */ 77 | bundleId: string; 78 | } & BaseOwner; 79 | 80 | // eslint-disable-next-line @typescript-eslint/naming-convention 81 | export type MacOSResult = { 82 | platform: 'macos'; 83 | 84 | owner: MacOSOwner; 85 | 86 | /** 87 | URL of the active browser tab if the active window is Safari (includes Technology Preview), Chrome (includes Beta, Dev, and Canary), Edge (includes Beta, Dev, and Canary), Brave (includes Beta and Nightly), Mighty, Ghost Browser, WaveBox, Sidekick, Opera (includes Beta and Developer), or Vivaldi. 88 | */ 89 | url?: string; 90 | } & BaseResult; 91 | 92 | export type LinuxResult = { 93 | platform: 'linux'; 94 | } & BaseResult; 95 | 96 | export type WindowsResult = { 97 | platform: 'windows'; 98 | 99 | /** 100 | Window content position and size, which excludes the title bar, menu bar, and frame. 101 | */ 102 | contentBounds: { 103 | x: number; 104 | y: number; 105 | width: number; 106 | height: number; 107 | }; 108 | } & BaseResult; 109 | 110 | export type Result = MacOSResult | LinuxResult | WindowsResult; 111 | 112 | /** 113 | Get metadata about the [active window](https://en.wikipedia.org/wiki/Active_window) (title, id, bounds, owner, etc). 114 | 115 | @example 116 | ``` 117 | import {activeWindow} from 'get-windows'; 118 | 119 | const result = await activeWindow(); 120 | 121 | if (!result) { 122 | return; 123 | } 124 | 125 | if (result.platform === 'macos') { 126 | // Among other fields, `result.owner.bundleId` is available on macOS. 127 | console.log(`Process title is ${result.title} with bundle id ${result.owner.bundleId}.`); 128 | } else if (result.platform === 'windows') { 129 | console.log(`Process title is ${result.title} with path ${result.owner.path}.`); 130 | } else { 131 | console.log(`Process title is ${result.title} with path ${result.owner.path}.`); 132 | } 133 | ``` 134 | */ 135 | export function activeWindow(options?: Options): Promise; 136 | 137 | /** 138 | Get metadata about the [active window](https://en.wikipedia.org/wiki/Active_window) synchronously (title, id, bounds, owner, etc). 139 | 140 | @example 141 | ``` 142 | import {activeWindowSync} from 'get-windows'; 143 | 144 | const result = activeWindowSync(); 145 | 146 | if (result) { 147 | if (result.platform === 'macos') { 148 | // Among other fields, `result.owner.bundleId` is available on macOS. 149 | console.log(`Process title is ${result.title} with bundle id ${result.owner.bundleId}.`); 150 | } else if (result.platform === 'windows') { 151 | console.log(`Process title is ${result.title} with path ${result.owner.path}.`); 152 | } else { 153 | console.log(`Process title is ${result.title} with path ${result.owner.path}.`); 154 | } 155 | } 156 | ``` 157 | */ 158 | export function activeWindowSync(options?: Options): Result | undefined; 159 | 160 | /** 161 | Get metadata about all open windows. 162 | 163 | Windows are returned in order from front to back. 164 | */ 165 | export function openWindows(options?: Options): Promise; 166 | 167 | /** 168 | Get metadata about all open windows synchronously. 169 | 170 | Windows are returned in order from front to back. 171 | */ 172 | export function openWindowsSync(options?: Options): Result[]; 173 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # get-windows 2 | 3 | > Get metadata about the [active window](https://en.wikipedia.org/wiki/Active_window) and open windows (title, id, bounds, owner, URL, etc) 4 | 5 | Works on macOS 10.14+, Linux ([note](#linux-support)), and Windows 7+. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install get-windows 11 | ``` 12 | 13 | **[This is an ESM package which requires you to use ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)** 14 | 15 | ## Usage 16 | 17 | ```js 18 | import {activeWindow} from 'get-windows'; 19 | 20 | console.log(await activeWindow(options)); 21 | /* 22 | { 23 | title: 'Unicorns - Google Search', 24 | id: 5762, 25 | bounds: { 26 | x: 0, 27 | y: 0, 28 | height: 900, 29 | width: 1440 30 | }, 31 | owner: { 32 | name: 'Google Chrome', 33 | processId: 310, 34 | bundleId: 'com.google.Chrome', 35 | path: '/Applications/Google Chrome.app' 36 | }, 37 | url: 'https://sindresorhus.com/unicorn', 38 | memoryUsage: 11015432 39 | } 40 | */ 41 | ``` 42 | 43 | ## API 44 | 45 | ### activeWindow(options?) 46 | 47 | Get metadata about the active window. 48 | 49 | #### options 50 | 51 | Type: `object` 52 | 53 | ##### accessibilityPermission **(macOS only)** 54 | 55 | Type: `boolean`\ 56 | Default: `true` 57 | 58 | Enable the accessibility permission check. Setting this to `false` will prevent the accessibility permission prompt on macOS versions 10.15 and newer. The `url` property won't be retrieved. 59 | 60 | ##### screenRecordingPermission **(macOS only)** 61 | 62 | Type: `boolean`\ 63 | Default: `true` 64 | 65 | Enable the screen recording permission check. Setting this to `false` will prevent the screen recording permission prompt on macOS versions 10.15 and newer. The `title` property in the result will always be set to an empty string. 66 | 67 | ### activeWindowSync(options?) 68 | 69 | Get metadata about the active window synchronously. 70 | 71 | ## Result 72 | 73 | Returns a `Promise` with the result, or `Promise` if there is no active window or if the information is not available. 74 | 75 | - `platform` *(string)* - `'macos'` | `'linux'` | `'windows'` 76 | - `title` *(string)* - Window title 77 | - `id` *(number)* - Window identifier 78 | - `bounds` *(Object)* - Window position and size 79 | - `x` *(number)* 80 | - `y` *(number)* 81 | - `width` *(number)* 82 | - `height` *(number)* 83 | - `contentBounds` *(Object)* - Window content position and size, which excludes the title bar, menu bar, and frame *(Windows only)* 84 | - `x` *(number)* 85 | - `y` *(number)* 86 | - `width` *(number)* 87 | - `height` *(number)* 88 | - `owner` *(Object)* - App that owns the window 89 | - `name` *(string)* - Name of the app 90 | - `processId` *(number)* - Process identifier 91 | - `bundleId` *(string)* - Bundle identifier *(macOS only)* 92 | - `path` *(string)* - Path to the app 93 | - `url` *(string?)* - URL of the active browser tab if the active window *(macOS only)* 94 | - Supported browsers: Safari (includes Technology Preview), Chrome (includes Beta, Dev, and Canary), Edge (includes Beta, Dev, and Canary), Brave (includes Beta and Nightly), Mighty, Ghost Browser, Wavebox, Sidekick, Opera (includes Beta and Developer), or Vivaldi 95 | - `memoryUsage` *(number)* - Memory usage by the window owner process 96 | 97 | ### openWindows() 98 | 99 | Get metadata about all open windows. 100 | 101 | Windows are returned in order from front to back. 102 | 103 | Returns `Promise`. 104 | 105 | ### openWindowsSync() 106 | 107 | Get metadata about all open windows synchronously. 108 | 109 | Windows are returned in order from front to back. 110 | 111 | Returns `Result[]`. 112 | 113 | ## OS support 114 | 115 | It works on macOS 10.14+, Linux, and Windows 7+. 116 | 117 | **Note**: On Windows, there isn't a clear notion of a "Window ID". Instead it returns the memory address of the window "handle" in the `id` property. That "handle" is unique per window, so it can be used to identify them. [Read more…](https://msdn.microsoft.com/en-us/library/windows/desktop/ms632597(v=vs.85).aspx#window_handle) 118 | 119 | ### Linux support 120 | 121 | Wayland is not supported. For security reasons, Wayland does not provide a way to identify the active window. [Read more…](https://stackoverflow.com/questions/45465016) 122 | 123 | ## Electron usage 124 | 125 | If you use this package in an Electron app that is sandboxed and you want to get the `.url` property, you need to add the [proper entitlements and usage description](https://github.com/sindresorhus/get-windows/issues/99#issuecomment-870874546). 126 | 127 | ## Users 128 | 129 | - [active-win-log](https://github.com/uglow/active-win-log) - Window-usage logging CLI. 130 | - [active-app-qmk-layer-updater](https://github.com/zigotica/active-app-qmk-layer-updater) - Sends the active app info to a QMK device to change keymap layers automatically. 131 | 132 | ## Related 133 | 134 | - [windows-cli](https://github.com/sindresorhus/windows-cli) - CLI for this package 135 | 136 | ## Development 137 | 138 | To bypass the `gyp` build: 139 | 140 | ```sh 141 | npm install --ignore-scripts 142 | ``` 143 | -------------------------------------------------------------------------------- /Sources/GetWindowsCLI/main.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | func getActiveBrowserTabURLAppleScriptCommand(_ appId: String) -> String? { 4 | switch appId { 5 | case "com.google.Chrome", "com.google.Chrome.beta", "com.google.Chrome.dev", "com.google.Chrome.canary", "com.brave.Browser", "com.brave.Browser.beta", "com.brave.Browser.nightly", "com.microsoft.edgemac", "com.microsoft.edgemac.Beta", "com.microsoft.edgemac.Dev", "com.microsoft.edgemac.Canary", "com.mighty.app", "com.ghostbrowser.gb1", "com.bookry.wavebox", "com.pushplaylabs.sidekick", "com.operasoftware.Opera", "com.operasoftware.OperaNext", "com.operasoftware.OperaDeveloper", "com.operasoftware.OperaGX", "com.vivaldi.Vivaldi", "company.thebrowser.Browser": 6 | return "tell app id \"\(appId)\" to get the URL of active tab of front window" 7 | case "com.apple.Safari", "com.apple.SafariTechnologyPreview": 8 | return "tell app id \"\(appId)\" to get URL of front document" 9 | default: 10 | return nil 11 | } 12 | } 13 | 14 | func exitWithoutResult() -> Never { 15 | print("null") 16 | exit(0) 17 | } 18 | 19 | func printOutput(_ output: Any) -> Never { 20 | guard let string = try? toJson(output) else { 21 | exitWithoutResult() 22 | } 23 | 24 | print(string) 25 | exit(0) 26 | } 27 | 28 | func getWindowInformation(window: [String: Any], windowOwnerPID: pid_t) -> [String: Any]? { 29 | // Skip transparent windows, like with Chrome. 30 | if (window[kCGWindowAlpha as String] as! Double) == 0 { // Documented to always exist. 31 | return nil 32 | } 33 | 34 | let bounds = CGRect(dictionaryRepresentation: window[kCGWindowBounds as String] as! CFDictionary)! // Documented to always exist. 35 | 36 | // Skip tiny windows, like the Chrome link hover statusbar. 37 | let minWinSize: CGFloat = 50 38 | if bounds.width < minWinSize || bounds.height < minWinSize { 39 | return nil 40 | } 41 | 42 | // This should not fail as we're only dealing with apps, but we guard it just to be safe. 43 | guard let app = NSRunningApplication(processIdentifier: windowOwnerPID) else { 44 | return nil 45 | } 46 | 47 | let appName = window[kCGWindowOwnerName as String] as? String ?? app.bundleIdentifier ?? "" 48 | 49 | let windowTitle = disableScreenRecordingPermission ? "" : window[kCGWindowName as String] as? String ?? "" 50 | 51 | if app.bundleIdentifier == "com.apple.dock" { 52 | return nil 53 | } 54 | 55 | var output: [String: Any] = [ 56 | "platform": "macos", 57 | "title": windowTitle, 58 | "id": window[kCGWindowNumber as String] as! Int, // Documented to always exist. 59 | "bounds": [ 60 | "x": bounds.origin.x, 61 | "y": bounds.origin.y, 62 | "width": bounds.width, 63 | "height": bounds.height 64 | ], 65 | "owner": [ 66 | "name": appName, 67 | "processId": windowOwnerPID, 68 | "bundleId": app.bundleIdentifier ?? "", // I don't think this could happen, but we also don't want to crash. 69 | "path": app.bundleURL?.path ?? "" // I don't think this could happen, but we also don't want to crash. 70 | ], 71 | "memoryUsage": window[kCGWindowMemoryUsage as String] as? Int ?? 0 72 | ] 73 | 74 | // Run the AppleScript to get the URL if the active window is a compatible browser and accessibility permissions are enabled. 75 | if 76 | !disableAccessibilityPermission, 77 | let bundleIdentifier = app.bundleIdentifier, 78 | let script = getActiveBrowserTabURLAppleScriptCommand(bundleIdentifier), 79 | let url = runAppleScript(source: script) 80 | { 81 | output["url"] = url 82 | } 83 | 84 | return output 85 | } 86 | 87 | let disableAccessibilityPermission = CommandLine.arguments.contains("--no-accessibility-permission") 88 | let disableScreenRecordingPermission = CommandLine.arguments.contains("--no-screen-recording-permission") 89 | let enableOpenWindowsList = CommandLine.arguments.contains("--open-windows-list") 90 | 91 | // Show accessibility permission prompt if needed. Required to get the URL of the active tab in browsers. 92 | if !disableAccessibilityPermission { 93 | if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) { 94 | print("get-windows requires the accessibility permission in “System Settings › Privacy & Security › Accessibility”.") 95 | exit(1) 96 | } 97 | } 98 | 99 | // Show screen recording permission prompt if needed. Required to get the complete window title. 100 | if 101 | !disableScreenRecordingPermission, 102 | !hasScreenRecordingPermission() 103 | { 104 | print("get-windows requires the screen recording permission in “System Settings › Privacy & Security › Screen Recording”.") 105 | exit(1) 106 | } 107 | 108 | guard 109 | let frontmostAppPID = NSWorkspace.shared.frontmostApplication?.processIdentifier, 110 | let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] 111 | else { 112 | exitWithoutResult() 113 | } 114 | 115 | var openWindows = [[String: Any]](); 116 | 117 | for window in windows { 118 | let windowOwnerPID = window[kCGWindowOwnerPID as String] as! pid_t // Documented to always exist. 119 | if !enableOpenWindowsList && windowOwnerPID != frontmostAppPID { 120 | continue 121 | } 122 | 123 | guard let windowInformation = getWindowInformation(window: window, windowOwnerPID: windowOwnerPID) else { 124 | continue 125 | } 126 | 127 | if !enableOpenWindowsList { 128 | printOutput(windowInformation) 129 | } else { 130 | openWindows.append(windowInformation) 131 | } 132 | } 133 | 134 | if !openWindows.isEmpty { 135 | printOutput(openWindows) 136 | } 137 | 138 | exitWithoutResult() 139 | -------------------------------------------------------------------------------- /lib/linux.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {promisify} from 'node:util'; 3 | import fs from 'node:fs'; 4 | import childProcess from 'node:child_process'; 5 | 6 | const execFile = promisify(childProcess.execFile); 7 | const readFile = promisify(fs.readFile); 8 | const readlink = promisify(fs.readlink); 9 | 10 | const xpropBinary = 'xprop'; 11 | const xwininfoBinary = 'xwininfo'; 12 | const xpropActiveArguments = ['-root', '\t$0', '_NET_ACTIVE_WINDOW']; 13 | const xpropOpenArguments = ['-root', '_NET_CLIENT_LIST_STACKING']; 14 | const xpropDetailsArguments = ['-id']; 15 | 16 | const processOutput = output => { 17 | const result = {}; 18 | 19 | for (const row of output.trim().split('\n')) { 20 | if (row.includes('=')) { 21 | const [key, value] = row.split('='); 22 | result[key.trim()] = value.trim(); 23 | } else if (row.includes(':')) { 24 | const [key, value] = row.split(':'); 25 | result[key.trim()] = value.trim(); 26 | } 27 | } 28 | 29 | return result; 30 | }; 31 | 32 | const parseLinux = ({stdout, boundsStdout, activeWindowId}) => { 33 | const result = processOutput(stdout); 34 | const bounds = processOutput(boundsStdout); 35 | 36 | const windowIdProperty = 'WM_CLIENT_LEADER(WINDOW)'; 37 | const resultKeys = Object.keys(result); 38 | const windowId = (resultKeys.indexOf(windowIdProperty) > 0 39 | && Number.parseInt(result[windowIdProperty].split('#').pop(), 16)) || activeWindowId; 40 | 41 | const processId = Number.parseInt(result['_NET_WM_PID(CARDINAL)'], 10); 42 | 43 | if (Number.isNaN(processId)) { 44 | throw new Error('Failed to parse process ID'); // eslint-disable-line unicorn/prefer-type-error 45 | } 46 | 47 | return { 48 | platform: 'linux', 49 | title: JSON.parse(result['_NET_WM_NAME(UTF8_STRING)'] || result['WM_NAME(STRING)']) || null, 50 | id: windowId, 51 | owner: { 52 | name: JSON.parse(result['WM_CLASS(STRING)'].split(',').pop()), 53 | processId, 54 | }, 55 | bounds: { 56 | x: Number.parseInt(bounds['Absolute upper-left X'], 10), 57 | y: Number.parseInt(bounds['Absolute upper-left Y'], 10), 58 | width: Number.parseInt(bounds.Width, 10), 59 | height: Number.parseInt(bounds.Height, 10), 60 | }, 61 | }; 62 | }; 63 | 64 | const getActiveWindowId = activeWindowIdStdout => Number.parseInt(activeWindowIdStdout.split('\t')[1], 16); 65 | 66 | const getMemoryUsageByPid = async pid => { 67 | const statm = await readFile(`/proc/${pid}/statm`, 'utf8'); 68 | return Number.parseInt(statm.split(' ')[1], 10) * 4096; 69 | }; 70 | 71 | const getMemoryUsageByPidSync = pid => { 72 | const statm = fs.readFileSync(`/proc/${pid}/statm`, 'utf8'); 73 | return Number.parseInt(statm.split(' ')[1], 10) * 4096; 74 | }; 75 | 76 | const getPathByPid = pid => readlink(`/proc/${pid}/exe`); 77 | 78 | const getPathByPidSync = pid => { 79 | try { 80 | return fs.readlinkSync(`/proc/${pid}/exe`); 81 | } catch {} 82 | }; 83 | 84 | async function getWindowInformation(windowId) { 85 | const [{stdout}, {stdout: boundsStdout}] = await Promise.all([ 86 | execFile(xpropBinary, [...xpropDetailsArguments, windowId], {env: {...process.env, LC_ALL: 'C.utf8'}}), 87 | execFile(xwininfoBinary, [...xpropDetailsArguments, windowId]), 88 | ]); 89 | 90 | const data = parseLinux({ 91 | activeWindowId: windowId, 92 | boundsStdout, 93 | stdout, 94 | }); 95 | const [memoryUsage, path] = await Promise.all([ 96 | getMemoryUsageByPid(data.owner.processId), 97 | getPathByPid(data.owner.processId).catch(() => {}), 98 | ]); 99 | data.memoryUsage = memoryUsage; 100 | data.owner.path = path; 101 | return data; 102 | } 103 | 104 | function getWindowInformationSync(windowId) { 105 | const stdout = childProcess.execFileSync(xpropBinary, [...xpropDetailsArguments, windowId], {encoding: 'utf8', env: {...process.env, LC_ALL: 'C.utf8'}}); 106 | const boundsStdout = childProcess.execFileSync(xwininfoBinary, [...xpropDetailsArguments, windowId], {encoding: 'utf8'}); 107 | 108 | const data = parseLinux({ 109 | activeWindowId: windowId, 110 | boundsStdout, 111 | stdout, 112 | }); 113 | data.memoryUsage = getMemoryUsageByPidSync(data.owner.processId); 114 | data.owner.path = getPathByPidSync(data.owner.processId); 115 | return data; 116 | } 117 | 118 | export async function activeWindow() { 119 | try { 120 | const {stdout: activeWindowIdStdout} = await execFile(xpropBinary, xpropActiveArguments); 121 | const activeWindowId = getActiveWindowId(activeWindowIdStdout); 122 | 123 | if (!activeWindowId) { 124 | return; 125 | } 126 | 127 | return getWindowInformation(activeWindowId); 128 | } catch { 129 | return undefined; 130 | } 131 | } 132 | 133 | export function activeWindowSync() { 134 | try { 135 | const activeWindowIdStdout = childProcess.execFileSync(xpropBinary, xpropActiveArguments, {encoding: 'utf8'}); 136 | const activeWindowId = getActiveWindowId(activeWindowIdStdout); 137 | 138 | if (!activeWindowId) { 139 | return; 140 | } 141 | 142 | return getWindowInformationSync(activeWindowId); 143 | } catch { 144 | return undefined; 145 | } 146 | } 147 | 148 | export async function openWindows() { 149 | try { 150 | const {stdout: openWindowIdStdout} = await execFile(xpropBinary, xpropOpenArguments); 151 | 152 | // Get open windows Ids 153 | const windowsIds = openWindowIdStdout.split('#')[1].trim().replace('\n', '').split(','); 154 | 155 | if (!windowsIds || windowsIds.length === 0) { 156 | return; 157 | } 158 | 159 | const openWindows = []; 160 | 161 | for await (const windowId of windowsIds) { 162 | openWindows.push(await getWindowInformation(Number.parseInt(windowId, 16))); 163 | } 164 | 165 | return openWindows; 166 | } catch { 167 | return undefined; 168 | } 169 | } 170 | 171 | export function openWindowsSync() { 172 | try { 173 | const openWindowIdStdout = childProcess.execFileSync(xpropBinary, xpropOpenArguments, {encoding: 'utf8'}); 174 | const windowsIds = openWindowIdStdout.split('#')[1].trim().replace('\n', '').split(','); 175 | 176 | if (!windowsIds || windowsIds.length === 0) { 177 | return; 178 | } 179 | 180 | const openWindows = []; 181 | 182 | for (const windowId of windowsIds) { 183 | const windowInformation = getWindowInformationSync(Number.parseInt(windowId, 16)); 184 | openWindows.push(windowInformation); 185 | } 186 | 187 | return openWindows; 188 | } catch (error) { 189 | console.log(error); 190 | return undefined; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Sources/windows/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | typedef int(__stdcall *lp_GetScaleFactorForMonitor)(HMONITOR, DEVICE_SCALE_FACTOR *); 14 | 15 | struct OwnerWindowInfo { 16 | std::string path; 17 | std::string name; 18 | }; 19 | 20 | template 21 | T getValueFromCallbackData(const Napi::CallbackInfo &info, unsigned handleIndex) { 22 | return reinterpret_cast(info[handleIndex].As().Int64Value()); 23 | } 24 | 25 | // Get wstring from string 26 | std::wstring get_wstring(const std::string str) { 27 | return std::wstring(str.begin(), str.end()); 28 | } 29 | 30 | std::string getFileName(const std::string &value) { 31 | char separator = '/'; 32 | #ifdef _WIN32 33 | separator = '\\'; 34 | #endif 35 | size_t index = value.rfind(separator, value.length()); 36 | 37 | if (index != std::string::npos) { 38 | return (value.substr(index + 1, value.length() - index)); 39 | } 40 | 41 | return (""); 42 | } 43 | 44 | // Convert wstring into utf8 string 45 | std::string toUtf8(const std::wstring &str) { 46 | std::string ret; 47 | int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), str.length(), NULL, 0, NULL, NULL); 48 | if (len > 0) { 49 | ret.resize(len); 50 | WideCharToMultiByte(CP_UTF8, 0, str.c_str(), str.length(), &ret[0], len, NULL, NULL); 51 | } 52 | 53 | return ret; 54 | } 55 | 56 | // Return window title in utf8 string 57 | std::string getWindowTitle(const HWND hwnd) { 58 | int bufsize = GetWindowTextLengthW(hwnd) + 1; 59 | LPWSTR t = new WCHAR[bufsize]; 60 | GetWindowTextW(hwnd, t, bufsize); 61 | 62 | std::wstring ws(t); 63 | std::string title = toUtf8(ws); 64 | 65 | return title; 66 | } 67 | 68 | // Return description from file version info 69 | std::string getDescriptionFromFileVersionInfo(const BYTE *pBlock) { 70 | UINT bufLen = 0; 71 | struct LANGANDCODEPAGE { 72 | WORD wLanguage; 73 | WORD wCodePage; 74 | } * lpTranslate; 75 | 76 | LANGANDCODEPAGE codePage{0x040904E4}; 77 | // Get language struct 78 | if (VerQueryValueW((LPVOID *)pBlock, (LPCWSTR)L"\\VarFileInfo\\Translation", (LPVOID *)&lpTranslate, &bufLen)) { 79 | codePage = lpTranslate[0]; 80 | } 81 | 82 | wchar_t fileDescriptionKey[256]; 83 | wsprintfW(fileDescriptionKey, L"\\StringFileInfo\\%04x%04x\\FileDescription", codePage.wLanguage, codePage.wCodePage); 84 | wchar_t *fileDescription = NULL; 85 | UINT fileDescriptionSize; 86 | // Get description file 87 | if (VerQueryValueW((LPVOID *)pBlock, fileDescriptionKey, (LPVOID *)&fileDescription, &fileDescriptionSize)) { 88 | return toUtf8(fileDescription); 89 | } 90 | 91 | return ""; 92 | } 93 | 94 | // Return process path and name 95 | OwnerWindowInfo getProcessPathAndName(const HANDLE &phlde) { 96 | DWORD dwSize{MAX_PATH}; 97 | wchar_t exeName[MAX_PATH]{}; 98 | QueryFullProcessImageNameW(phlde, 0, exeName, &dwSize); 99 | std::string path = toUtf8(exeName); 100 | std::string name = getFileName(path); 101 | 102 | DWORD dwHandle = 0; 103 | wchar_t *wspath(exeName); 104 | DWORD infoSize = GetFileVersionInfoSizeW(wspath, &dwHandle); 105 | 106 | if (infoSize != 0) { 107 | BYTE *pVersionInfo = new BYTE[infoSize]; 108 | std::unique_ptr skey_automatic_cleanup(pVersionInfo); 109 | if (GetFileVersionInfoW(wspath, NULL, infoSize, pVersionInfo) != 0) { 110 | std::string nname = getDescriptionFromFileVersionInfo(pVersionInfo); 111 | if (nname != "") { 112 | name = nname; 113 | } 114 | } 115 | } 116 | 117 | return {path, name}; 118 | } 119 | 120 | OwnerWindowInfo newOwner; 121 | 122 | BOOL CALLBACK EnumChildWindowsProc(HWND hwnd, LPARAM lParam) { 123 | // Get process ID 124 | DWORD processId{0}; 125 | GetWindowThreadProcessId(hwnd, &processId); 126 | OwnerWindowInfo *ownerInfo = (OwnerWindowInfo *)lParam; 127 | // Get process Handler 128 | HANDLE phlde{OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, processId)}; 129 | 130 | if (phlde != NULL) { 131 | newOwner = getProcessPathAndName(phlde); 132 | CloseHandle(hwnd); 133 | if (ownerInfo->path != newOwner.path) { 134 | return FALSE; 135 | } 136 | } 137 | 138 | return TRUE; 139 | } 140 | 141 | // Return window information 142 | Napi::Value getWindowInformation(const HWND &hwnd, const Napi::CallbackInfo &info) { 143 | Napi::Env env{info.Env()}; 144 | 145 | if (hwnd == NULL) { 146 | return env.Null(); 147 | } 148 | 149 | // Get process ID 150 | DWORD processId{0}; 151 | GetWindowThreadProcessId(hwnd, &processId); 152 | // Get process Handler 153 | HANDLE phlde{OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, processId)}; 154 | 155 | if (phlde == NULL) { 156 | return env.Null(); 157 | } 158 | 159 | OwnerWindowInfo ownerInfo = getProcessPathAndName(phlde); 160 | 161 | // ApplicationFrameHost & Universal Windows Platform Support 162 | if (getFileName(ownerInfo.path) == "ApplicationFrameHost.exe") { 163 | newOwner = (OwnerWindowInfo) * new OwnerWindowInfo(); 164 | BOOL result = EnumChildWindows(hwnd, (WNDENUMPROC)EnumChildWindowsProc, (LPARAM)&ownerInfo); 165 | if (result == FALSE && newOwner.name.size()) 166 | { 167 | ownerInfo = newOwner; 168 | } 169 | } 170 | 171 | if (ownerInfo.name == "Widgets.exe") { 172 | return env.Null(); 173 | } 174 | 175 | PROCESS_MEMORY_COUNTERS memoryCounter; 176 | BOOL memoryResult = GetProcessMemoryInfo(phlde, &memoryCounter, sizeof(memoryCounter)); 177 | 178 | CloseHandle(phlde); 179 | 180 | if (memoryResult == 0) { 181 | return env.Null(); 182 | } 183 | 184 | RECT lpWinRect; 185 | BOOL rectWinResult = GetWindowRect(hwnd, &lpWinRect); 186 | 187 | RECT lpClientRect; 188 | BOOL rectClientResult = GetClientRect(hwnd, &lpClientRect); 189 | 190 | if (rectWinResult == 0 || rectClientResult == 0 ) { 191 | return env.Null(); 192 | } 193 | 194 | Napi::Object owner = Napi::Object::New(env); 195 | 196 | owner.Set(Napi::String::New(env, "processId"), processId); 197 | owner.Set(Napi::String::New(env, "path"), ownerInfo.path); 198 | owner.Set(Napi::String::New(env, "name"), ownerInfo.name); 199 | 200 | // bounds window 201 | Napi::Object bounds = Napi::Object::New(env); 202 | 203 | bounds.Set(Napi::String::New(env, "x"), lpWinRect.left); 204 | bounds.Set(Napi::String::New(env, "y"), lpWinRect.top); 205 | bounds.Set(Napi::String::New(env, "width"), lpWinRect.right - lpWinRect.left); 206 | bounds.Set(Napi::String::New(env, "height"), lpWinRect.bottom - lpWinRect.top); 207 | 208 | // bounds content 209 | POINT rectTopLeft = {lpClientRect.left, lpClientRect.top}; 210 | ClientToScreen(hwnd, &rectTopLeft); 211 | POINT rectBottomRight = {lpClientRect.right, lpClientRect.bottom}; 212 | ClientToScreen(hwnd, &rectBottomRight); 213 | 214 | Napi::Object contentBounds = Napi::Object::New(env); 215 | 216 | contentBounds.Set(Napi::String::New(env, "x"), rectTopLeft.x); 217 | contentBounds.Set(Napi::String::New(env, "y"), rectTopLeft.y); 218 | contentBounds.Set(Napi::String::New(env, "width"), rectBottomRight.x - rectTopLeft.x); 219 | contentBounds.Set(Napi::String::New(env, "height"), rectBottomRight.y - rectTopLeft.y); 220 | 221 | Napi::Object activeWinObj = Napi::Object::New(env); 222 | 223 | activeWinObj.Set(Napi::String::New(env, "platform"), Napi::String::New(env, "windows")); 224 | activeWinObj.Set(Napi::String::New(env, "id"), (LONG)hwnd); 225 | activeWinObj.Set(Napi::String::New(env, "title"), getWindowTitle(hwnd)); 226 | activeWinObj.Set(Napi::String::New(env, "owner"), owner); 227 | activeWinObj.Set(Napi::String::New(env, "bounds"), bounds); 228 | activeWinObj.Set(Napi::String::New(env, "contentBounds"), contentBounds); 229 | activeWinObj.Set(Napi::String::New(env, "memoryUsage"), memoryCounter.WorkingSetSize); 230 | 231 | return activeWinObj; 232 | } 233 | 234 | // List of HWND used for EnumDesktopWindows callback 235 | std::vector _windows; 236 | 237 | // EnumDesktopWindows callback 238 | BOOL CALLBACK EnumDekstopWindowsProc(HWND hwnd, LPARAM lParam) { 239 | if (IsWindow(hwnd) && IsWindowEnabled(hwnd) && IsWindowVisible(hwnd)) { 240 | WINDOWINFO winInfo{}; 241 | GetWindowInfo(hwnd, &winInfo); 242 | 243 | if ( 244 | (winInfo.dwExStyle & WS_EX_TOOLWINDOW) == 0 245 | && (winInfo.dwStyle & WS_CAPTION) == WS_CAPTION 246 | && (winInfo.dwStyle & WS_CHILD) == 0 247 | ) { 248 | int ClockedVal; 249 | DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, (PVOID)&ClockedVal, sizeof(ClockedVal)); 250 | if (ClockedVal == 0) { 251 | _windows.push_back(hwnd); 252 | } 253 | } 254 | } 255 | 256 | return TRUE; 257 | }; 258 | 259 | // Method to get active window 260 | Napi::Value getActiveWindow(const Napi::CallbackInfo &info) { 261 | HWND hwnd = GetForegroundWindow(); 262 | Napi::Value value = getWindowInformation(hwnd, info); 263 | return value; 264 | } 265 | 266 | // Method to get an array of open windows 267 | Napi::Array getOpenWindows(const Napi::CallbackInfo &info) { 268 | Napi::Env env{info.Env()}; 269 | 270 | Napi::Array values = Napi::Array::New(env); 271 | 272 | _windows.clear(); 273 | 274 | if (EnumDesktopWindows(NULL, (WNDENUMPROC)EnumDekstopWindowsProc, NULL)) { 275 | uint32_t i = 0; 276 | for (HWND _win : _windows) { 277 | Napi::Value value = getWindowInformation(_win, info); 278 | if (value != env.Null()) { 279 | values.Set(i++, value); 280 | } 281 | } 282 | } 283 | 284 | return values; 285 | } 286 | 287 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 288 | exports.Set(Napi::String::New(env, "getActiveWindow"), Napi::Function::New(env, getActiveWindow)); 289 | exports.Set(Napi::String::New(env, "getOpenWindows"), Napi::Function::New(env, getOpenWindows)); 290 | return exports; 291 | } 292 | 293 | NODE_API_MODULE(addon, Init) 294 | --------------------------------------------------------------------------------