Skip to content

Commit e7a44b7

Browse files
Merge pull request #82 from bluecadet/feature/window-management
Feature/window management
2 parents 5ba86f9 + d9b0d14 commit e7a44b7

File tree

8 files changed

+457
-1468
lines changed

8 files changed

+457
-1468
lines changed

.changeset/poor-islands-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bluecadet/launchpad-monitor": minor
3+
---
4+
5+
Swapped window management engine for fewer dependencies. Removed 'fake key' setting.

package-lock.json

Lines changed: 355 additions & 948 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/monitor/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ Global options for how window order should be managed.
133133
| - | - | - | - |
134134
| <a name="module_monitor-options.WindowsApiOptions+nodeVersion">`nodeVersion`</a> | <code>string</code>| <code>'>=17.4.0'</code> | The minimum major node version to support window ordering.<br>Node versions < 17 seem to have a fatal bug with the native<br>API, which will intermittently cause V8 to crash hard.<br><br>See: https://github.com/node-ffi-napi/ref-napi/issues/54#issuecomment-1029639256 |
135135
| <a name="module_monitor-options.WindowsApiOptions+debounceDelay">`debounceDelay`</a> | <code>number</code>| <code>3000</code> | The delay until windows are ordered after launch of in ms.<br><br>If your app takes a long time to open all of its windows, set this number to a higher value to ensure it can be on top of the launchpad terminal window.<br><br>Keeping this high also reduces the CPU load if apps relaunch often. |
136-
| <a name="module_monitor-options.WindowsApiOptions+fakeKey">`fakeKey`</a> | <code>string</code>| <code>'control'</code> | Windows OS is very strict with when and how apps can move windows to the foreground or backgruond. As a workaround, Launchpad emulates a keypress to make the current process active.<br><br>This setting configures which key is used to emulate in order to gain control over window foregrounding/backgrounding. This key gets emulated after an app launches or re-launches.<br><br>See: https://robotjs.io/docs/syntax#keys for available options, https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-allowsetforegroundwindow#remarks for window management requirements. |
137136

138137

139138
### AppLogOptions

packages/monitor/lib/launchpad-monitor.js

Lines changed: 12 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import chalk from 'chalk';
66
import autoBind from 'auto-bind';
77
import pDebounce from 'p-debounce';
88
import pm2 from 'pm2';
9-
import semver from 'semver';
109
import { spawn } from 'cross-spawn';
1110
import { SubEmitterSocket } from 'axon'; // used by PM2
1211

1312
import { LogManager, Logger } from '@bluecadet/launchpad-utils';
1413
import AppLogRouter from './app-log-router.js';
1514
import { AppLogOptions, MonitorOptions, WindowOptions } from './monitor-options.js';
15+
import sortWindows, { SortApp } from './utils/sort-windows.js';
1616

