source: trunk/appswitch/appswitch/main.c@ 350

Last change on this file since 350 was 350, checked in by Nicholas Riley, 17 years ago

appswitch.1: Updated for appswitch 1.1.

VERSION: Updated for appswitch 1.1.

main.c: Updated copyright date in one place I forgot. Updated for
appswitch 1.1. Worked around Leopard CPS/Process Manager issue.

README: Updated for appswitch 1.1. Summarized changes. Fixed a
couple of omissions (e.g., -a). Removed now-fixed bugs; added the -S
breakage issue with Leopard and Spaces.

appswitch.xcodeproj: No substantive changes.

File size: 20.4 KB
Line 
1/*
2 appswitch - a command-line application switcher
3 Nicholas Riley <appswitch@sabi.net>
4
5 Copyright (c) 2003-07, Nicholas Riley
6 All rights reserved.
7
8 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9
10 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12 * Neither the name of this software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
13
14 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
16*/
17
18#define DEBUG 0
19
20#include <unistd.h>
21#include <signal.h>
22#include <sys/ioctl.h>
23#include <ApplicationServices/ApplicationServices.h>
24#include "CPS.h"
25
26const char *APP_NAME;
27
28#define VERSION "1.1"
29
30struct {
31 CFStringRef creator;
32 CFStringRef bundleID;
33 CFStringRef name;
34 pid_t pid;
35 CFStringRef path;
36 enum {
37 MATCH_UNKNOWN, MATCH_FRONT, MATCH_CREATOR, MATCH_BUNDLE_ID, MATCH_NAME, MATCH_PID, MATCH_PATH, MATCH_ALL
38 } matchType;
39 enum {
40 APP_NONE, APP_SWITCH, APP_SHOW, APP_HIDE, APP_QUIT, APP_KILL, APP_KILL_HARD, APP_LIST, APP_PRINT_PID, APP_FRONTMOST
41 } appAction;
42 Boolean longList;
43 enum {
44 ACTION_NONE, ACTION_SHOW_ALL, ACTION_HIDE_OTHERS
45 } action;
46 enum {
47 FINAL_NONE, FINAL_SWITCH
48 } finalAction;
49} OPTS =
50{
51 kLSUnknownCreator, NULL, NULL, -1, NULL, MATCH_UNKNOWN, APP_NONE, ACTION_NONE, FINAL_NONE, false
52};
53
54typedef struct {
55 OSStatus status;
56 const char *desc;
57} errRec, errList[];
58
59static errList ERRS = {
60 // Process Manager errors
61 { appIsDaemon, "application is background-only", },
62 { procNotFound, "application not found" },
63 { connectionInvalid, "application is not background-only", },
64 // CoreGraphics errors
65 { kCGErrorIllegalArgument, "window server error.\nAre you logged in?" },
66 { kCGErrorInvalidContext, "application context unavailable" },
67 { fnfErr, "file not found" },
68 // (abused) errors
69 { permErr, "no permission" },
70 { 0, NULL }
71};
72
73void usage() {
74 fprintf(stderr, "usage: %s [-sShHqkKlLPfF] [-c creator] [-i bundleID] [-a name] [-p pid] [path]\n"
75 " -s show application, bring windows to front (do not switch)\n"
76 " -S show all applications\n"
77 " -h hide application\n"
78 " -H hide other applications\n"
79 " -q quit application\n"
80 " -k kill application (SIGTERM)\n"
81 " -K kill application hard (SIGKILL)\n"
82 " -l list applications\n"
83 " -L list applications including full paths and bundle identifiers\n"
84 " -P print application process ID\n"
85 " -f bring application's frontmost window to front\n"
86 " -F bring current application's windows to front\n"
87 " -c creator match application by four-character creator code ('ToyS')\n"
88 " -i bundle ID match application by bundle identifier (com.apple.ScriptEditor2)\n"
89 " -p pid match application by process identifier\n"
90 " -a name match application by name\n"
91 , APP_NAME);
92 fprintf(stderr, "appswitch "VERSION" (c) 2003-07 Nicholas Riley <http://web.sabi.net/nriley/software/>.\n"
93 "Please send bugs, suggestions, etc. to <appswitch@sabi.net>.\n");
94
95 exit(1);
96}
97
98char *osstatusstr(OSStatus err) {
99 errRec *rec;
100 const char *errDesc = "unknown error";
101 char * const failedStr = "(unable to retrieve error message)";
102 static char *str = NULL;
103 size_t len;
104 if (str != NULL && str != failedStr) free(str);
105 for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
106 if (rec->status == err) {
107 errDesc = rec->desc;
108 break;
109 }
110 len = strlen(errDesc) + 10 * sizeof(char);
111 str = (char *)malloc(len);
112 if (str != NULL)
113 snprintf(str, len, "%s (%ld)", errDesc, err);
114 else
115 str = failedStr;
116 return str;
117}
118
119void osstatusexit(OSStatus err, const char *fmt, ...) {
120 va_list ap;
121 const char *errDesc = osstatusstr(err);
122 va_start(ap, fmt);
123 fprintf(stderr, "%s: ", APP_NAME);
124 vfprintf(stderr, fmt, ap);
125 fprintf(stderr, ": %s\n", errDesc);
126 exit(1);
127}
128
129void errexit(const char *fmt, ...) {
130 va_list ap;
131 va_start(ap, fmt);
132 fprintf(stderr, "%s: ", APP_NAME);
133 vfprintf(stderr, fmt, ap);
134 fprintf(stderr, "\n");
135 exit(1);
136}
137
138void getargs(int argc, char * const argv[]) {
139 extern char *optarg;
140 extern int optind;
141 int ch;
142
143 if (argc == 1) usage();
144
145 const char *opts = "c:i:p:a:sShHqkKlLPfF";
146
147 while ( (ch = getopt(argc, argv, opts)) != -1) {
148 switch (ch) {
149 case 'p':
150 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
151 if (sscanf(optarg, "%d", &OPTS.pid) != 1 || OPTS.pid < 0)
152 errexit("invalid process identifier (argument of -p)");
153 OPTS.matchType = MATCH_PID;
154 break;
155 case 'c':
156 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
157 OPTS.creator = CFStringCreateWithFileSystemRepresentation(NULL, optarg);
158 if (OPTS.creator == NULL) errexit("invalid creator (wrong text encoding?)");
159 if (CFStringGetLength(OPTS.creator) != 4) errexit("creator (argument of -c) must be four characters long");
160 OPTS.matchType = MATCH_CREATOR;
161 break;
162 case 'i':
163 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
164 OPTS.bundleID = CFStringCreateWithFileSystemRepresentation(NULL, optarg);
165 if (OPTS.bundleID == NULL) errexit("invalid bundle ID (wrong text encoding?)");
166 OPTS.matchType = MATCH_BUNDLE_ID;
167 break;
168 case 'a':
169 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
170 OPTS.name = CFStringCreateWithFileSystemRepresentation(NULL, optarg);
171 if (OPTS.name == NULL) errexit("invalid application name (wrong text encoding?)");
172 OPTS.matchType = MATCH_NAME;
173 break;
174 case 's':
175 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
176 OPTS.appAction = APP_SHOW;
177 break;
178 case 'h':
179 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
180 OPTS.appAction = APP_HIDE;
181 break;
182 case 'q':
183 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
184 OPTS.appAction = APP_QUIT;
185 break;
186 case 'k':
187 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
188 OPTS.appAction = APP_KILL;
189 break;
190 case 'K':
191 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
192 OPTS.appAction = APP_KILL_HARD;
193 break;
194 case 'l':
195 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
196 OPTS.appAction = APP_LIST;
197 break;
198 case 'L':
199 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
200 OPTS.appAction = APP_LIST;
201 OPTS.longList = true;
202 break;
203 case 'P':
204 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
205 OPTS.appAction = APP_PRINT_PID;
206 break;
207 case 'f':
208 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
209 OPTS.appAction = APP_FRONTMOST;
210 break;
211 case 'S':
212 if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
213 OPTS.action = ACTION_SHOW_ALL;
214 break;
215 case 'H':
216 if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
217 OPTS.action = ACTION_HIDE_OTHERS;
218 break;
219 case 'F':
220 if (OPTS.finalAction != FINAL_NONE) errexit("choose only one -F option");
221 OPTS.finalAction = FINAL_SWITCH;
222 break;
223 default: usage();
224 }
225 }
226
227 argc -= optind;
228 argv += optind;
229
230 if (OPTS.matchType != MATCH_UNKNOWN && argc != 0) usage();
231
232 if (OPTS.matchType == MATCH_UNKNOWN) {
233 if (argc == 0) {
234 if (OPTS.appAction == APP_LIST) {
235 OPTS.matchType = MATCH_ALL;
236 } else if (OPTS.action != ACTION_NONE || OPTS.finalAction != FINAL_NONE) {
237 OPTS.matchType = MATCH_FRONT;
238 } else usage();
239 } else if (argc == 1) {
240 OPTS.path = CFStringCreateWithFileSystemRepresentation(NULL, argv[0]);
241 if (OPTS.path == NULL) errexit("invalid path (wrong text encoding?)");
242 OPTS.matchType = MATCH_PATH;
243 } else usage();
244 }
245
246 if (OPTS.matchType != MATCH_FRONT && OPTS.appAction == APP_NONE)
247 OPTS.appAction = APP_SWITCH;
248
249}
250
251ProcessSerialNumber frontApplication() {
252 ProcessSerialNumber psn;
253 OSStatus err = GetFrontProcess(&psn);
254 if (err != noErr) osstatusexit(err, "can't get frontmost process");
255#if DEBUG
256 fprintf(stderr, "front application PSN %ld.%ld\n", psn.lowLongOfPSN, psn.highLongOfPSN);
257#endif
258 return psn;
259}
260
261OSStatus quitApplication(ProcessSerialNumber *psn) {
262 AppleEvent event;
263 AEAddressDesc appDesc;
264 OSStatus err;
265
266 AEInitializeDesc(&appDesc);
267 err = AECreateDesc(typeProcessSerialNumber, psn, sizeof(*psn), &appDesc);
268 if (err != noErr) return err;
269
270 err = AECreateAppleEvent(kCoreEventClass, kAEQuitApplication, &appDesc, kAutoGenerateReturnID, kAnyTransactionID, &event);
271 if (err != noErr) return err;
272
273 AppleEvent nullReply = {typeNull, nil};
274 err = AESendMessage(&event, &nullReply, kAENoReply, kNoTimeOut);
275 (void)AEDisposeDesc(&event);
276 if (err != noErr) return err;
277
278 (void)AEDisposeDesc(&nullReply); // according to docs, don't call unless AESend returned successfully
279
280 return noErr;
281}
282
283pid_t getPID(const ProcessSerialNumber *psn) {
284 pid_t pid;
285 OSStatus err = GetProcessPID(psn, &pid);
286 if (err != noErr) osstatusexit(err, "can't get process ID");
287 return pid;
288}
289
290bool infoStringMatches(CFDictionaryRef info, CFStringRef key, CFStringRef matchStr) {
291 CFStringRef str = CFDictionaryGetValue(info, key);
292 if (str == NULL)
293 return false;
294 /* note: this means we might match names/paths that are wrong, but works better in the common case */
295 return CFStringCompare(str, matchStr, kCFCompareCaseInsensitive) == kCFCompareEqualTo;
296}
297
298CFStringRef stringTrimmedToWidth(CFStringRef str, CFIndex width) {
299 if (str == NULL)
300 str = CFSTR("");
301 CFIndex length = CFStringGetLength(str);
302 if (length == width)
303 return CFRetain(str);
304
305 CFMutableStringRef padStr = CFStringCreateMutableCopy(NULL, width, str);
306 CFStringPad(padStr, CFSTR(" "), width, 0);
307 return padStr;
308}
309
310ProcessSerialNumber matchApplication(void) {
311 if (OPTS.matchType == MATCH_FRONT) return frontApplication();
312
313 OSStatus err;
314 ProcessSerialNumber psn = {
315 kNoProcess, kNoProcess
316 };
317 pid_t pid;
318 CFStringRef format = NULL;
319 CFIndex nameWidth = 19;
320 CFIndex pathWidth = 0;
321 if (OPTS.appAction == APP_LIST) {
322 int termwidth = 80;
323 struct winsize ws;
324 char *banner = " PSN PID TYPE CREA NAME ";
325 // 123456789.0 12345 1234 1234 1234567890123456789
326 if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
327 ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
328 ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ws) != -1) ||
329 ws.ws_col != 0) termwidth = ws.ws_col;
330 char *formatButPath = "%9ld.%ld %5ld %@ %@ %@";
331 // XXX don't ever release 'format', should fix if we get called repeatedly
332 if (OPTS.longList) {
333 pathWidth = 1;
334 printf("%s PATH (bundle identifier)\n", banner);
335 format = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s %%@"), formatButPath);
336 } else {
337 pathWidth = termwidth - strlen(banner) - 1;
338 if (pathWidth >= 4) {
339 printf("%s PATH\n", banner);
340 format = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s %%@"), formatButPath);
341 } else {
342 pathWidth = 0;
343 format = CFStringCreateWithCString(NULL, formatButPath, kCFStringEncodingUTF8);
344 }
345 }
346 }
347
348 CFDictionaryRef info = NULL;
349 while ( (err = GetNextProcess(&psn)) == noErr) {
350 if (info != NULL) CFRelease(info);
351 info = ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
352 if (info == NULL) errexit("can't get information for process with PSN %ld.%ld",
353 psn.lowLongOfPSN, psn.highLongOfPSN);
354
355 switch (OPTS.matchType) {
356 case MATCH_ALL:
357 break;
358 case MATCH_CREATOR: if (!infoStringMatches(info, CFSTR("FileCreator"), OPTS.creator)) continue;
359 break;
360 case MATCH_NAME: if (!infoStringMatches(info, CFSTR("CFBundleName"), OPTS.name)) continue;
361 break;
362 case MATCH_PID: err = GetProcessPID(&psn, &pid); if (err != noErr || OPTS.pid != pid) continue;
363 break;
364 case MATCH_PATH: if (!infoStringMatches(info, CFSTR("BundlePath"), OPTS.path) &&
365 !infoStringMatches(info, CFSTR("CFBundleExecutable"), OPTS.path)) continue;
366 break;
367 case MATCH_BUNDLE_ID: if (!infoStringMatches(info, CFSTR("CFBundleIdentifier"), OPTS.bundleID)) continue;
368 break;
369 default:
370 errexit("internal error: invalid match type");
371 }
372 if (OPTS.appAction == APP_LIST) {
373 if (GetProcessPID(&psn, &pid) != noErr)
374 pid = -1;
375 CFStringRef path = NULL;
376 // XXX padding/truncation probably breaks with double-width characters
377 if (pathWidth) {
378 path = CFDictionaryGetValue(info, CFSTR("BundlePath"));
379 if (path == NULL)
380 path = CFDictionaryGetValue(info, CFSTR("CFBundleExecutable"));
381 if (!OPTS.longList)
382 path = stringTrimmedToWidth(path, pathWidth);
383 }
384 CFStringRef name = stringTrimmedToWidth(CFDictionaryGetValue(info, CFSTR("CFBundleName")), nameWidth);
385 CFStringRef type = stringTrimmedToWidth(CFDictionaryGetValue(info, CFSTR("FileType")), 4);
386 CFStringRef creator = stringTrimmedToWidth(CFDictionaryGetValue(info, CFSTR("FileCreator")), 4);
387 CFStringRef line = CFStringCreateWithFormat(NULL, NULL, format,
388 psn.lowLongOfPSN, psn.highLongOfPSN, pid, type, creator, name, path);
389 CFRelease(name);
390 CFRelease(type);
391 CFRelease(creator);
392 if (!OPTS.longList)
393 CFRelease(path);
394 else {
395 CFStringRef bundleID = CFDictionaryGetValue(info, CFSTR("CFBundleIdentifier"));
396 if (bundleID != NULL && CFStringGetLength(bundleID) != 0) {
397 CFStringRef origLine = line;
398 line = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@)"), line, bundleID);
399 CFRelease(origLine);
400 }
401 }
402 char *cStr = (char *)CFStringGetCStringPtr(line, CFStringGetSystemEncoding());
403 if (cStr != NULL) {
404 puts(cStr);
405 } else {
406 CFIndex cStrLength = CFStringGetMaximumSizeOfFileSystemRepresentation(line);
407 cStr = (char *)malloc(cStrLength * sizeof(char));
408 if (!CFStringGetFileSystemRepresentation(line, cStr, cStrLength)) {
409 CFShow(cStr);
410 errexit("internal error: string encoding conversion failed");
411 }
412 puts(cStr);
413 free(cStr);
414 }
415 continue;
416 }
417 return psn;
418 }
419 if (err != procNotFound) osstatusexit(err, "can't get next process");
420
421 if (OPTS.appAction == APP_LIST) return frontApplication();
422
423 errexit("can't find matching process");
424 return psn; // not reached
425}
426
427int main(int argc, char * const argv[]) {
428 OSStatus err = noErr;
429
430 APP_NAME = argv[0];
431 getargs(argc, argv);
432
433 ProcessSerialNumber psn;
434
435 // required in Leopard to prevent paramErr - rdar://problem/5579375
436 err = GetCurrentProcess(&psn);
437 if (err != noErr) osstatusexit(err, "can't contact window server");
438
439 psn = matchApplication();
440
441 const char *verb = NULL;
442 switch (OPTS.appAction) {
443 case APP_NONE: break;
444 case APP_LIST: break; // already handled in matchApplication
445 case APP_SWITCH: err = SetFrontProcess(&psn); verb = "set front"; break;
446 case APP_SHOW: err = ShowHideProcess(&psn, true); verb = "show"; break;
447 case APP_HIDE: err = ShowHideProcess(&psn, false); verb = "hide"; break;
448 case APP_QUIT: err = quitApplication(&psn); verb = "quit"; break;
449 case APP_KILL: err = KillProcess(&psn); verb = "send SIGTERM to"; break;
450 case APP_KILL_HARD:
451 {
452 // no Process Manager equivalent - rdar://problem/4808400
453 if (kill(getPID(&psn), SIGKILL) == -1)
454 err = (errno == ESRCH) ? procNotFound : (errno == EPERM ? permErr : paramErr);
455 verb = "send SIGKILL to";
456 break;
457 }
458 case APP_PRINT_PID: printf("%d\n", getPID(&psn)); break;
459 case APP_FRONTMOST: err = SetFrontProcessWithOptions(&psn, kSetFrontProcessFrontWindowOnly);
460 verb = "bring frontmost window to front"; break;
461 default:
462 errexit("internal error: invalid application action");
463 }
464 if (err != noErr) osstatusexit(err, "can't %s process", verb);
465
466 switch (OPTS.action) {
467 case ACTION_NONE: break;
468 // no Process Manager equivalents - rdar://problem/4808397
469 case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
470 case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
471 default:
472 errexit("internal error: invalid action");
473 }
474 if (err != noErr) osstatusexit(err, "can't %s processes", verb);
475
476 switch (OPTS.finalAction) {
477 case FINAL_NONE: break;
478 case FINAL_SWITCH:
479 psn = frontApplication();
480#if DEBUG
481 fprintf(stderr, "posting show request for %ld.%ld\n", psn.lowLongOfPSN, psn.highLongOfPSN);
482#endif
483 if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
484 err = ShowHideProcess(&psn, true) || SetFrontProcess(&psn);
485 verb = "bring current application's windows to the front";
486 break;
487 default:
488 errexit("internal error: invalid final action");
489 }
490 if (err != noErr) osstatusexit(err, "can't %s", verb);
491
492 exit(0);
493}
Note: See TracBrowser for help on using the repository browser.