@coreify/quitx 1.0.0 → 1.1.0

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli.js +611 -190
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 1.1.0
9
+
10
+ ### Added
11
+
12
+ - Group background instances option (`groupBackground`, default `true`) to aggregate duplicate background helper processes into one entry with count and terminate all PIDs upon quitting.
13
+ - Never quit music apps protection (`neverQuitMusic`) with hybrid detection: macOS category `public.app-category.music`, bundle IDs, well-known music apps (Spotify, Apple Music, Tidal, Cider, Vox, etc.), and custom music apps.
14
+ - Interactive custom music app management (`musicApps`) in configuration menu.
15
+ - Deselect apps by default option (`defaultSelectAll`) to start interactive picker with no apps pre-selected.
16
+ - Include Empty Trash in list option (`includeTrash`) with explicit warning that quitting permanently and irreversibly deletes macOS Trash.
17
+ - Cleaner, intuitive configuration menu options aligned with native preferences.
18
+
19
+ ### Fixed
20
+
21
+ - Fixed Windows silent exit: normalized path separators in CLI entry point detection so Windows/Linux platforms cleanly print OS guard error (`✖ quitx only works on macOS. Windows and Linux are not supported.`) with exit code 1.
22
+ - Unified running app discovery across home selector and exclude/music add menus.
23
+ - Handled multi-PID termination cleanly when quitting grouped background instances.
24
+
8
25
  ## 1.0.0
9
26
 
10
27
  ### Added
package/dist/cli.js CHANGED
@@ -6,7 +6,6 @@ import { homedir } from 'os';
6
6
  import { join } from 'path';
7
7
  import { execa } from 'execa';
8
8
  import { spawn } from 'child_process';
9
- import { MultiSelectPrompt } from '@clack/core';
10
9
 
11
10
  var CONFIG_DIR = join(homedir(), ".quitx");
12
11
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
@@ -14,27 +13,40 @@ var DEFAULT_CONFIG = {
14
13
  exclude: [],
15
14
  force: "normal",
16
15
  includeFinder: false,
17
- includeBackground: false
16
+ includeTrash: false,
17
+ includeBackground: false,
18
+ groupBackground: true,
19
+ defaultSelectAll: true,
20
+ neverQuitMusic: false,
21
+ musicApps: []
18
22
  };
19
23
  function loadConfig(filePath = CONFIG_FILE) {
20
24
  try {
21
25
  if (!existsSync(filePath)) {
22
- return { ...DEFAULT_CONFIG, exclude: [] };
26
+ return { ...DEFAULT_CONFIG, exclude: [], musicApps: [] };
23
27
  }
24
28
  const raw = readFileSync(filePath, "utf-8");
25
29
  const parsed = JSON.parse(raw);
26
30
  if (typeof parsed !== "object" || parsed === null) {
27
- return { ...DEFAULT_CONFIG, exclude: [] };
31
+ return { ...DEFAULT_CONFIG, exclude: [], musicApps: [] };
28
32
  }
29
33
  const obj = parsed;
34
+ const defaultSelectVal = typeof obj.defaultSelectAll === "boolean" ? obj.defaultSelectAll : obj.defaultSelect === "deselect-all" || obj.defaultSelection === "deselect-all" || obj.defaultSelect === false ? false : typeof obj.defaultSelect === "boolean" ? obj.defaultSelect : DEFAULT_CONFIG.defaultSelectAll;
35
+ const groupBgVal = typeof obj.groupBackground === "boolean" ? obj.groupBackground : typeof obj.groupBackgroundInstances === "boolean" ? obj.groupBackgroundInstances : DEFAULT_CONFIG.groupBackground;
36
+ const neverQuitMusicVal = typeof obj.neverQuitMusic === "boolean" ? obj.neverQuitMusic : DEFAULT_CONFIG.neverQuitMusic;
30
37
  return {
31
38
  exclude: Array.isArray(obj.exclude) ? obj.exclude : [],
32
39
  force: obj.force === "force" || obj.force === "normal" ? obj.force : DEFAULT_CONFIG.force,
33
40
  includeFinder: typeof obj.includeFinder === "boolean" ? obj.includeFinder : DEFAULT_CONFIG.includeFinder,
34
- includeBackground: typeof obj.includeBackground === "boolean" ? obj.includeBackground : DEFAULT_CONFIG.includeBackground
41
+ includeTrash: typeof obj.includeTrash === "boolean" ? obj.includeTrash : DEFAULT_CONFIG.includeTrash,
42
+ includeBackground: typeof obj.includeBackground === "boolean" ? obj.includeBackground : DEFAULT_CONFIG.includeBackground,
43
+ groupBackground: groupBgVal,
44
+ defaultSelectAll: defaultSelectVal,
45
+ neverQuitMusic: neverQuitMusicVal,
46
+ musicApps: Array.isArray(obj.musicApps) ? obj.musicApps : DEFAULT_CONFIG.musicApps
35
47
  };
36
48
  } catch {
37
- return { ...DEFAULT_CONFIG, exclude: [] };
49
+ return { ...DEFAULT_CONFIG, exclude: [], musicApps: [] };
38
50
  }
39
51
  }
40
52
  function saveConfig(config, filePath = CONFIG_FILE) {
@@ -72,6 +84,31 @@ function removeExcludedApps(apps, filePath = CONFIG_FILE) {
72
84
  saveConfig(next, filePath);
73
85
  return next;
74
86
  }
87
+ function addMusicApps(apps, filePath = CONFIG_FILE) {
88
+ const current = loadConfig(filePath);
89
+ const set = new Set(current.musicApps.map((a) => a.toLowerCase().trim()));
90
+ const updated = [...current.musicApps];
91
+ for (const app of apps) {
92
+ const trimmed = app.trim();
93
+ if (trimmed && !set.has(trimmed.toLowerCase())) {
94
+ set.add(trimmed.toLowerCase());
95
+ updated.push(trimmed);
96
+ }
97
+ }
98
+ const next = { ...current, musicApps: updated };
99
+ saveConfig(next, filePath);
100
+ return next;
101
+ }
102
+ function removeMusicApps(apps, filePath = CONFIG_FILE) {
103
+ const current = loadConfig(filePath);
104
+ const toRemove = new Set(apps.map((a) => a.toLowerCase().trim()));
105
+ const updated = current.musicApps.filter(
106
+ (app) => !toRemove.has(app.toLowerCase().trim())
107
+ );
108
+ const next = { ...current, musicApps: updated };
109
+ saveConfig(next, filePath);
110
+ return next;
111
+ }
75
112
 
76
113
  // src/macos/jxa.ts
77
114
  var APPKIT_PREAMBLE = "ObjC.import('AppKit');";
@@ -98,6 +135,10 @@ async function runJXA(script, args = [], executor = execa) {
98
135
  ]);
99
136
  return stdout.trim();
100
137
  }
