@bobfrankston/winpos 2.0.47 → 2.0.49

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,747 +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
- i++;
238
- if (i < args.length) {
239
- opts.saveFile = args[i];
240
- }
241
- break;
242
- case 'title':
243
- i++;
244
- if (i < args.length) {
245
- opts.windowTitle = args[i];
246
- }
247
- break;
248
- case 'min':
249
- opts.minimize = true;
250
- break;
251
- case 'max':
252
- opts.maximize = true;
253
- break;
254
- case 'help':
255
- case '?':
256
- return opts; // Will trigger usage display
257
- default:
258
- // Unknown flag - could be negative number for position
259
- if (/^-?\d+/.test(arg)) {
260
- opts.positionalArgs.push(arg);
261
- } else {
262
- console.log(`Unknown option: ${arg}`);
263
- }
264
- break;
265
- }
266
- } else {
267
- opts.positionalArgs.push(arg);
268
- }
269
- i++;
270
- }
271
-
272
- // First positional arg is window title (if using new flag-based syntax and -title not set)
273
- const usingFlagSyntax = opts.pos || opts.size || opts.loadFile || opts.saveFile || opts.minimize || opts.maximize;
274
- if (opts.positionalArgs.length > 0 && usingFlagSyntax) {
275
- if (!opts.windowTitle) {
276
- opts.windowTitle = opts.positionalArgs[0];
277
- opts.positionalArgs = opts.positionalArgs.slice(1);
278
- }
279
-
280
- // Error if stray positional args remain when using flag-based syntax
281
- if (opts.positionalArgs.length > 0) {
282
- console.error(`Error: unexpected arguments: ${opts.positionalArgs.join(' ')}`);
283
- console.error('When using flags, only window title should be positional');
284
- process.exit(1);
285
- }
286
- }
287
-
288
- return opts;
289
- }
290
-
291
- /**
292
- * Apply a single window config
293
- */
294
- function applyWindowConfig(config: WindowConfig, screensInfo: ScreenInfo[]): void {
295
- // Build pattern for matching
296
- let pattern: string;
297
- if (config.regex) {
298
- pattern = `/${config.name}/`;
299
- } else {
300
- pattern = config.name;
301
- }
302
-
303
- let matchedWindows = findWindowsByPattern(pattern);
304
- matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
305
-
306
- if (matchedWindows.length === 0) {
307
- console.log(`No windows found matching '${config.name}'`);
308
- return;
309
- }
310
-
311
- // Handle minimize
312
- if (config.minimize) {
313
- for (const window of matchedWindows) {
314
- if (dbgVerbose) console.log(`[DBG] Minimizing: "${window.title}"`);
315
- minimizeWindow(window.hwnd);
316
- }
317
- return;
318
- }
319
-
320
- // Handle maximize
321
- if (config.maximize) {
322
- for (const window of matchedWindows) {
323
- if (dbgVerbose) console.log(`[DBG] Maximizing: "${window.title}"`);
324
- maximizeWindow(window.hwnd);
325
- }
326
- return;
327
- }
328
-
329
- // Handle positioning
330
- if (!config.pos) return;
331
-
332
- const screenIndex = config.pos.screen ?? 0;
333
- if (screenIndex < 0 || screenIndex >= screensInfo.length) {
334
- console.log(`Invalid screen ${screenIndex} for window '${config.name}'`);
335
- return;
336
- }
337
-
338
- const targetScreen = screensInfo[screenIndex];
339
- const bounds = targetScreen.bounds;
340
-
341
- const x = parseDimension(String(config.pos.x), bounds.Width) + bounds.Left;
342
- const y = parseDimension(String(config.pos.y), bounds.Height) + bounds.Top;
343
-
344
- let width = config.size ? parseDimension(String(config.size.w), bounds.Width) : 0;
345
- let height = config.size ? parseDimension(String(config.size.h), bounds.Height) : 0;
346
-
347
- for (const window of matchedWindows) {
348
- let w = width, h = height;
349
- if (w === 0) w = window.rect.Right - window.rect.Left;
350
- if (h === 0) h = window.rect.Bottom - window.rect.Top;
351
-
352
- if (dbgVerbose) {
353
- console.log(`[DBG] Window: "${window.title}" -> Position: (${x}, ${y}) Size: ${w}x${h} Screen: ${screenIndex}`);
354
- }
355
-
356
- if (isMinimized(window.hwnd) || isMaximized(window.hwnd)) {
357
- restoreWindow(window.hwnd);
358
- }
359
-
360
- user32.MoveWindow(window.hwnd, x, y, w, h, true);
361
- user32.SetForegroundWindow(window.hwnd);
362
- }
363
- }
364
-
365
- let dbg = false;
366
- let dbgVerbose = false; // -dbg flag for detailed output
367
- let screens: ScreenInfo[] = [];
368
-
369
- /**
370
- * Convert screen-relative coordinates to global coordinates
371
- * @param x - X coordinate relative to screen
372
- * @param y - Y coordinate relative to screen
373
- * @param screenIndex - Screen number (0-based)
374
- * @returns Global coordinates {x, y} or null if invalid screen
375
- */
376
- export function screenPosToPos(x: number, y: number, screenIndex: number): { x: number; y: number } {
377
- // Initialize screens if not already done
378
- if (screens.length === 0) {
379
- try {
380
- screens = sortScreens(enumerateScreens());
381
- } catch (e: any) {
382
- if (e.message && e.message.includes('Bun')) {
383
- console.error(e.message);
384
- return null;
385
- }
386
- throw e;
387
- }
388
- }
389
-
390
- if (screenIndex < 0 || screenIndex >= screens.length) {
391
- return null;
392
- }
393
-
394
- const targetScreen = screens[screenIndex];
395
- const bounds = targetScreen.bounds;
396
-
397
- return {
398
- x: x + bounds.Left,
399
- y: y + bounds.Top
400
- };
401
- }
402
-
403
- function parseDimension(input: string, fullDimension: number): number {
404
- if (input.endsWith('%')) {
405
- const percentage = parseFloat(input.slice(0, -1)) / 100.0;
406
- return Math.floor(fullDimension * percentage);
407
- }
408
- const val = parseInt(input, 10);
409
- return val < 0 ? fullDimension + val : val;
410
- }
411
-
412
- function adjustWindow(args: string[]) {
413
- const windowTitle = args[0];
414
-
415
- // Check if user wants to minimize the window (min or -min in position)
416
- const shouldMinimize = args[1].toLowerCase() === 'min' || args[1].toLowerCase() === '-min';
417
-
418
- if (shouldMinimize) {
419
- if (dbg)
420
- console.log(`Minimizing window '${windowTitle}'`);
421
-
422
- let matchedWindows = findWindowsByPattern(windowTitle);
423
-
424
- // Filter out command windows running tswinpos
425
- matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
426
-
427
- for (const window of matchedWindows) {
428
- if (dbg)
429
- console.log(`Minimizing window '${window.title}'`);
430
-
431
- if (dbgVerbose)
432
- console.log(`[DBG] Minimizing: "${window.title}"`);
433
-
434
- minimizeWindow(window.hwnd);
435
- }
436
-
437
- if (matchedWindows.length === 0)
438
- console.log(`No windows found matching '${windowTitle}'.`);
439
-
440
- return;
441
- }
442
-
443
- const screenIndex = parseInt(args[3], 10);
444
-
445
- if (screenIndex < 0 || screenIndex >= screens.length) {
446
- usage(`Invalid screen number: ${screenIndex}. Must be between 0 and ${screens.length - 1}`);
447
- return;
448
- }
449
-
450
- const targetScreen = screens[screenIndex];
451
- const bounds = targetScreen.bounds;
452
-
453
- const x = parseDimension(args[1], bounds.Width) + bounds.Left;
454
- const y = parseDimension(args[2], bounds.Height) + bounds.Top;
455
- let width = args.length > 4 ? parseDimension(args[4], bounds.Width) : 0;
456
- let height = args.length > 5 ? parseDimension(args[5], bounds.Height) : 0;
457
-
458
- if (dbg)
459
- console.log(`Trying window '${windowTitle}'`);
460
-
461
- let matchedWindows = findWindowsByPattern(windowTitle);
462
-
463
- // Filter out command windows running tswinpos
464
- matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
465
-
466
- for (const window of matchedWindows) {
467
- if (dbg)
468
- console.log(`Moving window '${window.title}' to (${x}, ${y}) on screen ${screenIndex}.`);
469
-
470
- if (width === 0)
471
- width = window.rect.Right - window.rect.Left;
472
- if (height === 0)
473
- height = window.rect.Bottom - window.rect.Top;
474
-
475
- if (dbgVerbose)
476
- console.log(`[DBG] Window: "${window.title}" -> Position: (${x}, ${y}) Size: ${width}x${height} Screen: ${screenIndex}`);
477
-
478
- // Restore window if minimized or maximized to ensure it moves to the correct position
479
- if (isMinimized(window.hwnd) || isMaximized(window.hwnd)) {
480
- restoreWindow(window.hwnd);
481
- }
482
-
483
- user32.MoveWindow(window.hwnd, x, y, width, height, true);
484
- user32.SetForegroundWindow(window.hwnd);
485
- }
486
-
487
- if (matchedWindows.length === 0)
488
- console.log(`No windows found matching '${windowTitle}'.`);
489
- }
490
-
491
- function getScreenForWindow(windowRect: RECT): number {
492
- // Find which screen contains the center of the window
493
- const centerX = (windowRect.Left + windowRect.Right) / 2;
494
- const centerY = (windowRect.Top + windowRect.Bottom) / 2;
495
-
496
- for (let i = 0; i < screens.length; i++) {
497
- const screen = screens[i];
498
- if (centerX >= screen.bounds.Left && centerX < screen.bounds.Right &&
499
- centerY >= screen.bounds.Top && centerY < screen.bounds.Bottom) {
500
- return i;
501
- }
502
- }
503
-
504
- // If not found, return the primary screen (or first screen)
505
- return screens.findIndex(s => s.primary) || 0;
506
- }
507
-
508
- const padn = (n: number, p = 6) => n.toString().padStart(p);
509
-
510
- /**
511
- * Show position info for windows matching pattern
512
- */
513
- function showWindowInfo(pattern: string): void {
514
- let matchedWindows = findWindowsByPattern(pattern);
515
- matchedWindows = matchedWindows.filter(w => !w.title.toLowerCase().includes('tswinpos'));
516
-
517
- if (matchedWindows.length === 0) {
518
- console.log(`No windows found matching '${pattern}'`);
519
- return;
520
- }
521
-
522
- for (const window of matchedWindows) {
523
- const screenIndex = getScreenForWindow(window.rect);
524
- const screen = screens[screenIndex];
525
-
526
- const x = window.rect.Left - screen.bounds.Left;
527
- const y = window.rect.Top - screen.bounds.Top;
528
- const width = window.rect.Right - window.rect.Left;
529
- const height = window.rect.Bottom - window.rect.Top;
530
-
531
- console.log(`${window.title}`);
532
- console.log(` pos: ${x},${y},${screenIndex} size: ${width},${height}`);
533
- }
534
- }
535
-
536
- function listAllWindows(includeIgnored: boolean = false) {
537
- console.log('====================================');
538
- console.log('Screens');
539
- for (let i = 0; i < screens.length; i++) {
540
- const screen = screens[i];
541
- // console.log(`${i} Screen: ${screen.deviceName} (${screen.bounds.Top}/${screen.bounds.Bottom}x${screen.bounds.Left}/${screen.bounds.Right})`);
542
- const b = screen.bounds;
543
- console.log(`${i} Screen: ${screen.deviceName} ${padn(b.Left)},${padn(b.Top)} (lower right ${padn(b.Right)} ${padn(b.Bottom)})`);
544
- }
545
- console.log();
546
-
547
- const windows = enumerateWindows(includeIgnored).sort((a, b) => a.title.localeCompare(b.title));
548
-
549
- console.log('Title X Y Scr Width Height');
550
- console.log('-----------------------------------------------------------------------------------');
551
- for (const window of windows) {
552
- const screenIndex = getScreenForWindow(window.rect);
553
- const screen = screens[screenIndex];
554
-
555
- // Normalize coordinates relative to the screen
556
- const x = window.rect.Left - screen.bounds.Left;
557
- const y = window.rect.Top - screen.bounds.Top;
558
- const width = window.rect.Right - window.rect.Left;
559
- const height = window.rect.Bottom - window.rect.Top;
560
-
561
- // Truncate title if too long
562
- const title = window.title.length > 45 ? window.title.substring(0, 42) + '...' : window.title;
563
-
564
- const actual = screenIndex > 0 ? ` ${padn(window.rect.Left)},${padn(window.rect.Top)}` : '';
565
- 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}`);
566
- }
567
- console.log();
568
- console.log('Command format: tswinpos <title> <x> <y> <screen> [<width> <height>]');
569
- }
570
-
571
- function usage(msg: string = '') {
572
- if (msg)
573
- console.log(msg);
574
-
575
- console.log('Usage: winpos [options] <title> <x> <y> <screen> [<width> <height>]');
576
- console.log(' winpos [options] <title> -pos x,y[,screen] [-size w,h]');
577
- console.log(' winpos [options] -title <name> -pos x,y[,screen] [-size w,h]');
578
- console.log(' winpos <title> min | -min');
579
- console.log(' winpos <config.json>');
580
- console.log(' winpos -load <config.json> [-save <output.json>]');
581
- console.log();
582
- console.log('Options:');
583
- console.log(' -d Debug mode');
584
- console.log(' -dbg Verbose debug output');
585
- console.log(' -c Return screen count as exit code');
586
- console.log(' -v/-version Show version');
587
- console.log(' -title name Window title/pattern (alternative to positional)');
588
- console.log(' -pos x,y[,s] Position (x,y with optional screen index)');
589
- console.log(' -size w,h Window size (width,height)');
590
- console.log(' -min Minimize window');
591
- console.log(' -max Maximize window');
592
- console.log(' -load file Load config from JSON file');
593
- console.log(' -save file Save config to JSON file (merges if file exists)');
594
- console.log();
595
- console.log('Position/size values can be pixels or percentages (e.g. 50%)');
596
- console.log('Title can be regex (/pattern/) or prefix (title*)');
597
- console.log('* = list windows, ** = list all including ignored');
598
- console.log();
599
- console.log('JSON format: {"name":"app","regex":false,"pos":{"x":0,"y":0,"screen":0},"size":{"w":800,"h":600}}');
600
- console.log(' or array: [{...}, {...}]');
601
- console.log();
602
- console.log('Screen | X Y | W H ');
603
- console.log('------------------------------------');
604
- for (let i = 0; i < screens.length; i++) {
605
- const screen = screens[i];
606
- 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)}`);
607
- }
608
-
609
- process.exit(1);
610
- }
611
-
612
- export function run(args: string[]) {
613
- // Initialize screens
614
- try {
615
- screens = sortScreens(enumerateScreens());
616
- } catch (e: any) {
617
- if (e.message && e.message.includes('Bun')) {
618
- console.error(e.message);
619
- process.exit(1);
620
- }
621
- throw e; // Re-throw if it's a different error
622
- }
623
-
624
- // Load ignore patterns from the same directory as the script
625
- const ignoresPath = join(import.meta.dirname, 'ignores.txt');
626
- loadIgnorePatterns(ignoresPath);
627
-
628
- // Parse arguments using new parser
629
- const opts = parseArgs(args);
630
-
631
- // Set debug flags
632
- dbg = opts.debug;
633
- dbgVerbose = opts.debugVerbose;
634
-
635
- if (dbg) console.log('Debug mode enabled.');
636
- if (dbgVerbose) console.log(`[DBG] winpos version ${packageJson.default.version}`);
637
-
638
- // Handle -c (screen count)
639
- if (opts.showCount) {
640
- console.log(screens.length);
641
- process.exit(screens.length);
642
- }
643
-
644
- // Handle load file (JSON config)
645
- if (opts.loadFile) {
646
- try {
647
- const config = loadConfigFile(opts.loadFile);
648
- let configs = Array.isArray(config) ? config : [config];
649
-
650
- // If CLI params specify a window, merge into loaded config
651
- if (opts.windowTitle && (opts.pos || opts.size || opts.minimize || opts.maximize)) {
652
- const cliConfig = buildConfigFromOptions(opts);
653
- if (cliConfig) {
654
- // Find matching entry by name and override it
655
- const idx = configs.findIndex(c => c.name === cliConfig.name);
656
- if (idx >= 0) {
657
- configs[idx] = cliConfig;
658
- } else {
659
- configs.push(cliConfig);
660
- }
661
- }
662
- }
663
-
664
- // Apply all configs
665
- for (const cfg of configs) {
666
- applyWindowConfig(cfg, screens);
667
- }
668
-
669
- // Save merged config if requested
670
- if (opts.saveFile) {
671
- const absPath = isAbsolute(opts.saveFile) ? opts.saveFile : resolve(process.cwd(), opts.saveFile);
672
- const finalConfig: ConfigFile = configs.length === 1 ? configs[0] : configs;
673
- writeFileSync(absPath, JSON.stringify(finalConfig, null, 2), 'utf-8');
674
- console.log(`Config saved to: ${absPath}`);
675
- }
676
- return;
677
- } catch (e: any) {
678
- console.error(`Error loading config: ${e.message}`);
679
- process.exit(1);
680
- }
681
- }
682
-
683
- // Handle new flag-based syntax (-pos, -size, -min, -max)
684
- if (opts.pos || opts.size || opts.minimize || opts.maximize) {
685
- if (!opts.windowTitle) {
686
- usage('Window title required when using -pos, -size, -min, or -max');
687
- return;
688
- }
689
-
690
- const config = buildConfigFromOptions(opts);
691
- if (config) {
692
- applyWindowConfig(config, screens);
693
-
694
- // Save if requested
695
- if (opts.saveFile) {
696
- saveConfigFile(opts.saveFile, config, existsSync(opts.saveFile));
697
- }
698
- }
699
- return;
700
- }
701
-
702
- // If only -title provided (no -pos/-size/-min/-max), show window position info
703
- if (opts.windowTitle && !opts.loadFile) {
704
- showWindowInfo(opts.windowTitle);
705
- return;
706
- }
707
-
708
- // Fall back to legacy positional argument handling
709
- const posArgs = opts.positionalArgs;
710
-
711
- if (dbg)
712
- console.log(`DEBUG[${packageJson.default.version}]: ${posArgs.join(' ')}`);
713
-
714
- if (posArgs.length === 1 && posArgs[0] === '*') {
715
- listAllWindows();
716
- return;
717
- }
718
-
719
- if (posArgs.length === 1 && posArgs[0] === '**') {
720
- listAllWindows(true);
721
- return;
722
- }
723
-
724
- // Check for minimize command (title + min/-min)
725
- if (posArgs.length === 2 && (posArgs[1].toLowerCase() === 'min' || posArgs[1].toLowerCase() === '-min')) {
726
- adjustWindow(posArgs);
727
- return;
728
- }
729
-
730
- // Single positional arg = show window info
731
- if (posArgs.length === 1) {
732
- showWindowInfo(posArgs[0]);
733
- return;
734
- }
735
-
736
- if (posArgs.length < 4) {
737
- usage();
738
- return;
739
- }
740
-
741
- adjustWindow(posArgs);
742
- }
743
-
744
- // CLI entry point using import.meta.main
745
- if (import.meta.main) {
746
- run(process.argv.slice(2));
747
- }