├── .eslintrc.json ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── binding.gyp ├── demo ├── demo.html └── index.ts ├── module ├── linux │ ├── demo │ │ ├── Makefile │ │ └── main.cpp │ ├── napi │ │ ├── main.cpp │ │ ├── module.cpp │ │ └── module.h │ └── src │ │ ├── ActiveWindow.cpp │ │ ├── ActiveWindow.h │ │ ├── IconCache.cpp │ │ └── IconCache.h ├── macos │ ├── demo │ │ ├── Makefile │ │ └── main.mm │ ├── napi │ │ ├── main.mm │ │ ├── module.h │ │ └── module.mm │ └── src │ │ ├── ActiveWindow.h │ │ ├── ActiveWindow.mm │ │ ├── IconCache.cpp │ │ └── IconCache.h └── windows │ ├── demo │ ├── Makefile │ └── main.cpp │ ├── napi │ ├── main.cpp │ ├── module.cpp │ └── module.h │ └── src │ ├── ActiveWindow.cpp │ ├── ActiveWindow.h │ ├── GdiPlusUtils.cpp │ ├── GdiPlusUtils.h │ ├── IconCache.cpp │ └── IconCache.h ├── package-lock.json ├── package.json ├── src ├── index.ts └── types.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript" 13 | ], 14 | "plugins": ["@typescript-eslint", "prettier", "import"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "project": "./tsconfig.json", 18 | "tsconfigRootDir": "." 19 | }, 20 | "rules": { 21 | "no-console": 1, 22 | "prettier/prettier": 2, 23 | "import/no-dynamic-require": 2, 24 | "import/no-useless-path-segments": 1, 25 | "import/order": [1, { "newlines-between": "always" }], 26 | "import/no-named-as-default": 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules/ 3 | 4 | # eslint cache 5 | .eslintcache 6 | 7 | # build artifacts 8 | dist/ 9 | **/build/ 10 | prebuilds/ 11 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | ADDON_BUILD_IMAGE: "gitlab.paymoapp.com:5555/envscripts/img-ubuntu-widget-build" 3 | GIT_SUBMODULE_STRATEGY: recursive 4 | GIT_DEPTH: 50 5 | XCODE_TOOLS: "/Applications/Xcode14_3_1.app" 6 | 7 | # Mixins 8 | .only_any_v_tags: 9 | only: 10 | - /^v[0-9]+\.[0-9]+\.[0-9]+/ 11 | except: 12 | - branches 13 | 14 | .only_release_v_tags: 15 | only: 16 | - /^v[0-9]+\.[0-9]+\.[0-9]+$/ 17 | except: 18 | - branches 19 | 20 | .job_test: 21 | stage: test 22 | rules: 23 | - if: $CI_MERGE_REQUEST_ID 24 | - if: $CI_COMMIT_TAG 25 | before_script: 26 | - npm ci --ignore-scripts 27 | 28 | .job_build_addon: 29 | stage: build-addon 30 | before_script: 31 | - npm ci --ignore-scripts 32 | script: 33 | - npm run prebuild -- --upload $PREBUILD_GH_TOKEN 34 | needs: 35 | - lint 36 | - typecheck 37 | 38 | stages: 39 | - test 40 | - build-addon 41 | - publish 42 | 43 | # Test stage 44 | lint: 45 | extends: 46 | - .job_test 47 | script: 48 | - npm run lint 49 | 50 | typecheck: 51 | extends: 52 | - .job_test 53 | script: 54 | - npm run typecheck 55 | 56 | # Build-addon stage 57 | build-addon-windows: 58 | extends: 59 | - .only_any_v_tags 60 | - .job_build_addon 61 | tags: 62 | - win 63 | 64 | build-addon-mac-x64: 65 | extends: 66 | - .only_any_v_tags 67 | - .job_build_addon 68 | tags: 69 | - osx 70 | hooks: 71 | pre_get_sources_script: 72 | - sudo xcode-select -s $XCODE_TOOLS 73 | script: 74 | - npm run prebuild -- --arch x64 --upload $PREBUILD_GH_TOKEN 75 | 76 | build-addon-mac-arm64: 77 | extends: 78 | - .only_any_v_tags 79 | - .job_build_addon 80 | tags: 81 | - osx 82 | hooks: 83 | pre_get_sources_script: 84 | - sudo xcode-select -s $XCODE_TOOLS 85 | script: 86 | - npm run prebuild -- --arch arm64 --upload $PREBUILD_GH_TOKEN 87 | 88 | build-addon-linux: 89 | extends: 90 | - .only_any_v_tags 91 | - .job_build_addon 92 | image: $ADDON_BUILD_IMAGE 93 | tags: 94 | - docker 95 | 96 | # Publish stage 97 | build-library: 98 | extends: 99 | - .only_any_v_tags 100 | stage: publish 101 | before_script: 102 | - npm ci --ignore-scripts 103 | script: 104 | - npm run build:ts 105 | - npm set //registry.npmjs.org/:_authToken $NPM_TOKEN 106 | - npm publish --access public 107 | - LAST_RELEASE="$(git describe --abbrev=0 --tags ${CI_COMMIT_TAG}^)" 108 | - CHANGELOG="$(git diff -U0 $LAST_RELEASE $CI_COMMIT_TAG CHANGELOG.md | grep -E "^\+" | grep -vE "^\+\+\+" | grep -vE "^\+#+ \[" | sed "s/^\+//")" 109 | - > 110 | curl -sSL -X POST -H "JOB-TOKEN: $CI_JOB_TOKEN" -H "Content-type: application/json" -d "{ \"name\": $(echo $CI_COMMIT_TAG | jq -Ra .), \"tag_name\": $(echo $CI_COMMIT_TAG | jq -Ra .), \"description\": $(echo "$CHANGELOG" | jq -Rsa .) }" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases 111 | artifacts: 112 | paths: 113 | - dist 114 | expire_in: 12h 115 | needs: 116 | - lint 117 | - typecheck 118 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "module/linux/lib/base64"] 2 | path = module/linux/src/base64 3 | url = https://github.com/ReneNyffenegger/cpp-base64.git 4 | branch = master 5 | [submodule "module/linux/lib/SimpleIni"] 6 | path = module/linux/src/SimpleIni 7 | url = https://github.com/brofield/simpleini.git 8 | branch = master 9 | [submodule "module/windows/lib/base64"] 10 | path = module/windows/src/base64 11 | url = https://github.com/ReneNyffenegger/cpp-base64.git 12 | branch = master 13 | [submodule "module/macos/lib/base64"] 14 | path = module/macos/src/base64 15 | url = https://github.com/ReneNyffenegger/cpp-base64.git 16 | branch = master 17 | [submodule "module/linux/lib/lodepng"] 18 | path = module/linux/src/lodepng 19 | url = https://github.com/lvandeve/lodepng.git 20 | branch = master 21 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "useTabs": true, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.1.2](https://github.com/paymoapp/node-active-window/compare/v2.1.1...v2.1.2) (2024-12-16) 6 | 7 | 8 | ### Documentation 9 | 10 | * Update badge to point to correct license ([58230a0](https://github.com/paymoapp/node-active-window/commit/58230a0e71065716680b9c2b95fbee185783f91b)) 11 | 12 | ### [2.1.1](https://github.com/paymoapp/node-active-window/compare/v2.1.0...v2.1.1) (2024-05-23) 13 | 14 | 15 | ### Build/CI 16 | 17 | * Removed msvs_version from build script ([bf0ebaa](https://github.com/paymoapp/node-active-window/commit/bf0ebaa0f22f4d1df631f4135d9a459d0de61ce5)) 18 | 19 | ## [2.1.0](https://github.com/paymoapp/node-active-window/compare/v2.0.6...v2.1.0) (2024-05-23) 20 | 21 | 22 | ### Features 23 | 24 | * Replaced license with MIT ([fc2c752](https://github.com/paymoapp/node-active-window/commit/fc2c7527feb6b3f8c547d0caf2c56fbb5641c0fc)) 25 | 26 | ### [2.0.6](https://github.com/paymoapp/node-active-window/compare/v2.0.5...v2.0.6) (2023-10-20) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **module.linux:** Fixed sigsegv when _NET_WM_PID is not set ([04246f0](https://github.com/paymoapp/node-active-window/commit/04246f0710fff6ed8dfa903e0d0f1a206e1bbcc7)) 32 | 33 | 34 | ### Build/CI 35 | 36 | * **gitlab:** Set global location for msvs_version config parameter ([562ee4a](https://github.com/paymoapp/node-active-window/commit/562ee4ad0654ced8b67592740ded269a460438e5)) 37 | 38 | ### [2.0.5](https://github.com/paymoapp/node-active-window/compare/v2.0.4...v2.0.5) (2023-10-13) 39 | 40 | 41 | ### Build/CI 42 | 43 | * **gitlab:** Fixed the XCODE_TOOLS path ([adc8bc5](https://github.com/paymoapp/node-active-window/commit/adc8bc5d9beccfeb7e053c3a5affb25aa5963e73)) 44 | 45 | ### [2.0.4](https://github.com/paymoapp/node-active-window/compare/v2.0.3...v2.0.4) (2023-10-13) 46 | 47 | 48 | ### Build/CI 49 | 50 | * **gitlab:** Changed XCODE_TOOLS ([3b37397](https://github.com/paymoapp/node-active-window/commit/3b37397dd756053250d0bd54b2771ccab9932679)) 51 | 52 | ### [2.0.3](https://github.com/paymoapp/node-active-window/compare/v2.0.2...v2.0.3) (2023-10-13) 53 | 54 | 55 | ### Build/CI 56 | 57 | * **gitlab:** Set required xcode command line tools ([76a8e54](https://github.com/paymoapp/node-active-window/commit/76a8e54ae308b574ef5fafc36dc10ea3ee7d0e44)) 58 | 59 | ### [2.0.2](https://github.com/paymoapp/node-active-window/compare/v2.0.1...v2.0.2) (2023-10-12) 60 | 61 | 62 | ### Improvements 63 | 64 | * **module.linux:** Replace TSFN with TypedTSFN ([acbd6e6](https://github.com/paymoapp/node-active-window/commit/acbd6e626a3233b5761ce6b5fb07d6c90f8e79c7)) 65 | * **module.macos:** Replace TSFN with TypedTSFN ([e2bac3d](https://github.com/paymoapp/node-active-window/commit/e2bac3d77862e849dd371923dcaa8fac483b2800)) 66 | * **module.window:** Replace TSFN with TypedTSFN ([6766906](https://github.com/paymoapp/node-active-window/commit/67669066a717fef72ae88d0c6b042acac373d4f4)) 67 | 68 | ### [2.0.1](https://github.com/paymoapp/node-active-window/compare/v2.0.0...v2.0.1) (2023-10-11) 69 | 70 | 71 | ### Improvements 72 | 73 | * **module.linux:** Changed the way methods are exported from the addon ([6730ca0](https://github.com/paymoapp/node-active-window/commit/6730ca0156ddc51b4a705cde3721a64a163e17c6)) 74 | * **module.macos:** Changed the way methods are exported from the addon ([73dce35](https://github.com/paymoapp/node-active-window/commit/73dce356e1d72b27e4b038688e19bc5979ebc14f)) 75 | * **module.window:** Changed the way methods are exported from the addon ([7737045](https://github.com/paymoapp/node-active-window/commit/7737045b03d1028fe59b0115fe8c0bebeb8b4f59)) 76 | 77 | ## [2.0.0](https://github.com/paymoapp/node-active-window/compare/v1.2.6...v2.0.0) (2023-10-04) 78 | 79 | 80 | ### ⚠ BREAKING CHANGES 81 | 82 | * **module.macos:** Changed the API for the initialize method 83 | 84 | ### Bug Fixes 85 | 86 | * **module.macos:** Modified runLoop logic that caused the entire process to hang on newer electron versions ([bd3f3d3](https://github.com/paymoapp/node-active-window/commit/bd3f3d3ac35feac92b56410643404a3845e6e32f)) 87 | 88 | 89 | ### Documentation 90 | 91 | * Added documentation for API changes and added usage guide ([4808c10](https://github.com/paymoapp/node-active-window/commit/4808c103c33483b7e3ca4db56d3e0d4bd9fd032c)) 92 | 93 | ### [1.2.6](https://github.com/paymoapp/node-active-window/compare/v1.2.5...v1.2.6) (2023-09-15) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * **module.linux:** Fixed segmentation fault when executable path was empty ([4bd7d9e](https://github.com/paymoapp/node-active-window/commit/4bd7d9e402d3fc8769bd13714d459fa79eaff70e)) 99 | 100 | ### [1.2.5](https://github.com/paymoapp/node-active-window/compare/v1.2.4...v1.2.5) (2023-09-11) 101 | 102 | 103 | ### Build/CI 104 | 105 | * Fixed gitlab config to use docker for building addon for linux ([2b43a8f](https://github.com/paymoapp/node-active-window/commit/2b43a8f1411de2d3dc771ecaff73afd7ad1cb138)) 106 | 107 | ### [1.2.4](https://github.com/paymoapp/node-active-window/compare/v1.2.3...v1.2.4) (2023-09-11) 108 | 109 | 110 | ### Bug Fixes 111 | 112 | * **module.macos:** Full screen applications did not have a title field ([29ae255](https://github.com/paymoapp/node-active-window/commit/29ae2550f19a0fae1e3303f0fcfc0e8169d9f22b)) 113 | 114 | ### [1.2.3](https://github.com/paymoapp/node-active-window/compare/v1.2.2...v1.2.3) (2023-05-30) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **module.linux.demo:** Removed ICU from linking options since it's no longer used ([e71063d](https://github.com/paymoapp/node-active-window/commit/e71063d8e16b7ca1ce1d601207cfad61d9d9def2)) 120 | * **module.linux:** Hardcoded icons are not necessarily PNG files, we should handle SVG and XPM as well. XPM is not treated, but SVG can be easily resolved ([b8904ef](https://github.com/paymoapp/node-active-window/commit/b8904efd71f5ad92fa1496c0dc428b42673a0253)) 121 | 122 | ### [1.2.2](https://github.com/paymoapp/node-active-window/compare/v1.2.1...v1.2.2) (2023-04-18) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * **module.macos:** MacOS Catalina doesn't have the screenCaptureAcces CG APIs implemented, so implemented it using a hack ([483d671](https://github.com/paymoapp/node-active-window/commit/483d67136e5bc4c9f6bef75e468ad7c56483bb7a)) 128 | 129 | ### [1.2.1](https://github.com/paymoapp/node-active-window/compare/v1.2.0...v1.2.1) (2023-02-08) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * **module.linux:** Handle the case where XDG_DATA_DIRS or HOME env var is not set ([42d1655](https://github.com/paymoapp/node-active-window/commit/42d165567ec84304ac495e7c798d52085dad2a53)) 135 | 136 | ## [1.2.0](https://github.com/paymoapp/node-active-window/compare/v1.1.1...v1.2.0) (2022-10-17) 137 | 138 | 139 | ### Features 140 | 141 | * Use a LRU cache with the size 15 to cache application icons ([851369a](https://github.com/paymoapp/node-active-window/commit/851369af93002c0310fa467d5bc22eb00f1595e3)) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * **module.windows:** Fixed crash that occured every once and a while ([7c00ace](https://github.com/paymoapp/node-active-window/commit/7c00ace09295526353a447bc292053023a8ad5ec)) 147 | 148 | 149 | ### Improvements 150 | 151 | * **module.linux:** Implemented icon cache using an LRU cache and transitioned to unordered_map ([44e74a7](https://github.com/paymoapp/node-active-window/commit/44e74a7c824cb6e156def76a1adbd5e64bf27a5d)) 152 | * **module.macos:** Implemented icon cache using an LRU cache ([dc41b34](https://github.com/paymoapp/node-active-window/commit/dc41b349c767e59f6818ff3e56b11f9b83f3f966)) 153 | * **module.windows:** Implemented icon cache using an LRU cache and transitioned to unordered_map ([8e0683e](https://github.com/paymoapp/node-active-window/commit/8e0683ec07a28a3a2235708cdc29de0c1a4525c9)) 154 | 155 | 156 | ### Documentation 157 | 158 | * Added documentation for icon cache and benchmark mode ([50f544d](https://github.com/paymoapp/node-active-window/commit/50f544da2c8f164870f254cab9d25caceef6b1e8)) 159 | 160 | ### [1.1.1](https://github.com/paymoapp/node-active-window/compare/v1.1.0...v1.1.1) (2022-10-13) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * **lib:** Disable artificial runloop on osX by default ([edb6a82](https://github.com/paymoapp/node-active-window/commit/edb6a821874781832f3a00f4faffa781f87e84c9)) 166 | 167 | ## [1.1.0](https://github.com/paymoapp/node-active-window/compare/v1.0.16...v1.1.0) (2022-10-11) 168 | 169 | 170 | ### Features 171 | 172 | * **module.linux:** Fallback to _NET_WM_ICON if can not find desktop icon ([fa12dd9](https://github.com/paymoapp/node-active-window/commit/fa12dd92657a0a91ba1723a4e5dec959c5fff570)) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * **module.windows:** Fixed crash when SHCreateMemStream couldn't allocate a stream for getting the icon ([0248451](https://github.com/paymoapp/node-active-window/commit/02484517a830056ff0484bf8c452016e7c7f832c)) 178 | 179 | 180 | ### Refactor 181 | 182 | * **module.linux:** Using git submodules for library dependencies ([c52b587](https://github.com/paymoapp/node-active-window/commit/c52b5875a2e75dfaa37f3adde9c1a48ba8e7515f)) 183 | * **module.macos:** Using git submodules for library dependencies ([e397ca9](https://github.com/paymoapp/node-active-window/commit/e397ca9e5a6b529df6182d86086e79d1ffbef2f3)) 184 | * **module.windows:** Using git submodules for library dependencies ([981b119](https://github.com/paymoapp/node-active-window/commit/981b1196da02c1d4f4ce0e097bcf1b82830cf052)) 185 | 186 | 187 | ### Build/CI 188 | 189 | * **gitlab:** Clone submodules when building ([033c177](https://github.com/paymoapp/node-active-window/commit/033c17702f81be8262918f0a596a8b3ab75f922b)) 190 | 191 | ### [1.0.16](https://github.com/paymoapp/node-active-window/compare/v1.0.15...v1.0.16) (2022-10-05) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * **module.windows:** Fixed possible null pointer references ([cd07d45](https://github.com/paymoapp/node-active-window/commit/cd07d45654b316eb853e8e7020694d4e09f7429f)) 197 | 198 | ### [1.0.15](https://github.com/paymoapp/node-active-window/compare/v1.0.14...v1.0.15) (2022-09-21) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * **module.macos:** Fixed memory leak ([80214c8](https://github.com/paymoapp/node-active-window/commit/80214c8e753e021fd06576673d262fcc65d67b25)) 204 | 205 | ### [1.0.14](https://github.com/paymoapp/node-active-window/compare/v1.0.13...v1.0.14) (2022-09-19) 206 | 207 | 208 | ### Bug Fixes 209 | 210 | * **module.linux:** Removed debug value ([e83e85e](https://github.com/paymoapp/node-active-window/commit/e83e85e3d5630b206fa3b7ee0619cef5c1086674)) 211 | 212 | ### [1.0.13](https://github.com/paymoapp/node-active-window/compare/v1.0.12...v1.0.13) (2022-09-15) 213 | 214 | 215 | ### Bug Fixes 216 | 217 | * **module.linux:** Added XErrorHandler to avoid crashes on XErrors ([21f0a6e](https://github.com/paymoapp/node-active-window/commit/21f0a6e270d4d0380c4139aea7b34bd6bc4e7fff)) 218 | 219 | ### [1.0.12](https://github.com/paymoapp/node-active-window/compare/v1.0.11...v1.0.12) (2022-09-06) 220 | 221 | 222 | ### Build/CI 223 | 224 | * Automatic release changelog generation on GitHub ([dc42c44](https://github.com/paymoapp/node-active-window/commit/dc42c44e50cda4f92084e8a306171f4bd8d0aa47)) 225 | 226 | ### [1.0.11](https://github.com/paymoapp/node-active-window/compare/v1.0.10...v1.0.11) (2022-09-06) 227 | 228 | 229 | ### Build/CI 230 | 231 | * Migrate to prebuild ([177e9af](https://github.com/paymoapp/node-active-window/commit/177e9af993422d345ade22efb606772ccfe6f427)) 232 | 233 | ### [1.0.10](https://github.com/paymoapp/node-active-window/compare/v1.0.9...v1.0.10) (2022-08-16) 234 | 235 | 236 | ### Build/CI 237 | 238 | * Fix mac build architecture ([9b69c97](https://github.com/paymoapp/node-active-window/commit/9b69c973136d4ead82f0e63d9c1f32dd763f04af)) 239 | 240 | ### [1.0.9](https://github.com/paymoapp/node-active-window/compare/v1.0.8...v1.0.9) (2022-07-18) 241 | 242 | 243 | ### Documentation 244 | 245 | * Fixed namespace in README ([efb4692](https://github.com/paymoapp/node-active-window/commit/efb4692ff5ce0ad0e1116a649c373cb7a586c33a)) 246 | 247 | ### [1.0.8](https://github.com/paymoapp/node-active-window/compare/v1.0.7...v1.0.8) (2022-06-29) 248 | 249 | ### [1.0.7](https://gitlab.paymoapp.com/paymo/node-active-window/compare/v1.0.6...v1.0.7) (2022-06-29) 250 | 251 | 252 | ### Build/CI 253 | 254 | * Publish package to NPM registry ([642261c](https://gitlab.paymoapp.com/paymo/node-active-window/commit/642261cdee3057569c1200267c00660808aa98c0)) 255 | 256 | ### [1.0.6](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v1.0.5...v1.0.6) (2022-06-29) 257 | 258 | 259 | ### Bug Fixes 260 | 261 | * Relative path of main in package.json ([dab02a5](https://gitlab.paymoapp.com/gergo/node-active-window/commit/dab02a575ba9ea5ed083c3e82c0c55ddc5d226ae)) 262 | 263 | ### [1.0.5](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v1.0.4...v1.0.5) (2022-06-29) 264 | 265 | 266 | ### Bug Fixes 267 | 268 | * Try to make NPM run the install script ([6cf26b7](https://gitlab.paymoapp.com/gergo/node-active-window/commit/6cf26b73328fa015a1eb899f6429f08f2dde05fa)) 269 | 270 | ### [1.0.4](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v1.0.3...v1.0.4) (2022-06-28) 271 | 272 | ### [1.0.3](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v1.0.2...v1.0.3) (2022-06-28) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * Include binding.gyp file and addon source code in the package ([963ddad](https://gitlab.paymoapp.com/gergo/node-active-window/commit/963ddad06c5a67f619e023fdd8cc8db287f466cd)) 278 | 279 | ### [1.0.2](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v1.0.1...v1.0.2) (2022-06-28) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * Use postinstall script to download addon when installed as a dependency ([03f2ee3](https://gitlab.paymoapp.com/gergo/node-active-window/commit/03f2ee3a4bdae60f23796737a852dfcb8eaa08a9)) 285 | 286 | ### [1.0.1](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v1.0.0...v1.0.1) (2022-06-28) 287 | 288 | 289 | ### Build/CI 290 | 291 | * **gitlab:** Fixed changelog escaping ([eba272b](https://gitlab.paymoapp.com/gergo/node-active-window/commit/eba272b5cef8d22a9606485d71e4ddd0bb97fbff)) 292 | 293 | ## [1.0.0](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-12...v1.0.0) (2022-06-28) 294 | 295 | 296 | ### Build/CI 297 | 298 | * **gitlab:** Remove tag name from release description ([0bbb098](https://gitlab.paymoapp.com/gergo/node-active-window/commit/0bbb098f27b18bf4c5ed1ccf8c86cbd4c7797b01)) 299 | 300 | ### [0.1.1-12](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-11...v0.1.1-12) (2022-06-28) 301 | 302 | ### [0.1.1-11](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-10...v0.1.1-11) (2022-06-28) 303 | 304 | 305 | ### Build/CI 306 | 307 | * **gitlab:** Create gitlab releases only with bash ([4170015](https://gitlab.paymoapp.com/gergo/node-active-window/commit/41700158033429dab44fcd273bfa681e08396adf)) 308 | 309 | ### [0.1.1-10](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-9...v0.1.1-10) (2022-06-28) 310 | 311 | 312 | ### Build/CI 313 | 314 | * **gitlab:** Create gitlab release after each tag ([9cb8bea](https://gitlab.paymoapp.com/gergo/node-active-window/commit/9cb8bea065bfc8fef723926a19ffc2ec402a8601)) 315 | 316 | ### [0.1.1-9](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-8...v0.1.1-9) (2022-06-28) 317 | 318 | ### [0.1.1-8](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-7...v0.1.1-8) (2022-06-28) 319 | 320 | 321 | ### Build/CI 322 | 323 | * **gitlab:** Publish to gitlab registry ([ee551bc](https://gitlab.paymoapp.com/gergo/node-active-window/commit/ee551bc33b5d18a29be7ae53443d5901edc15bda)) 324 | 325 | ### [0.1.1-7](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-6...v0.1.1-7) (2022-06-24) 326 | 327 | ### [0.1.1-6](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-5...v0.1.1-6) (2022-06-24) 328 | 329 | 330 | ### Build/CI 331 | 332 | * Using img-ubuntu-widget-build for building the linux addon ([e0857f8](https://gitlab.paymoapp.com/gergo/node-active-window/commit/e0857f8617217d646ee0fc614a63f4e8e7aee9f0)) 333 | 334 | ### [0.1.1-5](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-4...v0.1.1-5) (2022-06-23) 335 | 336 | ### [0.1.1-4](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-3...v0.1.1-4) (2022-06-23) 337 | 338 | 339 | ### Build/CI 340 | 341 | * **gitlab:** Use --target_arch at publish time ([d3d2ec2](https://gitlab.paymoapp.com/gergo/node-active-window/commit/d3d2ec2a71722b2ae5ad0291f2a2fb2af1105f05)) 342 | 343 | ### [0.1.1-3](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-2...v0.1.1-3) (2022-06-23) 344 | 345 | 346 | ### Build/CI 347 | 348 | * **gitlab:** M1 has arm64 arch not arm ([67475d7](https://gitlab.paymoapp.com/gergo/node-active-window/commit/67475d7ff7f2d48bd0aca18baca38d0e071dae54)) 349 | 350 | ### [0.1.1-2](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-1...v0.1.1-2) (2022-06-23) 351 | 352 | 353 | ### Build/CI 354 | 355 | * Fixing mac build ([00dc5ca](https://gitlab.paymoapp.com/gergo/node-active-window/commit/00dc5cac83ecafb89c2012836de699b27dfd3a2c)) 356 | 357 | ### [0.1.1-1](https://gitlab.paymoapp.com/gergo/node-active-window/compare/v0.1.1-0...v0.1.1-1) (2022-06-23) 358 | 359 | ### 0.1.1-0 (2022-06-22) 360 | 361 | 362 | ### Features 363 | 364 | * Added javascript-side demo ([7503dce](https://gitlab.paymoapp.com/gergo/node-active-window/commit/7503dce9ed655d8d0535c0a834fc161e874fdef4)) 365 | * Enabled prebuilt binding files using node-pre-gyp ([a8de768](https://gitlab.paymoapp.com/gergo/node-active-window/commit/a8de768868ef9e34a3a75f983c1f7ea9f92b5c43)) 366 | * **module.linux:** Added bindings for subscribe and unsubscribe, modified demo and javascript library to support subscriptions ([d26fbc9](https://gitlab.paymoapp.com/gergo/node-active-window/commit/d26fbc96a00d7c911ea644a4d5bb2eb922e18f71)) 367 | * **module.linux:** Added napi mappings for linux library, added initialize function ([9018519](https://gitlab.paymoapp.com/gergo/node-active-window/commit/901851901a32269cb8586886ccb39f138051a823)) 368 | * **module.linux:** Added watch mode with background thread and callbacks ([56f4276](https://gitlab.paymoapp.com/gergo/node-active-window/commit/56f42765db4a25b6fa80db7645668d8f4026c6ee)) 369 | * **module.linux:** Get active window and active window title using xlib ([2c76920](https://gitlab.paymoapp.com/gergo/node-active-window/commit/2c7692040ac59ca60504a8f75fd2db8b88aa290c)) 370 | * **module.linux:** Get application icon using app cache built during startup ([26bc9fe](https://gitlab.paymoapp.com/gergo/node-active-window/commit/26bc9feb4ed37ad7d888a6be0eb7adcae9a6aa40)) 371 | * **module.linux:** Get title, pid and application name - in this case, class - of active window ([3c86011](https://gitlab.paymoapp.com/gergo/node-active-window/commit/3c860119e2e246b038fd92764c769ebfc5d745d7)) 372 | * **module.linux:** Loading all application desktop entries and resolving the icon paths for them ([a60feff](https://gitlab.paymoapp.com/gergo/node-active-window/commit/a60feffec25ceb7a4789db5339cedfc7773258e3)) 373 | * **module.macos:** Added napi mappings for macos library ([82b18fe](https://gitlab.paymoapp.com/gergo/node-active-window/commit/82b18fe0114a4b01fafc0729f189489f674c61c6)) 374 | * **module.macos:** Added watch mode with hacky runloop ([1fcba28](https://gitlab.paymoapp.com/gergo/node-active-window/commit/1fcba2896f3022bda6f7a07536f6c7f8b5788d53)) 375 | * **module.macos:** Implemented bindings and added javascript code that continuously calls the NSRunLoop when it detects darwin ([9d49792](https://gitlab.paymoapp.com/gergo/node-active-window/commit/9d49792f0abbe86904f327b78dbdbb7cb6821881)) 376 | * **module.macos:** Implemented macos library to get active window info ([635043b](https://gitlab.paymoapp.com/gergo/node-active-window/commit/635043b1fdd48e4b501ab034ee6793cc10e22bbd)) 377 | * **module.windows:** Added bindings for subscription ([658c277](https://gitlab.paymoapp.com/gergo/node-active-window/commit/658c277cf3fef493f9e52633f06a1a55d2647c32)) 378 | * **module.windows:** Added napi binding ([4796d5f](https://gitlab.paymoapp.com/gergo/node-active-window/commit/4796d5f4e6e107f51ad59f2f7a13a6d344b625b3)) 379 | * **module.windows:** Added watch mode with background thread ([8fceff0](https://gitlab.paymoapp.com/gergo/node-active-window/commit/8fceff0566c800d34f5fd3e6149452fbcfa157cc)) 380 | * **module.windows:** Extract application icon from exe and convert it to base64 encoded png data url ([30f8715](https://gitlab.paymoapp.com/gergo/node-active-window/commit/30f8715f5cd7606aa68071e871d4153a875cebe8)) 381 | * **module.windows:** Extract application icon from executable and save it in a file for now *WIP* ([2c14d1b](https://gitlab.paymoapp.com/gergo/node-active-window/commit/2c14d1bb7d909d40cdaaa93b0ed21cf30c2da6b2)) 382 | * **module.windows:** Extract application icon from UWP apps ([febb0e3](https://gitlab.paymoapp.com/gergo/node-active-window/commit/febb0e34bfecea1b79d6eca936a72f2a402838cf)) 383 | * **module.windows:** Get application name and path for UWP apps ([395fae3](https://gitlab.paymoapp.com/gergo/node-active-window/commit/395fae325b535aed4b4d6b6d7a6657bd1281b4ff)) 384 | * **module.windows:** Get window in foreground and it's title, application name, application path and PID ([5e84d0c](https://gitlab.paymoapp.com/gergo/node-active-window/commit/5e84d0c152f5981481370cfab56057dce8838156)) 385 | * Updated javascript code to support requesting permissions ([634759a](https://gitlab.paymoapp.com/gergo/node-active-window/commit/634759a1c56968311188bea34b7d6e6b2462b041)) 386 | 387 | 388 | ### Bug Fixes 389 | 390 | * **demo:** Added request permissions call ([2bca603](https://gitlab.paymoapp.com/gergo/node-active-window/commit/2bca6037abc2a88a245db33a555d8157c47063bf)) 391 | * **library:** The callback for addon.subscribe can get null and the value needs to be encoded ([1d2d9b6](https://gitlab.paymoapp.com/gergo/node-active-window/commit/1d2d9b6831b2fbf73e2111ce74d8bda6ba19d352)) 392 | * **module.macos:** Check if we have macOS catalina to check screen capture permissions ([3a36423](https://gitlab.paymoapp.com/gergo/node-active-window/commit/3a36423140a3b8cefd4d4c5583def1ba8c477b9f)) 393 | * **module.macos:** Implemented RunLoop to update frontmost window ([e2a93f7](https://gitlab.paymoapp.com/gergo/node-active-window/commit/e2a93f77fda78a0ac9bf3494dc99a6a02392160d)) 394 | * **module.windows:** Also watch for title changed events ([c3864a1](https://gitlab.paymoapp.com/gergo/node-active-window/commit/c3864a1f34d4bd1fa522c67099cda1c80a8dbb0e)) 395 | * **module.windows:** Clean up after itself ([f3c8618](https://gitlab.paymoapp.com/gergo/node-active-window/commit/f3c86188a79bf9d5b688e0f179be93e55737f47c)) 396 | * **module.windows:** Close process handle ([31881ca](https://gitlab.paymoapp.com/gergo/node-active-window/commit/31881cac636a8a791c0523fa40ce4f13f7212f6a)) 397 | * **module.windows:** Don't crash when UWP app is not yet initialized ([c0911e8](https://gitlab.paymoapp.com/gergo/node-active-window/commit/c0911e8ccb788ddcf657304d0bd898e2d3ecb60b)) 398 | * **module.windows:** Fixed buffer overflow in getWindowTitle and replaced &buf[0] to buf.data() to access raw array of vectors ([bd70a97](https://gitlab.paymoapp.com/gergo/node-active-window/commit/bd70a9722c8c508419f53e9331f63cfaf7294dce)) 399 | 400 | 401 | ### Documentation 402 | 403 | * Added documentation for JS library ([8dd91dc](https://gitlab.paymoapp.com/gergo/node-active-window/commit/8dd91dc19370ebe1a5b3fd39a9df3a94387a217a)) 404 | * Added documentation for windows and macos native modules ([5fd30ce](https://gitlab.paymoapp.com/gergo/node-active-window/commit/5fd30ce6bd9c20f7b036ac2c834f82dcf6c5ede5)) 405 | * **module.linux:** Added documentation for linux native library ([e18a015](https://gitlab.paymoapp.com/gergo/node-active-window/commit/e18a015b8530043375ca6f8d0e71c6aa45f9a8ce)) 406 | 407 | 408 | ### Build/CI 409 | 410 | * **docs:** Added script to automatically generate README table of contents ([9d4878b](https://gitlab.paymoapp.com/gergo/node-active-window/commit/9d4878b2c7132f18c9893f3ec8a05b621c1d72e8)) 411 | * **gitlab:** Added gitlab-ci configuration ([4c50352](https://gitlab.paymoapp.com/gergo/node-active-window/commit/4c5035292971b9f8f7f495aad9ac627f8e4132f4)) 412 | * **gitlab:** Fix typo ([ff78131](https://gitlab.paymoapp.com/gergo/node-active-window/commit/ff78131ca80f9d011aa4f851617069c099367f05)) 413 | * Include N-API version in prebuilt binaries ([0b2ce15](https://gitlab.paymoapp.com/gergo/node-active-window/commit/0b2ce155e24d11aea0a5a18e3bf92b6d08c340ce)) 414 | * Initialized project ([d2f19a2](https://gitlab.paymoapp.com/gergo/node-active-window/commit/d2f19a25ae1be025762a82718e8157aafcf22c0c)) 415 | * **module.windows:** Use visual studio nmake to build demo instead of mingw ([e33fdb9](https://gitlab.paymoapp.com/gergo/node-active-window/commit/e33fdb900eef68abe9d116ff18e38d29998f574d)) 416 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paymo LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Active Window 2 | 3 | [![NPM](https://img.shields.io/npm/v/@paymoapp/active-window)](https://www.npmjs.com/package/@paymoapp/active-window) 4 | [![Typescript](https://img.shields.io/npm/types/@paymoapp/active-window)](https://www.npmjs.com/package/@paymoapp/active-window) 5 | [![N-API](https://raw.githubusercontent.com/nodejs/abi-stable-node/doc/assets/Node-API%20v6%20Badge.svg)](https://github.com/nodejs/node-addon-api) 6 | [![License](https://img.shields.io/github/license/paymoapp/node-active-window)](https://raw.githubusercontent.com/paymoapp/node-active-window/refs/heads/master/LICENSE) 7 | 8 | NodeJS library using native modules to get the active window and some metadata (including the application icon) on Windows, MacOS and Linux. 9 | 10 | ### Table of Contents 11 | 12 | 13 | 14 | - [Getting started](#getting-started) 15 | - [Installation](#installation) 16 | - [Native addon](#native-addon) 17 | - [Usage](#usage) 18 | - [Example](#example) 19 | - [API](#api) 20 | - [Data structures](#data-structures) 21 | - [Functions](#functions) 22 | - [Native libraries](#native-libraries) 23 | - [Linux (`module/linux`)](#linux-modulelinux) 24 | - [Data structures](#data-structures-1) 25 | - [Public functions](#public-functions) 26 | - [Example](#example-1) 27 | - [Building](#building) 28 | - [Windows (`module/windows`)](#windows-modulewindows) 29 | - [Data structures](#data-structures-2) 30 | - [Public functions](#public-functions-1) 31 | - [Example](#example-2) 32 | - [Building](#building-1) 33 | - [MacOS (`module/macos`)](#macos-modulemacos) 34 | - [Data structures](#data-structures-3) 35 | - [Public functions](#public-functions-2) 36 | - [Example](#example-3) 37 | - [Building](#building-2) 38 | 39 | 40 | 41 | ## Getting started 42 | 43 | #### Installation 44 | 45 | ```bash 46 | npm install --save @paymoapp/active-window 47 | ``` 48 | 49 | #### Native addon 50 | 51 | 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. 52 | 53 | The project uses [prebuild](https://github.com/prebuild/prebuild) to supply prebuilt libraries. 54 | 55 | The project uses Node-API version 6, you can check [this table](https://nodejs.org/api/n-api.html#node-api-version-matrix) to see which node versions are supported. 56 | 57 | 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`. 58 | 59 | 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. 60 | 61 | #### Usage 62 | 63 | You need to import the library and call the [`initialize`](#𝑓--initialize) function. __For MacOS see the method's documentation for additional required steps and caveats.__ 64 | 65 | On MacOS you need to check for the screen recording permission (using [`requestPermissions`](#𝑓--requestpermissions)), otherwise you won't be able to fetch the window titles. 66 | 67 | Then you can use the [`getActiveWindow`](#𝑓--getactivewindow) or [`subscribe`](#𝑓--subscribe) methods to fetch the current active window or watch for window changes. 68 | 69 | #### Example 70 | 71 | 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`. 72 | 73 | ```ts 74 | import ActiveWindow from '@paymoapp/active-window'; 75 | 76 | ActiveWindow.initialize(); 77 | 78 | if (!ActiveWindow.requestPermissions()) { 79 | console.log('Error: You need to grant screen recording permission in System Preferences > Security & Privacy > Privacy > Screen Recording'); 80 | process.exit(0); 81 | } 82 | 83 | const activeWin = ActiveWindow.getActiveWindow(); 84 | 85 | console.log('Window title:', activeWin.title); 86 | console.log('Application:', activeWin.application); 87 | console.log('Application path:', activeWin.path); 88 | console.log('Application PID:', activeWin.pid); 89 | console.log('Application icon:', activeWin.icon); 90 | ``` 91 | 92 | ## API 93 | 94 | #### Data structures 95 | 96 | ###### 🗃    WindowInfo 97 | 98 | ```ts 99 | interface WindowInfo { 100 | title: string; 101 | application: string; 102 | path: string; 103 | pid: number; 104 | icon: string; 105 | windows?: { 106 | isUWPApp: boolean; 107 | uwpPackage: string; 108 | }; 109 | } 110 | ``` 111 | 112 | This is the only object you will receive when interacting with this library. It contains information about the currently active window: 113 | 114 | - `title` - The title of the current window 115 | - `application` - The name of the application. On Windows you should use the `uwpPackage` parameter instead if `isUWPApp` is set to true 116 | - `path` - The path to the application's executable 117 | - `pid` - Process identifier of the application 118 | - `icon` - Base64 encoded string representing the application icon 119 | - `windows` - Object containing Windows platform specific information, undefined on other platforms 120 | - `windows.isUWPApp` - Set to `true` if the active window is owned by an [Universal Windows Platform](https://docs.microsoft.com/en-us/windows/uwp/get-started/universal-application-platform-guide) application 121 | - `windows.uwpPackage` - Contains the package family name of the UWP application 122 | 123 | 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). 124 | 125 | #### Functions 126 | 127 | ###### 𝑓    getActiveWindow 128 | 129 | ```ts 130 | interface IActiveWindow { 131 | getActiveWindow(): WindowInfo 132 | // ... 133 | } 134 | ``` 135 | 136 | 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). 137 | 138 | ###### 𝑓    subscribe 139 | 140 | ```ts 141 | interface IActiveWindow { 142 | subscribe(callback: (windowInfo: WindowInfo | null) => void): number; 143 | // ... 144 | } 145 | ``` 146 | 147 | Subscribe to changes of the active window. The supplied callback will be called with `null` if there're no focused windows at the moment. 148 | 149 | The function returns a number representing the ID of the watch. You should store this value to remove the event listener later on. 150 | 151 | ###### 𝑓    unsubscribe 152 | 153 | ```ts 154 | interface IActiveWindow { 155 | unsubscribe(watchId: number): void; 156 | // ... 157 | } 158 | ``` 159 | 160 | Remove the event listener associated with the supplied watch ID. Use this to unsubscribe from the active window changed events. 161 | 162 | ###### 𝑓    initialize 163 | 164 | ```ts 165 | interface IActiveWindow { 166 | initialize(opts?: { osxRunLoop: false | 'get' | 'all' }): void; 167 | // ... 168 | } 169 | ``` 170 | 171 | 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. 172 | 173 | If you're not using this library in a GUI application there might be no run loop running for the main thread. In this case you __MUST__ set the `osxRunLoop` property to get results. On Electron you have a working run loop in the main process and you should use IPCs to access the lib from the renderer. 174 | 175 | Possible values for `osxRunLoop`: 176 | - `false` or not set __(default)__: Don't run the run loop manually. 177 | - `get`: Run the run loop only for the `getActiveWindow` calls. Subscriptions will not work with this mode. 178 | - `all`: Run the run loop both for `getActiveWindow` calls and for subscriptions. 179 | 180 | ###### 𝑓    requestPermissions 181 | 182 | ```ts 183 | interface IActiveWindow { 184 | requestPermissions(): boolean; 185 | // ... 186 | } 187 | ``` 188 | 189 | On the MacOS platform you need to request screen recording permission to fetch the title of the current window. 190 | 191 | 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. 192 | 193 | You can call this function regardless of the current platform. On unsupported platforms it will simply return `true`. 194 | 195 | 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. 196 | 197 | If the user fails to grant the required permissions, the `title` property of the returned `WindowInfo` will be an empty string. 198 | 199 | ## Native libraries 200 | 201 | 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. 202 | 203 | 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`. 204 | 205 | The demo has 4 running modes: 206 | - _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 207 | - _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 208 | - _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 209 | - _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 210 | 211 | ### Linux (`module/linux`) 212 | 213 | #### Data structures 214 | 215 | ###### 🗃    WindowInfo 216 | 217 | ```c++ 218 | struct WindowInfo { 219 | std::string title; // UTF8 encoded string of window title. Empty string if couldn't fetch 220 | std::string application; // UTF8 encoded string of application name. Empty string if couldn't fetch 221 | std::string path; // UTF8 encoded string of application path. Empty string if couldn't fetch 222 | int pid; // PID of process. -1 if couldn't fetch 223 | std::string icon; // base64 encoded PNG icon. Empty string if couldn't fetch 224 | } 225 | ``` 226 | 227 | ###### 🗃    watch_t 228 | 229 | ```c++ 230 | typedef unsigned int watch_t; 231 | ``` 232 | 233 | ###### 🗃    watch_callback 234 | 235 | ```c++ 236 | typedef std::function watch_callback; 237 | ``` 238 | 239 | #### Public functions 240 | 241 | ###### 𝑓    Constructor 242 | 243 | ```c++ 244 | ActiveWindow(unsigned int iconCacheSize = 0); 245 | ``` 246 | 247 | 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. 248 | 249 | 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. 250 | 251 | ###### 𝑓    getActiveWindow 252 | 253 | ```c++ 254 | WindowInfo* getActiveWindow(); 255 | ``` 256 | 257 | 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`. 258 | 259 | ###### 𝑓    buildAppCache 260 | 261 | ```c++ 262 | void buildAppCache(); 263 | ``` 264 | 265 | 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. 266 | 267 | ###### 𝑓    watchActiveWindow 268 | 269 | ```c++ 270 | watch_t watchActiveWindow(watch_callback cb); 271 | ``` 272 | 273 | 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. 274 | 275 | 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. 276 | 277 | 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. 278 | 279 | You should save the returned watch ID to unsubscribe later. 280 | 281 | ###### 𝑓    unwatchActiveWindow 282 | 283 | ```c++ 284 | void unwatchActiveWindow(watch_t watch); 285 | ``` 286 | 287 | Removes the watch associated with the supplied watch ID. 288 | 289 | 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. 290 | 291 | #### Example 292 | 293 | See `module/linux/demo/main.cpp` for an example. 294 | 295 | ```c++ 296 | #include 297 | #include "ActiveWindow.h" 298 | 299 | using namespace std; 300 | using namespace PaymoActiveWindow; 301 | 302 | int main() { 303 | ActiveWindow* activeWindow = new ActiveWindow(10); 304 | 305 | WindowInfo* windowInfo = activeWindow->getActiveWindow(); 306 | 307 | if (windowInfo == NULL) { 308 | cout<<"Could not get active window\n"; 309 | } 310 | else { 311 | cout<<"Title: "<title<<"\n"; 312 | cout<<"Application: "<application<<"\n"; 313 | cout<<"Executable path: "<path<<"\n"; 314 | cout<<"PID: "<pid<<"\n"; 315 | cout<<"Icon: "<icon<<"\n"; 316 | } 317 | 318 | delete windowInfo; 319 | delete activeWindow; 320 | 321 | return 0; 322 | } 323 | ``` 324 | 325 | #### Building 326 | 327 | See `module/linux/demo/Makefile` for a sample makefile. You need to check the `lib` target. 328 | 329 | You should use C++17 for building and you need to link the following libraries: 330 | - X11 (`-lX11`) - libx11-dev 331 | - PThread (`-lpthread`) - libpthread-stubs0-dev 332 | 333 | ### Windows (`module/windows`) 334 | 335 | #### Data structures 336 | 337 | ###### 🗃    WindowInfo 338 | 339 | ```c++ 340 | struct WindowInfo { 341 | std::wstring title = L""; // UTF16 encoded string of window title. Empty string if couldn't fetch 342 | std::wstring application = L""; // UTF16 encoded string of application. Empty string if couldn't fetch 343 | std::wstring path = L""; // UTF16 encoded string of executable path. Empty string if couldn't fetch 344 | unsigned int pid = 0; // Process PID 345 | bool isUWPApp = false; // if application is detected to be an Universal Windows Platform application 346 | std::wstring uwpPackage = L""; // UTF16 encoded string of the UWP package name. Empty string if couldn't fetch 347 | std::string icon = ""; // base64 encoded icon. Empty string if couldn't fetch 348 | }; 349 | ``` 350 | 351 | ###### 🗃    watch_t 352 | 353 | ```c++ 354 | typedef unsigned int watch_t; 355 | ``` 356 | 357 | ###### 🗃    watch_callback 358 | 359 | ```c++ 360 | typedef std::function watch_callback; 361 | ``` 362 | 363 | #### Public functions 364 | 365 | ###### 𝑓    Constructor 366 | 367 | ```c++ 368 | ActiveWindow(unsigned int iconCacheSize = 0); 369 | ``` 370 | 371 | 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. 372 | 373 | 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. 374 | 375 | ###### 𝑓    getActiveWindow 376 | 377 | ```c++ 378 | WindowInfo* getActiveWindow(); 379 | ``` 380 | 381 | 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`. 382 | 383 | ###### 𝑓    watchActiveWindow 384 | 385 | ```c++ 386 | watch_t watchActiveWindow(watch_callback cb); 387 | ``` 388 | 389 | 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. 390 | 391 | 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. 392 | 393 | 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. 394 | 395 | You should save the returned watch ID to unsubscribe later. 396 | 397 | ###### 𝑓    unwatchActiveWindow 398 | 399 | ```c++ 400 | void unwatchActiveWindow(watch_t watch); 401 | ``` 402 | 403 | Removes the watch associated with the supplied watch ID. 404 | 405 | 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. 406 | 407 | #### Example 408 | 409 | See `module/windows/demo/main.cpp` for an example. 410 | 411 | ```c++ 412 | #include 413 | #include "ActiveWindow.h" 414 | 415 | using namespace std; 416 | using namespace PaymoActiveWindow; 417 | 418 | int main() { 419 | ActiveWindow* activeWindow = new ActiveWindow(10); 420 | 421 | WindowInfo* windowInfo = activeWindow->getActiveWindow(); 422 | 423 | if (windowInfo == NULL) { 424 | cout<<"Could not get active window\n"; 425 | } 426 | else { 427 | wcout<title<<"\n"; 428 | wcout<application<<"\n"; 429 | wcout<path<<"\n"; 430 | cout<<"PID: "<pid<<"\n"; 431 | cout<<"Is UWP application: "<<(windowInfo->isUWPApp ? "Yes" : "No")<<"\n"; 432 | if (windowInfo->isUWPApp) { 433 | wcout<<"UWP package name: "<uwpPackage<<"\n"; 434 | } 435 | cout<<"Icon: "<icon<<"\n"; 436 | } 437 | 438 | delete windowInfo; 439 | delete activeWindow; 440 | 441 | return 0; 442 | } 443 | ``` 444 | 445 | #### Building 446 | 447 | 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. 448 | 449 | 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: 450 | 451 | ```batch 452 | "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64 453 | ``` 454 | 455 | You should use C++17 for building and you need to link the following libraries: 456 | - User32.lib 457 | - Shell32.lib 458 | - Version.lib 459 | - Shlwapi.lib 460 | - Gdi32.lib 461 | - Gdiplus.lib 462 | - Windowsapp.lib 463 | 464 | ### MacOS (`module/macos`) 465 | 466 | #### Data structures 467 | 468 | ###### 🗃    WindowInfo 469 | 470 | ```c++ 471 | struct WindowInfo { 472 | std::string title; // UTF8 encoded string of window title. Empty string if couldn't fetch 473 | std::string application; // UTF8 encoded string of application name 474 | std::string path; // UTF8 encoded string of application path 475 | int pid; // PID of process 476 | std::string icon; // base64 encoded PNG icon 477 | } 478 | ``` 479 | 480 | ###### 🗃    watch_t 481 | 482 | ```c++ 483 | typedef unsigned int watch_t; 484 | ``` 485 | 486 | ###### 🗃    watch_callback 487 | 488 | ```c++ 489 | typedef std::function watch_callback; 490 | ``` 491 | 492 | #### Public functions 493 | 494 | ###### 𝑓    Constructor 495 | 496 | ```c++ 497 | ActiveWindow(unsigned int iconCacheSize = 0); 498 | ``` 499 | 500 | 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. 501 | 502 | 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. 503 | 504 | ###### 𝑓    getActiveWindow 505 | 506 | ```c++ 507 | WindowInfo* getActiveWindow(); 508 | ``` 509 | 510 | 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`. 511 | 512 | ###### 𝑓    requestScreenCaptureAccess 513 | 514 | ```c++ 515 | bool requestScreenCaptureAccess(); 516 | ``` 517 | 518 | 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). 519 | 520 | 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. 521 | 522 | The application needs to be relaunched after granting the permission. 523 | 524 | ###### 𝑓    watchActiveWindow 525 | 526 | ```c++ 527 | watch_t watchActiveWindow(watch_callback cb); 528 | ``` 529 | 530 | 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. 531 | 532 | 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. 533 | 534 | 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. 535 | 536 | The callbacks are also executed on the main thread, so you shouldn't do anything blocking in them. 537 | 538 | You should save the returned watch ID to unsubscribe later. 539 | 540 | ###### 𝑓    unwatchActiveWindow 541 | 542 | ```c++ 543 | void unwatchActiveWindow(watch_t watch); 544 | ``` 545 | 546 | Removes the watch associated with the supplied watch ID. 547 | 548 | ###### 𝑓    runLoop 549 | 550 | ```c++ 551 | void runLoop(); 552 | ``` 553 | 554 | 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. 555 | 556 | You should only use this function only if you don't have a running RunLoop on your main thread. 557 | 558 | #### Example 559 | 560 | See `module/macos/demo/main.mm` for an example. 561 | 562 | ```c++ 563 | #include 564 | #include "ActiveWindow.h" 565 | 566 | using namespace std; 567 | using namespace PaymoActiveWindow; 568 | 569 | int main() { 570 | ActiveWindow* activeWindow = new ActiveWindow(); 571 | 572 | WindowInfo* windowInfo = activeWindow->getActiveWindow(); 573 | 574 | if (windowInfo == NULL) { 575 | cout<<"Could not get active window\n"; 576 | } 577 | else { 578 | cout<<"Title: "<title<<"\n"; 579 | cout<<"Application: "<application<<"\n"; 580 | cout<<"Executable path: "<path<<"\n"; 581 | cout<<"PID: "<pid<<"\n"; 582 | cout<<"Icon: "<icon<<"\n"; 583 | } 584 | 585 | delete windowInfo; 586 | delete activeWindow; 587 | 588 | return 0; 589 | } 590 | ``` 591 | 592 | #### Building 593 | 594 | See `module/macos/demo/Makefile` for a sample makefile. You need to check the `lib` target. 595 | 596 | You should use C++17 for building and you need to link the following libraries: 597 | - `-lc++` 598 | - `-framework Foundation` 599 | - `-framework AppKit` 600 | - `-framework ApplicationServices` 601 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "PaymoActiveWindow", 5 | "conditions": [ 6 | ["OS=='win'", { 7 | "sources": [ 8 | "module/windows/napi/main.cpp", 9 | "module/windows/napi/module.cpp", 10 | "module/windows/src/ActiveWindow.cpp", 11 | "module/windows/src/IconCache.cpp", 12 | "module/windows/src/base64/base64.cpp", 13 | "module/windows/src/GdiPlusUtils.cpp" 14 | ], 15 | "libraries": [ 16 | "User32.lib", 17 | "Shell32.lib", 18 | "Version.lib", 19 | "Shlwapi.lib", 20 | "Gdi32.lib", 21 | "Gdiplus.lib", 22 | "Windowsapp.lib" 23 | ], 24 | "msvs_settings": { 25 | "VCCLCompilerTool": { 26 | "AdditionalOptions": [ 27 | "/std:c++17" 28 | ] 29 | } 30 | } 31 | }], 32 | ["OS=='linux'", { 33 | "sources": [ 34 | "module/linux/napi/main.cpp", 35 | "module/linux/napi/module.cpp", 36 | "module/linux/src/ActiveWindow.cpp", 37 | "module/linux/src/IconCache.cpp", 38 | "module/linux/src/base64/base64.cpp", 39 | "module/linux/src/SimpleIni/ConvertUTF.c", 40 | "module/linux/src/lodepng/lodepng.cpp" 41 | ], 42 | "libraries": [ 43 | "-lX11", 44 | "-lpthread", 45 | "-static-libstdc++" 46 | ], 47 | "cflags_cc": [ 48 | "-fexceptions", 49 | "--std=c++17" 50 | ] 51 | }], 52 | ["OS=='mac'", { 53 | "sources": [ 54 | "module/macos/napi/main.mm", 55 | "module/macos/napi/module.mm", 56 | "module/macos/src/ActiveWindow.mm", 57 | "module/macos/src/IconCache.cpp", 58 | "module/macos/src/base64/base64.cpp" 59 | ], 60 | "libraries": [ 61 | "-lc++", 62 | "-framework Foundation", 63 | "-framework AppKit", 64 | "-framework ApplicationServices" 65 | ], 66 | "xcode_settings": { 67 | "CLANG_CXX_LANGUAGE_STANDARD": "c++17", 68 | "GCC_ENABLE_CPP_EXCEPTIONS": "YES" 69 | } 70 | }] 71 | ], 72 | "include_dirs": [ 73 | " 2 | 3 | 4 | node-active-window demo 5 | 6 | 19 | 20 | 21 |