138
+ async function runAppleScript(script, executor = execa) {
139
+ const { stdout } = await executor("osascript", ["-e", script]);
140
+ return stdout.trim();
141
+ }
101
142
 
102
143
  // src/macos/apps.ts
103
144
  function buildDiscoveryScript(includeBackground = false) {
@@ -114,7 +155,18 @@ function run() {
114
155
  const name = app.localizedName ? app.localizedName.js : '';
115
156
  const bundleId = app.bundleIdentifier ? app.bundleIdentifier.js : '';
116
157
  const pid = String(app.processIdentifier);
117
- lines.push(name + '\\t' + bundleId + '\\t' + pid);
158
+ const isBg = app.activationPolicy !== Regular ? '1' : '0';
159
+ let isMusic = '0';
160
+ if (app.bundleURL) {
161
+ const bundle = $.NSBundle.bundleWithURL(app.bundleURL);
162
+ if (bundle && bundle.infoDictionary) {
163
+ const cat = bundle.infoDictionary.objectForKey('LSApplicationCategoryType');
164
+ if (cat && cat.js === 'public.app-category.music') {
165
+ isMusic = '1';
166
+ }
167
+ }
168
+ }
169
+ lines.push(name + '\\t' + bundleId + '\\t' + pid + '\\t' + isBg + '\\t' + isMusic);
118
170
  }
119
171
  return lines.join('\\n');
120
172
  }`.trim();
@@ -148,35 +200,104 @@ function parseAppListOutput(stdout) {
148
200
  const results = [];
149
201
  for (const line of lines) {
150
202
  const parts = line.split(" ");
151
- const rawName = parts[0]?.trim();
203
+ const rawName = parts[0]?.replace(/[\u200B-\u200D\u200E\u200F\uFEFF]/g, "").trim();
152
204
  if (!rawName) continue;
153
205
  const rawBundle = parts[1]?.trim();
154
206
  const bundleId = rawBundle && rawBundle !== "missing value" && rawBundle.length > 0 ? rawBundle : void 0;
155
207
  const rawPid = parts[2]?.trim();
156
208
  const pidNum = rawPid ? parseInt(rawPid, 10) : void 0;
157
209
  const pid = !Number.isNaN(pidNum) && pidNum !== void 0 && pidNum > 0 ? pidNum : void 0;
210
+ const rawIsBg = parts[3]?.trim();
211
+ const isBackground = rawIsBg === "1";
212
+ const rawIsMusic = parts[4]?.trim();
213
+ const isMusic = rawIsMusic === "1";
158
214
  let name = rawName;
159
215
  if (bundleId && rawName === bundleId) {
160
216
  name = formatDynamicName(bundleId);
161
217
  }
162
- results.push({ name, bundleId, pid });
218
+ results.push({
219
+ name,
220
+ bundleId,
221
+ pid,
222
+ ...isBackground ? { isBackground: true } : {},
223
+ ...isMusic ? { isMusic: true } : {}
224
+ });
163
225
  }
164
226
  return results;
165
227
  }
166
- return trimmed.split(",").map((name) => name.trim()).filter(Boolean).map((name) => ({ name }));
228
+ return trimmed.split(",").map(
229
+ (name) => name.replace(/[\u200B-\u200D\u200E\u200F\uFEFF]/g, "").trim()
230
+ ).filter(Boolean).map((name) => ({ name }));
167
231
  }
168
232
  function sortApps(apps) {
169
233
  return [...apps].sort(
170
234
  (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
171
235
  );
172
236
  }
237
+ var KNOWN_MUSIC_BUNDLE_IDS = /* @__PURE__ */ new Set([
238
+ "com.apple.music",
239
+ "com.spotify.client",
240
+ "com.tidal.desktop",
241
+ "com.github.th-ch.youtube-music",
242
+ "app.ytmdesktop.ytmdesktop",
243
+ "com.amazon.music",
244
+ "com.deezer.deezer-desktop",
245
+ "com.coppertino.vox",
246
+ "com.audirvana.audirvana-plus",
247
+ "com.audirvana.audirvana",
248
+ "com.swinsian.swinsian",
249
+ "cider.sh",
250
+ "com.cider.cider"
251
+ ]);
252
+ var KNOWN_MUSIC_APP_NAMES = /* @__PURE__ */ new Set([
253
+ "music",
254
+ "apple music",
255
+ "spotify",
256
+ "tidal",
257
+ "youtube music",
258
+ "amazon music",
259
+ "deezer",
260
+ "qobuz",
261
+ "soundcloud",
262
+ "pandora",
263
+ "vox",
264
+ "audirvana",
265
+ "foobar2000",
266
+ "swinsian",
267
+ "cider"
268
+ ]);
269
+ function isMusicApp(app, customMusicApps = []) {
270
+ if (app.isMusic) {
271
+ return true;
272
+ }
273
+ const nameLower = app.name.toLowerCase().trim();
274
+ const bundleLower = app.bundleId?.toLowerCase().trim();
275
+ if (bundleLower && KNOWN_MUSIC_BUNDLE_IDS.has(bundleLower)) {
276
+ return true;
277
+ }
278
+ if (KNOWN_MUSIC_APP_NAMES.has(nameLower)) {
279
+ return true;
280
+ }
281
+ if (customMusicApps.length > 0) {
282
+ const customSet = new Set(
283
+ customMusicApps.map((a) => a.toLowerCase().trim())
284
+ );
285
+ if (customSet.has(nameLower) || bundleLower && customSet.has(bundleLower)) {
286
+ return true;
287
+ }
288
+ }
289
+ return false;
290
+ }
173
291
  function filterApps(apps, optionsOrExclude = {}) {
174
292
  const opts = Array.isArray(optionsOrExclude) ? { exclude: optionsOrExclude } : optionsOrExclude;
175
293
  const exclude = opts.exclude ?? [];
176
294
  const includeFinder = opts.includeFinder ?? false;
177
- const seen = /* @__PURE__ */ new Set();
295
+ const includeTrash = opts.includeTrash ?? false;
296
+ const groupBackground = opts.groupBackground ?? true;
297
+ const neverQuitMusic = opts.neverQuitMusic ?? false;
298
+ const customMusicApps = opts.musicApps ?? [];
178
299
  const excludeSet = new Set(exclude.map((e) => e.toLowerCase().trim()));
179
- const filtered = [];
300
+ const eligible = [];
180
301
  for (const app of apps) {
181
302
  const nameLower = app.name.toLowerCase().trim();
182
303
  const bundleLower = app.bundleId?.toLowerCase().trim();
@@ -188,23 +309,73 @@ function filterApps(apps, optionsOrExclude = {}) {
188
309
  continue;
189
310
  }
190
311
  }
191
- if (excludeSet.has(nameLower) || bundleLower && excludeSet.has(bundleLower)) {
312
+ if (!includeTrash) {
313
+ if (nameLower === "trash" || bundleLower === "com.apple.trash") {
314
+ continue;
315
+ }
316
+ }
317
+ if (neverQuitMusic && isMusicApp(app, customMusicApps)) {
192
318
  continue;
193
319
  }
194
- const key = bundleLower ?? nameLower;
195
- if (seen.has(key)) {
320
+ if (excludeSet.has(nameLower) || bundleLower && excludeSet.has(bundleLower)) {
196
321
  continue;
197
322
  }
198
- seen.add(key);
199
- filtered.push(app);
323
+ eligible.push(app);
200
324
  }
201
- return sortApps(filtered);
325
+ const result = [];
326
+ const grouped = /* @__PURE__ */ new Map();
327
+ for (const app of eligible) {
328
+ const nameLower = app.name.toLowerCase().trim();
329
+ const bundleLower = app.bundleId?.toLowerCase().trim();
330
+ const isBg = app.isBackground === true;
331
+ if (isBg && !groupBackground) {
332
+ result.push({ ...app });
333
+ } else {
334
+ let key = bundleLower ?? nameLower;
335
+ let cleanName = app.name;
336
+ if (isBg) {
337
+ const base = app.name.replace(/\s*\([^)]*\)$/, "").trim();
338
+ if (base) {
339
+ key = base.toLowerCase();
340
+ cleanName = base;
341
+ }
342
+ }
343
+ const existing = grouped.get(key);
344
+ if (existing) {
345
+ if (app.pid) {
346
+ if (!existing.pids) {
347
+ existing.pids = existing.pid ? [existing.pid] : [];
348
+ }
349
+ if (!existing.pids.includes(app.pid)) {
350
+ existing.pids.push(app.pid);
351
+ }
352
+ existing.count = existing.pids.length;
353
+ } else {
354
+ existing.count = (existing.count ?? 1) + 1;
355
+ }
356
+ } else {
357
+ const item = { ...app, name: cleanName };
358
+ grouped.set(key, item);
359
+ result.push(item);
360
+ }
361
+ }
362
+ }
363
+ if (includeTrash && !excludeSet.has("trash") && !excludeSet.has("com.apple.trash")) {
364
+ const hasTrash = result.some(
365
+ (a) => a.name.toLowerCase() === "trash" || a.bundleId?.toLowerCase() === "com.apple.trash"
366
+ );
367
+ if (!hasTrash) {
368
+ result.push({ name: "Trash", bundleId: "com.apple.trash" });
369
+ }
370
+ }
371
+ return sortApps(result);
202
372
  }
203
373
  async function getRunningApps(optionsOrExclude, executor) {
204
374
  let exec;
205
- let filterOpts = {};
375
+ let filterOpts;
206
376
  let includeBackground = false;
207
377
  if (typeof optionsOrExclude === "function") {
378
+ filterOpts = {};
208
379
  exec = optionsOrExclude;
209
380
  } else if (Array.isArray(optionsOrExclude)) {
210
381
  filterOpts = { exclude: optionsOrExclude };
@@ -213,11 +384,25 @@ async function getRunningApps(optionsOrExclude, executor) {
213
384
  const opts = optionsOrExclude;
214
385
  filterOpts = {
215
386
  exclude: opts.exclude ?? [],
216
- includeFinder: opts.includeFinder ?? false
387
+ includeFinder: opts.includeFinder ?? false,
388
+ includeTrash: opts.includeTrash ?? false,
389
+ groupBackground: opts.groupBackground ?? true,
390
+ neverQuitMusic: opts.neverQuitMusic ?? false,
391
+ musicApps: opts.musicApps ?? []
217
392
  };
218
393
  includeBackground = opts.includeBackground ?? false;
219
394
  exec = executor;
220
395
  } else {
396
+ const config = loadConfig();
397
+ filterOpts = {
398
+ exclude: config.exclude,
399
+ includeFinder: config.includeFinder,
400
+ includeTrash: config.includeTrash,
401
+ groupBackground: config.groupBackground,
402
+ neverQuitMusic: config.neverQuitMusic,
403
+ musicApps: config.musicApps
404
+ };
405
+ includeBackground = config.includeBackground;
221
406
  exec = executor;
222
407
  }
223
408
  const script = buildDiscoveryScript(includeBackground);
@@ -240,8 +425,19 @@ async function isAppRunning(target, executor) {
240
425
  if (typeof target === "number") {
241
426
  return isProcessAlive(target);
242
427
  }
243
- if (typeof target === "object" && target.pid !== void 0 && target.pid > 0) {
244
- return isProcessAlive(target.pid);
428
+ if (typeof target === "object") {
429
+ if (target.bundleId === "com.apple.trash" || target.name.toLowerCase() === "trash") {
430
+ return false;
431
+ }
432
+ if (target.pids && target.pids.length > 0) {
433
+ return target.pids.some((pid) => isProcessAlive(pid));
434
+ }
435
+ if (target.pid !== void 0 && target.pid > 0) {
436
+ return isProcessAlive(target.pid);
437
+ }
438
+ }
439
+ if (typeof target === "string" && (target.toLowerCase() === "trash" || target.toLowerCase() === "com.apple.trash")) {
440
+ return false;
245
441
  }
246
442
  const name = typeof target === "string" ? target : target.name;
247
443
  const bundleId = typeof target === "object" ? target.bundleId ?? "" : "";
@@ -353,13 +549,32 @@ function isCurrentTerminalApp(app, env = process.env) {
353
549
  }
354
550
 
355
551
  // src/macos/quit.ts
552
+ var EMPTY_TRASH_SCRIPT = `
553
+ ignoring application responses
554
+ tell application "Finder"
555
+ try
556
+ set warns before emptying to false
557
+ empty the trash
558
+ end try
559
+ end tell
560
+ end ignoring
561
+ `.trim();
356
562
  var QUIT_JXA_SCRIPT = buildAppMatcherScript(`
