@bobfrankston/winpos 2.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +20 -0
- package/.gitattributes +16 -0
- package/DEVELOPMENT.md +148 -0
- package/README.md +126 -0
- package/ffi-wrapper.d.ts +45 -0
- package/ffi-wrapper.d.ts.map +1 -0
- package/ffi-wrapper.js +172 -0
- package/ffi-wrapper.js.map +1 -0
- package/ffi-wrapper.ts +213 -0
- package/ignores.txt +75 -0
- package/index.d.ts +12 -0
- package/index.d.ts.map +1 -0
- package/index.js +213 -0
- package/index.js.map +1 -0
- package/index.ts +294 -0
- package/package.json +41 -0
- package/screens.d.ts +18 -0
- package/screens.d.ts.map +1 -0
- package/screens.js +83 -0
- package/screens.js.map +1 -0
- package/screens.ts +101 -0
- package/tabs.d.ts +42 -0
- package/tabs.d.ts.map +1 -0
- package/tabs.js +115 -0
- package/tabs.js.map +1 -0
- package/tabs.ts +133 -0
- package/tsconfig.json +28 -0
- package/windows.d.ts +51 -0
- package/windows.d.ts.map +1 -0
- package/windows.js +170 -0
- package/windows.js.map +1 -0
- package/windows.ts +200 -0
package/index.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tswinpos - TypeScript Windows Positioning Utility
|
|
4
|
+
* Cross-platform Node.js/Bun implementation with FFI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { user32, RECT } from './ffi-wrapper.js';
|
|
8
|
+
import { enumerateScreens, sortScreens, ScreenInfo } from './screens.js';
|
|
9
|
+
import {
|
|
10
|
+
enumerateWindows,
|
|
11
|
+
findWindowsByPattern,
|
|
12
|
+
loadIgnorePatterns,
|
|
13
|
+
WindowInfo,
|
|
14
|
+
WindowState,
|
|
15
|
+
getWindowState,
|
|
16
|
+
setWindowState,
|
|
17
|
+
isMinimized,
|
|
18
|
+
isMaximized,
|
|
19
|
+
isNormal,
|
|
20
|
+
minimizeWindow,
|
|
21
|
+
maximizeWindow,
|
|
22
|
+
restoreWindow
|
|
23
|
+
} from './windows.js';
|
|
24
|
+
import { enumerateTabs, TabInfo, mightHaveTabs, TAB_ENUMERATION_NOTE } from './tabs.js';
|
|
25
|
+
import { join } from 'path';
|
|
26
|
+
import * as packageJson from './package.json' with { type: 'json' };
|
|
27
|
+
|
|
28
|
+
// Re-export public API for library usage
|
|
29
|
+
export {
|
|
30
|
+
RECT,
|
|
31
|
+
ScreenInfo,
|
|
32
|
+
WindowInfo,
|
|
33
|
+
WindowState,
|
|
34
|
+
TabInfo,
|
|
35
|
+
enumerateScreens,
|
|
36
|
+
sortScreens,
|
|
37
|
+
enumerateWindows,
|
|
38
|
+
findWindowsByPattern,
|
|
39
|
+
getWindowState,
|
|
40
|
+
setWindowState,
|
|
41
|
+
isMinimized,
|
|
42
|
+
isMaximized,
|
|
43
|
+
isNormal,
|
|
44
|
+
minimizeWindow,
|
|
45
|
+
maximizeWindow,
|
|
46
|
+
restoreWindow,
|
|
47
|
+
enumerateTabs,
|
|
48
|
+
mightHaveTabs,
|
|
49
|
+
TAB_ENUMERATION_NOTE,
|
|
50
|
+
user32
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let dbg = false;
|
|
54
|
+
let screens: ScreenInfo[] = [];
|
|
55
|
+
|
|
56
|
+
function parseDimension(input: string, fullDimension: number): number {
|
|
57
|
+
if (input.endsWith('%')) {
|
|
58
|
+
const percentage = parseFloat(input.slice(0, -1)) / 100.0;
|
|
59
|
+
return Math.floor(fullDimension * percentage);
|
|
60
|
+
}
|
|
61
|
+
const val = parseInt(input, 10);
|
|
62
|
+
return val < 0 ? fullDimension + val : val;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function adjustWindow(args: string[]) {
|
|
66
|
+
const windowTitle = args[0];
|
|
67
|
+
|
|
68
|
+
// Check if user wants to minimize the window (min or -min in position)
|
|
69
|
+
const shouldMinimize = args[1].toLowerCase() === 'min' || args[1].toLowerCase() === '-min';
|
|
70
|
+
|
|
71
|
+
if (shouldMinimize) {
|
|
72
|
+
if (dbg)
|
|
73
|
+
console.log(`Minimizing window '${windowTitle}'`);
|
|
74
|
+
|
|
75
|
+
let matchedWindows = findWindowsByPattern(windowTitle);
|
|
76
|
+
|
|
77
|
+
// Filter out command windows running tswinpos
|
|
78
|
+
matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
|
|
79
|
+
|
|
80
|
+
for (const window of matchedWindows) {
|
|
81
|
+
if (dbg)
|
|
82
|
+
console.log(`Minimizing window '${window.title}'`);
|
|
83
|
+
|
|
84
|
+
minimizeWindow(window.hwnd);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (matchedWindows.length === 0)
|
|
88
|
+
console.log(`No windows found matching '${windowTitle}'.`);
|
|
89
|
+
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const screenIndex = parseInt(args[3], 10);
|
|
94
|
+
|
|
95
|
+
if (screenIndex < 0 || screenIndex >= screens.length) {
|
|
96
|
+
usage(`Invalid screen number: ${screenIndex}. Must be between 0 and ${screens.length - 1}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const targetScreen = screens[screenIndex];
|
|
101
|
+
const bounds = targetScreen.bounds;
|
|
102
|
+
|
|
103
|
+
const x = parseDimension(args[1], bounds.Width) + bounds.Left;
|
|
104
|
+
const y = parseDimension(args[2], bounds.Height) + bounds.Top;
|
|
105
|
+
let width = args.length > 4 ? parseDimension(args[4], bounds.Width) : 0;
|
|
106
|
+
let height = args.length > 5 ? parseDimension(args[5], bounds.Height) : 0;
|
|
107
|
+
|
|
108
|
+
if (dbg)
|
|
109
|
+
console.log(`Trying window '${windowTitle}'`);
|
|
110
|
+
|
|
111
|
+
let matchedWindows = findWindowsByPattern(windowTitle);
|
|
112
|
+
|
|
113
|
+
// Filter out command windows running tswinpos
|
|
114
|
+
matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
|
|
115
|
+
|
|
116
|
+
for (const window of matchedWindows) {
|
|
117
|
+
if (dbg)
|
|
118
|
+
console.log(`Moving window '${window.title}' to (${x}, ${y}) on screen ${screenIndex}.`);
|
|
119
|
+
|
|
120
|
+
if (width === 0)
|
|
121
|
+
width = window.rect.Right - window.rect.Left;
|
|
122
|
+
if (height === 0)
|
|
123
|
+
height = window.rect.Bottom - window.rect.Top;
|
|
124
|
+
|
|
125
|
+
// Restore window if minimized or maximized to ensure it moves to the correct position
|
|
126
|
+
if (isMinimized(window.hwnd) || isMaximized(window.hwnd)) {
|
|
127
|
+
restoreWindow(window.hwnd);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
user32.MoveWindow(window.hwnd, x, y, width, height, true);
|
|
131
|
+
user32.SetForegroundWindow(window.hwnd);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (matchedWindows.length === 0)
|
|
135
|
+
console.log(`No windows found matching '${windowTitle}'.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getScreenForWindow(windowRect: RECT): number {
|
|
139
|
+
// Find which screen contains the center of the window
|
|
140
|
+
const centerX = (windowRect.Left + windowRect.Right) / 2;
|
|
141
|
+
const centerY = (windowRect.Top + windowRect.Bottom) / 2;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < screens.length; i++) {
|
|
144
|
+
const screen = screens[i];
|
|
145
|
+
if (centerX >= screen.bounds.Left && centerX < screen.bounds.Right &&
|
|
146
|
+
centerY >= screen.bounds.Top && centerY < screen.bounds.Bottom) {
|
|
147
|
+
return i;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If not found, return the primary screen (or first screen)
|
|
152
|
+
return screens.findIndex(s => s.primary) || 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function listAllWindows(includeIgnored: boolean = false) {
|
|
156
|
+
console.log('====================================');
|
|
157
|
+
console.log('Screens');
|
|
158
|
+
for (let i = 0; i < screens.length; i++) {
|
|
159
|
+
const screen = screens[i];
|
|
160
|
+
console.log(`${i} Screen: ${screen.deviceName} (${screen.bounds.Top}/${screen.bounds.Bottom}x${screen.bounds.Left}/${screen.bounds.Right})`);
|
|
161
|
+
}
|
|
162
|
+
console.log();
|
|
163
|
+
|
|
164
|
+
const windows = enumerateWindows(includeIgnored).sort((a, b) => a.title.localeCompare(b.title));
|
|
165
|
+
|
|
166
|
+
console.log('Title X Y Scr Width Height');
|
|
167
|
+
console.log('-----------------------------------------------------------------------------------');
|
|
168
|
+
for (const window of windows) {
|
|
169
|
+
const screenIndex = getScreenForWindow(window.rect);
|
|
170
|
+
const screen = screens[screenIndex];
|
|
171
|
+
|
|
172
|
+
// Normalize coordinates relative to the screen
|
|
173
|
+
const x = window.rect.Left - screen.bounds.Left;
|
|
174
|
+
const y = window.rect.Top - screen.bounds.Top;
|
|
175
|
+
const width = window.rect.Right - window.rect.Left;
|
|
176
|
+
const height = window.rect.Bottom - window.rect.Top;
|
|
177
|
+
|
|
178
|
+
// Truncate title if too long
|
|
179
|
+
const title = window.title.length > 45 ? window.title.substring(0, 42) + '...' : window.title;
|
|
180
|
+
|
|
181
|
+
console.log(`${title.padEnd(45)} ${x.toString().padStart(6)} ${y.toString().padStart(6)} ${screenIndex.toString().padStart(5)} ${width.toString().padStart(7)} ${height.toString().padStart(7)}`);
|
|
182
|
+
}
|
|
183
|
+
console.log();
|
|
184
|
+
console.log('Command format: tswinpos <title> <x> <y> <screen> [<width> <height>]');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function usage(msg: string = '') {
|
|
188
|
+
if (msg)
|
|
189
|
+
console.log(msg);
|
|
190
|
+
|
|
191
|
+
console.log('Usage: tswinpos [-d -c -version *] <window title> <x> <y> <screen> [<width> <height>]');
|
|
192
|
+
console.log(' tswinpos <window title> min | -min');
|
|
193
|
+
console.log(' -d debug, -c return count, -version (or -v) show version');
|
|
194
|
+
console.log(' where <screen> is a number from 0 to N-1, where N is the number of screens.');
|
|
195
|
+
console.log(' <x> and <y> can be a number of pixels or a percentage (e.g. 50%)');
|
|
196
|
+
console.log(' Use "min" or "-min" in place of <x> to minimize the window to the taskbar');
|
|
197
|
+
console.log(' title can be a regex pattern (if enclosed in /.../) or a prefix (if it ends with \'*\')');
|
|
198
|
+
console.log(' <width> and <height> are optional and can be a number of pixels or a percentage (e.g. 50%) or 0 (to keep the current size)');
|
|
199
|
+
console.log('To list all windows: tswinpos *');
|
|
200
|
+
console.log('To list all windows (including ignored): tswinpos **');
|
|
201
|
+
console.log('To return the window count use -c');
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(' +---+---+');
|
|
204
|
+
console.log(' | 2 | 3 |');
|
|
205
|
+
console.log(' +---+---+');
|
|
206
|
+
console.log(' | 0 | 1 |');
|
|
207
|
+
console.log(' +---+---+');
|
|
208
|
+
console.log();
|
|
209
|
+
console.log('Screen | X Y | W H ');
|
|
210
|
+
console.log('------------------------------------');
|
|
211
|
+
for (let i = 0; i < screens.length; i++) {
|
|
212
|
+
const screen = screens[i];
|
|
213
|
+
console.log(`${i.toString().padStart(6)} | ${screen.bounds.Left.toString().padStart(5)} ${screen.bounds.Top.toString().padStart(5)} | ${screen.bounds.Width.toString().padStart(5)} ${screen.bounds.Height.toString().padStart(5)}`);
|
|
214
|
+
}
|
|
215
|
+
console.log();
|
|
216
|
+
console.log('0-Lower right, 1-Lower Left, 2-Upper left, 3-Upper right');
|
|
217
|
+
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function run(args: string[]) {
|
|
222
|
+
// Initialize screens
|
|
223
|
+
try {
|
|
224
|
+
screens = sortScreens(enumerateScreens());
|
|
225
|
+
} catch (e: any) {
|
|
226
|
+
if (e.message && e.message.includes('Bun')) {
|
|
227
|
+
console.error(e.message);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
throw e; // Re-throw if it's a different error
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Load ignore patterns from the same directory as the script
|
|
234
|
+
const ignoresPath = join(import.meta.dirname, 'ignores.txt');
|
|
235
|
+
loadIgnorePatterns(ignoresPath);
|
|
236
|
+
|
|
237
|
+
// Parse options
|
|
238
|
+
while (args.length > 0 && args[0].startsWith('-')) {
|
|
239
|
+
const opt = args[0].substring(1);
|
|
240
|
+
switch (opt) {
|
|
241
|
+
case 'd':
|
|
242
|
+
dbg = true;
|
|
243
|
+
console.log('Debug mode enabled.');
|
|
244
|
+
break;
|
|
245
|
+
case 'c':
|
|
246
|
+
console.log(screens.length);
|
|
247
|
+
process.exit(screens.length);
|
|
248
|
+
break;
|
|
249
|
+
case 'version':
|
|
250
|
+
case 'v':
|
|
251
|
+
console.log(`tswinpos version ${packageJson.default.version}`);
|
|
252
|
+
process.exit(0);
|
|
253
|
+
break;
|
|
254
|
+
default:
|
|
255
|
+
usage(`Unknown option: ${args[0]}`);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
args = args.slice(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Handle commands
|
|
262
|
+
|
|
263
|
+
// console.log(DEBUG: args.length=', args.length, 'args[0]=', args[0], 'check=', args.length === 1 && args[0] === '*');
|
|
264
|
+
if (dbg)
|
|
265
|
+
console.log(`DEBUG[${packageJson.default.version}]: ${args.join(' ')}`);
|
|
266
|
+
if (args.length === 1 && args[0] === '*') {
|
|
267
|
+
listAllWindows();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (args.length === 1 && args[0] === '**') {
|
|
272
|
+
listAllWindows(true);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for minimize command (title + min/-min)
|
|
277
|
+
// TODO: Refactor to eliminate duplicate min/minimize check - consolidate with adjustWindow logic
|
|
278
|
+
if (args.length === 2 && (args[1].toLowerCase() === 'min' || args[1].toLowerCase() === '-min')) {
|
|
279
|
+
adjustWindow(args);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (args.length < 4) {
|
|
284
|
+
usage();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
adjustWindow(args);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// CLI entry point using import.meta.main
|
|
292
|
+
if (import.meta.main) {
|
|
293
|
+
run(process.argv.slice(2));
|
|
294
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bobfrankston/winpos",
|
|
3
|
+
"version": "2.0.20",
|
|
4
|
+
"description": "TypeScript implementation of winpos - Windows window positioning utility",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"winpos": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"watch": "tsc -w",
|
|
14
|
+
"prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
|
|
15
|
+
"preversion": "npm run build && git add -A",
|
|
16
|
+
"postversion": "git push && git push --tags",
|
|
17
|
+
"release": "npm run prerelease:local && npm version patch && npm publish",
|
|
18
|
+
"release:ps1": "releaseapp.ps1",
|
|
19
|
+
"installer": "npm run release && npm install -g @bobfrankston/winpos"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"windows",
|
|
23
|
+
"window-management",
|
|
24
|
+
"positioning",
|
|
25
|
+
"multi-monitor"
|
|
26
|
+
],
|
|
27
|
+
"author": "Bob Frankston",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^24.9.1"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"koffi": "^2.9.2"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/screens.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen management - enumerate and sort displays
|
|
3
|
+
*/
|
|
4
|
+
export interface ScreenInfo {
|
|
5
|
+
deviceName: string;
|
|
6
|
+
bounds: {
|
|
7
|
+
Left: number;
|
|
8
|
+
Top: number;
|
|
9
|
+
Right: number;
|
|
10
|
+
Bottom: number;
|
|
11
|
+
Width: number;
|
|
12
|
+
Height: number;
|
|
13
|
+
};
|
|
14
|
+
primary: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function enumerateScreens(): ScreenInfo[];
|
|
17
|
+
export declare function sortScreens(screens: ScreenInfo[]): ScreenInfo[];
|
|
18
|
+
//# sourceMappingURL=screens.d.ts.map
|
package/screens.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screens.d.ts","sourceRoot":"","sources":["screens.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,gBAAgB,IAAI,UAAU,EAAE,CAwE/C;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAO/D"}
|
package/screens.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen management - enumerate and sort displays
|
|
3
|
+
*/
|
|
4
|
+
import { user32, isBun, MONITORINFOF_PRIMARY } from './ffi-wrapper.js';
|
|
5
|
+
export function enumerateScreens() {
|
|
6
|
+
const screens = [];
|
|
7
|
+
let monitorIndex = 0;
|
|
8
|
+
// Bun FFI for monitor enumeration not yet implemented
|
|
9
|
+
if (isBun) {
|
|
10
|
+
throw new Error('Multi-monitor support is not yet implemented for Bun. Please use Node.js.');
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
// Define callback function - closures should work in Node.js koffi
|
|
14
|
+
const monitorCallback = (hMonitor, hdcMonitor, lprcMonitor, dwData) => {
|
|
15
|
+
try {
|
|
16
|
+
const monitorInfo = {};
|
|
17
|
+
if (user32.GetMonitorInfoW(hMonitor, monitorInfo)) {
|
|
18
|
+
const rcMonitor = monitorInfo.rcMonitor;
|
|
19
|
+
screens.push({
|
|
20
|
+
deviceName: `\\\\.\\DISPLAY${monitorIndex + 1}`,
|
|
21
|
+
bounds: {
|
|
22
|
+
Left: rcMonitor.Left,
|
|
23
|
+
Top: rcMonitor.Top,
|
|
24
|
+
Right: rcMonitor.Right,
|
|
25
|
+
Bottom: rcMonitor.Bottom,
|
|
26
|
+
Width: rcMonitor.Right - rcMonitor.Left,
|
|
27
|
+
Height: rcMonitor.Bottom - rcMonitor.Top,
|
|
28
|
+
},
|
|
29
|
+
primary: (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) !== 0,
|
|
30
|
+
});
|
|
31
|
+
monitorIndex++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error('Error in monitor callback:', e);
|
|
36
|
+
}
|
|
37
|
+
return true; // Continue enumeration
|
|
38
|
+
};
|
|
39
|
+
// Enumerate all display monitors
|
|
40
|
+
user32.EnumDisplayMonitors(monitorCallback, 0n);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.error('Error enumerating monitors:', e);
|
|
44
|
+
// Fallback to single screen
|
|
45
|
+
screens.push({
|
|
46
|
+
deviceName: '\\\\.\\DISPLAY1',
|
|
47
|
+
bounds: {
|
|
48
|
+
Left: 0,
|
|
49
|
+
Top: 0,
|
|
50
|
+
Right: 1920,
|
|
51
|
+
Bottom: 1080,
|
|
52
|
+
Width: 1920,
|
|
53
|
+
Height: 1080,
|
|
54
|
+
},
|
|
55
|
+
primary: true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// If no screens were found, add a default one
|
|
59
|
+
if (screens.length === 0) {
|
|
60
|
+
screens.push({
|
|
61
|
+
deviceName: '\\\\.\\DISPLAY1',
|
|
62
|
+
bounds: {
|
|
63
|
+
Left: 0,
|
|
64
|
+
Top: 0,
|
|
65
|
+
Right: 1920,
|
|
66
|
+
Bottom: 1080,
|
|
67
|
+
Width: 1920,
|
|
68
|
+
Height: 1080,
|
|
69
|
+
},
|
|
70
|
+
primary: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return screens;
|
|
74
|
+
}
|
|
75
|
+
export function sortScreens(screens) {
|
|
76
|
+
// Sort by top (descending), then by left (ascending)
|
|
77
|
+
return screens.sort((a, b) => {
|
|
78
|
+
if (a.bounds.Top === b.bounds.Top)
|
|
79
|
+
return a.bounds.Left - b.bounds.Left;
|
|
80
|
+
return b.bounds.Top - a.bounds.Top;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=screens.js.map
|
package/screens.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screens.js","sourceRoot":"","sources":["screens.ts"],"names":[],"mappings":"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"}
|
package/screens.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen management - enumerate and sort displays
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { user32, RECT, isBun, MONITORINFOF_PRIMARY } from './ffi-wrapper.js';
|
|
6
|
+
|
|
7
|
+
export interface ScreenInfo {
|
|
8
|
+
deviceName: string;
|
|
9
|
+
bounds: {
|
|
10
|
+
Left: number;
|
|
11
|
+
Top: number;
|
|
12
|
+
Right: number;
|
|
13
|
+
Bottom: number;
|
|
14
|
+
Width: number;
|
|
15
|
+
Height: number;
|
|
16
|
+
};
|
|
17
|
+
primary: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function enumerateScreens(): ScreenInfo[] {
|
|
21
|
+
const screens: ScreenInfo[] = [];
|
|
22
|
+
let monitorIndex = 0;
|
|
23
|
+
|
|
24
|
+
// Bun FFI for monitor enumeration not yet implemented
|
|
25
|
+
if (isBun) {
|
|
26
|
+
throw new Error('Multi-monitor support is not yet implemented for Bun. Please use Node.js.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Define callback function - closures should work in Node.js koffi
|
|
31
|
+
const monitorCallback = (hMonitor: any, hdcMonitor: any, lprcMonitor: any, dwData: any): boolean => {
|
|
32
|
+
try {
|
|
33
|
+
const monitorInfo: any = {};
|
|
34
|
+
if (user32.GetMonitorInfoW(hMonitor, monitorInfo)) {
|
|
35
|
+
const rcMonitor = monitorInfo.rcMonitor;
|
|
36
|
+
screens.push({
|
|
37
|
+
deviceName: `\\\\.\\DISPLAY${monitorIndex + 1}`,
|
|
38
|
+
bounds: {
|
|
39
|
+
Left: rcMonitor.Left,
|
|
40
|
+
Top: rcMonitor.Top,
|
|
41
|
+
Right: rcMonitor.Right,
|
|
42
|
+
Bottom: rcMonitor.Bottom,
|
|
43
|
+
Width: rcMonitor.Right - rcMonitor.Left,
|
|
44
|
+
Height: rcMonitor.Bottom - rcMonitor.Top,
|
|
45
|
+
},
|
|
46
|
+
primary: (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) !== 0,
|
|
47
|
+
});
|
|
48
|
+
monitorIndex++;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error('Error in monitor callback:', e);
|
|
52
|
+
}
|
|
53
|
+
return true; // Continue enumeration
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Enumerate all display monitors
|
|
57
|
+
user32.EnumDisplayMonitors(monitorCallback, 0n);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error('Error enumerating monitors:', e);
|
|
60
|
+
// Fallback to single screen
|
|
61
|
+
screens.push({
|
|
62
|
+
deviceName: '\\\\.\\DISPLAY1',
|
|
63
|
+
bounds: {
|
|
64
|
+
Left: 0,
|
|
65
|
+
Top: 0,
|
|
66
|
+
Right: 1920,
|
|
67
|
+
Bottom: 1080,
|
|
68
|
+
Width: 1920,
|
|
69
|
+
Height: 1080,
|
|
70
|
+
},
|
|
71
|
+
primary: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If no screens were found, add a default one
|
|
76
|
+
if (screens.length === 0) {
|
|
77
|
+
screens.push({
|
|
78
|
+
deviceName: '\\\\.\\DISPLAY1',
|
|
79
|
+
bounds: {
|
|
80
|
+
Left: 0,
|
|
81
|
+
Top: 0,
|
|
82
|
+
Right: 1920,
|
|
83
|
+
Bottom: 1080,
|
|
84
|
+
Width: 1920,
|
|
85
|
+
Height: 1080,
|
|
86
|
+
},
|
|
87
|
+
primary: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return screens;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function sortScreens(screens: ScreenInfo[]): ScreenInfo[] {
|
|
95
|
+
// Sort by top (descending), then by left (ascending)
|
|
96
|
+
return screens.sort((a, b) => {
|
|
97
|
+
if (a.bounds.Top === b.bounds.Top)
|
|
98
|
+
return a.bounds.Left - b.bounds.Left;
|
|
99
|
+
return b.bounds.Top - a.bounds.Top;
|
|
100
|
+
});
|
|
101
|
+
}
|
package/tabs.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab enumeration support using Windows UI Automation API
|
|
3
|
+
*
|
|
4
|
+
* NOTE: Tab enumeration is application-specific and not all apps expose tabs
|
|
5
|
+
* through Windows APIs. This module uses UI Automation to attempt to find tabs.
|
|
6
|
+
*
|
|
7
|
+
* Windows Terminal, Chrome, Edge, and other modern apps may expose tabs via
|
|
8
|
+
* UI Automation, but success varies by application.
|
|
9
|
+
*/
|
|
10
|
+
export interface TabInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
automationId: string;
|
|
13
|
+
index: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attempt to enumerate tabs for a window
|
|
17
|
+
*
|
|
18
|
+
* @param hwnd Window handle
|
|
19
|
+
* @returns Array of tab information, or null if not supported/available
|
|
20
|
+
*
|
|
21
|
+
* NOTE: This is a placeholder for future implementation. Tab enumeration
|
|
22
|
+
* requires COM interop with UI Automation API which is complex with FFI.
|
|
23
|
+
*
|
|
24
|
+
* For now, this returns null. A full implementation would require:
|
|
25
|
+
* - COM initialization (CoInitialize)
|
|
26
|
+
* - IUIAutomation interface
|
|
27
|
+
* - Element tree traversal
|
|
28
|
+
* - Property reading from automation elements
|
|
29
|
+
*/
|
|
30
|
+
export declare function enumerateTabs(hwnd: bigint): Promise<TabInfo[] | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a window potentially has tabs
|
|
33
|
+
* This is a heuristic based on window class name
|
|
34
|
+
*/
|
|
35
|
+
export declare function mightHaveTabs(windowTitle: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Alternative approach: Enumerate child windows
|
|
38
|
+
* Some applications implement tabs as child windows
|
|
39
|
+
*/
|
|
40
|
+
export declare function enumerateChildWindows(hwnd: bigint): bigint[];
|
|
41
|
+
export declare const TAB_ENUMERATION_NOTE = "\nTab Enumeration Limitations:\n\nWindows does not provide a standard API for enumerating tabs within applications.\nTabs are application-specific UI elements. To enumerate tabs, you would need:\n\n1. UI Automation API (requires COM interop - complex with FFI)\n2. Application-specific APIs (e.g., Chrome DevTools Protocol)\n3. Accessibility APIs (Windows Automation)\n\nCurrent status: Not implemented due to COM complexity.\n\nAlternative approaches:\n- Use application-specific automation (e.g., Puppeteer for Chrome)\n- Use Windows Automation PowerShell cmdlets\n- Use specialized tools like UIAutomationSpy\n\nFor Windows Terminal specifically, tabs are accessible via Windows.UI.Xaml\nautomation, but require UWP/WinRT interop.\n";
|
|
42
|
+
//# sourceMappingURL=tabs.d.ts.map
|
package/tabs.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,OAAO;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACjB;AA8BD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAiB3E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAgB1D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAI5D;AAGD,eAAO,MAAM,oBAAoB,ouBAmBhC,CAAC"}
|