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

Last change on this file since 97 was 97, checked in by Nicholas Riley, 18 years ago

appswitch 1.0b2

File size: 14.4 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.0b2"
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_KILL, APP_LIST, APP_PRINT_PID
39    } appAction;
40    enum {
41        ACTION_NONE, ACTION_SHOW_ALL, ACTION_HIDE_OTHERS
42    } action;
43    enum {
44        FINAL_NONE, FINAL_SWITCH
45    } finalAction;
46} OPTS =
47{
48    kLSUnknownCreator, NULL, NULL, -1, NULL, MATCH_UNKNOWN, APP_NONE, ACTION_NONE, FINAL_NONE
49};
50
51typedef struct {
52    OSStatus status;
53    const char *desc;
54} errRec, errList[];
55
56static errList ERRS = {
57    // Process Manager errors
58    { appIsDaemon, "application is background-only\n", },
59    { procNotFound, "unable to connect to system service.\nAre you logged in?" },
60    // CoreGraphics errors
61    { kCGErrorIllegalArgument, "window server error.\nAre you logged in?" },
62    { fnfErr, "file not found" },
63    { 0, NULL }
64};
65
66void usage() {
67    fprintf(stderr, "usage: %s [-sShHkFlP] [-c creator] [-i bundleID] [-a name] [-p pid] [path]\n"
68            "  -s            show application, bring windows to front (do not switch)\n"
69            "  -S            show all applications\n"
70            "  -h            hide application\n"
71            "  -H            hide other applications\n"
72            "  -k            kill application\n"
73            "  -l            list applications\n"
74            "  -P            print application process ID\n"
75            "  -F            bring current application's windows to front\n"
76            "  -c creator    match application by four-character creator code ('ToyS')\n"
77            "  -i bundle ID  match application by bundle identifier (com.apple.scripteditor)\n"
78            "  -p pid        match application by process identifier [slower]\n"
79            "  -a name       match application by name\n"
80            , APP_NAME);
81    fprintf(stderr, "appswitch "VERSION" (c) 2003 Nicholas Riley <http://web.sabi.net/nriley/software/>.\n"
82            "Please send bugs, suggestions, etc. to <appswitch@sabi.net>.\n");
83
84    exit(1);
85}
86
87char *osstatusstr(OSStatus err) {
88    errRec *rec;
89    const char *errDesc = "unknown error";
90    char * const failedStr = "(unable to retrieve error message)";
91    static char *str = NULL;
92    size_t len;
93    if (str != NULL && str != failedStr) free(str);
94    for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
95        if (rec->status == err) {
96            errDesc = rec->desc;
97            break;
98        }
99            len = strlen(errDesc) + 10 * sizeof(char);
100    str = (char *)malloc(len);
101    if (str != NULL)
102        snprintf(str, len, "%s (%ld)", errDesc, err);
103    else
104        str = failedStr;
105    return str;
106}
107
108void osstatusexit(OSStatus err, const char *fmt, ...) {
109    va_list ap;
110    const char *errDesc = osstatusstr(err);
111    va_start(ap, fmt);
112    fprintf(stderr, "%s: ", APP_NAME);
113    vfprintf(stderr, fmt, ap);
114    fprintf(stderr, ": %s\n", errDesc);
115    exit(1);
116}
117
118void errexit(const char *fmt, ...) {
119    va_list ap;
120    va_start(ap, fmt);
121    fprintf(stderr, "%s: ", APP_NAME);
122    vfprintf(stderr, fmt, ap);
123    fprintf(stderr, "\n");
124    exit(1);
125}
126
127void getargs(int argc, char * const argv[]) {
128    extern char *optarg;
129    extern int optind;
130    int ch;
131
132    if (argc == 1) usage();
133
134    const char *opts = "c:i:p:a:sShHklPF";
135
136    while ( (ch = getopt(argc, argv, opts)) != -1) {
137        switch (ch) {
138            case 'p':
139                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
140                if (sscanf(optarg, "%d", &OPTS.pid) != 1 || OPTS.pid < 0)
141                    errexit("invalid process identifier (argument of -p)");
142                OPTS.matchType = MATCH_PID;
143                break;
144            case 'c':
145                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
146                if (strlen(optarg) != 4) errexit("creator (argument of -c) must be four characters long");
147                OPTS.creator = *(OSTypePtr)optarg;
148                OPTS.matchType = MATCH_CREATOR;
149                break;
150            case 'i':
151                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
152                OPTS.bundleID = CFStringCreateWithCString(NULL, optarg, CFStringGetSystemEncoding());
153                OPTS.matchType = MATCH_BUNDLE_ID;
154                break;
155            case 'a':
156                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
157                OPTS.name = strdup(optarg);
158                OPTS.matchType = MATCH_NAME;
159                break;
160            case 's':
161                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -k, -l, -P options");
162                OPTS.appAction = APP_SHOW;
163                break;
164            case 'h':
165                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -k, -l, -P options");
166                OPTS.appAction = APP_HIDE;
167                break;
168            case 'k':
169                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -k, -l, -P options");
170                OPTS.appAction = APP_KILL;
171                break;
172            case 'l':
173                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -k, -l, -P options");
174                OPTS.appAction = APP_LIST;
175                break;
176            case 'P':
177                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -k, -l, -P options");
178                OPTS.appAction = APP_PRINT_PID;
179                break;
180            case 'S':
181                if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
182                OPTS.action = ACTION_SHOW_ALL;
183                break;
184            case 'H':
185                if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
186                OPTS.action = ACTION_HIDE_OTHERS;
187                break;
188            case 'F':
189                if (OPTS.finalAction != FINAL_NONE) errexit("choose only one -F option");
190                OPTS.finalAction = FINAL_SWITCH;
191                break;
192            default: usage();
193        }
194    }
195
196    argc -= optind;
197    argv += optind;
198
199    if (OPTS.matchType != MATCH_UNKNOWN && argc != 0) usage();
200
201    if (OPTS.matchType == MATCH_UNKNOWN) {
202        if (argc == 0) {
203            if (OPTS.appAction == APP_LIST) {
204                OPTS.matchType = MATCH_ALL;
205            } else if (OPTS.action != ACTION_NONE || OPTS.finalAction != FINAL_NONE) {
206                OPTS.matchType = MATCH_FRONT;
207            } else usage();
208        } else if (argc == 1) {
209            OPTS.path = argv[0];
210            OPTS.matchType = MATCH_PATH;
211        } else usage();
212    }
213
214    if (OPTS.matchType != MATCH_FRONT && OPTS.appAction == APP_NONE)
215        OPTS.appAction = APP_SWITCH;
216
217}
218
219CPSProcessSerNum frontApplication() {
220    CPSProcessSerNum psn;
221    OSStatus err = CPSGetFrontProcess(&psn);
222    if (err != noErr) osstatusexit(err, "can't get frontmost process");
223#if DEBUG
224    fprintf(stderr, "front application PSN %ld.%ld\n", psn.hi, psn.lo);
225#endif
226    return psn;
227}
228
229CPSProcessSerNum matchApplication(CPSProcessInfoRec *info) {
230    long pathMaxLength = pathconf("/", _PC_PATH_MAX);
231    long nameMaxLength = pathconf("/", _PC_NAME_MAX);
232
233    char *path = (char *)malloc(pathMaxLength);
234    char *name = (char *)malloc(nameMaxLength);;
235
236    if (path == NULL || name == NULL) errexit("can't allocate memory for path or filename buffer");
237
238    if (OPTS.matchType == MATCH_FRONT) return frontApplication();
239
240    OSStatus err;
241    CPSProcessSerNum psn = {
242        kNoProcess, kNoProcess
243    };
244    int len;
245    char *format = NULL;
246    if (OPTS.appAction == APP_LIST) {
247        int termwidth = 80;
248        struct winsize ws;
249        char *banner = "       PSN   PID TYPE CREA NAME                ";
250                     // 12345678.0 12345 1234 1234 12345678901234567890
251        printf("%s PATH\n", banner);
252        if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
253             ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
254             ioctl(STDIN_FILENO,  TIOCGWINSZ, (char *)&ws) != -1) ||
255            ws.ws_col != 0) termwidth = ws.ws_col;
256        int pathlen = termwidth - strlen(banner) - 1;
257        asprintf(&format, "%%8ld.%%ld %%5ld %%c%%c%%c%%c %%c%%c%%c%%c %%-20.20s %%-%d.%ds\n", pathlen, pathlen);
258    }
259   
260    while ( (err = CPSGetNextProcess(&psn)) == noErr) {
261        err = CPSGetProcessInfo(&psn, info, path, pathMaxLength, &len, name, nameMaxLength);
262        if (err != noErr) osstatusexit(err, "can't get information for process PSN %ld.%ld", psn.hi, psn.lo);
263
264#if DEBUG
265        fprintf(stderr, "%ld.%ld: %s : %s\n", psn.hi, psn.lo, name, path);
266#endif
267
268        switch (OPTS.matchType) {
269            case MATCH_ALL:
270                break;
271            case MATCH_CREATOR: if (OPTS.creator != info->ExecFileCreator) continue;
272                break;
273            case MATCH_NAME: if (strcmp(name, OPTS.name) != 0) continue;
274                break;
275            case MATCH_PID: if (OPTS.pid != info->UnixPID) continue;
276                break;
277            case MATCH_PATH: if (strcmp(path, OPTS.path) != 0) continue;
278                break;
279            case MATCH_BUNDLE_ID:
280               {
281                   CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, path, strlen(path), false);
282                   if (url == NULL) errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
283                   CFBundleRef bundle = CFBundleCreate(NULL, url);
284                   if (bundle != NULL) {
285                       CFStringRef bundleID = CFBundleGetIdentifier(bundle);
286#if DEBUG
287                       CFShow(bundleID);
288#endif
289                       if (bundleID != NULL && CFStringCompare(OPTS.bundleID, bundleID, kCFCompareCaseInsensitive) == kCFCompareEqualTo)
290                           break;
291                   }
292                   CFRelease(url);
293                   continue;
294               }
295            default:
296                errexit("internal error: invalid match type");
297        }
298        if (OPTS.appAction == APP_LIST) {
299            char *type = (char *)&(info->ExecFileType), *crea = (char *)&(info->ExecFileCreator);
300#define CXX(c) ( (c) < ' ' ? '?' : (c) )
301#define OSTYPE_CHAR_ARGS(t) CXX(t[0]), CXX(t[1]), CXX(t[2]), CXX(t[3])
302            printf(format, psn.hi, psn.lo, info->UnixPID,
303                   OSTYPE_CHAR_ARGS(type), OSTYPE_CHAR_ARGS(crea),
304                   name, path);
305            continue;
306        }
307        return psn;
308    }
309    if (err != procNotFound) osstatusexit(err, "can't get next process");
310
311    if (OPTS.appAction == APP_LIST) return frontApplication();
312
313    errexit("can't find matching process");
314    return psn;
315}
316
317int main (int argc, char * const argv[]) {
318    OSStatus err = noErr;
319
320    APP_NAME = argv[0];
321    getargs(argc, argv);
322
323    // need to establish connection with window server
324    InitCursor();
325
326    CPSProcessInfoRec info;
327    CPSProcessSerNum psn = matchApplication(&info);
328
329    const char *verb;
330    switch (OPTS.appAction) {
331        case APP_NONE: break;
332        case APP_LIST: break; // already handled in matchApplication
333        case APP_SWITCH: err = CPSSetFrontProcess(&psn); verb = "set front"; break;
334        case APP_SHOW: err = CPSPostShowReq(&psn); verb = "show"; break;
335        case APP_HIDE: err = CPSPostHideReq(&psn); verb = "hide"; break;
336        case APP_KILL: err = CPSPostKillRequest(&psn, kNilOptions); verb = "kill"; break;
337        case APP_PRINT_PID:
338            if (info.UnixPID <= 0) errexit("can't get process ID");
339            printf("%lu\n", info.UnixPID); // pid_t is signed, but this field isn't
340            break;
341        default:
342            errexit("internal error: invalid application action");
343    }
344    if (err != noErr) osstatusexit(err, "can't %s process", verb);
345
346    switch (OPTS.action) {
347        case ACTION_NONE: break;
348        case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
349        case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
350        default:
351            errexit("internal error: invalid action");
352    }
353    if (err != noErr) osstatusexit(err, "can't %s processes", verb);
354
355    switch (OPTS.finalAction) {
356        case FINAL_NONE: break;
357        case FINAL_SWITCH:
358            psn = frontApplication();
359#if DEBUG
360            fprintf(stderr, "posting show request for %ld.%ld\n", psn.hi, psn.lo);
361#endif
362            if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
363            err = CPSPostShowReq(&psn) || CPSSetFrontProcess(&psn);
364            verb = "bring current application's windows to the front";
365            break;
366        default:
367            errexit("internal error: invalid final action");   
368    }
369    if (err != noErr) osstatusexit(err, "can't %s", verb);
370
371    exit(0);
372}
Note: See TracBrowser for help on using the repository browser.