source: trunk/appswitch/main.c @ 91

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

appswitch 1.0b1 code changes

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.0b1"
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            printf(format, psn.hi, psn.lo, info->UnixPID,
301                   type[0], type[1], type[2], type[3],
302                   crea[0], crea[1], crea[2], crea[3],
303                   name, path);
304            continue;
305        }
306        return psn;
307    }
308    if (err != procNotFound) osstatusexit(err, "can't get next process");
309
310    if (OPTS.appAction == APP_LIST) return frontApplication();
311
312    errexit("can't find matching process");
313    return psn;
314}
315
316int main (int argc, char * const argv[]) {
317    OSStatus err = noErr;
318
319    APP_NAME = argv[0];
320    getargs(argc, argv);
321
322    // need to establish connection with window server
323    InitCursor();
324
325    CPSProcessInfoRec info;
326    CPSProcessSerNum psn = matchApplication(&info);
327
328    const char *verb;
329    switch (OPTS.appAction) {
330        case APP_NONE: break;
331        case APP_LIST: break; // already handled in matchApplication
332        case APP_SWITCH: err = CPSSetFrontProcess(&psn); verb = "set front"; break;
333        case APP_SHOW: err = CPSPostShowReq(&psn); verb = "show"; break;
334        case APP_HIDE: err = CPSPostHideReq(&psn); verb = "hide"; break;
335        case APP_KILL: err = CPSPostKillRequest(&psn, kNilOptions); verb = "kill"; break;
336        case APP_PRINT_PID:
337            if (info.UnixPID <= 0) errexit("can't get process ID");
338            printf("%lu\n", info.UnixPID); // pid_t is signed, but this field isn't
339            break;
340        default:
341            errexit("internal error: invalid application action");
342    }
343    if (err != noErr) osstatusexit(err, "can't %s process", verb);
344
345    switch (OPTS.action) {
346        case ACTION_NONE: break;
347        case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
348        case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
349        default:
350            errexit("internal error: invalid action");
351    }
352    if (err != noErr) osstatusexit(err, "can't %s processes", verb);
353
354    switch (OPTS.finalAction) {
355        case FINAL_NONE: break;
356        case FINAL_SWITCH:
357            psn = frontApplication();
358#if DEBUG
359            fprintf(stderr, "posting show request for %ld.%ld\n", psn.hi, psn.lo);
360#endif
361            if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
362            err = CPSPostShowReq(&psn) || CPSSetFrontProcess(&psn);
363            verb = "bring current application's windows to the front";
364            break;
365        default:
366            errexit("internal error: invalid final action");   
367    }
368    if (err != noErr) osstatusexit(err, "can't %s", verb);
369
370    exit(0);
371}
Note: See TracBrowser for help on using the repository browser.