From http://www.jwz.org/xscreensaver/xscreensaver-5.16.tar.gz
[xscreensaver] / utils / textclient.c
1 /* xscreensaver, Copyright (c) 2012 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  *
11  * Running programs under a pipe or pty and returning bytes from them.
12  * Uses these X resources:
13  * 
14  * program:             What to run.  Usually "xscreensaver-text".
15  * relaunchDelay: secs  How long after the command dies before restarting.
16  * usePty: bool         Whether to run the command interactively.
17  * metaSendsESC: bool   Whether to send Alt-x as ESC x in pty-mode.
18  * swapBSDEL: bool      Swap Backspace and Delete in pty-mode.
19  */
20
21 #include "utils.h"
22
23 #ifndef USE_IPHONE /* whole file -- see OSX/iostextclient.m */
24
25 #include "textclient.h"
26 #include "resources.h"
27
28 #ifndef HAVE_COCOA
29 # define XK_MISCELLANY
30 # include <X11/keysymdef.h>
31 # include <X11/Xatom.h>
32 # include <X11/Intrinsic.h>
33 #endif
34
35 #include <stdio.h>
36
37 #include <signal.h>
38 #include <sys/wait.h>
39
40 #ifdef HAVE_UNISTD_H
41 # include <unistd.h>
42 # include <fcntl.h>  /* for O_RDWR */
43 #endif
44
45 #ifdef HAVE_FORKPTY
46 # include <sys/ioctl.h>
47 # ifdef HAVE_PTY_H
48 #  include <pty.h>
49 # endif
50 # ifdef HAVE_UTIL_H
51 #  include <util.h>
52 # endif
53 #endif /* HAVE_FORKPTY */
54
55 /*#define DEBUG*/
56
57 extern const char *progname;
58
59 struct text_data {
60   Display *dpy;
61   char *program;
62   int pix_w, pix_h, char_w, char_h;
63
64   Bool pty_p;
65   XtIntervalId pipe_timer;
66   FILE *pipe;
67   pid_t pid;
68   XtInputId pipe_id;
69   Bool input_available_p;
70   Time subproc_relaunch_delay;
71   XComposeStatus compose;
72
73   Bool meta_sends_esc_p;
74   Bool swap_bs_del_p;
75   Bool meta_done_once;
76   unsigned int meta_mask;
77
78   const char *out_buffer;
79   int out_column;
80 };
81
82
83 static void
84 subproc_cb (XtPointer closure, int *source, XtInputId *id)
85 {
86   text_data *d = (text_data *) closure;
87 # ifdef DEBUG
88   if (! d->input_available_p)
89     fprintf (stderr, "%s: textclient: input available\n", progname);
90 # endif
91   d->input_available_p = True;
92 }
93
94
95 static void
96 launch_text_generator (text_data *d)
97 {
98   XtAppContext app = XtDisplayToApplicationContext (d->dpy);
99   char buf[255];
100   const char *oprogram = d->program;
101   char *program = (char *) malloc (strlen (oprogram) + 50);
102
103   strcpy (program, "( ");
104   strcat (program, oprogram);
105
106   /* Kludge!  Special-case "xscreensaver-text" to tell it how wide
107      the screen is.  We used to do this by just always feeding
108      `program' through sprintf() and setting the default value to
109      "xscreensaver-text --cols %d", but that makes things blow up
110      if someone ever uses a --program that includes a % anywhere.
111    */
112   if (!strcmp (oprogram, "xscreensaver-text"))
113     sprintf (program + strlen(program), " --cols %d", d->char_w);
114
115   strcat (program, " ) 2>&1");
116
117 # ifdef DEBUG
118   fprintf (stderr, "%s: textclient: launch %s: %s\n", progname, 
119            (d->pty_p ? "pty" : "pipe"), program);
120 # endif
121
122 #ifdef HAVE_FORKPTY
123   if (d->pty_p)
124     {
125       int fd;
126       struct winsize ws;
127       
128       ws.ws_col = d->char_w;
129       ws.ws_row = d->char_h;
130       ws.ws_xpixel = d->pix_w;
131       ws.ws_ypixel = d->pix_h;
132       
133       d->pipe = NULL;
134       if ((d->pid = forkpty(&fd, NULL, NULL, &ws)) < 0)
135         {
136           /* Unable to fork */
137           sprintf (buf, "%.100s: forkpty", progname);
138           perror (buf);
139         }
140       else if (!d->pid)
141         {
142           /* This is the child fork. */
143           char *av[10];
144           int i = 0;
145           if (putenv ("TERM=vt100"))
146             abort();
147           av[i++] = "/bin/sh";
148           av[i++] = "-c";
149           av[i++] = program;
150           av[i] = 0;
151           execvp (av[0], av);
152           sprintf (buf, "%.100s: %.100s", progname, oprogram);
153           perror (buf);
154           exit (1);
155         }
156       else
157         {
158           /* This is the parent fork. */
159           d->pipe = fdopen (fd, "r+");
160           d->pipe_id =
161             XtAppAddInput (app, fileno (d->pipe),
162                            (XtPointer) (XtInputReadMask | XtInputExceptMask),
163                            subproc_cb, (XtPointer) d);
164 # ifdef DEBUG
165           fprintf (stderr, "%s: textclient: pid = %d\n", progname, d->pid);
166 # endif
167         }
168     }
169   else
170 #endif /* HAVE_FORKPTY */
171     {
172       /* don't mess up controlling terminal on "-pipe -program tcsh". */
173       static int protected_stdin_p = 0;
174       if (! protected_stdin_p) {
175         fclose (stdin);
176         open ("/dev/null", O_RDWR); /* re-allocate fd 0 */
177         protected_stdin_p = 1;
178       }
179
180       if ((d->pipe = popen (program, "r")))
181         {
182           d->pipe_id =
183             XtAppAddInput (app, fileno (d->pipe),
184                            (XtPointer) (XtInputReadMask | XtInputExceptMask),
185                            subproc_cb, (XtPointer) d);
186 # ifdef DEBUG
187           fprintf (stderr, "%s: textclient: popen\n", progname);
188 # endif
189         }
190       else
191         {
192           sprintf (buf, "%.100s: %.100s", progname, program);
193           perror (buf);
194         }
195     }
196
197   free (program);
198 }
199
200
201 static void
202 relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
203 {
204   text_data *d = (text_data *) closure;
205   /* if (!d->pipe_timer) abort(); */
206   d->pipe_timer = 0;
207 # ifdef DEBUG
208   fprintf (stderr, "%s: textclient: launch timer fired\n", progname);
209 # endif
210   launch_text_generator (d);
211 }
212
213
214 void
215 textclient_reshape (text_data *d,
216                     int pix_w, int pix_h,
217                     int char_w, int char_h)
218 {
219 # if defined(HAVE_FORKPTY) && defined(TIOCSWINSZ)
220
221   d->pix_w  = pix_w;
222   d->pix_h  = pix_h;
223   d->char_w = char_w;
224   d->char_h = char_h;
225
226 # ifdef DEBUG
227   fprintf (stderr, "%s: textclient: reshape: %dx%d, %dx%d\n", progname,
228            pix_w, pix_h, char_w, char_h);
229 # endif
230
231   if (d->pid && d->pipe)
232     {
233       /* Tell the sub-process that the screen size has changed. */
234       struct winsize ws;
235       ws.ws_col = char_w;
236       ws.ws_row = char_h;
237       ws.ws_xpixel = pix_w;
238       ws.ws_ypixel = pix_h;
239       ioctl (fileno (d->pipe), TIOCSWINSZ, &ws);
240       kill (d->pid, SIGWINCH);
241     }
242 # endif /* HAVE_FORKPTY && TIOCSWINSZ */
243
244
245   /* If we're running xscreensaver-text, then kill and restart it any
246      time the window is resized so that it gets an updated --cols arg
247      right away.  But if we're running something else, leave it alone.
248    */
249   if (!strcmp (d->program, "xscreensaver-text"))
250     {
251       if (d->pid)
252         kill (d->pid, SIGTERM);
253       if (d->pipe_id)
254         XtRemoveInput (d->pipe_id);
255       if (d->pipe)
256         pclose (d->pipe);
257       d->input_available_p = False;
258       relaunch_generator_timer (d, 0);
259     }
260 }
261
262
263 text_data *
264 textclient_open (Display *dpy)
265 {
266   text_data *d = (text_data *) calloc (1, sizeof (*d));
267
268 # ifdef DEBUG
269   fprintf (stderr, "%s: textclient: init\n", progname);
270 # endif
271
272   d->dpy = dpy;
273
274   if (get_boolean_resource (dpy, "usePty", "UsePty"))
275     {
276 # ifdef HAVE_FORKPTY
277       d->pty_p = True;
278 # else
279       fprintf (stderr,
280                "%s: no pty support on this system; using a pipe instead.\n",
281                progname);
282 # endif
283     }
284
285   d->subproc_relaunch_delay =
286     (1000 * get_integer_resource (dpy, "relaunchDelay", "Time"));
287
288   d->meta_sends_esc_p = get_boolean_resource (dpy, "metaSendsESC", "Boolean");
289   d->swap_bs_del_p    = get_boolean_resource (dpy, "swapBSDEL",    "Boolean");
290
291   d->program = get_string_resource (dpy, "program", "Program");
292
293
294 # ifdef HAVE_FORKPTY
295   /* Kludge for MacOS standalone mode: see OSX/SaverRunner.m. */
296   {
297     const char *s = getenv ("XSCREENSAVER_STANDALONE");
298     if (s && *s && strcmp(s, "0"))
299       {
300         d->pty_p = 1;
301         d->program = strdup (getenv ("SHELL"));
302       }
303   }
304 # endif
305
306   launch_text_generator (d);
307
308   return d;
309 }
310
311
312 void
313 textclient_close (text_data *d)
314 {
315 # ifdef DEBUG
316   fprintf (stderr, "%s: textclient: free\n", progname);
317 # endif
318
319   if (d->program)
320     free (d->program);
321   if (d->pipe_id)
322     XtRemoveInput (d->pipe_id);
323   if (d->pipe)
324     pclose (d->pipe);
325   if (d->pipe_timer)
326     XtRemoveTimeOut (d->pipe_timer);
327   memset (d, 0, sizeof (*d));
328   free (d);
329 }
330
331 int
332 textclient_getc (text_data *d)
333 {
334   XtAppContext app = XtDisplayToApplicationContext (d->dpy);
335   int ret = -1;
336
337   if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
338     XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
339
340   if (d->out_buffer && *d->out_buffer)
341     {
342       ret = *d->out_buffer;
343       d->out_buffer++;
344     }
345   else if (d->input_available_p && d->pipe)
346     {
347       unsigned char s[2];
348       int n = read (fileno (d->pipe), (void *) s, 1);
349       if (n > 0)
350         ret = s[0];
351       else              /* EOF */
352         {
353           if (d->pipe_id)
354             XtRemoveInput (d->pipe_id);
355           d->pipe_id = 0;
356
357           if (d->pid)
358             {
359 # ifdef DEBUG
360               fprintf (stderr, "%s: textclient: waitpid %d\n",
361                        progname, d->pid);
362 # endif
363               waitpid (d->pid, NULL, 0);
364               fclose (d->pipe);
365               d->pid = 0;
366             }
367           else
368             {
369 # ifdef DEBUG
370               fprintf (stderr, "%s: textclient: pclose\n", progname);
371 # endif
372               pclose (d->pipe);
373             }
374           d->pipe = 0;
375
376           if (d->out_column > 0)
377             {
378 # ifdef DEBUG
379               fprintf (stderr, "%s: textclient: adding blank line at EOF\n",
380                        progname);
381 # endif
382               d->out_buffer = "\r\n\r\n";
383             }
384
385 # ifdef DEBUG
386           fprintf (stderr, "%s: textclient: relaunching in %d\n", progname, 
387                    (int) d->subproc_relaunch_delay);
388 # endif
389           d->pipe_timer =
390             XtAppAddTimeOut (app, d->subproc_relaunch_delay,
391                              relaunch_generator_timer,
392                              (XtPointer) d);
393         }
394       d->input_available_p = False;
395     }
396
397   if (ret == '\r' || ret == '\n')
398     d->out_column = 0;
399   else if (ret > 0)
400     d->out_column++;
401
402 # ifdef DEBUG
403   if (ret <= 0)
404     fprintf (stderr, "%s: textclient: getc: %d\n", progname, ret);
405   else if (ret < ' ')
406     fprintf (stderr, "%s: textclient: getc: %03o\n", progname, ret);
407   else
408     fprintf (stderr, "%s: textclient: getc: '%c'\n", progname, (char) ret);
409 # endif
410
411   return ret;
412 }
413
414
415 /* The interpretation of the ModN modifiers is dependent on what keys
416    are bound to them: Mod1 does not necessarily mean "meta".  It only
417    means "meta" if Meta_L or Meta_R are bound to it.  If Meta_L is on
418    Mod5, then Mod5 is the one that means Meta.  Oh, and Meta and Alt
419    aren't necessarily the same thing.  Icepicks in my forehead!
420  */
421 static unsigned int
422 do_icccm_meta_key_stupidity (Display *dpy)
423 {
424   unsigned int modbits = 0;
425 # ifndef HAVE_COCOA
426   int i, j, k;
427   XModifierKeymap *modmap = XGetModifierMapping (dpy);
428   for (i = 3; i < 8; i++)
429     for (j = 0; j < modmap->max_keypermod; j++)
430       {
431         int code = modmap->modifiermap[i * modmap->max_keypermod + j];
432         KeySym *syms;
433         int nsyms = 0;
434         if (code == 0) continue;
435         syms = XGetKeyboardMapping (dpy, code, 1, &nsyms);
436         for (k = 0; k < nsyms; k++)
437           if (syms[k] == XK_Meta_L || syms[k] == XK_Meta_R ||
438               syms[k] == XK_Alt_L  || syms[k] == XK_Alt_R)
439             modbits |= (1 << i);
440         XFree (syms);
441       }
442   XFreeModifiermap (modmap);
443 # endif /* HAVE_COCOA */
444   return modbits;
445 }
446
447
448 /* Returns a mask of the bit or bits of a KeyPress event that mean "meta". 
449  */
450 static unsigned int
451 meta_modifier (text_data *d)
452 {
453   if (!d->meta_done_once)
454     {
455       /* Really, we are supposed to recompute this if a KeymapNotify
456          event comes in, but fuck it. */
457       d->meta_done_once = True;
458       d->meta_mask = do_icccm_meta_key_stupidity (d->dpy);
459 # ifdef DEBUG
460       fprintf (stderr, "%s: textclient: ICCCM Meta is 0x%08X\n",
461                progname, d->meta_mask);
462 # endif
463     }
464   return d->meta_mask;
465 }
466
467
468 Bool
469 textclient_putc (text_data *d, XKeyEvent *k)
470 {
471   KeySym keysym;
472   unsigned char c = 0;
473   XLookupString (k, (char *) &c, 1, &keysym, &d->compose);
474   if (c != 0 && d->pipe)
475     {
476       if (!d->swap_bs_del_p) ;
477       else if (c == 127) c = 8;
478       else if (c == 8)   c = 127;
479
480       /* If meta was held down, send ESC, or turn on the high bit. */
481       if (k->state & meta_modifier (d))
482         {
483           if (d->meta_sends_esc_p)
484             fputc ('\033', d->pipe);
485           else
486             c |= 0x80;
487         }
488
489       fputc (c, d->pipe);
490       fflush (d->pipe);
491       k->type = 0;  /* don't interpret this event defaultly. */
492
493 # ifdef DEBUG
494       fprintf (stderr, "%s: textclient: putc '%c'\n", progname, (char) c);
495 # endif
496
497       return True;
498     }
499   return False;
500 }
501
502 #endif /* !USE_IPHONE -- whole file */