node-active-window demo

22 |

You will see applications pop up in the table below

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Window TitleApplicationExecutable pathPIDIconExtra
36 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable no-console */ 3 | 4 | import http from 'http'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | import ws from 'ws'; 9 | 10 | import ActiveWindow, { WindowInfo } from '../src'; 11 | 12 | ActiveWindow.initialize({ osxRunLoop: 'all' }); 13 | ActiveWindow.requestPermissions(); 14 | 15 | const server = http.createServer((req, res) => { 16 | fs.readFile(path.join(__dirname, 'demo.html'), (err, data) => { 17 | if (err) { 18 | res.writeHead(500); 19 | res.end('Internal server error'); 20 | return; 21 | } 22 | 23 | res.writeHead(200); 24 | res.end(data); 25 | }); 26 | }); 27 | 28 | const wss = new ws.Server({ server: server, path: '/ws' }); 29 | const broadcastData = (wss: ws.Server, data: any) => { 30 | wss.clients.forEach(client => { 31 | if (client.readyState == ws.OPEN) { 32 | client.send(JSON.stringify(data)); 33 | } 34 | }); 35 | }; 36 | 37 | server.listen(3000, () => { 38 | console.log('Started listening'); 39 | console.log('Open http://localhost:3000 to get started'); 40 | }); 41 | 42 | let winInfo: WindowInfo | null = null; 43 | 44 | const objectDeepCompare = (a: any, b: any): boolean => { 45 | return Object.keys(a).reduce((acc: boolean, cur) => { 46 | if (cur == 'icon') { 47 | return acc; 48 | } 49 | 50 | if (!b) { 51 | return false; 52 | } 53 | 54 | if (typeof a[cur] == 'object') { 55 | return acc && objectDeepCompare(a[cur], b[cur]); 56 | } 57 | 58 | return acc && a[cur] == b[cur]; 59 | }, true); 60 | }; 61 | 62 | const watchId = ActiveWindow.subscribe(curWinInfo => { 63 | console.log('Got new window info'); 64 | if (curWinInfo == null) { 65 | console.log('Got null window'); 66 | return; 67 | } 68 | 69 | if (!objectDeepCompare(curWinInfo, winInfo)) { 70 | // different 71 | console.log('Broadcasting changes...'); 72 | broadcastData(wss, curWinInfo); 73 | winInfo = curWinInfo; 74 | } 75 | }); 76 | 77 | console.log('Started watching with ID:', watchId); 78 | 79 | process.on('SIGINT', () => { 80 | console.log('Closing'); 81 | ActiveWindow.unsubscribe(watchId); 82 | 83 | wss.clients.forEach(client => { 84 | client.terminate(); 85 | }); 86 | 87 | wss.close(); 88 | server.close(); 89 | 90 | setTimeout(() => { 91 | console.log('Exited'); 92 | process.exit(0); 93 | }, 1000); 94 | }); 95 | -------------------------------------------------------------------------------- /module/linux/demo/Makefile: -------------------------------------------------------------------------------- 1 | CXX=g++ 2 | RM=rm -R 3 | 4 | LIBDIR=../src 5 | BUILDDIR=build 6 | 7 | CXXFLAGS=--std=c++17 8 | LDOPTS=-lX11 -lpthread 9 | 10 | SRCS=$(LIBDIR)/ActiveWindow.cpp $(LIBDIR)/IconCache.cpp $(LIBDIR)/SimpleIni/ConvertUTF.c $(LIBDIR)/base64/base64.cpp $(LIBDIR)/lodepng/lodepng.cpp 11 | OBJS=$(addsuffix .o,$(addprefix $(BUILDDIR)/,$(notdir $(basename $(SRCS))))) 12 | 13 | define build-cpp 14 | $(BUILDDIR)/$(basename $(notdir $(1))).o: $(1) $(addsuffix .h,$(basename $(1))) $(BUILDDIR)/ 15 | @echo Bulding $(1) 16 | $(CXX) $(CXXFLAGS) -c -o $(BUILDDIR)/$(basename $(notdir $(1))).o $(1) 17 | endef 18 | 19 | all: $(BUILDDIR)/demo 20 | 21 | $(BUILDDIR)/demo: $(BUILDDIR)/demo.o lib 22 | @echo Linking demo executable 23 | $(CXX) -o $@ $(OBJS) $(BUILDDIR)/demo.o $(LDOPTS) 24 | 25 | lib: $(OBJS) 26 | @echo Built library 27 | 28 | $(BUILDDIR)/demo.o: main.cpp $(LIBDIR)/ActiveWindow.h $(BUILDDIR)/ 29 | @echo Building main.cpp for demo 30 | $(CXX) $(CXXFLAGS) -c -o $@ $< 31 | 32 | $(foreach _t,$(SRCS),$(eval $(call build-cpp,$(_t)))) 33 | 34 | $(BUILDDIR)/: 35 | @echo Creating build directory 36 | @mkdir $(BUILDDIR) 37 | 38 | clean: 39 | $(RM) $(BUILDDIR) 40 | 41 | run: 42 | $(BUILDDIR)/demo $(MODE) 43 | -------------------------------------------------------------------------------- /module/linux/demo/main.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/ActiveWindow.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void printWindowInfo(PaymoActiveWindow::WindowInfo* inf) { 9 | std::cout<<"Title: \""<title<<"\""<application<<"\""<path<<"\""<pid<<"\""<icon<getActiveWindow(); 20 | 21 | if (inf == NULL) { 22 | std::cout<<"Error: Could not get window info"<buildAppCache(); 42 | std::cout<<"App cache built\n\n"<watchActiveWindow([](PaymoActiveWindow::WindowInfo* inf) { 68 | std::cout<<"[Notif] Active window has changed!\n"; 69 | 70 | if (inf == NULL) { 71 | std::cout<<"Empty"<unwatchActiveWindow(watchId); 87 | std::cout<<"Watch removed"<getActiveWindow(); 95 | delete inf; 96 | } 97 | clock_t end = clock(); 98 | 99 | double elapsedTime = (double)(end - start) / CLOCKS_PER_SEC; 100 | 101 | std::cout<<"Elapsed clocks: "<<(end - start)<<"\nElapsed seconds: "< 2 | #include "module.h" 3 | 4 | Napi::Object InitAll(Napi::Env env, Napi::Object exports) { 5 | module::Init(env, exports); 6 | 7 | return exports; 8 | } 9 | 10 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll); 11 | -------------------------------------------------------------------------------- /module/linux/napi/module.cpp: -------------------------------------------------------------------------------- 1 | #include "module.h" 2 | 3 | void module::Init(Napi::Env env, Napi::Object exports) { 4 | env.SetInstanceData(new PaymoActiveWindow::ActiveWindow(15)); 5 | 6 | exports.Set("getActiveWindow", Napi::Function::New(env)); 7 | exports.Set("subscribe", Napi::Function::New(env)); 8 | exports.Set("unsubscribe", Napi::Function::New(env)); 9 | exports.Set("initialize", Napi::Function::New(env)); 10 | } 11 | 12 | Napi::Value module::getActiveWindow(const Napi::CallbackInfo& info) { 13 | Napi::Env env = info.Env(); 14 | 15 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 16 | if (activeWindow == NULL) { 17 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 18 | return env.Null(); 19 | } 20 | 21 | PaymoActiveWindow::WindowInfo* windowInfo = activeWindow->getActiveWindow(); 22 | if (windowInfo == NULL) { 23 | Napi::Error::New(env, "Failed to get active window").ThrowAsJavaScriptException(); 24 | return env.Null(); 25 | } 26 | 27 | Napi::Object result = encodeWindowInfo(env, windowInfo); 28 | 29 | delete windowInfo; 30 | 31 | return result; 32 | } 33 | 34 | Napi::Value module::subscribe(const Napi::CallbackInfo& info) { 35 | Napi::Env env = info.Env(); 36 | 37 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 38 | if (activeWindow == NULL) { 39 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 40 | return env.Null(); 41 | } 42 | 43 | if (info.Length() != 1) { 44 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 45 | return env.Null(); 46 | } 47 | 48 | if (!info[0].IsFunction()) { 49 | Napi::TypeError::New(env, "Expected first argument to be function").ThrowAsJavaScriptException(); 50 | return env.Null(); 51 | } 52 | 53 | Napi::TypedThreadSafeFunction tsfn = Napi::TypedThreadSafeFunction< 54 | void, 55 | PaymoActiveWindow::WindowInfo, 56 | tsfnMainThreadCallback 57 | >::New(env, info[0].As(), "Active Window Callback", 0, 1); 58 | 59 | PaymoActiveWindow::watch_t watchId = activeWindow->watchActiveWindow([tsfn](PaymoActiveWindow::WindowInfo* windowInfo) { 60 | if (windowInfo == NULL) { 61 | tsfn.BlockingCall((PaymoActiveWindow::WindowInfo*)NULL); 62 | return; 63 | } 64 | 65 | // clone window info 66 | PaymoActiveWindow::WindowInfo* arg = new PaymoActiveWindow::WindowInfo(); 67 | *arg = *windowInfo; 68 | 69 | tsfn.BlockingCall(arg); 70 | }); 71 | 72 | return Napi::Number::New(env, watchId); 73 | } 74 | 75 | void module::unsubscribe(const Napi::CallbackInfo& info) { 76 | Napi::Env env = info.Env(); 77 | 78 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 79 | if (activeWindow == NULL) { 80 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 81 | return; 82 | } 83 | 84 | if (info.Length() != 1) { 85 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 86 | return; 87 | } 88 | 89 | if (!info[0].IsNumber()) { 90 | Napi::TypeError::New(env, "Expected first argument to be number").ThrowAsJavaScriptException(); 91 | return; 92 | } 93 | 94 | PaymoActiveWindow::watch_t watchId = info[0].As().Uint32Value(); 95 | 96 | activeWindow->unwatchActiveWindow(watchId); 97 | } 98 | 99 | void module::initialize(const Napi::CallbackInfo& info) { 100 | Napi::Env env = info.Env(); 101 | 102 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 103 | if (activeWindow == NULL) { 104 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 105 | return; 106 | } 107 | 108 | activeWindow->buildAppCache(); 109 | } 110 | 111 | Napi::Object module::encodeWindowInfo(Napi::Env env, PaymoActiveWindow::WindowInfo* windowInfo) { 112 | Napi::Object result = Napi::Object::New(env); 113 | 114 | result.Set("title", Napi::String::New(env, windowInfo->title)); 115 | result.Set("application", Napi::String::New(env, windowInfo->application)); 116 | result.Set("path", Napi::String::New(env, windowInfo->path)); 117 | result.Set("pid", Napi::Number::New(env, windowInfo->pid)); 118 | result.Set("icon", Napi::String::New(env, windowInfo->icon)); 119 | 120 | return result; 121 | } 122 | 123 | void module::tsfnMainThreadCallback(Napi::Env env, Napi::Function jsCallback, void* ctx, PaymoActiveWindow::WindowInfo* data) { 124 | if (data == NULL) { 125 | jsCallback.Call({ env.Null() }); 126 | } 127 | else { 128 | jsCallback.Call({ encodeWindowInfo(env, data) }); 129 | delete data; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /module/linux/napi/module.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../src/ActiveWindow.h" 3 | 4 | #ifndef _PAYMO_MODULE_H 5 | #define _PAYMO_MODULE_H 6 | 7 | namespace module { 8 | void Init(Napi::Env env, Napi::Object exports); 9 | 10 | Napi::Value getActiveWindow(const Napi::CallbackInfo& info); 11 | Napi::Value subscribe(const Napi::CallbackInfo& info); 12 | void unsubscribe(const Napi::CallbackInfo& info); 13 | void initialize(const Napi::CallbackInfo& info); 14 | 15 | // helpers 16 | Napi::Object encodeWindowInfo(Napi::Env env, PaymoActiveWindow::WindowInfo* windowInfo); 17 | void tsfnMainThreadCallback(Napi::Env env, Napi::Function jsCallback, void* ctx, PaymoActiveWindow::WindowInfo* data); 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /module/linux/src/ActiveWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "ActiveWindow.h" 2 | 3 | namespace PaymoActiveWindow { 4 | ActiveWindow::ActiveWindow(unsigned int iconCacheSize) { 5 | XSetErrorHandler(ActiveWindow::xErrorHandler); 6 | this->display = XOpenDisplay(NULL); 7 | 8 | if (this->display == NULL) { 9 | throw std::logic_error("Failed to open display"); 10 | } 11 | 12 | if (iconCacheSize > 0) { 13 | this->iconCache = new IconCache(iconCacheSize); 14 | } 15 | } 16 | 17 | ActiveWindow::~ActiveWindow() { 18 | // stop watch thread 19 | if (this->watchThread != NULL) { 20 | this->threadShouldExit.store(true, std::memory_order_relaxed); 21 | this->watchThread->join(); 22 | delete this->watchThread; 23 | this->watchThread = NULL; 24 | } 25 | 26 | delete this->iconCache; 27 | this->iconCache = NULL; 28 | XCloseDisplay(this->display); 29 | this->display = NULL; 30 | } 31 | 32 | WindowInfo* ActiveWindow::getActiveWindow() { 33 | if (this->display == NULL) { 34 | return NULL; 35 | } 36 | 37 | Window activeWin = this->getFocusedWindow(); 38 | 39 | if (activeWin == None) { 40 | return NULL; 41 | } 42 | 43 | WindowInfo* info = new WindowInfo(); 44 | 45 | info->title = this->getWindowTitle(activeWin); 46 | info->application = this->getApplicationName(activeWin); 47 | info->pid = this->getWindowPid(activeWin); 48 | 49 | if (info->pid >= 0) { 50 | info->path = this->getProcessPath(info->pid); 51 | } 52 | 53 | info->icon = this->getIcon(info->application, activeWin); 54 | 55 | return info; 56 | } 57 | 58 | void ActiveWindow::buildAppCache() { 59 | std::string userHome = this->safeGetEnv("HOME"); 60 | std::string xdgDataDirs = this->safeGetEnv("XDG_DATA_DIRS"); 61 | 62 | // fetch applications from every data dir 63 | if (userHome != "") { 64 | this->loadDesktopEntriesFromDirectory(userHome + "/.local/share/applications"); 65 | } 66 | 67 | if (xdgDataDirs != "") { 68 | size_t startPos = 0; 69 | size_t delimPos = xdgDataDirs.find(":"); 70 | while (delimPos != std::string::npos) { 71 | this->loadDesktopEntriesFromDirectory(xdgDataDirs.substr(startPos, delimPos - startPos) + "/applications"); 72 | startPos = delimPos + 1; 73 | delimPos = xdgDataDirs.find(":", startPos); 74 | } 75 | this->loadDesktopEntriesFromDirectory(xdgDataDirs.substr(startPos, delimPos - startPos) + "/applications"); 76 | } 77 | } 78 | 79 | watch_t ActiveWindow::watchActiveWindow(watch_callback cb) { 80 | watch_t watchId = this->nextWatchId++; 81 | 82 | this->mutex.lock(); 83 | this->watches[watchId] = cb; 84 | this->mutex.unlock(); 85 | 86 | // start thread if not started 87 | if (this->watchThread == NULL) { 88 | this->threadShouldExit.store(false, std::memory_order_relaxed); 89 | this->watchThread = new std::thread(&ActiveWindow::runWatchThread, this); 90 | } 91 | 92 | return watchId; 93 | } 94 | 95 | void ActiveWindow::unwatchActiveWindow(watch_t watch) { 96 | this->mutex.lock(); 97 | this->watches.erase(watch); 98 | this->mutex.unlock(); 99 | } 100 | 101 | Window ActiveWindow::getFocusedWindow() { 102 | // we're going to use the _NET_ACTIVE_WINDOW property of the root window 103 | Atom property = XInternAtom(this->display, "_NET_ACTIVE_WINDOW", true); 104 | Window root = DefaultRootWindow(this->display); 105 | 106 | Atom actualType; 107 | int actualFormat; 108 | unsigned long itemsCount; 109 | unsigned long bytes; 110 | 111 | unsigned char* prop; 112 | 113 | if (XGetWindowProperty(this->display, root, property, 0, sizeof(Window), false, XA_WINDOW, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 114 | return None; 115 | } 116 | 117 | if (actualFormat == 0) { 118 | return None; 119 | } 120 | 121 | Window focused = *reinterpret_cast(prop); 122 | XFree(prop); 123 | 124 | return focused; 125 | } 126 | 127 | std::string ActiveWindow::getWindowTitle(Window win) { 128 | XTextProperty textProp; 129 | if (!XGetWMName(this->display, win, &textProp)) { 130 | return ""; 131 | } 132 | 133 | if (!textProp.value || !textProp.nitems) { 134 | return ""; 135 | } 136 | 137 | if (textProp.encoding == XA_STRING) { 138 | // ASCII window title 139 | std::string title(reinterpret_cast(textProp.value)); 140 | XFree(textProp.value); 141 | return title; 142 | } 143 | 144 | // UTF-8 window title 145 | char** list; 146 | int listCount; 147 | if (Xutf8TextPropertyToTextList(this->display, &textProp, &list, &listCount) < Success || listCount < 1 || !list) { 148 | XFree(textProp.value); 149 | return ""; 150 | } 151 | 152 | XFree(textProp.value); 153 | std::string title(reinterpret_cast(list[0])); 154 | XFreeStringList(list); 155 | 156 | return title; 157 | } 158 | 159 | std::string ActiveWindow::getApplicationName(Window win) { 160 | XClassHint classHint; 161 | 162 | if (!XGetClassHint(this->display, win, &classHint)) { 163 | return ""; 164 | } 165 | 166 | std::string application(classHint.res_class); 167 | XFree(classHint.res_name); 168 | XFree(classHint.res_class); 169 | 170 | return application; 171 | } 172 | 173 | pid_t ActiveWindow::getWindowPid(Window win) { 174 | Atom property = XInternAtom(this->display, "_NET_WM_PID", true); 175 | 176 | Atom actualType; 177 | int actualFormat; 178 | unsigned long itemsCount; 179 | unsigned long bytes; 180 | 181 | unsigned char* prop; 182 | 183 | if (XGetWindowProperty(this->display, win, property, 0, sizeof(pid_t), false, XA_CARDINAL, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 184 | return -1; 185 | } 186 | 187 | if (actualFormat == 0) { 188 | return -1; 189 | } 190 | 191 | pid_t pid = *reinterpret_cast(prop); 192 | XFree(prop); 193 | 194 | return pid; 195 | } 196 | 197 | std::string ActiveWindow::getProcessPath(pid_t pid) { 198 | std::stringstream procPath; 199 | procPath<<"/proc/"< 0); 226 | 227 | // remove last null which was converted to space 228 | if (path.length() > 0) { 229 | path.pop_back(); 230 | } 231 | 232 | close(fd); 233 | 234 | return path; 235 | } 236 | 237 | std::string ActiveWindow::getIcon(std::string app, Window win) { 238 | if (this->iconCache != NULL && this->iconCache->has(&app)) { 239 | return this->iconCache->get(&app); 240 | } 241 | 242 | std::string icon = this->getApplicationIcon(app); 243 | 244 | if (icon == "") { 245 | icon = this->getWindowIcon(win); 246 | } 247 | 248 | if (this->iconCache != NULL) { 249 | this->iconCache->set(&app, &icon); 250 | } 251 | 252 | return icon; 253 | } 254 | 255 | std::string ActiveWindow::getApplicationIcon(std::string app) { 256 | // check if there's a direct match (using WM_CLASS) 257 | if (this->appToIcon.find(app) != this->appToIcon.end()) { 258 | // we have an icon 259 | return this->encodeIcon(this->appToIcon[app]); 260 | } 261 | 262 | // find using string match 263 | std::string needle = this->processStringForIndex(app); 264 | for (std::unordered_map::iterator it = this->appToIcon.begin(); it != this->appToIcon.end(); it++) { 265 | if (it->first.find(needle) != std::string::npos) { 266 | // we have a likely match 267 | return this->encodeIcon(it->second); 268 | } 269 | } 270 | 271 | return ""; 272 | } 273 | 274 | std::string ActiveWindow::getWindowIcon(Window win) { 275 | Atom property = XInternAtom(this->display, "_NET_WM_ICON", true); 276 | 277 | Atom actualType; 278 | int actualFormat; 279 | unsigned long itemsCount; 280 | unsigned long bytes; 281 | unsigned char* prop; 282 | 283 | int maxSize = 0; 284 | int maxSizeOffset = 0; 285 | int offset = 0; 286 | 287 | do { 288 | if (XGetWindowProperty(this->display, win, property, offset, 1, 0, XA_CARDINAL, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 289 | return ""; 290 | } 291 | if (actualFormat == 0) { 292 | return ""; 293 | } 294 | int width = *reinterpret_cast(prop); 295 | XFree(prop); 296 | 297 | if (XGetWindowProperty(this->display, win, property, offset + 1, 1, 0, XA_CARDINAL, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 298 | return ""; 299 | } 300 | int height = *reinterpret_cast(prop); 301 | XFree(prop); 302 | 303 | int size = width * height; 304 | 305 | if (size > maxSize) { 306 | maxSize = size; 307 | maxSizeOffset = offset; 308 | } 309 | 310 | if (width >= targetIconSize) { 311 | break; 312 | } 313 | 314 | offset += size + 2; 315 | } while(maxSize * 4 < bytes); 316 | 317 | if (XGetWindowProperty(this->display, win, property, maxSizeOffset, 1, 0, XA_CARDINAL, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 318 | return ""; 319 | } 320 | int width = *reinterpret_cast(prop); 321 | XFree(prop); 322 | 323 | if (XGetWindowProperty(this->display, win, property, maxSizeOffset + 1, 1, 0, XA_CARDINAL, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 324 | return ""; 325 | } 326 | int height = *reinterpret_cast(prop); 327 | XFree(prop); 328 | 329 | int size = width * height; 330 | 331 | if (XGetWindowProperty(this->display, win, property, maxSizeOffset + 2, size, 0, XA_CARDINAL, &actualType, &actualFormat, &itemsCount, &bytes, &prop) != Success) { 332 | return ""; 333 | } 334 | 335 | unsigned long* pixmap = reinterpret_cast(prop); 336 | std::vector imgData; 337 | imgData.reserve(size * 4); 338 | 339 | for (int i = 0; i < size; i++) { 340 | unsigned long pixel = *pixmap++; 341 | imgData.push_back((pixel & 0x00ff0000) >> 16); // r 342 | imgData.push_back((pixel & 0x0000ff00) >> 8); // g 343 | imgData.push_back((pixel & 0x000000ff)); // b 344 | imgData.push_back((pixel & 0xff000000) >> 24); // a 345 | } 346 | 347 | XFree(prop); 348 | 349 | std::vector png; 350 | if (lodepng::encode(png, imgData, width, height, LCT_RGBA, 8)) { 351 | return ""; 352 | } 353 | 354 | std::string pngStr(png.begin(), png.end()); 355 | 356 | return "data:image/png;base64," + base64_encode(pngStr); 357 | } 358 | 359 | void ActiveWindow::loadDesktopEntriesFromDirectory(std::string dir) { 360 | DIR* d = opendir(dir.c_str()); 361 | 362 | if (d == NULL) { 363 | return; 364 | } 365 | 366 | struct dirent* ent; 367 | while ((ent = readdir(d)) != NULL) { 368 | std::string file = ent->d_name; 369 | 370 | if (file == "." || file == ".." || file.find_last_of(".") == std::string::npos || file.substr(file.find_last_of(".")) != ".desktop") { 371 | continue; 372 | } 373 | 374 | CSimpleIni ini; 375 | ini.SetUnicode(); 376 | if (ini.LoadFile((dir + "/" + file).c_str()) < 0) { 377 | continue; 378 | } 379 | 380 | std::string wmClass = ini.GetValue("Desktop Entry", "StartupWMClass", ""); 381 | std::string appName = ini.GetValue("Desktop Entry", "Name", ""); 382 | std::string appIcon = ini.GetValue("Desktop Entry", "Icon", ""); 383 | 384 | if (appIcon == "") { 385 | // we only need apps that have icons 386 | continue; 387 | } 388 | 389 | std::string iconPath = this->resolveIconPath(appIcon); 390 | 391 | if (iconPath == "") { 392 | // we couldn't resolve the icon path 393 | continue; 394 | } 395 | 396 | // WM class should take precedence 397 | std::string indexBy = wmClass != "" ? wmClass : this->processStringForIndex(appName); 398 | 399 | if (this->appToIcon.find(indexBy) != this->appToIcon.end()) { 400 | // already indexed 401 | continue; 402 | } 403 | 404 | this->appToIcon[indexBy] = iconPath; 405 | } 406 | 407 | closedir(d); 408 | } 409 | 410 | std::string ActiveWindow::processStringForIndex(std::string str) { 411 | std::string result = str; 412 | std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { 413 | return std::tolower(c); 414 | }); 415 | 416 | std::replace(result.begin(), result.end(), ' ', '-'); 417 | std::replace(result.begin(), result.end(), '_', '-'); 418 | 419 | return result; 420 | } 421 | 422 | std::string ActiveWindow::resolveIconPath(std::string icon) { 423 | if (icon[0] == '/') { 424 | // we already have a path 425 | if (access(icon.c_str(), F_OK) != -1) { 426 | return icon; 427 | } 428 | 429 | return ""; 430 | } 431 | 432 | std::string userHome = this->safeGetEnv("HOME"); 433 | std::string xdgDataDirs = this->safeGetEnv("XDG_DATA_DIRS"); 434 | std::string iconPath = ""; 435 | 436 | if (userHome != "") { 437 | // legacy icon folder 438 | iconPath = this->tryResolveIconWithDirectory(icon, userHome + "/.icons"); 439 | if (iconPath != "") { 440 | return iconPath; 441 | } 442 | 443 | // user icon folder 444 | iconPath = this->tryResolveIconWithDirectory(icon, userHome + "/.local/share/icons"); 445 | if (iconPath != "") { 446 | return iconPath; 447 | } 448 | } 449 | 450 | if (xdgDataDirs != "") { 451 | // icon folders in XDG_DATA_DIRS 452 | size_t startPos = 0; 453 | size_t delimPos = xdgDataDirs.find(":"); 454 | while (delimPos != std::string::npos) { 455 | iconPath = this->tryResolveIconWithDirectory(icon, xdgDataDirs.substr(startPos, delimPos - startPos) + "/icons"); 456 | if (iconPath != "") { 457 | return iconPath; 458 | } 459 | 460 | startPos = delimPos + 1; 461 | delimPos = xdgDataDirs.find(":", startPos); 462 | } 463 | iconPath = this->tryResolveIconWithDirectory(icon, xdgDataDirs.substr(startPos, delimPos - startPos) + "/icons"); 464 | if (iconPath != "") { 465 | return iconPath; 466 | } 467 | } 468 | 469 | // pixmaps folder 470 | iconPath = this->tryResolveIconWithDirectory(icon, "/usr/share/pixmaps"); 471 | 472 | return iconPath; 473 | } 474 | 475 | std::string ActiveWindow::tryResolveIconWithDirectory(std::string icon, std::string dir) { 476 | if (!this->dirExists(dir)) { 477 | return ""; 478 | } 479 | 480 | std::string iconPath = ""; 481 | 482 | // check for file directly in folder 483 | iconPath = this->buildIconPath(icon, dir); 484 | if (access(iconPath.c_str(), F_OK) != -1) { 485 | return iconPath; 486 | } 487 | 488 | // try to fetch icons for popular themes 489 | iconPath = this->tryResolveIconWithTheme(icon, dir, "Adwaita"); 490 | if (iconPath != "") { 491 | return iconPath; 492 | } 493 | 494 | iconPath = this->tryResolveIconWithTheme(icon, dir, "breeze"); 495 | if (iconPath != "") { 496 | return iconPath; 497 | } 498 | 499 | iconPath = this->tryResolveIconWithTheme(icon, dir, "oxygen"); 500 | if (iconPath != "") { 501 | return iconPath; 502 | } 503 | 504 | iconPath = this->tryResolveIconWithTheme(icon, dir, "gnome"); 505 | if (iconPath != "") { 506 | return iconPath; 507 | } 508 | 509 | iconPath = this->tryResolveIconWithTheme(icon, dir, "hicolor"); 510 | 511 | return iconPath; 512 | } 513 | 514 | std::string ActiveWindow::tryResolveIconWithTheme(std::string icon, std::string dir, std::string theme) { 515 | if (!this->dirExists(dir + "/" + theme)) { 516 | return ""; 517 | } 518 | 519 | // loop through different scales 520 | std::string iconPath = ""; 521 | for (std::vector::const_iterator it = this->iconScalesPreference.begin(); it != this->iconScalesPreference.end(); it++) { 522 | iconPath = this->buildIconPath(icon, dir + "/" + theme + "/" + *it + "/apps"); 523 | if (access(iconPath.c_str(), F_OK) != -1) { 524 | return iconPath; 525 | } 526 | } 527 | 528 | return ""; 529 | } 530 | 531 | std::string ActiveWindow::buildIconPath(std::string icon, std::string dir) { 532 | if (icon.find_last_of(".") != std::string::npos && icon.substr(icon.find_last_of(".")) == ".png") { 533 | return dir + "/" + icon; 534 | } 535 | 536 | return dir + "/" + icon + ".png"; 537 | } 538 | 539 | bool ActiveWindow::dirExists(std::string dir) { 540 | DIR* d = opendir(dir.c_str()); 541 | 542 | if (d == NULL) { 543 | return false; 544 | } 545 | 546 | closedir(d); 547 | return true; 548 | } 549 | 550 | std::string ActiveWindow::safeGetEnv(const char* envVar) { 551 | char* value = std::getenv(envVar); 552 | 553 | if (value == nullptr) { 554 | return ""; 555 | } 556 | 557 | return value; 558 | } 559 | 560 | std::string ActiveWindow::encodeIcon(std::string iconPath) { 561 | std::string fileExtension = iconPath.substr(iconPath.find_last_of(".") + 1); 562 | 563 | if (fileExtension != "png" && fileExtension != "svg") { 564 | // not supported file extension (XPM) 565 | // only PNG, SVG and XPM are valid options, but we can't handle XPM yet 566 | // ALSO: extension must be lowercase, no need for case conversion 567 | // Ref: https://developer-old.gnome.org/icon-theme-spec/ 568 | return ""; 569 | } 570 | 571 | int fd = open(iconPath.c_str(), O_RDONLY); 572 | 573 | if (fd < 0) { 574 | return ""; 575 | } 576 | 577 | struct stat st; 578 | fstat(fd, &st); 579 | 580 | std::vector buf(st.st_size); 581 | 582 | int bytesRead = read(fd, buf.data(), st.st_size); 583 | 584 | if (bytesRead != st.st_size) { 585 | close(fd); 586 | return ""; 587 | } 588 | 589 | close(fd); 590 | 591 | std::string icon(buf.begin(), buf.end()); 592 | 593 | std::string contentType = fileExtension == "png" ? "data:image/png" : "data:image/svg+xml"; 594 | 595 | return contentType + ";base64," + base64_encode(icon); 596 | } 597 | 598 | void ActiveWindow::runWatchThread() { 599 | Atom activeWindowProperty = XInternAtom(this->display, "_NET_ACTIVE_WINDOW", true); 600 | Atom windowTitleProperty = XInternAtom(this->display, "_NET_WM_NAME", true); 601 | Window root = DefaultRootWindow(this->display); 602 | 603 | XSetWindowAttributes setAttributes; 604 | setAttributes.event_mask = PropertyChangeMask; 605 | 606 | XChangeWindowAttributes(this->display, root, CWEventMask, &setAttributes); 607 | 608 | XEvent event; 609 | while (!this->threadShouldExit.load(std::memory_order_relaxed)) { 610 | if (XEventsQueued(this->display, QueuedAfterFlush) > 0) { 611 | // handle all queued events 612 | while (XEventsQueued(this->display, QueuedAlready) > 0) { 613 | // handle event 614 | XNextEvent(this->display, &event); 615 | 616 | if (event.type != PropertyNotify || (event.xproperty.atom != activeWindowProperty && event.xproperty.atom != windowTitleProperty)) { 617 | // not the event we're interested in 618 | continue; 619 | } 620 | 621 | Window currentWindow = this->getFocusedWindow(); 622 | if (currentWindow != None) { 623 | XChangeWindowAttributes(this->display, currentWindow, CWEventMask, &setAttributes); 624 | } 625 | 626 | WindowInfo* info = this->getActiveWindow(); 627 | 628 | // notify every callback 629 | this->mutex.lock(); 630 | for (std::unordered_map::iterator it = this->watches.begin(); it != this->watches.end(); it++) { 631 | try { 632 | it->second(info); 633 | } 634 | catch (...) { 635 | // doing nothing 636 | } 637 | } 638 | this->mutex.unlock(); 639 | 640 | delete info; 641 | } 642 | } 643 | 644 | // sleep 100ms 645 | usleep(100000); 646 | } 647 | } 648 | 649 | int ActiveWindow::xErrorHandler(Display* display, XErrorEvent* error) { 650 | char buff[BUFSIZ]; 651 | 652 | XGetErrorText(display, error->error_code, buff, BUFSIZ); 653 | std::string errText(buff); 654 | 655 | XGetErrorDatabaseText(display, "XRequest", std::to_string((int)error->request_code).c_str(), "", buff, BUFSIZ); 656 | std::string majorCode(buff); 657 | 658 | XGetErrorDatabaseText(display, "XRequest", std::to_string((int)error->minor_code).c_str(), "", buff, BUFSIZ); 659 | std::string minorCode(buff); 660 | 661 | std::cerr<<"[WARN] PaymoActiveWindow::ActiveWindow: Cought XErrorEvent"<type<error_code<<" ("<request_code<<" ("<minor_code<<" ("< 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include "IconCache.h" 21 | #include "SimpleIni/SimpleIni.h" 22 | #include "base64/base64.h" 23 | #include "lodepng/lodepng.h" 24 | 25 | #ifndef _PAYMO_ACTIVEWINDOW_H 26 | #define _PAYMO_ACTIVEWINDOW_H 27 | 28 | namespace PaymoActiveWindow { 29 | struct WindowInfo { 30 | std::string title = ""; 31 | std::string application = ""; 32 | std::string path = ""; 33 | int pid = 0; 34 | std::string icon = ""; 35 | }; 36 | 37 | typedef unsigned int watch_t; 38 | typedef std::function watch_callback; 39 | const int targetIconSize = 128; 40 | 41 | class ActiveWindow { 42 | public: 43 | ActiveWindow(unsigned int iconCacheSize = 0); 44 | ~ActiveWindow(); 45 | WindowInfo* getActiveWindow(); 46 | void buildAppCache(); 47 | watch_t watchActiveWindow(watch_callback cb); 48 | void unwatchActiveWindow(watch_t watch); 49 | private: 50 | Display* display = NULL; 51 | std::unordered_map appToIcon; 52 | IconCache* iconCache = NULL; 53 | const std::vector iconScalesPreference = { "256x256", "48x48", "22x22" }; 54 | 55 | watch_t nextWatchId = 1; 56 | 57 | std::thread* watchThread = NULL; 58 | std::mutex mutex; 59 | std::atomic threadShouldExit; 60 | std::unordered_map watches; 61 | 62 | Window getFocusedWindow(); 63 | std::string getWindowTitle(Window win); 64 | std::string getApplicationName(Window win); 65 | pid_t getWindowPid(Window win); 66 | std::string getProcessPath(pid_t pid); 67 | std::string getIcon(std::string app, Window win); 68 | std::string getApplicationIcon(std::string app); 69 | std::string getWindowIcon(Window win); 70 | void loadDesktopEntriesFromDirectory(std::string dir); 71 | std::string processStringForIndex(std::string str); 72 | std::string resolveIconPath(std::string icon); 73 | std::string tryResolveIconWithDirectory(std::string icon, std::string dir); 74 | std::string tryResolveIconWithTheme(std::string icon, std::string dir, std::string theme); 75 | std::string buildIconPath(std::string icon, std::string dir); 76 | bool dirExists(std::string dir); 77 | std::string safeGetEnv(const char* envVar); 78 | std::string encodeIcon(std::string iconPath); 79 | void runWatchThread(); 80 | static int xErrorHandler(Display* display, XErrorEvent* error); 81 | }; 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /module/linux/src/IconCache.cpp: -------------------------------------------------------------------------------- 1 | #include "IconCache.h" 2 | 3 | namespace PaymoActiveWindow { 4 | IconCache::IconCache(unsigned int size) { 5 | this->size = size; 6 | } 7 | 8 | bool IconCache::has(const std::string* key) { 9 | const std::lock_guard lock(this->mutex); 10 | 11 | return this->keysRefs.find(*key) != this->keysRefs.end(); 12 | } 13 | 14 | void IconCache::set(const std::string* key, const std::string* value) { 15 | const std::lock_guard lock(this->mutex); 16 | 17 | this->refreshKey(key); 18 | this->data[*key] = *value; 19 | } 20 | 21 | std::string IconCache::get(const std::string* key) { 22 | const std::lock_guard lock(this->mutex); 23 | 24 | this->refreshKey(key); 25 | return this->data[*key]; 26 | } 27 | 28 | void IconCache::refreshKey(const std::string* key) { 29 | if (this->keysRefs.find(*key) == this->keysRefs.end()) { 30 | // key not prezent in cache 31 | if (this->keys.size() == this->size) { 32 | // limit reached, delete LRU element 33 | this->data.erase(this->keys.back()); 34 | this->keysRefs.erase(this->keys.back()); 35 | this->keys.pop_back(); 36 | } 37 | } else { 38 | // remove existing reference to item 39 | this->keys.erase(this->keysRefs[*key]); 40 | } 41 | 42 | // add item to front of the list 43 | this->keys.push_front(*key); 44 | this->keysRefs[*key] = this->keys.begin(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /module/linux/src/IconCache.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #ifndef _PAYMO_ICONCACHE_H 7 | #define _PAYMO_ICONCACHE_H 8 | 9 | namespace PaymoActiveWindow { 10 | class IconCache { 11 | public: 12 | IconCache(unsigned int size = 0); 13 | bool has(const std::string* key); 14 | void set(const std::string* key, const std::string* value); 15 | std::string get(const std::string* key); 16 | private: 17 | void refreshKey(const std::string* key); 18 | unsigned int size; 19 | std::list keys; 20 | std::unordered_map::iterator> keysRefs; 21 | std::unordered_map data; 22 | std::mutex mutex; 23 | }; 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /module/macos/demo/Makefile: -------------------------------------------------------------------------------- 1 | CXX=clang 2 | RM=rm -R 3 | 4 | LIBDIR=../src 5 | BUILDDIR=build 6 | 7 | CXXFLAGS=-ObjC++ -std=c++17 8 | LDOPTS=-lc++ -framework Foundation -framework AppKit -framework ApplicationServices 9 | 10 | SRCS=$(LIBDIR)/ActiveWindow.mm $(LIBDIR)/IconCache.cpp $(LIBDIR)/base64/base64.cpp 11 | OBJS=$(addsuffix .o,$(addprefix $(BUILDDIR)/,$(notdir $(basename $(SRCS))))) 12 | 13 | define build-cpp 14 | $(BUILDDIR)/$(basename $(notdir $(1))).o: $(1) $(addsuffix .h,$(basename $(1))) $(BUILDDIR)/ 15 | @echo Bulding $(1) 16 | $(CXX) $(CXXFLAGS) -c -o $(BUILDDIR)/$(basename $(notdir $(1))).o $(1) 17 | endef 18 | 19 | all: $(BUILDDIR)/demo 20 | 21 | $(BUILDDIR)/demo: $(BUILDDIR)/demo.o lib 22 | @echo Linking demo executable 23 | $(CXX) -o $@ $(OBJS) $(BUILDDIR)/demo.o $(LDOPTS) 24 | 25 | lib: $(OBJS) 26 | @echo Built library 27 | 28 | $(BUILDDIR)/demo.o: main.mm $(LIBDIR)/ActiveWindow.h $(BUILDDIR) 29 | @echo Building main.mm for demo 30 | $(CXX) $(CXXFLAGS) -c -o $@ $< 31 | 32 | $(foreach _t,$(SRCS),$(eval $(call build-cpp,$(_t)))) 33 | 34 | $(BUILDDIR)/: 35 | @echo Creating build directory 36 | @mkdir $(BUILDDIR) 37 | 38 | clean: 39 | $(RM) $(BUILDDIR) 40 | 41 | run: 42 | $(BUILDDIR)/demo $(MODE) 43 | -------------------------------------------------------------------------------- /module/macos/demo/main.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #include "../src/ActiveWindow.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void printWindowInfo(PaymoActiveWindow::WindowInfo* inf) { 9 | std::cout<<"Title: \""<title<<"\""<application<<"\""<path<<"\""<pid<<"\""<icon<getActiveWindow(); 20 | 21 | if (inf == NULL) { 22 | std::cout<<"Error: Could not get window info"<requestScreenCaptureAccess(); 42 | std::cout<<"The permission was: "<<(scaResp ? "granted" : "denied")<<"\n"; 43 | 44 | if (argc < 2) { 45 | // default mode 46 | pollWindowInfo(aw); 47 | 48 | std::cout<<"Now sleeping 3 seconds for you to move to an other window\n\n\n"; 49 | [NSThread sleepForTimeInterval:3.0f]; 50 | 51 | pollWindowInfo(aw); 52 | } 53 | else if (strcmp(argv[1], "loop") == 0) { 54 | // infinite loop mode 55 | std::cout<<"Printing window info in infinite loop. Ctrl+C to exit"<runLoop() all the time."<watchActiveWindow([](PaymoActiveWindow::WindowInfo* inf) { 72 | std::cout<<"[Notif] Active window has changed!\n"; 73 | 74 | if (inf == NULL) { 75 | std::cout<<"Empty"<unwatchActiveWindow(watchId); 92 | std::cout<<"Watch removed"<getActiveWindow(); 100 | delete inf; 101 | } 102 | clock_t end = clock(); 103 | 104 | double elapsedTime = (double)(end - start) / CLOCKS_PER_SEC; 105 | 106 | std::cout<<"Elapsed clocks: "<<(end - start)<<"\nElapsed seconds: "< 2 | #include "module.h" 3 | 4 | Napi::Object InitAll(Napi::Env env, Napi::Object exports) { 5 | module::Init(env, exports); 6 | 7 | return exports; 8 | } 9 | 10 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll); 11 | -------------------------------------------------------------------------------- /module/macos/napi/module.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../src/ActiveWindow.h" 3 | 4 | #ifndef _PAYMO_MODULE_H 5 | #define _PAYMO_MODULE_H 6 | 7 | namespace module { 8 | void Init(Napi::Env env, Napi::Object exports); 9 | 10 | Napi::Value getActiveWindow(const Napi::CallbackInfo& info); 11 | Napi::Value subscribe(const Napi::CallbackInfo& info); 12 | void unsubscribe(const Napi::CallbackInfo& info); 13 | Napi::Value requestPermissions(const Napi::CallbackInfo& info); 14 | void runLoop(const Napi::CallbackInfo& info); 15 | 16 | // helpers 17 | Napi::Object encodeWindowInfo(Napi::Env env, PaymoActiveWindow::WindowInfo* windowInfo); 18 | void tsfnMainThreadCallback(Napi::Env env, Napi::Function jsCallback, void* ctx, PaymoActiveWindow::WindowInfo* data); 19 | } 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /module/macos/napi/module.mm: -------------------------------------------------------------------------------- 1 | #include "module.h" 2 | 3 | void module::Init(Napi::Env env, Napi::Object exports) { 4 | env.SetInstanceData(new PaymoActiveWindow::ActiveWindow(15)); 5 | 6 | exports.Set("getActiveWindow", Napi::Function::New(env)); 7 | exports.Set("subscribe", Napi::Function::New(env)); 8 | exports.Set("unsubscribe", Napi::Function::New(env)); 9 | exports.Set("requestPermissions", Napi::Function::New(env)); 10 | exports.Set("runLoop", Napi::Function::New(env)); 11 | } 12 | 13 | Napi::Value module::getActiveWindow(const Napi::CallbackInfo& info) { 14 | Napi::Env env = info.Env(); 15 | 16 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 17 | if (activeWindow == NULL) { 18 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 19 | return env.Null(); 20 | } 21 | 22 | PaymoActiveWindow::WindowInfo* windowInfo = activeWindow->getActiveWindow(); 23 | if (windowInfo == NULL) { 24 | Napi::Error::New(env, "Failed to get active window").ThrowAsJavaScriptException(); 25 | return env.Null(); 26 | } 27 | 28 | Napi::Object result = encodeWindowInfo(env, windowInfo); 29 | 30 | delete windowInfo; 31 | 32 | return result; 33 | } 34 | 35 | Napi::Value module::subscribe(const Napi::CallbackInfo& info) { 36 | Napi::Env env = info.Env(); 37 | 38 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 39 | if (activeWindow == NULL) { 40 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 41 | return env.Null(); 42 | } 43 | 44 | if (info.Length() != 1) { 45 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 46 | return env.Null(); 47 | } 48 | 49 | if (!info[0].IsFunction()) { 50 | Napi::TypeError::New(env, "Expected first argument to be function").ThrowAsJavaScriptException(); 51 | return env.Null(); 52 | } 53 | 54 | Napi::TypedThreadSafeFunction tsfn = Napi::TypedThreadSafeFunction< 55 | void, 56 | PaymoActiveWindow::WindowInfo, 57 | tsfnMainThreadCallback 58 | >::New(env, info[0].As(), "Active Window Callback", 0, 1); 59 | 60 | PaymoActiveWindow::watch_t watchId = activeWindow->watchActiveWindow([tsfn](PaymoActiveWindow::WindowInfo* windowInfo) { 61 | if (windowInfo == NULL) { 62 | tsfn.BlockingCall((PaymoActiveWindow::WindowInfo*)NULL); 63 | return; 64 | } 65 | 66 | // clone window info 67 | PaymoActiveWindow::WindowInfo* arg = new PaymoActiveWindow::WindowInfo(); 68 | *arg = *windowInfo; 69 | 70 | tsfn.BlockingCall(arg); 71 | }); 72 | 73 | return Napi::Number::New(env, watchId); 74 | } 75 | 76 | void module::unsubscribe(const Napi::CallbackInfo& info) { 77 | Napi::Env env = info.Env(); 78 | 79 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 80 | if (activeWindow == NULL) { 81 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 82 | return; 83 | } 84 | 85 | if (info.Length() != 1) { 86 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 87 | return; 88 | } 89 | 90 | if (!info[0].IsNumber()) { 91 | Napi::TypeError::New(env, "Expected first argument to be number").ThrowAsJavaScriptException(); 92 | return; 93 | } 94 | 95 | PaymoActiveWindow::watch_t watchId = info[0].As().Uint32Value(); 96 | 97 | activeWindow->unwatchActiveWindow(watchId); 98 | } 99 | 100 | void module::runLoop(const Napi::CallbackInfo& info) { 101 | Napi::Env env = info.Env(); 102 | 103 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 104 | if (activeWindow == NULL) { 105 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 106 | return; 107 | } 108 | 109 | activeWindow->runLoop(); 110 | } 111 | 112 | Napi::Value module::requestPermissions(const Napi::CallbackInfo& info) { 113 | Napi::Env env = info.Env(); 114 | 115 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 116 | if (activeWindow == NULL) { 117 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 118 | return env.Null(); 119 | } 120 | 121 | bool hasPermission = activeWindow->requestScreenCaptureAccess(); 122 | 123 | return Napi::Boolean::New(env, hasPermission); 124 | } 125 | 126 | Napi::Object module::encodeWindowInfo(Napi::Env env, PaymoActiveWindow::WindowInfo* windowInfo) { 127 | Napi::Object result = Napi::Object::New(env); 128 | 129 | result.Set("title", Napi::String::New(env, windowInfo->title)); 130 | result.Set("application", Napi::String::New(env, windowInfo->application)); 131 | result.Set("path", Napi::String::New(env, windowInfo->path)); 132 | result.Set("pid", Napi::Number::New(env, windowInfo->pid)); 133 | result.Set("icon", Napi::String::New(env, windowInfo->icon)); 134 | 135 | return result; 136 | } 137 | 138 | void module::tsfnMainThreadCallback(Napi::Env env, Napi::Function jsCallback, void* ctx, PaymoActiveWindow::WindowInfo* data) { 139 | if (data == NULL) { 140 | jsCallback.Call({ env.Null() }); 141 | } 142 | else { 143 | jsCallback.Call({ encodeWindowInfo(env, data) }); 144 | delete data; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /module/macos/src/ActiveWindow.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "IconCache.h" 9 | #include "base64/base64.h" 10 | 11 | #ifndef _PAYMO_ACTIVEWINDOW_H 12 | #define _PAYMO_ACTIVEWINDOW_H 13 | 14 | namespace PaymoActiveWindow { 15 | struct WindowInfo { 16 | std::string title = ""; 17 | std::string application = ""; 18 | std::string path = ""; 19 | int pid = 0; 20 | std::string icon = ""; 21 | }; 22 | 23 | typedef unsigned int watch_t; 24 | typedef std::function watch_callback; 25 | 26 | class ActiveWindow { 27 | public: 28 | ActiveWindow(unsigned int iconCacheSize = 0); 29 | ~ActiveWindow(); 30 | WindowInfo* getActiveWindow(); 31 | bool requestScreenCaptureAccess(); 32 | watch_t watchActiveWindow(watch_callback cb); 33 | void unwatchActiveWindow(watch_t watch); 34 | void runLoop(); 35 | private: 36 | bool hasScreenCaptureAccess = false; 37 | 38 | std::vector observers; 39 | IconCache* iconCache = NULL; 40 | 41 | watch_t nextWatchId = 1; 42 | 43 | std::map watches; 44 | 45 | std::string getAppIcon(NSImage* img, const std::string* path); 46 | std::string encodeNSImage(NSImage* img); 47 | std::string getWindowTitle(int pid); 48 | void registerObservers(); 49 | void handleNotification(); 50 | }; 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /module/macos/src/ActiveWindow.mm: -------------------------------------------------------------------------------- 1 | #include "ActiveWindow.h" 2 | 3 | namespace PaymoActiveWindow { 4 | ActiveWindow::ActiveWindow(unsigned int iconCacheSize) { 5 | this->registerObservers(); 6 | 7 | if (iconCacheSize > 0) { 8 | this->iconCache = new IconCache(iconCacheSize); 9 | } 10 | } 11 | 12 | ActiveWindow::~ActiveWindow() { 13 | // remove observers 14 | NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; 15 | NSNotificationCenter* notificationCenter = [workspace notificationCenter]; 16 | 17 | for (std::vector::iterator it = this->observers.begin(); it != this->observers.end(); it++) { 18 | [notificationCenter removeObserver:*it]; 19 | } 20 | 21 | delete this->iconCache; 22 | this->iconCache = NULL; 23 | } 24 | 25 | WindowInfo* ActiveWindow::getActiveWindow() { 26 | NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; 27 | NSRunningApplication* frontApp = [workspace frontmostApplication]; 28 | 29 | if (frontApp == nil) { 30 | return NULL; 31 | } 32 | 33 | WindowInfo* info = new WindowInfo(); 34 | 35 | info->application = [frontApp.localizedName UTF8String]; 36 | info->path = [frontApp.executableURL.path UTF8String]; 37 | info->pid = frontApp.processIdentifier; 38 | info->icon = this->getAppIcon(frontApp.icon, &info->path); 39 | 40 | info->title = this->getWindowTitle(info->pid); 41 | 42 | return info; 43 | } 44 | 45 | bool ActiveWindow::requestScreenCaptureAccess() { 46 | if (@available(macOS 11, *)) { 47 | // this api SHOULD work on 10.15 as well, 48 | // but it doesn't: https://developer.apple.com/forums/thread/683860 49 | this->hasScreenCaptureAccess = CGPreflightScreenCaptureAccess(); 50 | 51 | if (!this->hasScreenCaptureAccess) { 52 | // request 53 | this->hasScreenCaptureAccess = CGRequestScreenCaptureAccess(); 54 | } 55 | } 56 | else if (@available(macOS 10.15, *)) { 57 | // this is a hack to check if screen capture access is granted on catalina. 58 | // Source: https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/58985069#58985069 59 | this->hasScreenCaptureAccess = false; 60 | 61 | NSRunningApplication* currentApp = NSRunningApplication.currentApplication; 62 | int pid = currentApp.processIdentifier; 63 | 64 | CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); 65 | for (NSDictionary* window in (NSArray*)windows) { 66 | NSNumber* windowPid = window[(id)kCGWindowOwnerPID]; 67 | int wPid = [windowPid intValue]; 68 | 69 | if (wPid == pid) { 70 | // we can always access our own process 71 | continue; 72 | } 73 | 74 | NSString* windowTitle = window[(id)kCGWindowName]; 75 | 76 | if (windowTitle != nil) { 77 | this->hasScreenCaptureAccess = true; 78 | break; 79 | } 80 | } 81 | } 82 | else { 83 | // on older versions there's no permission for this 84 | this->hasScreenCaptureAccess = true; 85 | } 86 | 87 | return this->hasScreenCaptureAccess; 88 | } 89 | 90 | watch_t ActiveWindow::watchActiveWindow(watch_callback cb) { 91 | watch_t watchId = this->nextWatchId++; 92 | 93 | this->watches[watchId] = cb; 94 | 95 | return watchId; 96 | } 97 | 98 | void ActiveWindow::unwatchActiveWindow(watch_t watch) { 99 | this->watches.erase(watch); 100 | } 101 | 102 | void ActiveWindow::runLoop() { 103 | // run RunLoop for 0.1 ms or until first event is handled 104 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0001, true); 105 | } 106 | 107 | std::string ActiveWindow::getAppIcon(NSImage* img, const std::string* path) { 108 | if (this->iconCache != NULL && this->iconCache->has(path)) { 109 | return this->iconCache->get(path); 110 | } 111 | 112 | std::string icon = this->encodeNSImage(img); 113 | 114 | if (this->iconCache != NULL) { 115 | this->iconCache->set(path, &icon); 116 | } 117 | 118 | return icon; 119 | } 120 | 121 | std::string ActiveWindow::encodeNSImage(NSImage* img) { 122 | CGImageRef cgRef = [img CGImageForProposedRect:NULL context:nil hints:nil]; 123 | NSBitmapImageRep* imgRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef]; 124 | [imgRep setSize:[img size]]; 125 | NSData* pngData = [imgRep representationUsingType:NSBitmapImageFileTypePNG properties:[NSDictionary dictionary]]; 126 | 127 | unsigned int len = [pngData length]; 128 | std::vector buf(len); 129 | memcpy(buf.data(), [pngData bytes], len); 130 | 131 | std::string pngImg(buf.begin(), buf.end()); 132 | 133 | return "data:image/png;base64," + base64_encode(pngImg); 134 | } 135 | 136 | std::string ActiveWindow::getWindowTitle(int pid) { 137 | if (!this->hasScreenCaptureAccess) { 138 | // no title information without screen recording permission 139 | return ""; 140 | } 141 | 142 | CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); 143 | NSString* windowTitle = nil; 144 | 145 | for (NSDictionary* window in (NSArray*)windows) { 146 | NSNumber* windowPid = window[(id)kCGWindowOwnerPID]; 147 | int wPid = [windowPid intValue]; 148 | 149 | if (wPid != pid) { 150 | // not our window 151 | continue; 152 | } 153 | 154 | if (window[(id)kCGWindowName]) { 155 | // we have a title and since we iterate from top to bottom 156 | // this is the title we're looking for 157 | windowTitle = window[(id)kCGWindowName]; 158 | 159 | if (windowTitle.length == 0) { 160 | // sometimes a popup is open and it does not have title 161 | // try to get title for the window under it 162 | continue; 163 | } 164 | 165 | break; 166 | } 167 | } 168 | 169 | std::string title(windowTitle != nil ? [windowTitle UTF8String] : ""); 170 | CFRelease(windows); 171 | 172 | return title; 173 | } 174 | 175 | void ActiveWindow::registerObservers() { 176 | NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; 177 | NSNotificationCenter* notificationCenter = [workspace notificationCenter]; 178 | 179 | this->observers.push_back([notificationCenter addObserverForName:NSWorkspaceDidActivateApplicationNotification object:nil queue:nil usingBlock:^(NSNotification* notification) { 180 | this->handleNotification(); 181 | }]); 182 | } 183 | 184 | void ActiveWindow::handleNotification() { 185 | WindowInfo* info = this->getActiveWindow(); 186 | 187 | for (std::map::iterator it = this->watches.begin(); it != this->watches.end(); it++) { 188 | try { 189 | it->second(info); 190 | } 191 | catch (...) { 192 | // doing nothing 193 | } 194 | } 195 | 196 | delete info; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /module/macos/src/IconCache.cpp: -------------------------------------------------------------------------------- 1 | #include "IconCache.h" 2 | 3 | namespace PaymoActiveWindow { 4 | IconCache::IconCache(unsigned int size) { 5 | this->size = size; 6 | } 7 | 8 | bool IconCache::has(const std::string* key) { 9 | const std::lock_guard lock(this->mutex); 10 | 11 | return this->keysRefs.find(*key) != this->keysRefs.end(); 12 | } 13 | 14 | void IconCache::set(const std::string* key, const std::string* value) { 15 | const std::lock_guard lock(this->mutex); 16 | 17 | this->refreshKey(key); 18 | this->data[*key] = *value; 19 | } 20 | 21 | std::string IconCache::get(const std::string* key) { 22 | const std::lock_guard lock(this->mutex); 23 | 24 | this->refreshKey(key); 25 | return this->data[*key]; 26 | } 27 | 28 | void IconCache::refreshKey(const std::string* key) { 29 | if (this->keysRefs.find(*key) == this->keysRefs.end()) { 30 | // key not prezent in cache 31 | if (this->keys.size() == this->size) { 32 | // limit reached, delete LRU element 33 | this->data.erase(this->keys.back()); 34 | this->keysRefs.erase(this->keys.back()); 35 | this->keys.pop_back(); 36 | } 37 | } else { 38 | // remove existing reference to item 39 | this->keys.erase(this->keysRefs[*key]); 40 | } 41 | 42 | // add item to front of the list 43 | this->keys.push_front(*key); 44 | this->keysRefs[*key] = this->keys.begin(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /module/macos/src/IconCache.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #ifndef _PAYMO_ICONCACHE_H 7 | #define _PAYMO_ICONCACHE_H 8 | 9 | namespace PaymoActiveWindow { 10 | class IconCache { 11 | public: 12 | IconCache(unsigned int size = 0); 13 | bool has(const std::string* key); 14 | void set(const std::string* key, const std::string* value); 15 | std::string get(const std::string* key); 16 | private: 17 | void refreshKey(const std::string* key); 18 | unsigned int size; 19 | std::list keys; 20 | std::unordered_map::iterator> keysRefs; 21 | std::unordered_map data; 22 | std::mutex mutex; 23 | }; 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /module/windows/demo/Makefile: -------------------------------------------------------------------------------- 1 | LIBDIR=..\src 2 | BUILDDIR=build 3 | 4 | CLFLAGS=/std:c++17 5 | LIBS=User32.lib Shell32.lib Version.lib Shlwapi.lib Gdiplus.lib Gdi32.lib Windowsapp.lib 6 | 7 | all: create_dir demo 8 | 9 | demo: $(BUILDDIR)\demo.exe 10 | 11 | $(BUILDDIR)\demo.exe: $(BUILDDIR)\ActiveWindow.obj $(BUILDDIR)\IconCache.obj $(BUILDDIR)\base64.obj $(BUILDDIR)\GdiPlusUtils.obj $(BUILDDIR)\main.obj 12 | @echo Linking demo.exe 13 | link /out:$(BUILDDIR)\demo.exe $(BUILDDIR)\*.obj $(LIBS) 14 | 15 | $(BUILDDIR)\main.obj: main.cpp 16 | @echo Building main.cpp for demo 17 | cl /c /EHsc $(CLFLAGS) /Fo$(BUILDDIR)\ main.cpp 18 | 19 | $(BUILDDIR)\ActiveWindow.obj: $(LIBDIR)\ActiveWindow.cpp $(LIBDIR)\ActiveWindow.h 20 | @echo Building $(LIBDIR)\ActiveWindow.cpp 21 | cl /c /EHsc $(CLFLAGS) /Fo$(BUILDDIR)\ $(LIBDIR)\ActiveWindow.cpp 22 | 23 | $(BUILDDIR)\IconCache.obj: $(LIBDIR)\IconCache.cpp $(LIBDIR)\IconCache.h 24 | @echo Building $(LIBDIR)\IconCache.cpp 25 | cl /c /EHsc $(CLFLAGS) /Fo$(BUILDDIR)\ $(LIBDIR)\IconCache.cpp 26 | 27 | $(BUILDDIR)\GdiPlusUtils.obj: $(LIBDIR)\GdiPlusUtils.cpp $(LIBDIR)\GdiPlusUtils.h 28 | @echo Building $(LIBDIR)\GdiPlusUtils.cpp 29 | cl /c /EHsc $(CLFLAGS) /Fo$(BUILDDIR)\ $(LIBDIR)\GdiPlusUtils.cpp 30 | 31 | $(BUILDDIR)\base64.obj: $(LIBDIR)\base64\base64.cpp $(LIBDIR)\base64\base64.h 32 | @echo Building $(LIBDIR)\base64\base64.cpp 33 | cl /c /EHsc $(CLFLAGS) /Fo$(BUILDDIR)\ $(LIBDIR)\base64\base64.cpp 34 | 35 | create_dir: 36 | @if not exist $(BUILDDIR) mkdir $(BUILDDIR) 37 | 38 | clean: 39 | @if exist $(BUILDDIR) rmdir /s /q $(BUILDDIR) 40 | 41 | run: 42 | $(BUILDDIR)\demo.exe $(MODE) 43 | -------------------------------------------------------------------------------- /module/windows/demo/main.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/ActiveWindow.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | void printWindowInfo(PaymoActiveWindow::WindowInfo* inf) { 8 | std::wcout<title<application<path<pid<<"\""<isUWPApp ? "true" : "false")<uwpPackage<icon<getActiveWindow(); 21 | 22 | if (inf == NULL) { 23 | std::cout<<"Error: Could not get window info"<watchActiveWindow([](PaymoActiveWindow::WindowInfo* inf) { 79 | std::cout<<"[Notif] Active window has changed!\n"; 80 | 81 | if (inf == NULL) { 82 | std::cout<<"Empty"<unwatchActiveWindow(watchId); 98 | std::cout<<"Watch removed"<getActiveWindow(); 106 | delete inf; 107 | } 108 | double end = getCpuTime(); 109 | 110 | std::cout<<"Elapsed CPU seconds: "<<(end - start)< 2 | #include "module.h" 3 | 4 | Napi::Object InitAll(Napi::Env env, Napi::Object exports) { 5 | module::Init(env, exports); 6 | 7 | return exports; 8 | } 9 | 10 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll); 11 | -------------------------------------------------------------------------------- /module/windows/napi/module.cpp: -------------------------------------------------------------------------------- 1 | #include "module.h" 2 | 3 | void module::Init(Napi::Env env, Napi::Object exports) { 4 | env.SetInstanceData(new PaymoActiveWindow::ActiveWindow(15)); 5 | 6 | exports.Set("getActiveWindow", Napi::Function::New(env)); 7 | exports.Set("subscribe", Napi::Function::New(env)); 8 | exports.Set("unsubscribe", Napi::Function::New(env)); 9 | } 10 | 11 | Napi::Value module::getActiveWindow(const Napi::CallbackInfo& info) { 12 | Napi::Env env = info.Env(); 13 | 14 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 15 | if (activeWindow == NULL) { 16 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 17 | return env.Null(); 18 | } 19 | 20 | PaymoActiveWindow::WindowInfo* windowInfo = activeWindow->getActiveWindow(); 21 | if (windowInfo == NULL) { 22 | Napi::Error::New(env, "Failed to get active window").ThrowAsJavaScriptException(); 23 | return env.Null(); 24 | } 25 | 26 | Napi::Object result = encodeWindowInfo(env, windowInfo); 27 | 28 | delete windowInfo; 29 | 30 | return result; 31 | } 32 | 33 | Napi::Value module::subscribe(const Napi::CallbackInfo& info) { 34 | Napi::Env env = info.Env(); 35 | 36 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 37 | if (activeWindow == NULL) { 38 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 39 | return env.Null(); 40 | } 41 | 42 | if (info.Length() != 1) { 43 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 44 | return env.Null(); 45 | } 46 | 47 | if (!info[0].IsFunction()) { 48 | Napi::TypeError::New(env, "Expected first argument to be function").ThrowAsJavaScriptException(); 49 | return env.Null(); 50 | } 51 | 52 | Napi::TypedThreadSafeFunction tsfn = Napi::TypedThreadSafeFunction< 53 | void, 54 | PaymoActiveWindow::WindowInfo, 55 | tsfnMainThreadCallback 56 | >::New(env, info[0].As(), "Active Window Callback", 0, 1); 57 | 58 | PaymoActiveWindow::watch_t watchId = activeWindow->watchActiveWindow([tsfn](PaymoActiveWindow::WindowInfo* windowInfo) { 59 | if (windowInfo == NULL) { 60 | tsfn.BlockingCall((PaymoActiveWindow::WindowInfo*)NULL); 61 | return; 62 | } 63 | 64 | // clone window info 65 | PaymoActiveWindow::WindowInfo* arg = new PaymoActiveWindow::WindowInfo(); 66 | *arg = *windowInfo; 67 | 68 | tsfn.BlockingCall(arg); 69 | }); 70 | 71 | return Napi::Number::New(env, watchId); 72 | } 73 | 74 | void module::unsubscribe(const Napi::CallbackInfo& info) { 75 | Napi::Env env = info.Env(); 76 | 77 | PaymoActiveWindow::ActiveWindow* activeWindow = env.GetInstanceData(); 78 | if (activeWindow == NULL) { 79 | Napi::Error::New(env, "ActiveWindow module not initialized").ThrowAsJavaScriptException(); 80 | return; 81 | } 82 | 83 | if (info.Length() != 1) { 84 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 85 | return; 86 | } 87 | 88 | if (!info[0].IsNumber()) { 89 | Napi::TypeError::New(env, "Expected first argument to be number").ThrowAsJavaScriptException(); 90 | return; 91 | } 92 | 93 | PaymoActiveWindow::watch_t watchId = info[0].As().Uint32Value(); 94 | 95 | activeWindow->unwatchActiveWindow(watchId); 96 | } 97 | 98 | Napi::Object module::encodeWindowInfo(Napi::Env env, PaymoActiveWindow::WindowInfo* windowInfo) { 99 | Napi::Object result = Napi::Object::New(env); 100 | 101 | result.Set("title", Napi::String::New(env, std::u16string(windowInfo->title.begin(), windowInfo->title.end()))); 102 | result.Set("application", Napi::String::New(env, std::u16string(windowInfo->application.begin(), windowInfo->application.end()))); 103 | result.Set("path", Napi::String::New(env, std::u16string(windowInfo->path.begin(), windowInfo->path.end()))); 104 | result.Set("pid", Napi::Number::New(env, windowInfo->pid)); 105 | result.Set("windows.isUWPApp", Napi::Boolean::New(env, windowInfo->isUWPApp)); 106 | result.Set("windows.uwpPackage", Napi::String::New(env, std::u16string(windowInfo->uwpPackage.begin(), windowInfo->uwpPackage.end()))); 107 | result.Set("icon", Napi::String::New(env, windowInfo->icon)); 108 | 109 | return result; 110 | } 111 | 112 | void module::tsfnMainThreadCallback(Napi::Env env, Napi::Function jsCallback, void* ctx, PaymoActiveWindow::WindowInfo* data) { 113 | if (data == NULL) { 114 | jsCallback.Call({ env.Null() }); 115 | } 116 | else { 117 | jsCallback.Call({ encodeWindowInfo(env, data) }); 118 | delete data; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /module/windows/napi/module.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../src/ActiveWindow.h" 4 | 5 | #ifndef _PAYMO_MODULE_H 6 | #define _PAYMO_MODULE_H 7 | 8 | namespace module { 9 | void Init(Napi::Env env, Napi::Object exports); 10 | 11 | Napi::Value getActiveWindow(const Napi::CallbackInfo& info); 12 | Napi::Value subscribe(const Napi::CallbackInfo& info); 13 | void unsubscribe(const Napi::CallbackInfo& info); 14 | 15 | // helpers 16 | Napi::Object encodeWindowInfo(Napi::Env env, PaymoActiveWindow::WindowInfo* windowInfo); 17 | void tsfnMainThreadCallback(Napi::Env env, Napi::Function jsCallback, void* ctx, PaymoActiveWindow::WindowInfo* data); 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /module/windows/src/ActiveWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "ActiveWindow.h" 2 | 3 | namespace PaymoActiveWindow { 4 | std::mutex ActiveWindow::smutex; 5 | std::unordered_map ActiveWindow::winEventProcCbCtx; 6 | 7 | ActiveWindow::ActiveWindow(unsigned int iconCacheSize) { 8 | // initialize GDI+ 9 | Gdiplus::GdiplusStartupInput gdiPlusStartupInput; 10 | Gdiplus::GdiplusStartup(&this->gdiPlusToken, &gdiPlusStartupInput, NULL); 11 | if (GdiPlusUtils::GetEncoderClsId(L"image/png", &this->gdiPlusEncoder) < 0) { 12 | throw std::logic_error("Failed to get GDI+ encoder"); 13 | } 14 | 15 | // initialize COM 16 | CoInitializeEx(NULL, COINIT_MULTITHREADED); 17 | 18 | if (iconCacheSize > 0) { 19 | this->iconCache = new IconCache(iconCacheSize); 20 | } 21 | } 22 | 23 | ActiveWindow::~ActiveWindow() { 24 | // stop watch thread 25 | if (this->watchThread != NULL) { 26 | this->threadShouldExit.store(true, std::memory_order_relaxed); 27 | this->watchThread->join(); 28 | delete this->watchThread; 29 | this->watchThread = NULL; 30 | } 31 | 32 | delete this->iconCache; 33 | this->iconCache = NULL; 34 | 35 | // tear down GDI+ 36 | Gdiplus::GdiplusShutdown(this->gdiPlusToken); 37 | 38 | // tear down COM 39 | CoUninitialize(); 40 | } 41 | 42 | WindowInfo* ActiveWindow::getActiveWindow() { 43 | HWND h = GetForegroundWindow(); 44 | 45 | if (h == NULL) { 46 | return NULL; 47 | } 48 | 49 | WindowInfo* info = new WindowInfo(); 50 | 51 | // get window title 52 | info->title = this->getWindowTitle(h); 53 | 54 | // get process pid 55 | DWORD pid; 56 | GetWindowThreadProcessId(h, &pid); 57 | info->pid = (unsigned int)pid; 58 | 59 | // get process handle 60 | HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid); 61 | 62 | if (hProc == NULL) { 63 | delete info; 64 | return NULL; 65 | } 66 | 67 | // get app path 68 | info->path = this->getProcessPath(hProc); 69 | CloseHandle(hProc); 70 | 71 | // check if app is UWP app 72 | if (this->isUWPApp(info->path)) { 73 | info->isUWPApp = true; 74 | 75 | EnumChildWindowsCbParam* cbParam = new EnumChildWindowsCbParam(this); 76 | EnumChildWindows(h, EnumChildWindowsCb, (LPARAM)cbParam); 77 | 78 | if (!cbParam->ok) { 79 | delete cbParam; 80 | delete info; 81 | return NULL; 82 | } 83 | 84 | info->path = cbParam->path; 85 | // save handle of UWP process 86 | hProc = cbParam->hProc; 87 | delete cbParam; 88 | } 89 | 90 | // get app name 91 | info->application = this->getProcessName(info->path); 92 | if (info->application.size() == 0) { 93 | info->application = this->basename(info->path); 94 | } 95 | 96 | if (info->isUWPApp) { 97 | info->uwpPackage = this->getUWPPackage(hProc); 98 | info->icon = this->getUWPIcon(hProc); 99 | 100 | // we need to close the handle of the UWP process 101 | CloseHandle(hProc); 102 | } 103 | else { 104 | // get window icon 105 | info->icon = this->getWindowIcon(info->path); 106 | } 107 | 108 | return info; 109 | } 110 | 111 | watch_t ActiveWindow::watchActiveWindow(watch_callback cb) { 112 | watch_t watchId = this->nextWatchId++; 113 | 114 | this->mutex.lock(); 115 | this->watches[watchId] = cb; 116 | this->mutex.unlock(); 117 | 118 | // register hook if not registered 119 | if (this->watchThread == NULL) { 120 | this->threadShouldExit.store(false, std::memory_order_relaxed); 121 | this->watchThread = new std::thread(&ActiveWindow::runWatchThread, this); 122 | } 123 | 124 | return watchId; 125 | } 126 | 127 | void ActiveWindow::unwatchActiveWindow(watch_t watch) { 128 | this->mutex.lock(); 129 | this->watches.erase(watch); 130 | this->mutex.unlock(); 131 | } 132 | 133 | std::wstring ActiveWindow::getWindowTitle(HWND hWindow) { 134 | int len = GetWindowTextLengthW(hWindow); 135 | 136 | if(!len) { 137 | return L""; 138 | } 139 | 140 | std::vector buf(len + 1); 141 | if (!GetWindowTextW(hWindow, buf.data(), len + 1)) { 142 | return L""; 143 | } 144 | std::wstring title(buf.begin(), buf.begin() + len); 145 | 146 | return title; 147 | } 148 | 149 | std::wstring ActiveWindow::getProcessPath(HANDLE hProc) { 150 | std::vector buf(MAX_PATH); 151 | DWORD len = MAX_PATH; 152 | if (!QueryFullProcessImageNameW(hProc, 0, buf.data(), &len)) { 153 | return L""; 154 | } 155 | std::wstring name(buf.begin(), buf.begin() + len); 156 | 157 | return name; 158 | } 159 | 160 | std::wstring ActiveWindow::getProcessName(std::wstring path) { 161 | DWORD infoSize = GetFileVersionInfoSizeW(path.c_str(), NULL); 162 | 163 | if (!infoSize) { 164 | return L""; 165 | } 166 | 167 | LPBYTE data = new BYTE[infoSize]; 168 | if (!GetFileVersionInfoW(path.c_str(), 0, infoSize, data)) { 169 | delete data; 170 | return L""; 171 | } 172 | 173 | struct LANGCODEPAGE { 174 | WORD lang; 175 | WORD codePage; 176 | } *langData, active; 177 | 178 | active.lang = 0x0409; 179 | active.codePage = 0x04E4; 180 | 181 | UINT langDataLen = 0; 182 | if (VerQueryValueW(data, L"\\VarFileInfo\\Translation", (void**)&langData, &langDataLen)) { 183 | if (langDataLen) { 184 | active.lang = langData[0].lang; 185 | active.codePage = langData[0].codePage; 186 | } 187 | } 188 | 189 | // build path to query file description 190 | std::wstringstream localePath; 191 | std::ios_base::fmtflags flags(localePath.flags()); 192 | 193 | localePath<iconCache != NULL && this->iconCache->has(&path)) { 219 | return this->iconCache->get(&path); 220 | } 221 | 222 | HICON hIcon = this->getHighResolutionIcon(path); 223 | 224 | if (hIcon == NULL) { 225 | return ""; 226 | } 227 | 228 | IStream* pngStream = this->getPngFromIcon(hIcon); 229 | if (pngStream == NULL) { 230 | return ""; 231 | } 232 | 233 | std::string iconBase64 = this->encodeImageStream(pngStream); 234 | 235 | pngStream->Release(); 236 | 237 | if (iconBase64 == "") { 238 | return ""; 239 | } 240 | 241 | std::string icon = "data:image/png;base64," + iconBase64; 242 | 243 | if (this->iconCache != NULL) { 244 | this->iconCache->set(&path, &icon); 245 | } 246 | 247 | return icon; 248 | } 249 | 250 | std::string ActiveWindow::getUWPIcon(HANDLE hProc) { 251 | std::wstring pkgPath = this->getUWPPackagePath(hProc); 252 | 253 | if (pkgPath == L"") { 254 | return ""; 255 | } 256 | 257 | if (this->iconCache != NULL && this->iconCache->has(&pkgPath)) { 258 | return this->iconCache->get(&pkgPath); 259 | } 260 | 261 | IAppxManifestProperties* properties = this->getUWPPackageProperties(pkgPath); 262 | 263 | if (properties == NULL) { 264 | return ""; 265 | } 266 | 267 | LPWSTR logo = NULL; 268 | properties->GetStringValue(L"Logo", &logo); 269 | properties->Release(); 270 | std::wstring logoPath = pkgPath + L"\\" + logo; 271 | 272 | if (!PathFileExistsW(logoPath.c_str())) { 273 | // we need to use scale 100 274 | size_t dotPos = logoPath.find_last_of(L"."); 275 | logoPath.insert(dotPos, L".scale-100"); 276 | } 277 | 278 | IStream* pngStream = NULL; 279 | if (FAILED(SHCreateStreamOnFileEx(logoPath.c_str(), STGM_READ | STGM_SHARE_EXCLUSIVE, 0, FALSE, NULL, &pngStream))) { 280 | return ""; 281 | } 282 | 283 | std::string iconBase64 = this->encodeImageStream(pngStream); 284 | 285 | pngStream->Release(); 286 | 287 | if (iconBase64 == "") { 288 | return ""; 289 | } 290 | 291 | std::string icon = "data:image/png;base64," + iconBase64; 292 | 293 | if (this->iconCache != NULL) { 294 | this->iconCache->set(&pkgPath, &icon); 295 | } 296 | 297 | return icon; 298 | } 299 | 300 | std::wstring ActiveWindow::getUWPPackage(HANDLE hProc) { 301 | UINT32 len = 0; 302 | GetPackageFamilyName(hProc, &len, NULL); 303 | 304 | if (!len) { 305 | return L""; 306 | } 307 | 308 | std::vector buf(len); 309 | if (GetPackageFamilyName(hProc, &len, buf.data()) != ERROR_SUCCESS) { 310 | return L""; 311 | } 312 | 313 | std::wstring package(buf.begin(), buf.begin() + len - 1); 314 | 315 | return package; 316 | } 317 | 318 | std::wstring ActiveWindow::basename(std::wstring path) { 319 | size_t lastSlash = path.find_last_of(L"\\"); 320 | 321 | if (lastSlash == std::string::npos) { 322 | return path; 323 | } 324 | 325 | return path.substr(lastSlash + 1); 326 | } 327 | 328 | bool ActiveWindow::isUWPApp(std::wstring path) { 329 | return this->basename(path) == L"ApplicationFrameHost.exe"; 330 | } 331 | 332 | HICON ActiveWindow::getHighResolutionIcon(std::wstring path) { 333 | // get file info 334 | SHFILEINFOW fileInfo; 335 | if ((HANDLE)SHGetFileInfoW(path.c_str(), 0, &fileInfo, sizeof(fileInfo), SHGFI_SYSICONINDEX) == INVALID_HANDLE_VALUE) { 336 | return NULL; 337 | } 338 | 339 | // get jumbo icon list 340 | IImageList* imgList; 341 | if (FAILED(SHGetImageList(SHIL_JUMBO, IID_PPV_ARGS(&imgList)))) { 342 | return NULL; 343 | } 344 | 345 | // get first icon 346 | HICON hIcon; 347 | if (FAILED(imgList->GetIcon(fileInfo.iIcon, ILD_TRANSPARENT, &hIcon))) { 348 | imgList->Release(); 349 | return NULL; 350 | } 351 | 352 | imgList->Release(); 353 | 354 | return hIcon; 355 | } 356 | 357 | IStream* ActiveWindow::getPngFromIcon(HICON hIcon) { 358 | // convert icon to bitmap 359 | ICONINFO iconInf; 360 | if (!GetIconInfo(hIcon, &iconInf)) { 361 | return NULL; 362 | } 363 | 364 | BITMAP bmp; 365 | if (!GetObject(iconInf.hbmColor, sizeof(bmp), &bmp)) { 366 | return NULL; 367 | } 368 | 369 | Gdiplus::Bitmap tmp(iconInf.hbmColor, NULL); 370 | Gdiplus::BitmapData lockedBitmapData; 371 | Gdiplus::Rect rect(0, 0, tmp.GetWidth(), tmp.GetHeight()); 372 | 373 | if (tmp.LockBits(&rect, Gdiplus::ImageLockModeRead, tmp.GetPixelFormat(), &lockedBitmapData) != Gdiplus::Ok) { 374 | return NULL; 375 | } 376 | 377 | // get bitmap with transparency 378 | Gdiplus::Bitmap image(lockedBitmapData.Width, lockedBitmapData.Height, lockedBitmapData.Stride, PixelFormat32bppARGB, reinterpret_cast(lockedBitmapData.Scan0)); 379 | tmp.UnlockBits(&lockedBitmapData); 380 | 381 | // convert image to png 382 | IStream* pngStream = SHCreateMemStream(NULL, 0); 383 | if (pngStream == NULL) { 384 | return NULL; 385 | } 386 | 387 | Gdiplus::Status stat = image.Save(pngStream, &this->gdiPlusEncoder, NULL); 388 | 389 | // prepare stream for reading 390 | pngStream->Commit(STGC_DEFAULT); 391 | LARGE_INTEGER seekPos; 392 | seekPos.QuadPart = 0; 393 | pngStream->Seek(seekPos, STREAM_SEEK_SET, NULL); 394 | 395 | if (stat == Gdiplus::Ok) { 396 | return pngStream; 397 | } 398 | 399 | // failed to save to stream 400 | pngStream->Release(); 401 | return NULL; 402 | } 403 | 404 | std::wstring ActiveWindow::getUWPPackagePath(HANDLE hProc) { 405 | UINT32 pkgIdLen = 0; 406 | GetPackageId(hProc, &pkgIdLen, NULL); 407 | BYTE* pkgId = new BYTE[pkgIdLen]; 408 | GetPackageId(hProc, &pkgIdLen, pkgId); 409 | 410 | UINT32 len = 0; 411 | GetPackagePath((PACKAGE_ID*)pkgId, 0, &len, NULL); 412 | 413 | std::vector buf(len); 414 | if (GetPackagePath((PACKAGE_ID*)pkgId, 0, &len, buf.data()) != ERROR_SUCCESS) { 415 | delete pkgId; 416 | return L""; 417 | } 418 | 419 | std::wstring pkgPath(buf.begin(), buf.begin() + len - 1); 420 | 421 | delete pkgId; 422 | return pkgPath; 423 | } 424 | 425 | IAppxManifestProperties* ActiveWindow::getUWPPackageProperties(std::wstring pkgPath) { 426 | IAppxFactory* appxFactory = NULL; 427 | if (FAILED(CoCreateInstance(__uuidof(AppxFactory), NULL, CLSCTX_INPROC_SERVER, __uuidof(IAppxFactory), (LPVOID*)&appxFactory))) { 428 | return NULL; 429 | } 430 | 431 | IStream* manifestStream; 432 | std::wstring manifestPath = pkgPath + L"\\AppxManifest.xml"; 433 | if (FAILED(SHCreateStreamOnFileEx(manifestPath.c_str(), STGM_READ | STGM_SHARE_EXCLUSIVE, 0, FALSE, NULL, &manifestStream))) { 434 | appxFactory->Release(); 435 | return NULL; 436 | } 437 | 438 | IAppxManifestReader* manifestReader = NULL; 439 | if (FAILED(appxFactory->CreateManifestReader(manifestStream, &manifestReader))) { 440 | appxFactory->Release(); 441 | manifestStream->Release(); 442 | return NULL; 443 | } 444 | 445 | IAppxManifestProperties* properties = NULL; 446 | if (FAILED(manifestReader->GetProperties(&properties))) { 447 | appxFactory->Release(); 448 | manifestStream->Release(); 449 | manifestReader->Release(); 450 | return NULL; 451 | } 452 | 453 | appxFactory->Release(); 454 | manifestStream->Release(); 455 | manifestReader->Release(); 456 | return properties; 457 | } 458 | 459 | std::string ActiveWindow::encodeImageStream(IStream* pngStream) { 460 | // get stream size 461 | STATSTG streamStat; 462 | pngStream->Stat(&streamStat, STATFLAG_NONAME); 463 | 464 | // convert stream to string 465 | std::vector buf(streamStat.cbSize.QuadPart); 466 | ULONG read = 0; 467 | pngStream->Read((void*)buf.data(), streamStat.cbSize.QuadPart, &read); 468 | 469 | if (read == 0) { 470 | return ""; 471 | } 472 | 473 | std::string str(buf.begin(), buf.end()); 474 | return base64_encode(str); 475 | } 476 | 477 | void ActiveWindow::runWatchThread() { 478 | HWINEVENTHOOK hHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_OBJECT_NAMECHANGE, NULL, WinEventProcCb, 0, 0, WINEVENT_OUTOFCONTEXT); 479 | 480 | // store context for callback 481 | ActiveWindow::smutex.lock(); 482 | ActiveWindow::winEventProcCbCtx[hHook] = this; 483 | ActiveWindow::smutex.unlock(); 484 | 485 | MSG msg; 486 | UINT_PTR timer = SetTimer(NULL, NULL, 500, nullptr); // run message loop at least every 500 ms 487 | 488 | for (;;) { 489 | BOOL getMsgRet = GetMessage(&msg, NULL, 0, 0); 490 | 491 | if (getMsgRet == -1) { 492 | continue; 493 | } 494 | 495 | if (msg.message = WM_TIMER) { 496 | // check if we should exit 497 | if (this->threadShouldExit.load(std::memory_order_relaxed)) { 498 | break; 499 | } 500 | } 501 | 502 | TranslateMessage(&msg); 503 | DispatchMessage(&msg); 504 | } 505 | 506 | // remove timer 507 | KillTimer(NULL, timer); 508 | 509 | // remove hook 510 | UnhookWinEvent(hHook); 511 | 512 | // remove context 513 | ActiveWindow::smutex.lock(); 514 | ActiveWindow::winEventProcCbCtx.erase(hHook); 515 | ActiveWindow::smutex.unlock(); 516 | } 517 | 518 | BOOL CALLBACK ActiveWindow::EnumChildWindowsCb(HWND hWindow, LPARAM param) { 519 | EnumChildWindowsCbParam* cbParam = (EnumChildWindowsCbParam*)param; 520 | 521 | DWORD pid; 522 | GetWindowThreadProcessId(hWindow, &pid); 523 | HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid); 524 | 525 | if (hProc == NULL) { 526 | return true; 527 | } 528 | 529 | cbParam->path = cbParam->aw->getProcessPath(hProc); 530 | cbParam->hProc = hProc; 531 | 532 | UINT32 _len = 0; 533 | if (GetPackageFamilyName(cbParam->hProc, &_len, NULL) == APPMODEL_ERROR_NO_PACKAGE) { 534 | CloseHandle(hProc); 535 | return true; 536 | } 537 | 538 | cbParam->ok = true; 539 | return false; 540 | } 541 | 542 | VOID CALLBACK ActiveWindow::WinEventProcCb(HWINEVENTHOOK hHook, DWORD event, HWND hWindow, LONG idObject, LONG idChild, DWORD eventThread, DWORD eventTime) { 543 | if (event != EVENT_SYSTEM_FOREGROUND && event != EVENT_OBJECT_NAMECHANGE) { 544 | // not interested in these 545 | return; 546 | } 547 | 548 | HWND foregroundWindow = GetForegroundWindow(); 549 | 550 | if (event == EVENT_OBJECT_NAMECHANGE && hWindow != foregroundWindow) { 551 | // name changed, but not for current window 552 | return; 553 | } 554 | 555 | // get context 556 | ActiveWindow::smutex.lock(); 557 | ActiveWindow* aw = ActiveWindow::winEventProcCbCtx[hHook]; 558 | ActiveWindow::smutex.unlock(); 559 | 560 | WindowInfo* info = aw->getActiveWindow(); 561 | 562 | // notify every callback 563 | aw->mutex.lock(); 564 | for (std::unordered_map::iterator it = aw->watches.begin(); it != aw->watches.end(); it++) { 565 | try { 566 | it->second(info); 567 | } 568 | catch (...) { 569 | // doing nothing 570 | } 571 | } 572 | aw->mutex.unlock(); 573 | 574 | delete info; 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /module/windows/src/ActiveWindow.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "IconCache.h" 18 | #include "GdiPlusUtils.h" 19 | #include "base64/base64.h" 20 | 21 | #ifndef _PAYMO_ACTIVEWINDOW_H 22 | #define _PAYMO_ACTIVEWINDOW_H 23 | 24 | namespace PaymoActiveWindow { 25 | struct WindowInfo { 26 | std::wstring title = L""; 27 | std::wstring application = L""; 28 | std::wstring path = L""; 29 | unsigned int pid = 0; 30 | bool isUWPApp = false; 31 | std::wstring uwpPackage = L""; 32 | std::string icon = ""; 33 | }; 34 | 35 | typedef unsigned int watch_t; 36 | typedef std::function watch_callback; 37 | 38 | class ActiveWindow { 39 | public: 40 | ActiveWindow(unsigned int iconCacheSize = 0); 41 | ~ActiveWindow(); 42 | WindowInfo* getActiveWindow(); 43 | watch_t watchActiveWindow(watch_callback cb); 44 | void unwatchActiveWindow(watch_t watch); 45 | private: 46 | ULONG_PTR gdiPlusToken; 47 | CLSID gdiPlusEncoder; 48 | IconCache* iconCache = NULL; 49 | 50 | watch_t nextWatchId = 1; 51 | 52 | std::thread* watchThread = NULL; 53 | std::mutex mutex; 54 | std::atomic threadShouldExit; 55 | std::unordered_map watches; 56 | 57 | static std::mutex smutex; 58 | static std::unordered_map winEventProcCbCtx; 59 | 60 | std::wstring getWindowTitle(HWND hWindow); 61 | std::wstring getProcessPath(HANDLE hProc); 62 | std::wstring getProcessName(std::wstring path); 63 | std::string getWindowIcon(std::wstring path); 64 | std::string getUWPIcon(HANDLE hProc); 65 | std::wstring getUWPPackage(HANDLE hProc); 66 | std::wstring basename(std::wstring path); 67 | bool isUWPApp(std::wstring path); 68 | HICON getHighResolutionIcon(std::wstring path); 69 | IStream* getPngFromIcon(HICON hIcon); 70 | std::wstring getUWPPackagePath(HANDLE hProc); 71 | IAppxManifestProperties* getUWPPackageProperties(std::wstring pkgPath); 72 | std::wstring getUWPLargestIconPath(std::wstring iconPath); 73 | std::string encodeImageStream(IStream* pngStream); 74 | void runWatchThread(); 75 | static BOOL CALLBACK EnumChildWindowsCb(HWND hWindow, LPARAM param); 76 | static VOID CALLBACK WinEventProcCb(HWINEVENTHOOK hHook, DWORD event, HWND hWindow, LONG idObject, LONG idChild, DWORD eventThread, DWORD eventTime); 77 | }; 78 | 79 | struct EnumChildWindowsCbParam { 80 | ActiveWindow* aw; 81 | std::wstring path = L""; 82 | HANDLE hProc; 83 | bool ok = false; 84 | 85 | EnumChildWindowsCbParam(ActiveWindow* aw) { 86 | this->aw = aw; 87 | } 88 | }; 89 | } 90 | 91 | #endif 92 | -------------------------------------------------------------------------------- /module/windows/src/GdiPlusUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "GdiPlusUtils.h" 2 | 3 | namespace GdiPlusUtils { 4 | int GetEncoderClsId(const WCHAR* format, CLSID* pClsid) { 5 | UINT num = 0; 6 | UINT size = 0; 7 | 8 | Gdiplus::ImageCodecInfo* pImageCodecInfo = NULL; 9 | 10 | Gdiplus::GetImageEncodersSize(&num, &size); 11 | if (size == 0) { 12 | return -1; 13 | } 14 | 15 | pImageCodecInfo = (Gdiplus::ImageCodecInfo*)(malloc(size)); 16 | if (pImageCodecInfo == NULL) { 17 | return -1; 18 | } 19 | 20 | Gdiplus::GetImageEncoders(num, size, pImageCodecInfo); 21 | 22 | for (UINT j = 0; j < num; j++) { 23 | if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) { 24 | *pClsid = pImageCodecInfo[j].Clsid; 25 | free(pImageCodecInfo); 26 | return j; 27 | } 28 | } 29 | 30 | free(pImageCodecInfo); 31 | return -1; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /module/windows/src/GdiPlusUtils.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifndef _PAYMO_GDIPLUSUTILS_H 5 | #define _PAYMO_GDIPLUSUTILS_H 6 | 7 | namespace GdiPlusUtils { 8 | int GetEncoderClsId(const WCHAR* format, CLSID* pClsid); 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /module/windows/src/IconCache.cpp: -------------------------------------------------------------------------------- 1 | #include "IconCache.h" 2 | 3 | namespace PaymoActiveWindow { 4 | IconCache::IconCache(unsigned int size) { 5 | this->size = size; 6 | } 7 | 8 | bool IconCache::has(const std::wstring* key) { 9 | const std::lock_guard lock(this->mutex); 10 | 11 | return this->keysRefs.find(*key) != this->keysRefs.end(); 12 | } 13 | 14 | void IconCache::set(const std::wstring* key, const std::string* value) { 15 | const std::lock_guard lock(this->mutex); 16 | 17 | this->refreshKey(key); 18 | this->data[*key] = *value; 19 | } 20 | 21 | std::string IconCache::get(const std::wstring* key) { 22 | const std::lock_guard lock(this->mutex); 23 | 24 | this->refreshKey(key); 25 | return this->data[*key]; 26 | } 27 | 28 | void IconCache::refreshKey(const std::wstring* key) { 29 | if (this->keysRefs.find(*key) == this->keysRefs.end()) { 30 | // key not prezent in cache 31 | if (this->keys.size() == this->size) { 32 | // limit reached, delete LRU element 33 | this->data.erase(this->keys.back()); 34 | this->keysRefs.erase(this->keys.back()); 35 | this->keys.pop_back(); 36 | } 37 | } else { 38 | // remove existing reference to item 39 | this->keys.erase(this->keysRefs[*key]); 40 | } 41 | 42 | // add item to front of the list 43 | this->keys.push_front(*key); 44 | this->keysRefs[*key] = this->keys.begin(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /module/windows/src/IconCache.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #ifndef _PAYMO_ICONCACHE_H 7 | #define _PAYMO_ICONCACHE_H 8 | 9 | namespace PaymoActiveWindow { 10 | class IconCache { 11 | public: 12 | IconCache(unsigned int size = 0); 13 | bool has(const std::wstring* key); 14 | void set(const std::wstring* key, const std::string* value); 15 | std::string get(const std::wstring* key); 16 | private: 17 | void refreshKey(const std::wstring* key); 18 | unsigned int size; 19 | std::list keys; 20 | std::unordered_map::iterator> keysRefs; 21 | std::unordered_map data; 22 | std::mutex mutex; 23 | }; 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@paymoapp/active-window", 3 | "version": "2.1.2", 4 | "description": "NodeJS library using native modules to get the active window and some metadata (including the application icon) on Windows, MacOS and Linux.", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "install": "prebuild-install -r napi || node-gyp rebuild", 9 | "demo": "ts-node --project tsconfig.json demo/index.ts", 10 | "build": "npm run clean && npm run build:ts && npm run build:gyp", 11 | "build:ts": "tsc --project tsconfig.build.json", 12 | "build:gyp": "node-gyp rebuild", 13 | "prebuild": "prebuild --all -r napi --strip", 14 | "clean": "node-gyp clean && rimraf dist", 15 | "typecheck": "tsc --noEmit", 16 | "lint": "esw --color --ext .ts src demo", 17 | "lint:fix": "esw --color --fix --ext .ts src demo", 18 | "lint:watch": "esw --color --fix --watch --cache --ext .ts src demo", 19 | "format": "prettier --write src demo", 20 | "release": "standard-version", 21 | "release:pre": "standard-version --prerelease", 22 | "generate:readme-toc": "markdown-toc -i --bullets=\"-\" --maxdepth=4 README.md" 23 | }, 24 | "binary": { 25 | "napi_versions": [ 26 | 6 27 | ] 28 | }, 29 | "files": [ 30 | "binding.gyp", 31 | "dist/", 32 | "module/" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/paymoapp/node-active-window" 37 | }, 38 | "keywords": [ 39 | "node", 40 | "native", 41 | "active", 42 | "window" 43 | ], 44 | "author": "Paymo SRL", 45 | "license": "MIT", 46 | "devDependencies": { 47 | "@types/node": "^17.0.35", 48 | "@types/ws": "^8.5.3", 49 | "@typescript-eslint/eslint-plugin": "^5.26.0", 50 | "@typescript-eslint/parser": "^5.26.0", 51 | "eslint": "^8.16.0", 52 | "eslint-config-prettier": "^8.5.0", 53 | "eslint-plugin-import": "^2.26.0", 54 | "eslint-plugin-prettier": "^4.0.0", 55 | "eslint-watch": "^8.0.0", 56 | "markdown-toc": "^1.2.0", 57 | "node-gyp": "^9.0.0", 58 | "prebuild": "^13.0.1", 59 | "prettier": "^2.6.2", 60 | "rimraf": "^3.0.2", 61 | "standard-version": "^9.5.0", 62 | "ts-node": "^10.8.1", 63 | "typescript": "^4.6.4", 64 | "ws": "^8.7.0" 65 | }, 66 | "dependencies": { 67 | "node-addon-api": "^5.0.0", 68 | "prebuild-install": "^7.1.2" 69 | }, 70 | "standard-version": { 71 | "scripts": { 72 | "prerelease": "git fetch --all --tags" 73 | }, 74 | "types": [ 75 | { 76 | "type": "feat", 77 | "section": "Features" 78 | }, 79 | { 80 | "type": "fix", 81 | "section": "Bug Fixes" 82 | }, 83 | { 84 | "type": "imp", 85 | "section": "Improvements" 86 | }, 87 | { 88 | "type": "ci", 89 | "section": "Build/CI" 90 | }, 91 | { 92 | "type": "chore", 93 | "hidden": true 94 | }, 95 | { 96 | "type": "docs", 97 | "section": "Documentation" 98 | }, 99 | { 100 | "type": "refactor", 101 | "section": "Refactor" 102 | }, 103 | { 104 | "type": "test", 105 | "section": "Testing" 106 | } 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Module, 3 | NativeWindowInfo, 4 | WindowInfo, 5 | IActiveWindow, 6 | InitializeOptions 7 | } from './types'; 8 | 9 | const SUPPORTED_PLATFORMS = ['win32', 'linux', 'darwin']; 10 | 11 | let addon: Module | undefined; 12 | 13 | if (SUPPORTED_PLATFORMS.includes(process.platform)) { 14 | addon = require('../build/Release/PaymoActiveWindow.node'); // eslint-disable-line import/no-dynamic-require 15 | } else { 16 | throw new Error( 17 | `Unsupported platform. The supported platforms are: ${SUPPORTED_PLATFORMS.join( 18 | ',' 19 | )}` 20 | ); 21 | } 22 | 23 | class ActiveWindowClass implements IActiveWindow { 24 | private options: InitializeOptions = {}; 25 | 26 | private encodeWindowInfo(info: NativeWindowInfo): WindowInfo { 27 | return { 28 | title: info.title, 29 | application: info.application, 30 | path: info.path, 31 | pid: info.pid, 32 | icon: info.icon, 33 | ...(process.platform == 'win32' 34 | ? { 35 | windows: { 36 | isUWPApp: info['windows.isUWPApp'] || false, 37 | uwpPackage: info['windows.uwpPackage'] || '' 38 | } 39 | } 40 | : {}) 41 | }; 42 | } 43 | 44 | public initialize(options: InitializeOptions = {}): void { 45 | this.options = options; 46 | 47 | if (!addon) { 48 | throw new Error('Failed to load native addon'); 49 | } 50 | 51 | if (addon.initialize) { 52 | addon.initialize(); 53 | } 54 | 55 | // set up runloop on MacOS 56 | if (process.platform == 'darwin' && this.options.osxRunLoop == 'all') { 57 | const interval = setInterval(() => { 58 | if (addon && addon.runLoop) { 59 | addon.runLoop(); 60 | } else { 61 | clearInterval(interval); 62 | } 63 | }, 100); 64 | } 65 | } 66 | 67 | public requestPermissions(): boolean { 68 | if (!addon) { 69 | throw new Error('Failed to load native addon'); 70 | } 71 | 72 | if (addon.requestPermissions) { 73 | return addon.requestPermissions(); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | public getActiveWindow(): WindowInfo { 80 | if (!addon) { 81 | throw new Error('Failed to load native addon'); 82 | } 83 | 84 | // use runloop on MacOS if requested 85 | if ( 86 | process.platform == 'darwin' && 87 | this.options.osxRunLoop && 88 | addon.runLoop 89 | ) { 90 | addon.runLoop(); 91 | } 92 | 93 | const info = addon.getActiveWindow(); 94 | 95 | return this.encodeWindowInfo(info); 96 | } 97 | 98 | public subscribe( 99 | callback: (windowInfo: WindowInfo | null) => void 100 | ): number { 101 | if (!addon) { 102 | throw new Error('Failed to load native addon'); 103 | } 104 | 105 | const watchId = addon.subscribe(nativeWindowInfo => { 106 | callback( 107 | !nativeWindowInfo 108 | ? null 109 | : this.encodeWindowInfo(nativeWindowInfo) 110 | ); 111 | }); 112 | 113 | return watchId; 114 | } 115 | 116 | public unsubscribe(watchId: number): void { 117 | if (!addon) { 118 | throw new Error('Failed to load native addon'); 119 | } 120 | 121 | if (watchId < 0) { 122 | throw new Error('Watch ID must be a positive number'); 123 | } 124 | 125 | addon.unsubscribe(watchId); 126 | } 127 | } 128 | 129 | const ActiveWindow = new ActiveWindowClass(); 130 | 131 | export * from './types'; 132 | export { ActiveWindow }; 133 | export default ActiveWindow; 134 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface NativeWindowInfo { 2 | title: string; 3 | application: string; 4 | path: string; 5 | pid: number; 6 | icon: string; 7 | 'windows.isUWPApp'?: boolean; 8 | 'windows.uwpPackage'?: string; 9 | } 10 | 11 | export interface WindowInfo { 12 | title: string; 13 | application: string; 14 | path: string; 15 | pid: number; 16 | icon: string; 17 | windows?: { 18 | isUWPApp: boolean; 19 | uwpPackage: string; 20 | }; 21 | } 22 | 23 | export interface Module { 24 | getActiveWindow(): T; 25 | subscribe(callback: (windowInfo: T | null) => void): number; 26 | unsubscribe(watchId: number): void; 27 | initialize?(): void; 28 | requestPermissions?(): boolean; 29 | runLoop?(): void; 30 | } 31 | 32 | export interface InitializeOptions { 33 | osxRunLoop?: false | 'get' | 'all'; 34 | } 35 | 36 | export interface IActiveWindow extends Omit, 'runLoop'> { 37 | initialize(opts?: InitializeOptions): void; 38 | requestPermissions(): boolean; 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["esnext", "DOM"], 6 | "allowJs": true, 7 | "rootDirs": ["src"], 8 | "outDir": "dist", 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "isolatedModules": true, 16 | "declaration": true, 17 | "typeRoots": ["./typings", "./node_modules/@types"] 18 | }, 19 | "include": ["src", "demo"] 20 | } 21 | --------------------------------------------------------------------------------