source: releases/appswitch/1.0/appswitch/main.c@ 116

Last change on this file since 116 was 99, checked in by Nicholas Riley, 21 years ago

appswitch 1.0

File size: 18.2 KB
Line 
1/*
2 appswitch - a command-line application switcher
3 Nicholas Riley <appswitch@sabi.net>
4
5 Copyright (c) 2003, 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 <sys/ioctl.h>
22#include "CPS.h"
23
24const char *APP_NAME;
25
26#define VERSION "1.0"
27
28struct {
29 OSType creator;
30 CFStringRef bundleID;
31 char *name;
32 pid_t pid;
33 char *path;
34 enum {
35 MATCH_UNKNOWN, MATCH_FRONT, MATCH_CREATOR, MATCH_BUNDLE_ID, MATCH_NAME, MATCH_PID, MATCH_PATH, MATCH_ALL
36 } matchType;
37 enum {
38 APP_NONE, APP_SWITCH, APP_SHOW, APP_HIDE, APP_QUIT, APP_KILL, APP_KILL_HARD, APP_LIST, APP_PRINT_PID
39 } appAction;
40 Boolean longList;
41 enum {
42 ACTION_NONE, ACTION_SHOW_ALL, ACTION_HIDE_OTHERS
43 } action;
44 enum {
45 FINAL_NONE, FINAL_SWITCH
46 } finalAction;
47} OPTS =
48{
49 kLSUnknownCreator, NULL, NULL, -1, NULL, MATCH_UNKNOWN, APP_NONE, ACTION_NONE, FINAL_NONE, false
50};
51
52typedef struct {
53 OSStatus status;
54 const char *desc;
55} errRec, errList[];
56
57static errList ERRS = {
58 // Process Manager errors
59 { appIsDaemon, "application is background-only\n", },
60 { procNotFound, "unable to connect to system service.\nAre you logged in?" },
61 // CoreGraphics errors
62 { kCGErrorIllegalArgument, "window server error.\nAre you logged in?" },
63 { fnfErr, "file not found" },
64 { 0, NULL }
65};
66
67void usage() {
68 fprintf(stderr, "usage: %s [-sShHkFlP] [-c creator] [-i bundleID] [-a name] [-p pid] [path]\n"
69 " -s show application, bring windows to front (do not switch)\n"
70 " -S show all applications\n"
71 " -h hide application\n"
72 " -H hide other applications\n"
73 " -q quit application\n"
74 " -k kill application (SIGINT)\n"
75 " -K kill application hard (SIGKILL)\n"
76 " -l list applications\n"
77 " -L list applications including full paths and bundle identifiers\n"
78 " -P print application process ID\n"
79 " -F bring current application's windows to front\n"
80 " -c creator match application by four-character creator code ('ToyS')\n"
81 " -i bundle ID match application by bundle identifier (com.apple.scripteditor)\n"
82 " -p pid match application by process identifier [slower]\n"
83 " -a name match application by name\n"
84 , APP_NAME);
85 fprintf(stderr, "appswitch "VERSION" (c) 2003 Nicholas Riley <http://web.sabi.net/nriley/software/>.\n"
86 "Please send bugs, suggestions, etc. to <appswitch@sabi.net>.\n");
87
88 exit(1);
89}
90
91char *osstatusstr(OSStatus err) {
92 errRec *rec;
93 const char *errDesc = "unknown error";
94 char * const failedStr = "(unable to retrieve error message)";
95 static char *str = NULL;
96 size_t len;
97 if (str != NULL && str != failedStr) free(str);
98 for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
99 if (rec->status == err) {
100 errDesc = rec->desc;
101 break;
102 }
103 len = strlen(errDesc) + 10 * sizeof(char);
104 str = (char *)malloc(len);
105 if (str != NULL)
106 snprintf(str, len, "%s (%ld)", errDesc, err);
107 else
108 str = failedStr;
109 return str;
110}
111
112void osstatusexit(OSStatus err, const char *fmt, ...) {
113 va_list ap;
114 const char *errDesc = osstatusstr(err);
115 va_start(ap, fmt);
116 fprintf(stderr, "%s: ", APP_NAME);
117 vfprintf(stderr, fmt, ap);
118 fprintf(stderr, ": %s\n", errDesc);
119 exit(1);
120}
121
122void errexit(const char *fmt, ...) {
123 va_list ap;
124 va_start(ap, fmt);
125 fprintf(stderr, "%s: ", APP_NAME);
126 vfprintf(stderr, fmt, ap);
127 fprintf(stderr, "\n");
128 exit(1);
129}
130
131void getargs(int argc, char * const argv[]) {
132 extern char *optarg;
133 extern int optind;
134 int ch;
135
136 if (argc == 1) usage();
137
138 const char *opts = "c:i:p:a:sShHqkKlLPF";
139
140 while ( (ch = getopt(argc, argv, opts)) != -1) {
141 switch (ch) {
142 case 'p':
143 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
144 if (sscanf(optarg, "%d", &OPTS.pid) != 1 || OPTS.pid < 0)
145 errexit("invalid process identifier (argument of -p)");
146 OPTS.matchType = MATCH_PID;
147 break;
148 case 'c':
149 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
150 if (strlen(optarg) != 4) errexit("creator (argument of -c) must be four characters long");
151 OPTS.creator = *(OSTypePtr)optarg;
152 OPTS.matchType = MATCH_CREATOR;
153 break;
154 case 'i':
155 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
156 OPTS.bundleID = CFStringCreateWithCString(NULL, optarg, CFStringGetSystemEncoding());
157 OPTS.matchType = MATCH_BUNDLE_ID;
158 break;
159 case 'a':
160 if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
161 OPTS.name = strdup(optarg);
162 OPTS.matchType = MATCH_NAME;
163 break;
164 case 's':
165 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
166 OPTS.appAction = APP_SHOW;
167 break;
168 case 'h':
169 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
170 OPTS.appAction = APP_HIDE;
171 break;
172 case 'q':
173 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
174 OPTS.appAction = APP_QUIT;
175 break;
176 case 'k':
177 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
178 OPTS.appAction = APP_KILL;
179 break;
180 case 'K':
181 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
182 OPTS.appAction = APP_KILL_HARD;
183 break;
184 case 'l':
185 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
186 OPTS.appAction = APP_LIST;
187 break;
188 case 'L':
189 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
190 OPTS.appAction = APP_LIST;
191 OPTS.longList = true;
192 break;
193 case 'P':
194 if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -P options");
195 OPTS.appAction = APP_PRINT_PID;
196 break;
197 case 'S':
198 if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
199 OPTS.action = ACTION_SHOW_ALL;
200 break;
201 case 'H':
202 if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
203 OPTS.action = ACTION_HIDE_OTHERS;
204 break;
205 case 'F':
206 if (OPTS.finalAction != FINAL_NONE) errexit("choose only one -F option");
207 OPTS.finalAction = FINAL_SWITCH;
208 break;
209 default: usage();
210 }
211 }
212
213 argc -= optind;
214 argv += optind;
215
216 if (OPTS.matchType != MATCH_UNKNOWN && argc != 0) usage();
217
218 if (OPTS.matchType == MATCH_UNKNOWN) {
219 if (argc == 0) {
220 if (OPTS.appAction == APP_LIST) {
221 OPTS.matchType = MATCH_ALL;
222 } else if (OPTS.action != ACTION_NONE || OPTS.finalAction != FINAL_NONE) {
223 OPTS.matchType = MATCH_FRONT;
224 } else usage();
225 } else if (argc == 1) {
226 OPTS.path = argv[0];
227 OPTS.matchType = MATCH_PATH;
228 } else usage();
229 }
230
231 if (OPTS.matchType != MATCH_FRONT && OPTS.appAction == APP_NONE)
232 OPTS.appAction = APP_SWITCH;
233
234}
235
236CPSProcessSerNum frontApplication() {
237 CPSProcessSerNum psn;
238 OSStatus err = CPSGetFrontProcess(&psn);
239 if (err != noErr) osstatusexit(err, "can't get frontmost process");
240#if DEBUG
241 fprintf(stderr, "front application PSN %ld.%ld\n", psn.hi, psn.lo);
242#endif
243 return psn;
244}
245
246Boolean bundleIdentifierForApplication(CFStringRef *bundleID, char *path) {
247 CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, path, strlen(path), false);
248 if (url == NULL) return false;
249 CFBundleRef bundle = CFBundleCreate(NULL, url);
250 if (bundle != NULL) {
251 *bundleID = CFBundleGetIdentifier(bundle);
252#if DEBUG
253 CFShow(*bundleID);
254#endif
255 }
256 CFRelease(url);
257 return true;
258}
259
260OSStatus quitApplication(CPSProcessSerNum *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 // XXX AECreateAppleEvent is very slow in Mac OS X 10.2.4 and earlier.
270 // XXX This is Apple's bug: <http://lists.apple.com/archives/applescript-implementors/2003/Feb/19/aecreateappleeventfromco.txt>
271 err = AECreateAppleEvent(kCoreEventClass, kAEQuitApplication, &appDesc, kAutoGenerateReturnID, kAnyTransactionID, &event);
272 if (err != noErr) return err;
273
274 AppleEvent nullReply = {typeNull, nil};
275 err = AESendMessage(&event, &nullReply, kAENoReply, kNoTimeOut);
276 (void)AEDisposeDesc(&event);
277 if (err != noErr) return err;
278
279 (void)AEDisposeDesc(&nullReply); // according to docs, don't call unless AESend returned successfully
280
281 return noErr;
282}
283
284CPSProcessSerNum matchApplication(CPSProcessInfoRec *info) {
285 long pathMaxLength = pathconf("/", _PC_PATH_MAX);
286 long nameMaxLength = pathconf("/", _PC_NAME_MAX);
287
288 char *path = (char *)malloc(pathMaxLength);
289 char *name = (char *)malloc(nameMaxLength);;
290
291 if (path == NULL || name == NULL) errexit("can't allocate memory for path or filename buffer");
292
293 if (OPTS.matchType == MATCH_FRONT) return frontApplication();
294
295 OSStatus err;
296 CPSProcessSerNum psn = {
297 kNoProcess, kNoProcess
298 };
299 int len;
300 char *format = NULL;
301 if (OPTS.appAction == APP_LIST) {
302 int termwidth = 80;
303 struct winsize ws;
304 char *banner = " PSN PID TYPE CREA NAME ";
305 // 12345678.0 12345 1234 1234 12345678901234567890
306 if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
307 ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
308 ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ws) != -1) ||
309 ws.ws_col != 0) termwidth = ws.ws_col;
310 char *formatButPath = "%8ld.%ld %5ld %c%c%c%c %c%c%c%c %-20.20s";
311 int pathlen = termwidth - strlen(banner) - 1;
312 // XXX don't ever free 'format', should fix if we get called repeatedly
313 if (OPTS.longList) {
314 printf("%s PATH (bundle identifier)\n", banner);
315 asprintf(&format, "%s %%s", formatButPath);
316 } else if (pathlen >= 4) {
317 printf("%s PATH\n", banner);
318 asprintf(&format, "%s %%-%d.%ds", formatButPath, pathlen, pathlen);
319 } else {
320 format = formatButPath;
321 }
322 }
323
324 while ( (err = CPSGetNextProcess(&psn)) == noErr) {
325 err = CPSGetProcessInfo(&psn, info, path, pathMaxLength, &len, name, nameMaxLength);
326 if (err != noErr) osstatusexit(err, "can't get information for process PSN %ld.%ld", psn.hi, psn.lo);
327
328#if DEBUG
329 fprintf(stderr, "%ld.%ld: %s : %s\n", psn.hi, psn.lo, name, path);
330#endif
331
332 switch (OPTS.matchType) {
333 case MATCH_ALL:
334 break;
335 case MATCH_CREATOR: if (OPTS.creator != info->ExecFileCreator) continue;
336 break;
337 case MATCH_NAME: if (strcmp(name, OPTS.name) != 0) continue;
338 break;
339 case MATCH_PID: if (OPTS.pid != info->UnixPID) continue;
340 break;
341 case MATCH_PATH: if (strcmp(path, OPTS.path) != 0) continue;
342 break;
343 case MATCH_BUNDLE_ID:
344 {
345 CFStringRef bundleID;
346 if (!bundleIdentifierForApplication(&bundleID, path))
347 errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
348 if (bundleID != NULL && CFStringCompare(OPTS.bundleID, bundleID, kCFCompareCaseInsensitive) == kCFCompareEqualTo)
349 break;
350 continue;
351 }
352 default:
353 errexit("internal error: invalid match type");
354 }
355 if (OPTS.appAction == APP_LIST) {
356 char *type = (char *)&(info->ExecFileType), *crea = (char *)&(info->ExecFileCreator);
357#define CXX(c) ( (c) < ' ' ? ' ' : (c) )
358#define OSTYPE_CHAR_ARGS(t) CXX(t[0]), CXX(t[1]), CXX(t[2]), CXX(t[3])
359 printf(format, psn.hi, psn.lo, info->UnixPID,
360 OSTYPE_CHAR_ARGS(type), OSTYPE_CHAR_ARGS(crea),
361 name, path);
362 if (OPTS.longList) {
363 CFStringRef bundleID = NULL;
364 if (!bundleIdentifierForApplication(&bundleID, path))
365 errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
366 if (bundleID != NULL) {
367 char *bundleIDStr = (char *)CFStringGetCStringPtr(bundleID, CFStringGetSystemEncoding());
368 if (bundleIDStr == NULL) {
369 CFIndex bundleIDLength = CFStringGetLength(bundleID) + 1;
370 bundleIDStr = (char *)malloc(bundleIDLength * sizeof(char));
371 if (!CFStringGetCString(bundleID, bundleIDStr, bundleIDLength, CFStringGetSystemEncoding())) {
372 CFShow(bundleIDStr);
373 errexit("internal error: string encoding conversion failed for bundle identifier");
374 }
375 printf(" (%s)", bundleIDStr);
376 free(bundleIDStr);
377 } else {
378 printf(" (%s)", bundleIDStr);
379 }
380 CFRelease(bundleID);
381 }
382 }
383 putchar('\n');
384 continue;
385 }
386 return psn;
387 }
388 if (err != procNotFound) osstatusexit(err, "can't get next process");
389
390 if (OPTS.appAction == APP_LIST) return frontApplication();
391
392 errexit("can't find matching process");
393 return psn;
394}
395
396int main (int argc, char * const argv[]) {
397 OSStatus err = noErr;
398
399 APP_NAME = argv[0];
400 getargs(argc, argv);
401
402 // need to establish connection with window server
403 InitCursor();
404
405 CPSProcessInfoRec info;
406 CPSProcessSerNum psn = matchApplication(&info);
407
408 const char *verb;
409 switch (OPTS.appAction) {
410 case APP_NONE: break;
411 case APP_LIST: break; // already handled in matchApplication
412 case APP_SWITCH: err = CPSSetFrontProcess(&psn); verb = "set front"; break;
413 case APP_SHOW: err = CPSPostShowReq(&psn); verb = "show"; break;
414 case APP_HIDE: err = CPSPostHideReq(&psn); verb = "hide"; break;
415 case APP_QUIT: err = quitApplication(&psn); verb = "quit"; break;
416 case APP_KILL: err = CPSPostKillRequest(&psn, kNilOptions); verb = "kill"; break;
417 case APP_KILL_HARD: err = CPSPostKillRequest(&psn, bfCPSKillHard); verb = "kill"; break;
418 case APP_PRINT_PID:
419 if (info.UnixPID <= 0) errexit("can't get process ID");
420 printf("%lu\n", info.UnixPID); // pid_t is signed, but this field isn't
421 break;
422 default:
423 errexit("internal error: invalid application action");
424 }
425 if (err != noErr) osstatusexit(err, "can't %s process", verb);
426
427 switch (OPTS.action) {
428 case ACTION_NONE: break;
429 case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
430 case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
431 default:
432 errexit("internal error: invalid action");
433 }
434 if (err != noErr) osstatusexit(err, "can't %s processes", verb);
435
436 switch (OPTS.finalAction) {
437 case FINAL_NONE: break;
438 case FINAL_SWITCH:
439 psn = frontApplication();
440#if DEBUG
441 fprintf(stderr, "posting show request for %ld.%ld\n", psn.hi, psn.lo);
442#endif
443 if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
444 err = CPSPostShowReq(&psn) || CPSSetFrontProcess(&psn);
445 verb = "bring current application's windows to the front";
446 break;
447 default:
448 errexit("internal error: invalid final action");
449 }
450 if (err != noErr) osstatusexit(err, "can't %s", verb);
451
452 exit(0);
453}
Note: See TracBrowser for help on using the repository browser.