357
563
  const bundleId = argv[0] || '';
358
564
  const name = argv[1] || '';
565
+ const pidStr = argv[2] || '';
566
+ const targetPid = pidStr ? parseInt(pidStr, 10) : 0;
359
567
  const bid = app.bundleIdentifier ? app.bundleIdentifier.js : '';
360
568
  const nm = app.localizedName ? app.localizedName.js : '';
361
- if ((bundleId && bid === bundleId) || (name && nm === name)) {
362
- return String(app.terminate());
569
+ const appPid = app.processIdentifier;
570
+
571
+ const matches = targetPid
572
+ ? appPid === targetPid
573
+ : ((bundleId && bid === bundleId) || (name && nm.toLowerCase() === name.toLowerCase()));
574
+
575
+ if (matches) {
576
+ const res = typeof app.terminate === 'function' ? app.terminate() : app.terminate;
577
+ return String(res);
363
578
  }
364
579
  `);
365
580
  function shellQuote(value) {
@@ -368,7 +583,9 @@ function shellQuote(value) {
368
583
  function defaultDeferredQuitScheduler(app, force = false) {
369
584
  try {
370
585
  const bundleId = app.bundleId ?? "";
371
- const cmd = force && app.pid ? `sleep 0.4 && kill -9 ${app.pid}` : `sleep 0.4 && osascript -l JavaScript -e ${shellQuote(QUIT_JXA_SCRIPT)} ${shellQuote(bundleId)} ${shellQuote(app.name)}`;
586
+ const pids = app.pids && app.pids.length > 0 ? app.pids : app.pid ? [app.pid] : [];
587
+ const pidArg = app.pid ? ` ${shellQuote(String(app.pid))}` : "";
588
+ const cmd = force && pids.length > 0 ? `sleep 0.4 && kill -9 ${pids.join(" ")}` : `sleep 0.4 && osascript -l JavaScript -e ${shellQuote(QUIT_JXA_SCRIPT)} ${shellQuote(bundleId)} ${shellQuote(app.name)}${pidArg}`;
372
589
  const child = spawn("sh", ["-c", cmd], {
373
590
  detached: true,
374
591
  stdio: "ignore"
@@ -378,16 +595,48 @@ function defaultDeferredQuitScheduler(app, force = false) {
378
595
  }
379
596
  }
380
597
  async function sendQuitSignal(app, executor) {
381
- const res = await runJXA(
382
- QUIT_JXA_SCRIPT,
383
- [app.bundleId ?? "", app.name],
384
- executor
385
- );
598
+ if (app.bundleId === "com.apple.trash" || app.name.toLowerCase() === "trash") {
599
+ await runAppleScript(EMPTY_TRASH_SCRIPT, executor);
600
+ return;
601
+ }
602
+ if (app.pids && app.pids.length > 1) {
603
+ let anyQuit = false;
604
+ for (const pid of app.pids) {
605
+ const args2 = [app.bundleId ?? "", app.name, String(pid)];
606
+ try {
607
+ const res2 = await runJXA(QUIT_JXA_SCRIPT, args2, executor);
608
+ if (res2.trim().toLowerCase() === "true") {
609
+ anyQuit = true;
610
+ }
611
+ } catch {
612
+ }
613
+ }
614
+ if (!anyQuit) {
615
+ throw new Error(`Could not quit "${app.name}"`);
616
+ }
617
+ return;
618
+ }
619
+ const args = [app.bundleId ?? "", app.name];
620
+ if (app.pid && app.pid > 0) {
621
+ args.push(String(app.pid));
622
+ }
623
+ const res = await runJXA(QUIT_JXA_SCRIPT, args, executor);
386
624
  if (res.trim().toLowerCase() !== "true") {
387
625
  throw new Error(`Could not quit "${app.name}"`);
388
626
  }
389
627
  }
390
628
  function forceQuitApp(app) {
629
+ if (app.pids && app.pids.length > 0) {
630
+ let anyKilled = false;
631
+ for (const pid of app.pids) {
632
+ try {
633
+ process.kill(pid, "SIGKILL");
634
+ anyKilled = true;
635
+ } catch {
636
+ }
637
+ }
638
+ return anyKilled;
639
+ }
391
640
  if (app.pid && app.pid > 0) {
392
641
  try {
393
642
  process.kill(app.pid, "SIGKILL");
@@ -398,6 +647,7 @@ function forceQuitApp(app) {
398
647
  }
399
648
  return false;
400
649
  }
650
+ var DEFAULT_QUIT_TIMEOUT_MS = 8e3;
401
651
  async function quitApps(apps, options = {}, executor, deferredScheduler = defaultDeferredQuitScheduler) {
402
652
  if (apps.length === 0) {
403
653
  return [];
@@ -413,21 +663,33 @@ async function quitApps(apps, options = {}, executor, deferredScheduler = defaul
413
663
  }
414
664
  const results = [];
415
665
  if (otherApps.length > 0) {
416
- const timeoutMs = options.timeoutMs ?? 700;
666
+ const maxTimeoutMs = options.timeoutMs ?? DEFAULT_QUIT_TIMEOUT_MS;
667
+ const pollIntervalMs = Math.min(150, maxTimeoutMs);
417
668
  await Promise.allSettled(
418
669
  otherApps.map((app) => sendQuitSignal(app, executor))
419
670
  );
420
- await sleep(timeoutMs);
421
- for (const app of otherApps) {
422
- const stillRunning = await isAppRunning(app, executor);
423
- if (!stillRunning) {
424
- results.push({ app, success: true, forced: false });
425
- continue;
671
+ const pending = /* @__PURE__ */ new Map();
672
+ for (let i = 0; i < otherApps.length; i++) {
673
+ const app = otherApps[i];
674
+ const key = app.pid ?? `idx_${i}_${app.name}`;
675
+ pending.set(key, app);
676
+ }
677
+ const start = Date.now();
678
+ while (pending.size > 0 && Date.now() - start <= maxTimeoutMs) {
679
+ await sleep(pollIntervalMs);
680
+ for (const [key, app] of Array.from(pending.entries())) {
681
+ const stillRunning = await isAppRunning(app, executor);
682
+ if (!stillRunning) {
683
+ results.push({ app, success: true, forced: false });
684
+ pending.delete(key);
685
+ }
426
686
  }
687
+ }
688
+ for (const app of pending.values()) {
427
689
  if (options.force) {
428
690
  const killed = forceQuitApp(app);
429
691
  if (killed) {
430
- await sleep(100);
692
+ await sleep(150);
431
693
  const aliveAfterForce = await isAppRunning(app, executor);
432
694
  if (!aliveAfterForce) {
433
695
  results.push({ app, success: true, forced: true });
@@ -504,6 +766,7 @@ OPTIONS:
504
766
  -f, --force Force quit apps (SIGKILL) if normal quit fails
505
767
  -b, --background Include background processes in app list
506
768
  --include-finder Include Finder in the app list
769
+ --include-trash Include Trash in the app list (empties on quit)
507
770
  --exclude <apps> Add apps to persistent exclude list (comma-separated)
508
771
  --config Open interactive config manager
509
772
  --json Output data in JSON format
@@ -531,7 +794,7 @@ EXAMPLES:
531
794
  function renderVersion(version) {
532
795
  console.log(`quitx v${version}`);
533
796
  }
534
- var THANKS_MESSAGE = "\nThanks for using quitx..!";
797
+ var THANKS_MESSAGE = "\nThanks for using quitx..!\nFor more visit - quitx.js.org";
535
798
  function printThanks(options) {
536
799
  if (options?.json || options?.quiet) return;
537
800
  console.log(THANKS_MESSAGE);
@@ -541,125 +804,40 @@ var BACK_VALUE = "back";
541
804
  function backOption() {
542
805
  return { value: BACK_VALUE, label: "Back to main menu" };
543
806
  }
544
- function handleQuitAllToggle(currentValue, selectedValues) {
545
- if (currentValue === SELECT_ALL_VALUE) {
546
- if (selectedValues.includes(SELECT_ALL_VALUE)) {
547
- return [];
548
- }
549
- return [SELECT_ALL_VALUE];
550
- }
551
- if (selectedValues.includes(SELECT_ALL_VALUE)) {
552
- return [...selectedValues];
553
- }
554
- const isSelected = selectedValues.includes(currentValue);
555
- return isSelected ? selectedValues.filter((v) => v !== currentValue) : [...selectedValues, currentValue];
556
- }
557
- function attachQuitAllBehavior(prompt) {
558
- let _cursor = prompt.cursor ?? 0;
559
- let _value = prompt.value ? [...prompt.value] : [];
560
- Object.defineProperty(prompt, "cursor", {
561
- get() {
562
- return _cursor;
563
- },
564
- set(val) {
565
- if (_value.includes(SELECT_ALL_VALUE)) {
566
- _cursor = 0;
567
- } else {
568
- _cursor = val;
569
- }
570
- },
571
- configurable: true
572
- });
573
- Object.defineProperty(prompt, "value", {
574
- get() {
575
- return _value;
576
- },
577
- set(val) {
578
- if (!Array.isArray(val)) {
579
- _value = [];
580
- return;
581
- }
582
- if (val.includes(SELECT_ALL_VALUE)) {
583
- _value = [SELECT_ALL_VALUE];
584
- _cursor = 0;
585
- } else {
586
- _value = val.filter((v) => v !== SELECT_ALL_VALUE);
587
- }
588
- },
589
- configurable: true
590
- });
591
- prompt.toggleValue = function() {
592
- const cur = prompt.options[prompt.cursor ?? 0]?.value;
593
- if (!cur) return;
594
- prompt.value = handleQuitAllToggle(cur, prompt.value ?? []);
595
- };
596
- prompt.toggleAll = function() {
597
- if (prompt.value?.includes(SELECT_ALL_VALUE)) {
598
- prompt.value = [];
599
- } else {
600
- prompt.value = [SELECT_ALL_VALUE];
601
- }
602
- };
603
- prompt.toggleInvert = function() {
604
- if (prompt.value?.includes(SELECT_ALL_VALUE)) {
605
- prompt.value = [];
606
- } else {
607
- prompt.value = [SELECT_ALL_VALUE];
608
- }
609
- };
610
- }
611
- async function selectApps(apps) {
807
+ async function selectApps(apps, options = {}) {
612
808
  if (apps.length === 0) {
613
809
  return [];
614
810
  }
615
- const appOptions = apps.map((app) => {
616
- const hint = app.bundleId ?? (app.pid ? `PID: ${app.pid}` : void 0);
811
+ const bundleCounts = /* @__PURE__ */ new Map();
812
+ for (const app of apps) {
813
+ const k = app.bundleId ?? app.name;
814
+ bundleCounts.set(k, (bundleCounts.get(k) ?? 0) + 1);
815
+ }
816
+ const appOptions = apps.map((app, index) => {
817
+ let hint = app.bundleId ?? (app.pid ? `PID: ${app.pid}` : void 0);
818
+ if (app.count && app.count > 1) {
819
+ hint = app.bundleId ? `${app.bundleId} (${app.count} instances)` : `${app.count} instances`;
820
+ } else if (app.pid && app.isBackground) {
821
+ hint = app.bundleId ? `${app.bundleId} (PID: ${app.pid})` : `PID: ${app.pid}`;
822
+ }
823
+ const base = app.bundleId ?? app.name;
824
+ const isDup = (bundleCounts.get(base) ?? 0) > 1;
825
+ const value = isDup && app.pid ? `${base}:${app.pid}` : app.bundleId ?? `${app.name}-${app.pid ?? index}`;
826
+ const label = app.count && app.count > 1 ? `${app.name} (${app.count})` : app.name;
617
827
  return {
618
- value: app.bundleId ?? `${app.name}-${app.pid ?? 0}`,
619
- label: app.name,
828
+ value,
829
+ label,
620
830
  ...hint ? { hint } : {}
621
831
  };
622
832
  });
623
- const options = [
624
- {
625
- value: SELECT_ALL_VALUE,
626
- label: "Quit all apps",
627
- hint: `${apps.length} eligible apps`
628
- },
629
- ...appOptions
630
- ];
631
- const initialValues = [SELECT_ALL_VALUE];
632
- const proto = MultiSelectPrompt?.prototype;
633
- const origPrompt = proto?.prompt;
634
- const origToggleValue = proto?.toggleValue;
635
- const origToggleAll = proto?.toggleAll;
636
- const origToggleInvert = proto?.toggleInvert;
637
- let selected;
638
- try {
639
- if (proto && origPrompt) {
640
- proto.prompt = function() {
641
- if (this.options?.some((o) => o.value === SELECT_ALL_VALUE)) {
642
- attachQuitAllBehavior(
643
- this
644
- );
645
- }
646
- return origPrompt.call(this);
647
- };
648
- }
649
- selected = await multiselect({
650
- message: "Select apps to quit",
651
- options,
652
- required: false,
653
- initialValues
654
- });
655
- } finally {
656
- if (proto && origPrompt) proto.prompt = origPrompt;
657
- if (proto && origToggleValue) proto.toggleValue = origToggleValue;
658
- if (proto && origToggleAll) proto.toggleAll = origToggleAll;
659
- if (proto && origToggleInvert) {
660
- proto.toggleInvert = origToggleInvert;
661
- }
662
- }
833
+ const defaultSelectAll = options.defaultSelectAll ?? true;
834
+ const initialValues = defaultSelectAll ? appOptions.map((opt) => opt.value) : [];
835
+ const selected = await multiselect({
836
+ message: "Select apps to quit",
837
+ options: appOptions,
838
+ required: false,
839
+ initialValues
840
+ });
663
841
  if (isCancel(selected) || !Array.isArray(selected)) {
664
842
  return selected;
665
843
  }
@@ -671,9 +849,10 @@ async function selectApps(apps) {
671
849
  return [...apps];
672
850
  }
673
851
  const selectedSet = new Set(selectedArray);
674
- return apps.filter((app) => {
675
- const key = app.bundleId ?? `${app.name}-${app.pid ?? 0}`;
676
- return selectedSet.has(key);
852
+ return apps.filter((app, index) => {
853
+ const optValue = appOptions[index]?.value;
854
+ const key = app.bundleId ?? `${app.name}-${app.pid ?? index}`;
855
+ return optValue && selectedSet.has(optValue) || app.bundleId && selectedSet.has(app.bundleId) || selectedSet.has(key);
677
856
  });
678
857
  }
679
858
  async function shouldConfirmQuit(count, isAll = false, forceYes = false) {
@@ -699,11 +878,17 @@ async function allCommand(options = {}) {
699
878
  const config = loadConfig();
700
879
  const useForce = options.force ?? config.force === "force";
701
880
  const includeFinder = options.includeFinder ?? config.includeFinder;
881
+ const includeTrash = options.includeTrash ?? config.includeTrash;
702
882
  const includeBackground = options.includeBackground ?? config.includeBackground;
883
+ const neverQuitMusic = options.neverQuitMusic ?? config.neverQuitMusic;
703
884
  const apps = await getRunningApps({
704
885
  exclude: config.exclude,
705
886
  includeFinder,
706
- includeBackground
887
+ includeTrash,
888
+ includeBackground,
889
+ groupBackground: config.groupBackground,
890
+ neverQuitMusic,
891
+ musicApps: options.musicApps ?? config.musicApps
707
892
  });
708
893
  if (apps.length === 0) {
709
894
  if (options.json) {
@@ -798,6 +983,37 @@ async function toggleIncludeFinder() {
798
983
  `Include Finder: ${config.includeFinder ? "enabled" : "disabled"}`
799
984
  );
800
985
  }
986
+ async function toggleIncludeTrash() {
987
+ const config = loadConfig();
988
+ const choice = await select({
989
+ message: `Include Empty Trash in list (current: ${config.includeTrash ? "enabled" : "disabled"})`,
990
+ options: [
991
+ {
992
+ value: "disabled",
993
+ label: "Disabled",
994
+ hint: "Empty Trash is not shown in the app list."
995
+ },
996
+ {
997
+ value: "enabled",
998
+ label: "Enabled",
999
+ hint: "Shows 'Empty Trash' in list. Quitting it PERMANENTLY deletes all items in Trash (cannot be undone)."
1000
+ },
1001
+ backOption()
1002
+ ],
1003
+ initialValue: config.includeTrash ? "enabled" : "disabled"
1004
+ });
1005
+ if (isCancel(choice) || choice === BACK_VALUE) return;
1006
+ config.includeTrash = choice === "enabled";
1007
+ saveConfig(config);
1008
+ log.success(
1009
+ `Include Empty Trash in list: ${config.includeTrash ? "enabled" : "disabled"}`
1010
+ );
1011
+ if (config.includeTrash) {
1012
+ log.warn(
1013
+ "Note: Quitting 'Empty Trash' permanently and irreversibly deletes all files in macOS Trash."
1014
+ );
1015
+ }
1016
+ }
801
1017
  async function toggleIncludeBackground() {
802
1018
  const config = loadConfig();
803
1019
  const choice = await select({
@@ -824,6 +1040,85 @@ async function toggleIncludeBackground() {
824
1040
  `Background apps: ${config.includeBackground ? "enabled" : "disabled"}`
825
1041
  );
826
1042
  }
1043
+ async function toggleDefaultSelection() {
1044
+ const config = loadConfig();
1045
+ const currentVal = config.defaultSelectAll ? "select-all" : "deselect-all";
1046
+ const choice = await select({
1047
+ message: `Default App Selection (current: ${currentVal})`,
1048
+ options: [
1049
+ {
1050
+ value: "select-all",
1051
+ label: "Select all apps",
1052
+ hint: "All apps are pre-selected by default when launching quitx."
1053
+ },
1054
+ {
1055
+ value: "deselect-all",
1056
+ label: "Deselect all apps",
1057
+ hint: "No apps are selected by default. Pick specific apps to quit."
1058
+ },
1059
+ backOption()
1060
+ ],
1061
+ initialValue: currentVal
1062
+ });
1063
+ if (isCancel(choice) || choice === BACK_VALUE) return;
1064
+ config.defaultSelectAll = choice === "select-all";
1065
+ saveConfig(config);
1066
+ log.success(
1067
+ `Default selection: ${config.defaultSelectAll ? "select-all" : "deselect-all"}`
1068
+ );
1069
+ }
1070
+ async function toggleGroupBackground() {
1071
+ const config = loadConfig();
1072
+ const choice = await select({
1073
+ message: `Group Background Instances (current: ${config.groupBackground ? "enabled" : "disabled"})`,
1074
+ options: [
1075
+ {
1076
+ value: "enabled",
1077
+ label: "Enabled",
1078
+ hint: "Combines multiple instances of the same background app into a single item with instance count."
1079
+ },
1080
+ {
1081
+ value: "disabled",
1082
+ label: "Disabled",
1083
+ hint: "Lists each background instance separately with its process ID (PID)."
1084
+ },
1085
+ backOption()
1086
+ ],
1087
+ initialValue: config.groupBackground ? "enabled" : "disabled"
1088
+ });
1089
+ if (isCancel(choice) || choice === BACK_VALUE) return;
1090
+ config.groupBackground = choice === "enabled";
1091
+ saveConfig(config);
1092
+ log.success(
1093
+ `Group background instances: ${config.groupBackground ? "enabled" : "disabled"}`
1094
+ );
1095
+ }
1096
+ async function toggleNeverQuitMusic() {
1097
+ const config = loadConfig();
1098
+ const choice = await select({
1099
+ message: `Never Quit Music Apps (current: ${config.neverQuitMusic ? "enabled" : "disabled"})`,
1100
+ options: [
1101
+ {
1102
+ value: "enabled",
1103
+ label: "Enabled",
1104
+ hint: "Never show or quit music players (Spotify, Apple Music, Tidal, etc.) to keep your audio playing."
1105
+ },
1106
+ {
1107
+ value: "disabled",
1108
+ label: "Disabled",
1109
+ hint: "Include music apps in quit list normally."
1110
+ },
1111
+ backOption()
1112
+ ],
1113
+ initialValue: config.neverQuitMusic ? "enabled" : "disabled"
1114
+ });
1115
+ if (isCancel(choice) || choice === BACK_VALUE) return;
1116
+ config.neverQuitMusic = choice === "enabled";
1117
+ saveConfig(config);
1118
+ log.success(
1119
+ `Never quit music apps: ${config.neverQuitMusic ? "enabled" : "disabled"}`
1120
+ );
1121
+ }
827
1122
  async function manageExcludedApps() {
828
1123
  const choice = await select({
829
1124
  message: "Manage excluded apps",
@@ -848,13 +1143,12 @@ ${config.exclude.map((e) => ` - ${e}`).join("\n")}`
848
1143
  return;
849
1144
  }
