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

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

VERSION: Updated for 1.1d1.

main.c: Mostly switch to Process Manager. Remove obsolete comments.

README: Updated for 1.1d1.

appswitch.xcodeproj: Upgraded Xcode project.

File size: 18.5 KB
Line 
1/*
2 appswitch - a command-line application switcher
3 Nicholas Riley <appswitch@sabi.net>
4
5 Copyright (c) 2003-06, 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.1d1"
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 { fnfErr, "file not found" },
67 // (abused) errors
68 { permErr, "no permission" },
69 { 0, NULL }
70};
71
72void usage() {
73 fprintf(stderr, "usage: %s [-sShHqklLPfF] [-c creator] [-i bundleID] [-a name] [-p pid] [path]\n"
74 " -s show application, bring windows to front (do not switch)\n"
75 " -S show all applications\n"
76 " -h hide application\n"
77 " -H hide other applications\n"
78 " -q quit application\n"
79 " -k kill application (SIGTERM)\n"
80 " -K kill application hard (SIGKILL)\n"
81 " -l list applications\n"
82 " -L list applications including full paths and bundle identifiers\n"
83 " -P print application process ID\n"
84 " -f bring application's frontmost window to front\n"
85 " -F bring current application's windows to front\n"
86 " -c creator match application by four-character creator code ('ToyS')\n"
87 " -i bundle ID match application by bundle identifier (com.apple.ScriptEditor2)\n"
88 " -p pid match application by process identifier\n"
89 " -a name match application by name\n"
90 , APP_NAME);
91 fprintf(stderr, "appswitch "VERSION" (c) 2003-06 Nicholas Riley <http://web.sabi.net/nriley/software/>.\n"
92 "Please send bugs, suggestions, etc. to <appswitch@sabi.net>.\n");
93
94 exit(1);
95}
96
97char *osstatusstr(OSStatus err) {
98 errRec *rec;
99 const char *errDesc = "unknown error";
100 char * const failedStr = "(unable to retrieve error message)";
101 static char *str = NULL;
102 size_t len;
103 if (str != NULL && str != failedStr) free(str);
104 for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
105 if (rec->status == err) {
106 errDesc = rec->desc;
107 break;
108 }
109 len = strlen(errDesc) + 10 * sizeof(char);
110 str = (char *)malloc(len);
111 if (str != NULL)
112 snprintf(str, len, "%s (%ld)", errDesc, err);
113 else
114 str = failedStr;
115 return str;
116}
117
118void osstatusexit(OSStatus err, const char *fmt, ...) {
119 va_list ap;
120 const char *errDesc = osstatusstr(err);
121 va_start(ap, fmt);
122 fprintf(stderr, "%s: ", APP_NAME);
123 vfprintf(stderr, fmt, ap);
124 fprintf(stderr, ": %s\n", errDesc);
125 exit(1);
126}
127
128void errexit(const char *fmt, ...) {
129 va_list ap;
130 va_start(ap, fmt);
131 fprintf(stderr, "%s: ", APP_NAME);
132 vfprintf(stderr, fmt, ap);
133 fprintf(stderr, "\n");
134 exit(1);
135}
136
137void getargs(int argc, char * const argv[]) {
138 extern char *optarg;
139 extern int optind;
140 int ch;
141
142 if (argc == 1) usage();
143
144 const char *opts = "c:i:p:a:sShHqkKlLPfF";
145
146 while ( (ch = getopt(argc, argv, opts)) != -1) {
147 switch (ch) {
148 case 'p':
149 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
150 if (sscanf(optarg, "%d", &OPTS.pid) != 1 || OPTS.pid < 0)
151 errexit("invalid process identifier (argument of -p)");
152 OPTS.matchType = MATCH_PID;
153 break;
154 case 'c':
155 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
156 OPTS.creator = CFStringCreateWithFileSystemRepresentation(NULL, optarg);
157 if (OPTS.creator == NULL) errexit("invalid creator (wrong text encoding?)");
158 if (CFStringGetLength(OPTS.creator) != 4) errexit("creator (argument of -c) must be four characters long");
159 OPTS.matchType = MATCH_CREATOR;
160 break;
161 case 'i':
162 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
163 OPTS.bundleID = CFStringCreateWithFileSystemRepresentation(NULL, optarg);
164 if (OPTS.bundleID == NULL) errexit("invalid bundle ID (wrong text encoding?)");
165 OPTS.matchType = MATCH_BUNDLE_ID;
166 break;
167 case 'a':
168 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
169 OPTS.name = CFStringCreateWithFileSystemRepresentation(NULL, optarg);
170 if (OPTS.name == NULL) errexit("invalid application name (wrong text encoding?)");
171 OPTS.matchType = MATCH_NAME;
172 break;
173 case 's':
174 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
175 OPTS.appAction = APP_SHOW;
176 break;
177 case 'h':
178 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
179 OPTS.appAction = APP_HIDE;
180 break;
181 case 'q':
182 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
183 OPTS.appAction = APP_QUIT;
184 break;
185 case 'k':
186 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
187 OPTS.appAction = APP_KILL;
188 break;
189 case 'K':
190 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
191 OPTS.appAction = APP_KILL_HARD;
192 break;
193 case 'l':
194 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
195 OPTS.appAction = APP_LIST;
196 break;
197 case 'L':
198 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
199 OPTS.appAction = APP_LIST;
200 OPTS.longList = true;
201 break;
202 case 'P':
203 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
204 OPTS.appAction = APP_PRINT_PID;
205 break;
206 case 'f':
207 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P, -f options");
208 OPTS.appAction = APP_FRONTMOST;
209 break;
210 case 'S':
211 if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
212 OPTS.action = ACTION_SHOW_ALL;
213 break;
214 case 'H':
215 if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
216 OPTS.action = ACTION_HIDE_OTHERS;
217 break;
218 case 'F':
219 if (OPTS.finalAction != FINAL_NONE) errexit("choose only one -F option");
220 OPTS.finalAction = FINAL_SWITCH;
221 break;
222 default: usage();
223 }
224 }
225
226 argc -= optind;
227 argv += optind;
228
229 if (OPTS.matchType != MATCH_UNKNOWN && argc != 0) usage();
230
231 if (OPTS.matchType == MATCH_UNKNOWN) {
232 if (argc == 0) {
233 if (OPTS.appAction == APP_LIST) {
234 OPTS.matchType = MATCH_ALL;
235 } else if (OPTS.action != ACTION_NONE || OPTS.finalAction != FINAL_NONE) {
236 OPTS.matchType = MATCH_FRONT;
237 } else usage();
238 } else if (argc == 1) {
239 OPTS.path = CFStringCreateWithFileSystemRepresentation(NULL, argv[0]);
240 if (OPTS.path == NULL) errexit("invalid path (wrong text encoding?)");
241 OPTS.matchType = MATCH_PATH;
242 } else usage();
243 }
244
245 if (OPTS.matchType != MATCH_FRONT && OPTS.appAction == APP_NONE)
246 OPTS.appAction = APP_SWITCH;
247
248}
249
250ProcessSerialNumber frontApplication() {
251 ProcessSerialNumber psn;
252 OSStatus err = GetFrontProcess(&psn);
253 if (err != noErr) osstatusexit(err, "can't get frontmost process");
254#if DEBUG
255 fprintf(stderr, "front application PSN %ld.%ld\n", psn.lowLongOfPSN, psn.highLongOfPSN);
256#endif
257 return psn;
258}
259
260OSStatus quitApplication(ProcessSerialNumber *psn) {
261 AppleEvent event;
262 AEAddressDesc appDesc;
263 OSStatus err;
264
265 AEInitializeDesc(&appDesc);
266 err = AECreateDesc(typeProcessSerialNumber, psn, sizeof(*psn), &appDesc);
267 if (err != noErr) return err;
268
269 err = AECreateAppleEvent(kCoreEventClass, kAEQuitApplication, &appDesc, kAutoGenerateReturnID, kAnyTransactionID, &event);
270 if (err != noErr) return err;
271
272 AppleEvent nullReply = {typeNull, nil};
273 err = AESendMessage(&event, &nullReply, kAENoReply, kNoTimeOut);
274 (void)AEDisposeDesc(&event);
275 if (err != noErr) return err;
276
277 (void)AEDisposeDesc(&nullReply); // according to docs, don't call unless AESend returned successfully
278
279 return noErr;
280}
281
282pid_t getPID(const ProcessSerialNumber *psn) {
283 pid_t pid;
284 OSStatus err = GetProcessPID(psn, &pid);
285 if (err != noErr) osstatusexit(err, "can't get process ID");
286 return pid;
287}
288
289bool infoStringMatches(CFDictionaryRef info, CFStringRef key, CFStringRef matchStr) {
290 CFStringRef str = CFDictionaryGetValue(info, key);
291 if (str == NULL)
292 return false;
293 /* note: this means we might match names/paths that are wrong, but works better in the common case */
294 return CFStringCompare(str, matchStr, kCFCompareCaseInsensitive) == kCFCompareEqualTo;
295}
296
297char *getInfoCString(CFDictionaryRef info, CFStringRef key) {
298 CFStringRef str = CFDictionaryGetValue(info, key);
299 if (str == NULL)
300 return "";
301 static char *cStr = NULL;
302 static bool wasDynamic = false;
303 if (wasDynamic)
304 free(cStr);
305 cStr = (char *)CFStringGetCStringPtr(str, CFStringGetSystemEncoding());
306 if (cStr != NULL) {
307 wasDynamic = false;
308 } else {
309 CFIndex cStrLength = CFStringGetMaximumSizeOfFileSystemRepresentation(str);
310 cStr = (char *)malloc(cStrLength * sizeof(char));
311 if (!CFStringGetFileSystemRepresentation(str, cStr, cStrLength)) {
312 CFShow(cStr);
313 errexit("internal error: string encoding conversion failed");
314 }
315 wasDynamic = true;
316 }
317 return cStr;
318}
319
320ProcessSerialNumber matchApplication(void) {
321 if (OPTS.matchType == MATCH_FRONT) return frontApplication();
322
323 OSStatus err;
324 ProcessSerialNumber psn = {
325 kNoProcess, kNoProcess
326 };
327 pid_t pid;
328 char *format = NULL;
329 if (OPTS.appAction == APP_LIST) {
330 int termwidth = 80;
331 struct winsize ws;
332 char *banner = " PSN PID TYPE CREA NAME ";
333 // 123456789.0 12345 1234 1234 1234567890123456789
334 if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
335 ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
336 ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ws) != -1) ||
337 ws.ws_col != 0) termwidth = ws.ws_col;
338 char *formatButPath = "%9ld.%ld %5ld %4s %4s %-19.19s";
339 int pathlen = termwidth - strlen(banner) - 1;
340 // XXX don't ever free 'format', should fix if we get called repeatedly
341 if (OPTS.longList) {
342 printf("%s PATH (bundle identifier)\n", banner);
343 asprintf(&format, "%s %%s", formatButPath);
344 } else if (pathlen >= 4) {
345 printf("%s PATH\n", banner);
346 asprintf(&format, "%s %%-%d.%ds", formatButPath, pathlen, pathlen);
347 } else {
348 format = formatButPath;
349 }
350 }
351
352 CFDictionaryRef info = NULL;
353 while ( (err = GetNextProcess(&psn)) == noErr) {
354 if (info != NULL) CFRelease(info);
355 info = ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
356 if (info == NULL) errexit("can't get information for process with PSN %ld.%ld",
357 psn.lowLongOfPSN, psn.highLongOfPSN);
358
359 switch (OPTS.matchType) {
360 case MATCH_ALL:
361 break;
362 case MATCH_CREATOR: if (!infoStringMatches(info, CFSTR("FileCreator"), OPTS.creator)) continue;
363 break;
364 case MATCH_NAME: if (!infoStringMatches(info, CFSTR("CFBundleName"), OPTS.name)) continue;
365 break;
366 case MATCH_PID: err = GetProcessPID(&psn, &pid); if (err != noErr || OPTS.pid != pid) continue;
367 break;
368 case MATCH_PATH: if (!infoStringMatches(info, CFSTR("BundlePath"), OPTS.path)) continue;
369 break;
370 case MATCH_BUNDLE_ID: if (!infoStringMatches(info, CFSTR("CFBundleIdentifier"), OPTS.bundleID)) continue;
371 break;
372 default:
373 errexit("internal error: invalid match type");
374 }
375 if (OPTS.appAction == APP_LIST) {
376 if (GetProcessPID(&psn, &pid) != noErr)
377 pid = -1;
378 printf(format, psn.lowLongOfPSN, psn.highLongOfPSN, pid,
379 getInfoCString(info, CFSTR("FileType")), getInfoCString(info, CFSTR("FileCreator")),
380 getInfoCString(info, CFSTR("CFBundleName")), getInfoCString(info, CFSTR("BundlePath")));
381 if (OPTS.longList) {
382 char *bundleID = getInfoCString(info, CFSTR("CFBundleIdentifier"));
383 if (bundleID[0] != '\0')
384 printf(" (%s)", bundleID);
385 }
386 putchar('\n');
387 continue;
388 }
389 return psn;
390 }
391 if (err != procNotFound) osstatusexit(err, "can't get next process");
392
393 if (OPTS.appAction == APP_LIST) return frontApplication();
394
395 errexit("can't find matching process");
396 return psn;
397}
398
399int main(int argc, char * const argv[]) {
400 OSStatus err = noErr;
401
402 APP_NAME = argv[0];
403 getargs(argc, argv);
404
405 ProcessSerialNumber psn = matchApplication();
406
407 const char *verb = NULL;
408 switch (OPTS.appAction) {
409 case APP_NONE: break;
410 case APP_LIST: break; // already handled in matchApplication
411 case APP_SWITCH: err = SetFrontProcess(&psn); verb = "set front"; break;
412 case APP_SHOW: err = ShowHideProcess(&psn, true); verb = "show"; break;
413 case APP_HIDE: err = ShowHideProcess(&psn, false); verb = "hide"; break;
414 case APP_QUIT: err = quitApplication(&psn); verb = "quit"; break;
415 case APP_KILL: err = KillProcess(&psn); verb = "send SIGTERM to"; break;
416 case APP_KILL_HARD:
417 {
418 if (kill(getPID(&psn), SIGKILL) == -1)
419 err = (errno == ESRCH) ? procNotFound : (errno == EPERM ? permErr : paramErr);
420 verb = "send SIGKILL to";
421 break;
422 }
423 case APP_PRINT_PID: printf("%d\n", getPID(&psn)); break;
424 case APP_FRONTMOST: err = SetFrontProcessWithOptions(&psn, kSetFrontProcessFrontWindowOnly);
425 verb = "bring frontmost window to front"; break;
426 default:
427 errexit("internal error: invalid application action");
428 }
429 if (err != noErr) osstatusexit(err, "can't %s process", verb);
430
431 switch (OPTS.action) {
432 case ACTION_NONE: break;
433 case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
434 case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
435 default:
436 errexit("internal error: invalid action");
437 }
438 if (err != noErr) osstatusexit(err, "can't %s processes", verb);
439
440 switch (OPTS.finalAction) {
441 case FINAL_NONE: break;
442 case FINAL_SWITCH:
443 psn = frontApplication();
444#if DEBUG
445 fprintf(stderr, "posting show request for %ld.%ld\n", psn.lowLongOfPSN, psn.highLongOfPSN);
446#endif
447 if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
448 err = ShowHideProcess(&psn, true) || SetFrontProcess(&psn);
449 verb = "bring current application's windows to the front";
450 break;
451 default:
452 errexit("internal error: invalid final action");
453 }
454 if (err != noErr) osstatusexit(err, "can't %s", verb);
455
456 exit(0);
457}
Note: See TracBrowser for help on using the repository browser.