1717
export class LaunchpadMonitor {
1818
/** @type {MonitorOptions} */
@@ -37,11 +37,6 @@ export class LaunchpadMonitor {
3737
*/
3838
_pm2Bus = null;
3939

40-
/**
41-
* @type {Object}
42-
*/
43-
_windowsApi = null;
44-
4540
/**
4641
* Creates a new instance, starts it with the
4742
* config and resolves with the monitor instance.
@@ -392,76 +387,25 @@ export class LaunchpadMonitor {
392387
* @returns {Promise}
393388
*/
394389
async _applyWindowSettings(appNames = null) {
395-
396-
if (!this._isWindowsOS()) {
397-
this._logger.warn(`Not applying windows settings since this is only supported on Windows OS.`);
398-
return;
399-
}
400-
401-
const currVersion = process.version;
402-
const requVersion = this._config.windowsApi.nodeVersion;
403-
404-
if (!semver.satisfies(currVersion, requVersion)) {
405-
this._logger.warn(`Not applying window settings since your node version '${currVersion}' doesn't satisfy the required version '${requVersion}'. Please upgrade node to apply window settings like foreground/minimize/hide.`);
406-
return;
407-
}
408-
409390
appNames = this._validateAppNames(appNames);
410-
411-
this._logger.info(`Applying window settings to apps: ${appNames}...`);
412-
413-
let windowsApi = null;
414-
try {
415-
windowsApi = await this._getWindowsApi();
416-
} catch (err) {
417-
this._logger.error(`Could not retrieve Windows API libraries. Make sure optional deps are installed: 'npm i robotjs ffi-napi ref-napi'`, err);
418-
}
419-
420-
const fgPids = [];
421-
const minPids = [];
422-
const hidePids = [];
391+
const apps = [];
423392

424393
for (const appName of appNames) {
425-
const appOptions = this.getAppOptions(appName);
426-
const winOptions = appOptions.windows;
427-
const appProcess = await this.getAppProcess(appName);
394+
const sortApp = new SortApp();
395+
sortApp.options = this.getAppOptions(appName);
428396

429-
if (!appProcess || appProcess.pm2_env.status !== 'online') {
430-
this._logger.warn(`Not applying window settings to ${appName} because it's not online.`);
431-
return appProcess;
397+
try {
398+
const process = await this.getAppProcess(appName);
399+
sortApp.pid = process.pid;
400+
} catch (error) {
401+
this._logger.error(`Could not get process for app ${appName}`);
402+
continue;
432403
}
433404

434-
const appLabel = `${appName} (pid: ${appProcess.pid})`;
435-
436-
if (winOptions.foreground) {
437-
this._logger.debug(`...foregrounding ${appLabel}`);
438-
fgPids.push(appProcess.pid);
439-
}
440-
if (winOptions.minimize) {
441-
this._logger.debug(`...minimizing ${appLabel}`);
442-
minPids.push(appProcess.pid);
443-
}
444-
if (winOptions.hide) {
445-
this._logger.debug(`...hiding ${appLabel}`);
446-
hidePids.push(appProcess.pid);
447-
}
405+
apps.push(sortApp);
448406
}
449407

450-
windowsApi.sortWindows(fgPids, minPids, hidePids);
451-
452-
this._logger.debug(`...done applying window settings.`);
453-
}
454-
455-
async _getWindowsApi() {
456-
if (!this._windowsApi) {
457-
// Importing at runtime allows optional dependencies for non-Windows platforms
458-
this._windowsApi = await import('./windows-api.js');
459-
}
460-
return this._windowsApi;
461-
}
462-
463-
_isWindowsOS() {
464-
return process.platform === 'win32';
408+
return sortWindows(apps, this._logger, this._config.windowsApi.nodeVersion);
465409
}
466410

467411
async _connectPm2Bus() {

packages/monitor/lib/monitor-options.js

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ export class AppOptions {
5454
* @default null
5555
*/
5656
this.pm2 = pm2;
57+
5758
/**
5859
* Optional settings for moving this app's main windows to the foreground, minimize or hide them.
5960
* @type {WindowOptions}
6061
* @default new WindowOptions()
6162
*/
6263
this.windows = windows;
64+
6365
/**
6466
* Optional settings for how to log this app's output.
6567
* @type {AppLogOptions}
@@ -84,12 +86,14 @@ export class WindowOptions {
8486
* @default false
8587
*/
8688
this.foreground = foreground;
89+
8790
/**
8891
* Minimize this app's windows once all apps have been launched.
8992
* @type {boolean}
9093
* @default false
9194
*/
9295
this.minimize = minimize;
96+
9397
/**
9498
* Completely hide this app's windows once all apps have been launched. Helpful for headless apps, but note that this might cause issues with GUI-based apps.
9599
*
@@ -111,6 +115,7 @@ export const LogModes = {
111115
* @type {string}
112116
*/
113117
TailLogFile: 'file',
118+
114119
/**
115120
* Logs directly from the app's stdout/stderr bus. Can result in interrupted logs if the buffer isn't consistently flushed by an app.
116121
* @type {string}
@@ -134,6 +139,7 @@ export class AppLogOptions {
134139
* @default true
135140
*/
136141
this.logToLaunchpadDir = logToLaunchpadDir;
142+
137143
/**
138144
* How to grab the app's logs. Supported values:
139145
* - `'file'`: Logs by tailing the app's log files. Slight lag, but can result in better formatting than bus.
@@ -142,12 +148,14 @@ export class AppLogOptions {
142148
* @default 'file'
143149
*/
144150
this.mode = mode;
151+
145152
/**
146153
* Whether or not to include output from `stdout`
147154
* @type {boolean}
148155
* @default true
149156
*/
150157
this.showStdout = showStdout;
158+
151159
/**
152160
* Whether or not to include output from `stderr`
153161
* @type {boolean}
@@ -162,9 +170,8 @@ export class AppLogOptions {
162170
*/
163171
export class WindowsApiOptions {
164172
constructor({
165-
nodeVersion = '>=17.4.0',
166173
debounceDelay = 3000,
167-
fakeKey = 'control',
174+
nodeVersion = '>=17.4.0',
168175
...rest
169176
} = {}) {
170177
/**
@@ -176,6 +183,7 @@ export class WindowsApiOptions {
176183
* @default '>=17.4.0'
177184
*/
178185
this.nodeVersion = nodeVersion;
186+
179187
/**
180188
* The delay until windows are ordered after launch of in ms.
181189
*
@@ -186,17 +194,6 @@ export class WindowsApiOptions {
186194
* @default 3000
187195
*/
188196
this.debounceDelay = debounceDelay;
189-
/**
190-
* Windows OS is very strict with when and how apps can move windows to the foreground or backgruond. As a workaround, Launchpad emulates a keypress to make the current process active.
191-
*
192-
* This setting configures which key is used to emulate in order to gain control over window foregrounding/backgrounding. This key gets emulated after an app launches or re-launches.
193-
*
194-
* @see https://robotjs.io/docs/syntax#keys for available options
195-
* @see https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-allowsetforegroundwindow#remarks for window management requirements
196-
* @type {string}
197-
* @default 'control'
198-
*/
199-
this.fakeKey = fakeKey;
200197

201198
Object.assign(this, rest);
202199
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import semver from 'semver';
2+
import chalk from 'chalk';
3+
import { windowManager } from 'node-window-manager';
4+
import { Logger } from '@bluecadet/launchpad-utils';
5+
import { AppOptions } from '../monitor-options.js';
6+
7+
export class SortApp {
8+
/**
9+
* @type {AppOptions}
10+
*/
11+
options = null;
12+
/**
13+
* @type {number}
14+
*/
15+
pid = null;
16+
}
17+
18+
/**
19+
*
20+
* @param {Array.<SortApp>} apps
21+
* @param {Logger} logger
22+
* @param {string} minNodeVersion
23+
* @returns {Promise}
24+
*/
25+
const sortWindows = async (apps, logger = console, minNodeVersion = undefined) => {
26+
const currNodeVersion = process.version;
27+
if (!semver.satisfies(currNodeVersion, minNodeVersion)) {
28+
return Promise.reject(`Can't sort windows because the current node version '${currNodeVersion}' doesn't satisfy the required version '${minNodeVersion}'. Please upgrade node to apply window settings like foreground/minimize/hide.`);
29+
}
30+
31+
logger.debug(`Applying window settings to ${apps.length} ${apps.length === 1 ? 'app' : 'apps'}`);
32+
33+
const fgPids = new Set();
34+
const minPids = new Set();
35+
const hidePids = new Set();
36+
37+
windowManager.requestAccessibility();
38+
const visibleWindows = windowManager.getWindows().filter(win => win.isVisible());
39+
const visiblePids = new Set(visibleWindows.map(win => win.processId));
40+
41+
for (const app of apps) {
42+
if (!visiblePids.has(app.pid)) {
43+
logger.warn(`No window found for ${chalk.blue(app.options.pm2.name)} with pid ${chalk.blue(app.pid)}.`);
44+
continue;
45+
}
46+
47+
if (app.options.windows.hide) {
48+
hidePids.add(app.pid);
49+
}
50+
if (app.options.windows.minimize) {
51+
minPids.add(app.pid);
52+
}
53+
if (app.options.windows.foreground) {
54+
fgPids.add(app.pid);
55+
}
56+
}
57+
58+
visibleWindows.filter(win => hidePids.has(win.processId)).forEach(win => {
59+
logger.info(`Hiding ${chalk.blue(win.getTitle())} (pid: ${chalk.blue(win.processId)})`);
60+
win.hide();
61+
});
62+
visibleWindows.filter(win => minPids.has(win.processId)).forEach(win => {
63+
logger.info(`Minimizing ${chalk.blue(win.getTitle())} (pid: ${chalk.blue(win.processId)})`);
64+
win.minimize();
65+
});
66+
visibleWindows.filter(win => fgPids.has(win.processId)).forEach(win => {
67+
logger.info(`Foregrounding ${chalk.blue(win.getTitle())} (pid: ${chalk.blue(win.processId)})`);
68+
win.bringToTop();
69+
});
70+
71+
logger.debug(`Done applying window settings.`);
72+
};
73+
74+
export default sortWindows;

0 commit comments

Comments
 (0)