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

Last change on this file since 307 was 306, checked in by Nicholas Riley, 14 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.