source: trunk/appswitch/appswitch/main.m @ 592

Last change on this file since 592 was 592, checked in by Nicholas Riley, 10 years ago

Use Cocoa instead of CPS to show all/hide other applications.

File size: 20.7 KB
Line 
1/*
2 appswitch - a command-line application switcher
3 Nicholas Riley <appswitch@sabi.net>
4
5 Copyright (c) 2003-09, 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#import <AppKit/AppKit.h>
25
26const char *APP_NAME;
27
28#define VERSION "1.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    { 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:
470            [[NSAutoreleasePool alloc] init];
471            [[NSApplication sharedApplication] unhideAllApplications: nil];
472            err = noErr;
473            verb = "show all";
474            break;
475        case ACTION_HIDE_OTHERS:
476            [[NSAutoreleasePool alloc] init];
477            [[NSApplication sharedApplication] hideOtherApplications: nil];
478            err = noErr;
479            verb = "hide other";
480            break;
481        default:
482            errexit("internal error: invalid action");
483    }
484    if (err != noErr) osstatusexit(err, "can't %s processes", verb);
485
486    switch (OPTS.finalAction) {
487        case FINAL_NONE: break;
488        case FINAL_SWITCH:
489            psn = frontApplication();
490#if DEBUG
491            fprintf(stderr, "posting show request for %ld.%ld\n", psn.lowLongOfPSN, psn.highLongOfPSN);
492#endif
493            if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
494            err = ShowHideProcess(&psn, true) || SetFrontProcess(&psn);
495            verb = "bring current application's windows to the front";
496            break;
497        default:
498            errexit("internal error: invalid final action");   
499    }
500    if (err != noErr) osstatusexit(err, "can't %s", verb);
501
502    exit(0);
503}
Note: See TracBrowser for help on using the repository browser.