@bobfrankston/winpos 2.0.48 → 2.0.50
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/index.js +15 -8
- package/package.json +5 -4
- package/screens.js +4 -2
- package/ffi-wrapper.d.ts.map +0 -1
- package/ffi-wrapper.js.map +0 -1
- package/ffi-wrapper.ts +0 -216
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/index.ts +0 -751
- package/screens.d.ts.map +0 -1
- package/screens.js.map +0 -1
- package/screens.ts +0 -101
- package/tabs.d.ts.map +0 -1
- package/tabs.js.map +0 -1
- package/tabs.ts +0 -133
- package/tsconfig.json +0 -29
- package/windows.d.ts.map +0 -1
- package/windows.js.map +0 -1
- package/windows.ts +0 -200
package/index.ts
DELETED
|
@@ -1,751 +0,0 @@
|
|
|
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, dirname, resolve, isAbsolute } from 'path';
|
|
26
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
27
|
-
import * as packageJson from './package.json' with { type: 'json' };
|
|
28
|
-
|
|
29
|
-
// JSON config file format
|
|
30
|
-
export interface WindowConfig {
|
|
31
|
-
name: string; // Window title or pattern
|
|
32
|
-
regex?: boolean; // true = name is regex pattern, false/undefined = exact/prefix match
|
|
33
|
-
pos?: { x: number | string; y: number | string; screen?: number };
|
|
34
|
-
size?: { w: number | string; h: number | string };
|
|
35
|
-
minimize?: boolean; // true = minimize the window
|
|
36
|
-
maximize?: boolean; // true = maximize the window
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type ConfigFile = WindowConfig | WindowConfig[];
|
|
40
|
-
|
|
41
|
-
// Parsed CLI options
|
|
42
|
-
interface ParsedOptions {
|
|
43
|
-
debug: boolean;
|
|
44
|
-
debugVerbose: boolean;
|
|
45
|
-
showCount: boolean;
|
|
46
|
-
loadFile?: string;
|
|
47
|
-
saveFile?: string;
|
|
48
|
-
pos?: { x: string; y: string; screen?: number };
|
|
49
|
-
size?: { w: string; h: string };
|
|
50
|
-
windowTitle?: string;
|
|
51
|
-
minimize?: boolean;
|
|
52
|
-
maximize?: boolean;
|
|
53
|
-
positionalArgs: string[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Re-export public API for library usage
|
|
57
|
-
export {
|
|
58
|
-
RECT,
|
|
59
|
-
ScreenInfo,
|
|
60
|
-
WindowInfo,
|
|
61
|
-
WindowState,
|
|
62
|
-
TabInfo,
|
|
63
|
-
enumerateScreens,
|
|
64
|
-
sortScreens,
|
|
65
|
-
enumerateWindows,
|
|
66
|
-
findWindowsByPattern,
|
|
67
|
-
getWindowState,
|
|
68
|
-
setWindowState,
|
|
69
|
-
isMinimized,
|
|
70
|
-
isMaximized,
|
|
71
|
-
isNormal,
|
|
72
|
-
minimizeWindow,
|
|
73
|
-
maximizeWindow,
|
|
74
|
-
restoreWindow,
|
|
75
|
-
enumerateTabs,
|
|
76
|
-
mightHaveTabs,
|
|
77
|
-
TAB_ENUMERATION_NOTE,
|
|
78
|
-
user32
|
|
79
|
-
};
|
|
80
|
-
// WindowConfig and ConfigFile types are exported from interface definitions above
|
|
81
|
-
|
|
82
|
-
// Note: screenPosToPos is exported separately below (after the function definition)
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Load window config from JSON file
|
|
86
|
-
*/
|
|
87
|
-
function loadConfigFile(filePath: string): ConfigFile {
|
|
88
|
-
const cwd = process.cwd();
|
|
89
|
-
const absPath = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
|
|
90
|
-
if (dbgVerbose) {
|
|
91
|
-
console.log(`[DBG] cwd: ${cwd}, filePath: ${filePath}, absPath: ${absPath}`);
|
|
92
|
-
}
|
|
93
|
-
if (!existsSync(absPath)) {
|
|
94
|
-
throw new Error(`Config file not found: ${absPath}`);
|
|
95
|
-
}
|
|
96
|
-
const content = readFileSync(absPath, 'utf-8');
|
|
97
|
-
return JSON.parse(content) as ConfigFile;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Save window config to JSON file
|
|
102
|
-
* If file exists and contains array, merge; otherwise create/overwrite
|
|
103
|
-
*/
|
|
104
|
-
function saveConfigFile(filePath: string, config: WindowConfig, merge: boolean = false): void {
|
|
105
|
-
const absPath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
106
|
-
|
|
107
|
-
let finalConfig: ConfigFile;
|
|
108
|
-
|
|
109
|
-
if (merge && existsSync(absPath)) {
|
|
110
|
-
const existing = loadConfigFile(absPath);
|
|
111
|
-
if (Array.isArray(existing)) {
|
|
112
|
-
// Find and update existing entry with same name, or append
|
|
113
|
-
const idx = existing.findIndex(e => e.name === config.name);
|
|
114
|
-
if (idx >= 0) {
|
|
115
|
-
existing[idx] = config;
|
|
116
|
-
} else {
|
|
117
|
-
existing.push(config);
|
|
118
|
-
}
|
|
119
|
-
finalConfig = existing;
|
|
120
|
-
} else {
|
|
121
|
-
// Convert single entry to array
|
|
122
|
-
if (existing.name === config.name) {
|
|
123
|
-
finalConfig = config; // Replace single entry
|
|
124
|
-
} else {
|
|
125
|
-
finalConfig = [existing, config];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
finalConfig = config;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
writeFileSync(absPath, JSON.stringify(finalConfig, null, 2), 'utf-8');
|
|
133
|
-
console.log(`Config saved to: ${absPath}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Build WindowConfig from CLI options
|
|
138
|
-
*/
|
|
139
|
-
function buildConfigFromOptions(opts: ParsedOptions): WindowConfig | null {
|
|
140
|
-
if (!opts.windowTitle) return null;
|
|
141
|
-
|
|
142
|
-
const config: WindowConfig = {
|
|
143
|
-
name: opts.windowTitle
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Check if name is a regex pattern (enclosed in /.../)
|
|
147
|
-
if (config.name.startsWith('/') && config.name.endsWith('/')) {
|
|
148
|
-
config.regex = true;
|
|
149
|
-
config.name = config.name.slice(1, -1); // Store without delimiters
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (opts.pos) {
|
|
153
|
-
config.pos = { x: opts.pos.x, y: opts.pos.y };
|
|
154
|
-
if (opts.pos.screen !== undefined) {
|
|
155
|
-
config.pos.screen = opts.pos.screen;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (opts.size) {
|
|
160
|
-
config.size = { w: opts.size.w, h: opts.size.h };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (opts.minimize) {
|
|
164
|
-
config.minimize = true;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (opts.maximize) {
|
|
168
|
-
config.maximize = true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return config;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Parse CLI arguments into structured options
|
|
176
|
-
*/
|
|
177
|
-
function parseArgs(args: string[]): ParsedOptions {
|
|
178
|
-
const opts: ParsedOptions = {
|
|
179
|
-
debug: false,
|
|
180
|
-
debugVerbose: false,
|
|
181
|
-
showCount: false,
|
|
182
|
-
positionalArgs: []
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
let i = 0;
|
|
186
|
-
while (i < args.length) {
|
|
187
|
-
const arg = args[i];
|
|
188
|
-
|
|
189
|
-
// Check for .json file as first non-flag arg (auto-load)
|
|
190
|
-
if (!arg.startsWith('-') && arg.toLowerCase().endsWith('.json') && !opts.loadFile) {
|
|
191
|
-
opts.loadFile = arg;
|
|
192
|
-
i++;
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (arg.startsWith('-')) {
|
|
197
|
-
const flag = arg.substring(1).toLowerCase();
|
|
198
|
-
switch (flag) {
|
|
199
|
-
case 'd':
|
|
200
|
-
opts.debug = true;
|
|
201
|
-
break;
|
|
202
|
-
case 'dbg':
|
|
203
|
-
opts.debugVerbose = true;
|
|
204
|
-
break;
|
|
205
|
-
case 'c':
|
|
206
|
-
opts.showCount = true;
|
|
207
|
-
break;
|
|
208
|
-
case 'version':
|
|
209
|
-
case 'v':
|
|
210
|
-
console.log(`winpos version ${packageJson.default.version}`);
|
|
211
|
-
process.exit(0);
|
|
212
|
-
break;
|
|
213
|
-
case 'pos':
|
|
214
|
-
i++;
|
|
215
|
-
if (i < args.length) {
|
|
216
|
-
const parts = args[i].split(',');
|
|
217
|
-
opts.pos = { x: parts[0], y: parts[1] };
|
|
218
|
-
if (parts.length > 2) {
|
|
219
|
-
opts.pos.screen = parseInt(parts[2], 10);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
case 'size':
|
|
224
|
-
i++;
|
|
225
|
-
if (i < args.length) {
|
|
226
|
-
const parts = args[i].split(',');
|
|
227
|
-
opts.size = { w: parts[0], h: parts[1] };
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
case 'load':
|
|
231
|
-
i++;
|
|
232
|
-
if (i < args.length) {
|
|
233
|
-
opts.loadFile = args[i];
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
case 'save':
|
|
237
|
-
// -save can have optional filename; if next arg looks like a flag or is missing, use true as marker
|
|
238
|
-
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
239
|
-
i++;
|
|
240
|
-
opts.saveFile = args[i];
|
|
241
|
-
} else {
|
|
242
|
-
opts.saveFile = ''; // Empty string = use loadFile path
|
|
243
|
-
}
|
|
244
|
-
break;
|
|
245
|
-
case 'title':
|
|
246
|
-
i++;
|
|
247
|
-
if (i < args.length) {
|
|
248
|
-
opts.windowTitle = args[i];
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
case 'min':
|
|
252
|
-
opts.minimize = true;
|
|
253
|
-
break;
|
|
254
|
-
case 'max':
|
|
255
|
-
opts.maximize = true;
|
|
256
|
-
break;
|
|
257
|
-
case 'help':
|
|
258
|
-
case '?':
|
|
259
|
-
return opts; // Will trigger usage display
|
|
260
|
-
default:
|
|
261
|
-
// Unknown flag - could be negative number for position
|
|
262
|
-
if (/^-?\d+/.test(arg)) {
|
|
263
|
-
opts.positionalArgs.push(arg);
|
|
264
|
-
} else {
|
|
265
|
-
console.log(`Unknown option: ${arg}`);
|
|
266
|
-
}
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
opts.positionalArgs.push(arg);
|
|
271
|
-
}
|
|
272
|
-
i++;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// First positional arg is window title (if using new flag-based syntax and -title not set)
|
|
276
|
-
const usingFlagSyntax = opts.pos || opts.size || opts.loadFile || opts.saveFile || opts.minimize || opts.maximize;
|
|
277
|
-
if (opts.positionalArgs.length > 0 && usingFlagSyntax) {
|
|
278
|
-
if (!opts.windowTitle) {
|
|
279
|
-
opts.windowTitle = opts.positionalArgs[0];
|
|
280
|
-
opts.positionalArgs = opts.positionalArgs.slice(1);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Error if stray positional args remain when using flag-based syntax
|
|
284
|
-
if (opts.positionalArgs.length > 0) {
|
|
285
|
-
console.error(`Error: unexpected arguments: ${opts.positionalArgs.join(' ')}`);
|
|
286
|
-
console.error('When using flags, only window title should be positional');
|
|
287
|
-
process.exit(1);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return opts;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Apply a single window config
|
|
296
|
-
*/
|
|
297
|
-
function applyWindowConfig(config: WindowConfig, screensInfo: ScreenInfo[]): void {
|
|
298
|
-
// Build pattern for matching
|
|
299
|
-
let pattern: string;
|
|
300
|
-
if (config.regex) {
|
|
301
|
-
pattern = `/${config.name}/`;
|
|
302
|
-
} else {
|
|
303
|
-
pattern = config.name;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let matchedWindows = findWindowsByPattern(pattern);
|
|
307
|
-
matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
|
|
308
|
-
|
|
309
|
-
if (matchedWindows.length === 0) {
|
|
310
|
-
console.log(`No windows found matching '${config.name}'`);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Handle minimize
|
|
315
|
-
if (config.minimize) {
|
|
316
|
-
for (const window of matchedWindows) {
|
|
317
|
-
if (dbgVerbose) console.log(`[DBG] Minimizing: "${window.title}"`);
|
|
318
|
-
minimizeWindow(window.hwnd);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Handle maximize
|
|
324
|
-
if (config.maximize) {
|
|
325
|
-
for (const window of matchedWindows) {
|
|
326
|
-
if (dbgVerbose) console.log(`[DBG] Maximizing: "${window.title}"`);
|
|
327
|
-
maximizeWindow(window.hwnd);
|
|
328
|
-
}
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Handle positioning
|
|
333
|
-
if (!config.pos) return;
|
|
334
|
-
|
|
335
|
-
const screenIndex = config.pos.screen ?? 0;
|
|
336
|
-
if (screenIndex < 0 || screenIndex >= screensInfo.length) {
|
|
337
|
-
console.log(`Invalid screen ${screenIndex} for window '${config.name}'`);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const targetScreen = screensInfo[screenIndex];
|
|
342
|
-
const bounds = targetScreen.bounds;
|
|
343
|
-
|
|
344
|
-
const x = parseDimension(String(config.pos.x), bounds.Width) + bounds.Left;
|
|
345
|
-
const y = parseDimension(String(config.pos.y), bounds.Height) + bounds.Top;
|
|
346
|
-
|
|
347
|
-
let width = config.size ? parseDimension(String(config.size.w), bounds.Width) : 0;
|
|
348
|
-
let height = config.size ? parseDimension(String(config.size.h), bounds.Height) : 0;
|
|
349
|
-
|
|
350
|
-
for (const window of matchedWindows) {
|
|
351
|
-
let w = width, h = height;
|
|
352
|
-
if (w === 0) w = window.rect.Right - window.rect.Left;
|
|
353
|
-
if (h === 0) h = window.rect.Bottom - window.rect.Top;
|
|
354
|
-
|
|
355
|
-
if (dbgVerbose) {
|
|
356
|
-
console.log(`[DBG] Window: "${window.title}" -> Position: (${x}, ${y}) Size: ${w}x${h} Screen: ${screenIndex}`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (isMinimized(window.hwnd) || isMaximized(window.hwnd)) {
|
|
360
|
-
restoreWindow(window.hwnd);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
user32.MoveWindow(window.hwnd, x, y, w, h, true);
|
|
364
|
-
user32.SetForegroundWindow(window.hwnd);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
let dbg = false;
|
|
369
|
-
let dbgVerbose = false; // -dbg flag for detailed output
|
|
370
|
-
let screens: ScreenInfo[] = [];
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Convert screen-relative coordinates to global coordinates
|
|
374
|
-
* @param x - X coordinate relative to screen
|
|
375
|
-
* @param y - Y coordinate relative to screen
|
|
376
|
-
* @param screenIndex - Screen number (0-based)
|
|
377
|
-
* @returns Global coordinates {x, y} or null if invalid screen
|
|
378
|
-
*/
|
|
379
|
-
export function screenPosToPos(x: number, y: number, screenIndex: number): { x: number; y: number } {
|
|
380
|
-
// Initialize screens if not already done
|
|
381
|
-
if (screens.length === 0) {
|
|
382
|
-
try {
|
|
383
|
-
screens = sortScreens(enumerateScreens());
|
|
384
|
-
} catch (e: any) {
|
|
385
|
-
if (e.message && e.message.includes('Bun')) {
|
|
386
|
-
console.error(e.message);
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
throw e;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (screenIndex < 0 || screenIndex >= screens.length) {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const targetScreen = screens[screenIndex];
|
|
398
|
-
const bounds = targetScreen.bounds;
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
x: x + bounds.Left,
|
|
402
|
-
y: y + bounds.Top
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function parseDimension(input: string, fullDimension: number): number {
|
|
407
|
-
if (input.endsWith('%')) {
|
|
408
|
-
const percentage = parseFloat(input.slice(0, -1)) / 100.0;
|
|
409
|
-
return Math.floor(fullDimension * percentage);
|
|
410
|
-
}
|
|
411
|
-
const val = parseInt(input, 10);
|
|
412
|
-
return val < 0 ? fullDimension + val : val;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function adjustWindow(args: string[]) {
|
|
416
|
-
const windowTitle = args[0];
|
|
417
|
-
|
|
418
|
-
// Check if user wants to minimize the window (min or -min in position)
|
|
419
|
-
const shouldMinimize = args[1].toLowerCase() === 'min' || args[1].toLowerCase() === '-min';
|
|
420
|
-
|
|
421
|
-
if (shouldMinimize) {
|
|
422
|
-
if (dbg)
|
|
423
|
-
console.log(`Minimizing window '${windowTitle}'`);
|
|
424
|
-
|
|
425
|
-
let matchedWindows = findWindowsByPattern(windowTitle);
|
|
426
|
-
|
|
427
|
-
// Filter out command windows running tswinpos
|
|
428
|
-
matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
|
|
429
|
-
|
|
430
|
-
for (const window of matchedWindows) {
|
|
431
|
-
if (dbg)
|
|
432
|
-
console.log(`Minimizing window '${window.title}'`);
|
|
433
|
-
|
|
434
|
-
if (dbgVerbose)
|
|
435
|
-
console.log(`[DBG] Minimizing: "${window.title}"`);
|
|
436
|
-
|
|
437
|
-
minimizeWindow(window.hwnd);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (matchedWindows.length === 0)
|
|
441
|
-
console.log(`No windows found matching '${windowTitle}'.`);
|
|
442
|
-
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const screenIndex = parseInt(args[3], 10);
|
|
447
|
-
|
|
448
|
-
if (screenIndex < 0 || screenIndex >= screens.length) {
|
|
449
|
-
usage(`Invalid screen number: ${screenIndex}. Must be between 0 and ${screens.length - 1}`);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const targetScreen = screens[screenIndex];
|
|
454
|
-
const bounds = targetScreen.bounds;
|
|
455
|
-
|
|
456
|
-
const x = parseDimension(args[1], bounds.Width) + bounds.Left;
|
|
457
|
-
const y = parseDimension(args[2], bounds.Height) + bounds.Top;
|
|
458
|
-
let width = args.length > 4 ? parseDimension(args[4], bounds.Width) : 0;
|
|
459
|
-
let height = args.length > 5 ? parseDimension(args[5], bounds.Height) : 0;
|
|
460
|
-
|
|
461
|
-
if (dbg)
|
|
462
|
-
console.log(`Trying window '${windowTitle}'`);
|
|
463
|
-
|
|
464
|
-
let matchedWindows = findWindowsByPattern(windowTitle);
|
|
465
|
-
|
|
466
|
-
// Filter out command windows running tswinpos
|
|
467
|
-
matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
|
|
468
|
-
|
|
469
|
-
for (const window of matchedWindows) {
|
|
470
|
-
if (dbg)
|
|
471
|
-
console.log(`Moving window '${window.title}' to (${x}, ${y}) on screen ${screenIndex}.`);
|
|
472
|
-
|
|
473
|
-
if (width === 0)
|
|
474
|
-
width = window.rect.Right - window.rect.Left;
|
|
475
|
-
if (height === 0)
|
|
476
|
-
height = window.rect.Bottom - window.rect.Top;
|
|
477
|
-
|
|
478
|
-
if (dbgVerbose)
|
|
479
|
-
console.log(`[DBG] Window: "${window.title}" -> Position: (${x}, ${y}) Size: ${width}x${height} Screen: ${screenIndex}`);
|
|
480
|
-
|
|
481
|
-
// Restore window if minimized or maximized to ensure it moves to the correct position
|
|
482
|
-
if (isMinimized(window.hwnd) || isMaximized(window.hwnd)) {
|
|
483
|
-
restoreWindow(window.hwnd);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
user32.MoveWindow(window.hwnd, x, y, width, height, true);
|
|
487
|
-
user32.SetForegroundWindow(window.hwnd);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (matchedWindows.length === 0)
|
|
491
|
-
console.log(`No windows found matching '${windowTitle}'.`);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function getScreenForWindow(windowRect: RECT): number {
|
|
495
|
-
// Find which screen contains the center of the window
|
|
496
|
-
const centerX = (windowRect.Left + windowRect.Right) / 2;
|
|
497
|
-
const centerY = (windowRect.Top + windowRect.Bottom) / 2;
|
|
498
|
-
|
|
499
|
-
for (let i = 0; i < screens.length; i++) {
|
|
500
|
-
const screen = screens[i];
|
|
501
|
-
if (centerX >= screen.bounds.Left && centerX < screen.bounds.Right &&
|
|
502
|
-
centerY >= screen.bounds.Top && centerY < screen.bounds.Bottom) {
|
|
503
|
-
return i;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// If not found, return the primary screen (or first screen)
|
|
508
|
-
return screens.findIndex(s => s.primary) || 0;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const padn = (n: number, p = 6) => n.toString().padStart(p);
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Show position info for windows matching pattern
|
|
515
|
-
*/
|
|
516
|
-
function showWindowInfo(pattern: string): void {
|
|
517
|
-
let matchedWindows = findWindowsByPattern(pattern);
|
|
518
|
-
matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
|
|
519
|
-
|
|
520
|
-
if (matchedWindows.length === 0) {
|
|
521
|
-
console.log(`No windows found matching '${pattern}'`);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
for (const window of matchedWindows) {
|
|
526
|
-
const screenIndex = getScreenForWindow(window.rect);
|
|
527
|
-
const screen = screens[screenIndex];
|
|
528
|
-
|
|
529
|
-
const x = window.rect.Left - screen.bounds.Left;
|
|
530
|
-
const y = window.rect.Top - screen.bounds.Top;
|
|
531
|
-
const width = window.rect.Right - window.rect.Left;
|
|
532
|
-
const height = window.rect.Bottom - window.rect.Top;
|
|
533
|
-
|
|
534
|
-
console.log(`${window.title}`);
|
|
535
|
-
console.log(` pos: ${x},${y},${screenIndex} size: ${width},${height}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function listAllWindows(includeIgnored: boolean = false) {
|
|
540
|
-
console.log('====================================');
|
|
541
|
-
console.log('Screens');
|
|
542
|
-
for (let i = 0; i < screens.length; i++) {
|
|
543
|
-
const screen = screens[i];
|
|
544
|
-
// console.log(`${i} Screen: ${screen.deviceName} (${screen.bounds.Top}/${screen.bounds.Bottom}x${screen.bounds.Left}/${screen.bounds.Right})`);
|
|
545
|
-
const b = screen.bounds;
|
|
546
|
-
console.log(`${i} Screen: ${screen.deviceName} ${padn(b.Left)},${padn(b.Top)} (lower right ${padn(b.Right)} ${padn(b.Bottom)})`);
|
|
547
|
-
}
|
|
548
|
-
console.log();
|
|
549
|
-
|
|
550
|
-
const windows = enumerateWindows(includeIgnored).sort((a, b) => a.title.localeCompare(b.title));
|
|
551
|
-
|
|
552
|
-
console.log('Title X Y Scr Width Height');
|
|
553
|
-
console.log('-----------------------------------------------------------------------------------');
|
|
554
|
-
for (const window of windows) {
|
|
555
|
-
const screenIndex = getScreenForWindow(window.rect);
|
|
556
|
-
const screen = screens[screenIndex];
|
|
557
|
-
|
|
558
|
-
// Normalize coordinates relative to the screen
|
|
559
|
-
const x = window.rect.Left - screen.bounds.Left;
|
|
560
|
-
const y = window.rect.Top - screen.bounds.Top;
|
|
561
|
-
const width = window.rect.Right - window.rect.Left;
|
|
562
|
-
const height = window.rect.Bottom - window.rect.Top;
|
|
563
|
-
|
|
564
|
-
// Truncate title if too long
|
|
565
|
-
const title = window.title.length > 45 ? window.title.substring(0, 42) + '...' : window.title;
|
|
566
|
-
|
|
567
|
-
const actual = screenIndex > 0 ? ` ${padn(window.rect.Left)},${padn(window.rect.Top)}` : '';
|
|
568
|
-
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)}${actual}`);
|
|
569
|
-
}
|
|
570
|
-
console.log();
|
|
571
|
-
console.log('Command format: tswinpos <title> <x> <y> <screen> [<width> <height>]');
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function usage(msg: string = '') {
|
|
575
|
-
if (msg)
|
|
576
|
-
console.log(msg);
|
|
577
|
-
|
|
578
|
-
console.log('Usage: winpos [options] <title> <x> <y> <screen> [<width> <height>]');
|
|
579
|
-
console.log(' winpos [options] <title> -pos x,y[,screen] [-size w,h]');
|
|
580
|
-
console.log(' winpos [options] -title <name> -pos x,y[,screen] [-size w,h]');
|
|
581
|
-
console.log(' winpos <title> min | -min');
|
|
582
|
-
console.log(' winpos <config.json>');
|
|
583
|
-
console.log(' winpos -load <config.json> [-save <output.json>]');
|
|
584
|
-
console.log();
|
|
585
|
-
console.log('Options:');
|
|
586
|
-
console.log(' -d Debug mode');
|
|
587
|
-
console.log(' -dbg Verbose debug output');
|
|
588
|
-
console.log(' -c Return screen count as exit code');
|
|
589
|
-
console.log(' -v/-version Show version');
|
|
590
|
-
console.log(' -title name Window title/pattern (alternative to positional)');
|
|
591
|
-
console.log(' -pos x,y[,s] Position (x,y with optional screen index)');
|
|
592
|
-
console.log(' -size w,h Window size (width,height)');
|
|
593
|
-
console.log(' -min Minimize window');
|
|
594
|
-
console.log(' -max Maximize window');
|
|
595
|
-
console.log(' -load file Load config from JSON file');
|
|
596
|
-
console.log(' -save file Save config to JSON file (merges if file exists)');
|
|
597
|
-
console.log();
|
|
598
|
-
console.log('Position/size values can be pixels or percentages (e.g. 50%)');
|
|
599
|
-
console.log('Title can be regex (/pattern/) or prefix (title*)');
|
|
600
|
-
console.log('* = list windows, ** = list all including ignored');
|
|
601
|
-
console.log();
|
|
602
|
-
console.log('JSON format: {"name":"app","regex":false,"pos":{"x":0,"y":0,"screen":0},"size":{"w":800,"h":600}}');
|
|
603
|
-
console.log(' or array: [{...}, {...}]');
|
|
604
|
-
console.log();
|
|
605
|
-
console.log('Screen | X Y | W H ');
|
|
606
|
-
console.log('------------------------------------');
|
|
607
|
-
for (let i = 0; i < screens.length; i++) {
|
|
608
|
-
const screen = screens[i];
|
|
609
|
-
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)}`);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
process.exit(1);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export function run(args: string[]) {
|
|
616
|
-
// Initialize screens
|
|
617
|
-
try {
|
|
618
|
-
screens = sortScreens(enumerateScreens());
|
|
619
|
-
} catch (e: any) {
|
|
620
|
-
if (e.message && e.message.includes('Bun')) {
|
|
621
|
-
console.error(e.message);
|
|
622
|
-
process.exit(1);
|
|
623
|
-
}
|
|
624
|
-
throw e; // Re-throw if it's a different error
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Load ignore patterns from the same directory as the script
|
|
628
|
-
const ignoresPath = join(import.meta.dirname, 'ignores.txt');
|
|
629
|
-
loadIgnorePatterns(ignoresPath);
|
|
630
|
-
|
|
631
|
-
// Parse arguments using new parser
|
|
632
|
-
const opts = parseArgs(args);
|
|
633
|
-
|
|
634
|
-
// Set debug flags
|
|
635
|
-
dbg = opts.debug;
|
|
636
|
-
dbgVerbose = opts.debugVerbose;
|
|
637
|
-
|
|
638
|
-
if (dbg) console.log('Debug mode enabled.');
|
|
639
|
-
if (dbgVerbose) console.log(`[DBG] winpos version ${packageJson.default.version}`);
|
|
640
|
-
|
|
641
|
-
// Handle -c (screen count)
|
|
642
|
-
if (opts.showCount) {
|
|
643
|
-
console.log(screens.length);
|
|
644
|
-
process.exit(screens.length);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Handle load file (JSON config)
|
|
648
|
-
if (opts.loadFile) {
|
|
649
|
-
try {
|
|
650
|
-
const config = loadConfigFile(opts.loadFile);
|
|
651
|
-
let configs = Array.isArray(config) ? config : [config];
|
|
652
|
-
|
|
653
|
-
// If CLI params specify a window, merge into loaded config
|
|
654
|
-
if (opts.windowTitle && (opts.pos || opts.size || opts.minimize || opts.maximize)) {
|
|
655
|
-
const cliConfig = buildConfigFromOptions(opts);
|
|
656
|
-
if (cliConfig) {
|
|
657
|
-
// Find matching entry by name and override it
|
|
658
|
-
const idx = configs.findIndex(c => c.name === cliConfig.name);
|
|
659
|
-
if (idx >= 0) {
|
|
660
|
-
configs[idx] = cliConfig;
|
|
661
|
-
} else {
|
|
662
|
-
configs.push(cliConfig);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Apply all configs
|
|
668
|
-
for (const cfg of configs) {
|
|
669
|
-
applyWindowConfig(cfg, screens);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Save merged config if requested
|
|
673
|
-
if (opts.saveFile !== undefined) {
|
|
674
|
-
const savePath = opts.saveFile || opts.loadFile; // Default to loadFile if -save has no arg
|
|
675
|
-
const absPath = isAbsolute(savePath) ? savePath : resolve(process.cwd(), savePath);
|
|
676
|
-
const finalConfig: ConfigFile = configs.length === 1 ? configs[0] : configs;
|
|
677
|
-
writeFileSync(absPath, JSON.stringify(finalConfig, null, 2), 'utf-8');
|
|
678
|
-
console.log(`Config saved to: ${absPath}`);
|
|
679
|
-
}
|
|
680
|
-
return;
|
|
681
|
-
} catch (e: any) {
|
|
682
|
-
console.error(`Error loading config: ${e.message}`);
|
|
683
|
-
process.exit(1);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// Handle new flag-based syntax (-pos, -size, -min, -max)
|
|
688
|
-
if (opts.pos || opts.size || opts.minimize || opts.maximize) {
|
|
689
|
-
if (!opts.windowTitle) {
|
|
690
|
-
usage('Window title required when using -pos, -size, -min, or -max');
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const config = buildConfigFromOptions(opts);
|
|
695
|
-
if (config) {
|
|
696
|
-
applyWindowConfig(config, screens);
|
|
697
|
-
|
|
698
|
-
// Save if requested
|
|
699
|
-
if (opts.saveFile !== undefined && opts.saveFile) {
|
|
700
|
-
saveConfigFile(opts.saveFile, config, existsSync(opts.saveFile));
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// If only -title provided (no -pos/-size/-min/-max), show window position info
|
|
707
|
-
if (opts.windowTitle && !opts.loadFile) {
|
|
708
|
-
showWindowInfo(opts.windowTitle);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Fall back to legacy positional argument handling
|
|
713
|
-
const posArgs = opts.positionalArgs;
|
|
714
|
-
|
|
715
|
-
if (dbg)
|
|
716
|
-
console.log(`DEBUG[${packageJson.default.version}]: ${posArgs.join(' ')}`);
|
|
717
|
-
|
|
718
|
-
if (posArgs.length === 1 && posArgs[0] === '*') {
|
|
719
|
-
listAllWindows();
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (posArgs.length === 1 && posArgs[0] === '**') {
|
|
724
|
-
listAllWindows(true);
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Check for minimize command (title + min/-min)
|
|
729
|
-
if (posArgs.length === 2 && (posArgs[1].toLowerCase() === 'min' || posArgs[1].toLowerCase() === '-min')) {
|
|
730
|
-
adjustWindow(posArgs);
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Single positional arg = show window info
|
|
735
|
-
if (posArgs.length === 1) {
|
|
736
|
-
showWindowInfo(posArgs[0]);
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (posArgs.length < 4) {
|
|
741
|
-
usage();
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
adjustWindow(posArgs);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// CLI entry point using import.meta.main
|
|
749
|
-
if (import.meta.main) {
|
|
750
|
-
run(process.argv.slice(2));
|
|
751
|
-
}
|