@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.
- package/CHANGELOG.md +17 -0
- package/dist/cli.js +611 -190
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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(
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
195
|
-
if (seen.has(key)) {
|
|
320
|
+
if (excludeSet.has(nameLower) || bundleLower && excludeSet.has(bundleLower)) {
|
|
196
321
|
continue;
|
|
197
322
|
}
|
|
198
|
-
|
|
199
|
-
filtered.push(app);
|
|
323
|
+
eligible.push(app);
|
|
200
324
|
}
|
|
201
|
-
|
|
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"
|
|
244
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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
|
-
|
|
421
|
-
for (
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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(
|
|
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
|
|
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
|
|
616
|
-
|
|
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
|
|
619
|
-
label
|
|
828
|
+
value,
|
|
829
|
+
label,
|
|
620
830
|
...hint ? { hint } : {}
|
|
621
831
|
};
|
|
622
832
|
});
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
676
|
-
|
|
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
|
-
|
|
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
|
|
852
|
-
|
|
853
|
-
config.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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:
|
|
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:
|
|
1315
|
+
label: "Include Finder Windows in list",
|
|
922
1316
|
hint: `current: ${config.includeFinder ? "enabled" : "disabled"}`
|
|
923
1317
|
},
|
|
924
1318
|
{
|
|
925
|
-
value: "
|
|
926
|
-
label:
|
|
927
|
-
hint: `current: ${config.
|
|
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
|
|
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
|
|
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
|
|
986
|
-
|
|
987
|
-
currentConfig.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|