@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.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
- }