850
1145
  if (choice === "add") {
851
- const apps = await getRunningApps();
852
- const currentSet = new Set(
853
- config.exclude.map((e) => e.toLowerCase().trim())
854
- );
855
- const available = apps.filter(
856
- (a) => !currentSet.has(a.name.toLowerCase().trim()) && !(a.bundleId && currentSet.has(a.bundleId.toLowerCase().trim()))
857
- );
1146
+ const available = await getRunningApps({
1147
+ exclude: config.exclude,
1148
+ includeFinder: config.includeFinder,
1149
+ includeBackground: config.includeBackground,
1150
+ groupBackground: config.groupBackground
1151
+ });
858
1152
  if (available.length === 0) {
859
1153
  log.info("All running apps are already excluded.");
860
1154
  return;
@@ -899,8 +1193,88 @@ ${config.exclude.map((e) => ` - ${e}`).join("\n")}`
899
1193
  log.success(`Removed ${selected.length} apps from exclude list.`);
900
1194
  }
901
1195
  }
1196
+ async function manageCustomMusicApps() {
1197
+ const choice = await select({
1198
+ message: "Manage custom music apps",
1199
+ options: [
1200
+ { value: "add", label: "Add running apps to custom music list" },
1201
+ { value: "remove", label: "Remove apps from custom music list" },
1202
+ { value: "view", label: "View current custom music list" },
1203
+ backOption()
1204
+ ]
1205
+ });
1206
+ if (isCancel(choice) || choice === BACK_VALUE) return;
1207
+ const config = loadConfig();
1208
+ if (choice === "view") {
1209
+ if (config.musicApps.length === 0) {
1210
+ log.info("No custom music apps registered.");
1211
+ } else {
1212
+ log.info(
1213
+ `Custom music apps:
1214
+ ${config.musicApps.map((e) => ` - ${e}`).join("\n")}`
1215
+ );
1216
+ }
1217
+ return;
1218
+ }
1219
+ if (choice === "add") {
1220
+ const available = await getRunningApps({
1221
+ exclude: config.exclude,
1222
+ includeFinder: config.includeFinder,
1223
+ includeBackground: config.includeBackground,
1224
+ groupBackground: config.groupBackground,
1225
+ neverQuitMusic: false
1226
+ });
1227
+ const unadded = available.filter(
1228
+ (a) => !config.musicApps.some(
1229
+ (m) => m.toLowerCase() === a.name.toLowerCase() || a.bundleId && m.toLowerCase() === a.bundleId.toLowerCase()
1230
+ )
1231
+ );
1232
+ if (unadded.length === 0) {
1233
+ log.info("All running apps are already in custom music list.");
1234
+ return;
1235
+ }
1236
+ const selected = await multiselect({
1237
+ message: "Select apps to mark as music players",
1238
+ options: unadded.map((a) => ({
1239
+ value: a.name,
1240
+ label: a.name,
1241
+ ...a.bundleId ? { hint: a.bundleId } : {}
1242
+ })),
1243
+ required: false
1244
+ });
1245
+ if (isCancel(selected)) return;
1246
+ if (!Array.isArray(selected) || selected.length === 0) {
1247
+ log.info("No apps selected.");
1248
+ return;
1249
+ }
1250
+ addMusicApps(selected);
1251
+ log.success(`Added ${selected.length} apps to custom music list.`);
1252
+ return;
1253
+ }
1254
+ if (choice === "remove") {
1255
+ if (config.musicApps.length === 0) {
1256
+ log.info("No apps currently in custom music list.");
1257
+ return;
1258
+ }
1259
+ const selected = await multiselect({
1260
+ message: "Select apps to remove from custom music list",
1261
+ options: config.musicApps.map((name) => ({
1262
+ value: name,
1263
+ label: name
1264
+ })),
1265
+ required: false
1266
+ });
1267
+ if (isCancel(selected)) return;
1268
+ if (!Array.isArray(selected) || selected.length === 0) {
1269
+ log.info("No apps selected.");
1270
+ return;
1271
+ }
1272
+ removeMusicApps(selected);
1273
+ log.success(`Removed ${selected.length} apps from custom music list.`);
1274
+ }
1275
+ }
902
1276
  function resetConfig() {
903
- saveConfig({ ...DEFAULT_CONFIG, exclude: [] });
1277
+ saveConfig({ ...DEFAULT_CONFIG, exclude: [], musicApps: [] });
904
1278
  log.success("Config reset to defaults.");
905
1279
  }
906
1280
  async function configCommand() {
@@ -913,28 +1287,53 @@ async function configCommand() {
913
1287
  options: [
914
1288
  {
915
1289
  value: "quit-mode",
916
- label: `Toggle Quit Mode`,
917
- hint: `current: ${config.force}`
1290
+ label: "Quit Mode",
1291
+ hint: `current: ${config.force === "normal" ? "Normal quit" : "Force quit"}`
1292
+ },
1293
+ {
1294
+ value: "background",
1295
+ label: "View background apps",
1296
+ hint: `current: ${config.includeBackground ? "enabled" : "disabled"}`
1297
+ },
1298
+ {
1299
+ value: "group-background",
1300
+ label: "Group background instances",
1301
+ hint: `current: ${config.groupBackground ? "enabled" : "disabled"}`
1302
+ },
1303
+ {
1304
+ value: "never-quit-music",
1305
+ label: "Never quit music apps",
1306
+ hint: `current: ${config.neverQuitMusic ? "enabled" : "disabled"}`
1307
+ },
1308
+ {
1309
+ value: "default-select",
1310
+ label: "Deselect apps by default",
1311
+ hint: `current: ${!config.defaultSelectAll ? "enabled" : "disabled"}`
918
1312
  },
919
1313
  {
920
1314
  value: "finder",
921
- label: `Toggle Include Finder`,
1315
+ label: "Include Finder Windows in list",
922
1316
  hint: `current: ${config.includeFinder ? "enabled" : "disabled"}`
923
1317
  },
924
1318
  {
925
- value: "background",
926
- label: `Toggle Background Apps`,
927
- hint: `current: ${config.includeBackground ? "enabled" : "disabled"}`
1319
+ value: "trash",
1320
+ label: "Include Empty Trash in list",
1321
+ hint: `current: ${config.includeTrash ? "enabled (empties trash)" : "disabled"}`
928
1322
  },
929
1323
  {
930
1324
  value: "exclude",
931
- label: "Manage Excluded Apps",
1325
+ label: "Manage excluded apps",
932
1326
  hint: `${config.exclude.length} excluded`
933
1327
  },
1328
+ {
1329
+ value: "custom-music",
1330
+ label: "Manage custom music apps",
1331
+ hint: `${config.musicApps.length} custom apps`
1332
+ },
934
1333
  {
935
1334
  value: "reset",
936
- label: "Reset to Defaults",
937
- hint: "restore all settings"
1335
+ label: "Reset all",
1336
+ hint: "restore all settings to defaults"
938
1337
  },
939
1338
  { value: "exit", label: "Exit" }
940
1339
  ]
@@ -944,9 +1343,14 @@ async function configCommand() {
944
1343
  continue;
945
1344
  }
946
1345
  if (choice === "quit-mode") await toggleQuitMode();
947
- if (choice === "finder") await toggleIncludeFinder();
948
1346
  if (choice === "background") await toggleIncludeBackground();
1347
+ if (choice === "group-background") await toggleGroupBackground();
1348
+ if (choice === "never-quit-music") await toggleNeverQuitMusic();
1349
+ if (choice === "default-select") await toggleDefaultSelection();
1350
+ if (choice === "finder") await toggleIncludeFinder();
1351
+ if (choice === "trash") await toggleIncludeTrash();
949
1352
  if (choice === "exclude") await manageExcludedApps();
1353
+ if (choice === "custom-music") await manageCustomMusicApps();
950
1354
  if (choice === "reset") resetConfig();
951
1355
  }
952
1356
  showOutro("Config saved.");
@@ -982,13 +1386,12 @@ ${currentConfig.exclude.map((e) => ` - ${e}`).join("\n")}`
982
1386
  return 0;
983
1387
  }
984
1388
  if (choice === "add") {
985
- const apps = await getRunningApps();
986
- const currentSet = new Set(
987
- currentConfig.exclude.map((e) => e.toLowerCase().trim())
988
- );
989
- const available = apps.filter(
990
- (a) => !currentSet.has(a.name.toLowerCase().trim()) && !(a.bundleId && currentSet.has(a.bundleId.toLowerCase().trim()))
991
- );
1389
+ const available = await getRunningApps({
1390
+ exclude: currentConfig.exclude,
1391
+ includeFinder: currentConfig.includeFinder,
1392
+ includeBackground: currentConfig.includeBackground,
1393
+ groupBackground: currentConfig.groupBackground
1394
+ });
992
1395
  if (available.length === 0) {
993
1396
  showOutro("All running apps are already excluded.");
994
1397
  printThanks();
@@ -1077,11 +1480,17 @@ async function interactiveCommand(options = {}) {
1077
1480
  const config = loadConfig();
1078
1481
  const useForce = options.force ?? config.force === "force";
1079
1482
  const includeFinder = options.includeFinder ?? config.includeFinder;
1483
+ const includeTrash = options.includeTrash ?? config.includeTrash;
1080
1484
  const includeBackground = options.includeBackground ?? config.includeBackground;
1485
+ const neverQuitMusic = options.neverQuitMusic ?? config.neverQuitMusic;
1081
1486
  const apps = await getRunningApps({
1082
1487
  exclude: config.exclude,
1083
1488
  includeFinder,
1084
- includeBackground
1489
+ includeTrash,
1490
+ includeBackground,
1491
+ groupBackground: config.groupBackground,
1492
+ neverQuitMusic,
1493
+ musicApps: options.musicApps ?? config.musicApps
1085
1494
  });
1086
1495
  if (!options.json) {
1087
1496
  s.stop(
@@ -1120,7 +1529,9 @@ async function interactiveCommand(options = {}) {
1120
1529
  return 0;
1121
1530
  }
1122
1531
  } else {
1123
- const selected = await selectApps(apps);
1532
+ const selected = await selectApps(apps, {
1533
+ defaultSelectAll: config.defaultSelectAll
1534
+ });
1124
1535
  if (typeof selected === "symbol" || isCancel(selected)) {
1125
1536
  showCancel("Cancelled.");
1126
1537
  printThanks(options);
@@ -1134,11 +1545,7 @@ async function interactiveCommand(options = {}) {
1134
1545
  targetApps = selected;
1135
1546
  }
1136
1547
  if (!options.yes) {
1137
- const confirmation = await shouldConfirmQuit(
1138
- targetApps.length,
1139
- targetApps.length === apps.length,
1140
- false
1141
- );
1548
+ const confirmation = await shouldConfirmQuit(targetApps.length, false);
1142
1549
  if (isCancel(confirmation) || confirmation !== true) {
1143
1550
  showCancel("Cancelled.");
1144
1551
  printThanks(options);
@@ -1177,11 +1584,17 @@ async function interactiveCommand(options = {}) {
1177
1584
  async function listCommand(options = {}) {
1178
1585
  const config = loadConfig();
1179
1586
  const includeFinder = options.includeFinder ?? config.includeFinder;
1587
+ const includeTrash = options.includeTrash ?? config.includeTrash;
1180
1588
  const includeBackground = options.includeBackground ?? config.includeBackground;
1589
+ const neverQuitMusic = options.neverQuitMusic ?? config.neverQuitMusic;
1181
1590
  const apps = await getRunningApps({
1182
1591
  exclude: config.exclude,
1183
1592
  includeFinder,
1184
- includeBackground
1593
+ includeTrash,
1594
+ includeBackground,
1595
+ groupBackground: config.groupBackground,
1596
+ neverQuitMusic,
1597
+ musicApps: options.musicApps ?? config.musicApps
1185
1598
  });
1186
1599
  renderList(apps, options.json);
1187
1600
  return 0;
@@ -1243,8 +1656,12 @@ function parseCliArgs(args) {
1243
1656
  options.json = true;
1244
1657
  } else if (arg === "--include-finder") {
1245
1658
  options.includeFinder = true;
1659
+ } else if (arg === "--include-trash") {
1660
+ options.includeTrash = true;
1246
1661
  } else if (arg === "-b" || arg === "--background") {
1247
1662
  options.includeBackground = true;
1663
+ } else if (arg === "--never-quit-music") {
1664
+ options.neverQuitMusic = true;
1248
1665
  } else if (arg === "--config") {
1249
1666
  options.manageConfig = true;
1250
1667
  } else if (arg === "--exclude") {
@@ -1300,7 +1717,9 @@ function parseCliArgs(args) {
1300
1717
  }
1301
1718
  async function main(argv = process.argv.slice(2)) {
1302
1719
  if (!isMacOS()) {
1303
- console.error("\u2716 quitx only works on macOS.");
1720
+ console.error(
1721
+ "\u2716 quitx only works on macOS. Windows and Linux are not supported."
1722
+ );
1304
1723
  return 1;
1305
1724
  }
1306
1725
  ensureMacOS();
@@ -1352,7 +1771,9 @@ async function main(argv = process.argv.slice(2)) {
1352
1771
  return 1;
1353
1772
  }
1354
1773
  }
1355
- if (process.argv[1] && (process.argv[1].endsWith("/cli.ts") || process.argv[1].endsWith("/cli.js") || process.argv[1].endsWith("/quitx"))) {
1774
+ var scriptName = process.argv[1] ? process.argv[1].replace(/\\/g, "/").split("/").pop()?.toLowerCase() : "";
1775
+ var isMain = scriptName === "cli.ts" || scriptName === "cli.js" || scriptName === "quitx" || scriptName === "quitx.cmd" || scriptName === "quitx.ps1" || scriptName === "quitx.exe";
1776
+ if (isMain) {
1356
1777
  main().then((code) => {
1357
1778
  if (code !== 0) {
1358
1779
  process.exit(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreify/quitx",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A macOS CLI for viewing running GUI applications and quitting them properly.",
5
5
  "type": "module",
6
6
  "bin": {