1 /* xscreensaver, Copyright (c) 2012-2016 Jamie Zawinski <jwz@jwz.org>
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
11 * Running programs under a pipe or pty and returning bytes from them.
12 * Uses these X resources:
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.
20 * On iOS and Android, textclient-mobile.c is used instead.
25 #if !defined(USE_IPHONE) && !defined(HAVE_ANDROID) /* whole file */
27 #include "textclient.h"
28 #include "resources.h"
31 # define XK_MISCELLANY
32 # include <X11/keysymdef.h>
33 # include <X11/Xatom.h>
34 # include <X11/Intrinsic.h>
44 # include <fcntl.h> /* for O_RDWR */
48 # include <sys/ioctl.h>
55 # ifdef HAVE_SYS_TERMIOS_H
56 # include <sys/termios.h>
58 #endif /* HAVE_FORKPTY */
62 extern const char *progname;
67 int pix_w, pix_h, char_w, char_h;
71 XtIntervalId pipe_timer;
75 Bool input_available_p;
76 Time subproc_relaunch_delay;
77 XComposeStatus compose;
79 Bool meta_sends_esc_p;
82 unsigned int meta_mask;
84 const char *out_buffer;
90 subproc_cb (XtPointer closure, int *source, XtInputId *id)
92 text_data *d = (text_data *) closure;
94 if (! d->input_available_p)
95 fprintf (stderr, "%s: textclient: input available\n", progname);
97 d->input_available_p = True;
101 # define BACKSLASH(c) \
102 (! ((c >= 'a' && c <= 'z') || \
103 (c >= 'A' && c <= 'Z') || \
104 (c >= '0' && c <= '9') || \
105 c == '.' || c == '_' || c == '-' || c == '+' || c == '/'))
109 escape_str (char *s, const char *src)
113 if (BACKSLASH(c)) *s++ = '\\';
121 launch_text_generator (text_data *d)
123 XtAppContext app = XtDisplayToApplicationContext (d->dpy);
125 const char *oprogram = d->program;
128 size_t oprogram_size = strlen(oprogram);
132 /* /bin/sh on OS X 10.10 wipes out the PATH. */
133 const char *path = getenv("PATH");
134 size_t cmd_capacity = (oprogram_size + strlen(path)) * 2 + 100;
135 char *cmd = s = malloc (cmd_capacity);
136 strcpy (s, "export PATH=");
138 s = escape_str (s, path);
142 char *cmd = s = malloc ((strlen(oprogram)) * 2 + 100);
146 strcat (s, oprogram);
149 /* Kludge! Special-case "xscreensaver-text" to tell it how wide
150 the screen is. We used to do this by just always feeding
151 `program' through sprintf() and setting the default value to
152 "xscreensaver-text --cols %d", but that makes things blow up
153 if someone ever uses a --program that includes a % anywhere.
155 len = 17; /* strlen("xscreensaver-text") */
156 if (oprogram_size >= len &&
157 !memcmp (oprogram, "xscreensaver-text", len) &&
158 (oprogram[len] == ' ' || !oprogram[len]))
160 /* strstr is sloppy here. Technically, we should be parsing the command
161 line to identify flags and their arguments. This will blow up if one
162 of those pesky end users could set .textLiteral to "--cols".
164 if (d->char_w && !strstr (oprogram, "--cols "))
165 sprintf (s, " --cols %d", d->char_w);
166 if (d->max_lines && !strstr (oprogram, "--lines "))
167 sprintf (s, " --lines %d", d->max_lines);
171 /* Also special-case "xscreensaver-text" to specify the text content on
172 the command line. defaults(1) on macOS doesn't know about the default
173 screenhack resources that don't make it into the
174 ~/Library/Preferences/ByHost/org.jwz.xscreensaver.*.plist.
177 char *text_mode_flag = " --date";
178 char *value_res = NULL;
179 char *text_mode = get_string_resource (d->dpy, "textMode", "String");
183 if (!strcmp (text_mode, "1") || !strcmp (text_mode, "literal"))
185 text_mode_flag = " --text";
186 value_res = "textLiteral";
188 else if (!strcmp (text_mode, "2") || !strcmp (text_mode, "file"))
190 text_mode_flag = " --file";
191 value_res = "textFile";
193 else if (!strcmp (text_mode, "3") || !strcmp (text_mode, "url"))
195 text_mode_flag = " --url";
196 value_res = "textURL";
198 else if (!strcmp (text_mode, "4") || !strcmp (text_mode, "program"))
200 text_mode_flag = " --program";
201 value_res = "textProgram";
207 strcpy (s, text_mode_flag);
212 size_t old_s = s - cmd;
213 char *value = get_string_resource (d->dpy, value_res, "");
216 cmd = realloc(cmd, cmd_capacity + strlen(value) * 2);
220 s = escape_str(s, value);
223 # endif /* HAVE_COCOA */
226 strcpy (s, " ) 2>&1");
229 fprintf (stderr, "%s: textclient: launch %s: %s\n", progname,
230 (d->pty_p ? "pty" : "pipe"), cmd);
239 ws.ws_col = d->char_w;
240 ws.ws_row = d->char_h;
241 ws.ws_xpixel = d->pix_w;
242 ws.ws_ypixel = d->pix_h;
247 if (getenv ("MallocScribble"))
248 /* This is here to stop me from wasting my time trying to answer
249 this question the next time I forget about it. */
250 fprintf (stderr, "%s: WARNING: forkpty hates 'Enable Guard Malloc'\n",
254 if ((d->pid = forkpty(&fd, NULL, NULL, &ws)) < 0)
257 sprintf (buf, "%.100s: forkpty", progname);
262 /* This is the child fork. */
265 if (putenv ("TERM=vt100"))
274 fprintf (stderr, "%s: textclient: execvp:", progname);
275 for (j = 0; j < i; j++)
276 fprintf (stderr, " %s", av[j]);
277 fprintf (stderr, "\n");
281 sprintf (buf, "%.100s: %.100s", progname, oprogram);
287 /* This is the parent fork. */
288 if (d->pipe) abort();
289 d->pipe = fdopen (fd, "r+");
290 if (d->pipe_id) abort();
292 XtAppAddInput (app, fileno (d->pipe),
293 (XtPointer) (XtInputReadMask | XtInputExceptMask),
294 subproc_cb, (XtPointer) d);
296 fprintf (stderr, "%s: textclient: pid = %d\n", progname, d->pid);
301 #endif /* HAVE_FORKPTY */
303 /* don't mess up controlling terminal on "-pipe -program tcsh". */
304 static int protected_stdin_p = 0;
305 if (! protected_stdin_p) {
307 open ("/dev/null", O_RDWR); /* re-allocate fd 0 */
308 protected_stdin_p = 1;
311 if (d->pipe) abort();
312 if ((d->pipe = popen (cmd, "r")))
314 if (d->pipe_id) abort();
316 XtAppAddInput (app, fileno (d->pipe),
317 (XtPointer) (XtInputReadMask | XtInputExceptMask),
318 subproc_cb, (XtPointer) d);
320 fprintf (stderr, "%s: textclient: popen\n", progname);
325 sprintf (buf, "%.100s: %.100s", progname, cmd);
335 relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
337 text_data *d = (text_data *) closure;
338 /* if (!d->pipe_timer) abort(); */
341 fprintf (stderr, "%s: textclient: launch timer fired\n", progname);
343 launch_text_generator (d);
348 start_timer (text_data *d)
350 XtAppContext app = XtDisplayToApplicationContext (d->dpy);
353 fprintf (stderr, "%s: textclient: relaunching in %d\n", progname,
354 (int) d->subproc_relaunch_delay);
357 XtRemoveTimeOut (d->pipe_timer);
359 XtAppAddTimeOut (app, d->subproc_relaunch_delay,
360 relaunch_generator_timer,
366 close_pipe (text_data *d)
371 fprintf (stderr, "%s: textclient: kill %d\n", progname, d->pid);
373 kill (d->pid, SIGTERM);
378 XtRemoveInput (d->pipe_id);
384 fprintf (stderr, "%s: textclient: pclose\n", progname);
395 textclient_reshape (text_data *d,
396 int pix_w, int pix_h,
397 int char_w, int char_h,
400 # if defined(HAVE_FORKPTY) && defined(TIOCSWINSZ)
406 d->max_lines = max_lines;
409 fprintf (stderr, "%s: textclient: reshape: %dx%d, %dx%d\n", progname,
410 pix_w, pix_h, char_w, char_h);
413 if (d->pid && d->pipe)
415 /* Tell the sub-process that the screen size has changed. */
419 ws.ws_xpixel = pix_w;
420 ws.ws_ypixel = pix_h;
421 ioctl (fileno (d->pipe), TIOCSWINSZ, &ws);
422 kill (d->pid, SIGWINCH);
424 fprintf (stderr, "%s: textclient: SIGWINCH\n", progname);
427 # endif /* HAVE_FORKPTY && TIOCSWINSZ */
430 /* If we're running xscreensaver-text, then kill and restart it any
431 time the window is resized so that it gets an updated --cols arg
432 right away. But if we're running something else, leave it alone.
434 if (!strcmp (d->program, "xscreensaver-text"))
437 fprintf (stderr, "%s: textclient: reshape relaunch\n", progname);
440 d->input_available_p = False;
447 textclient_open (Display *dpy)
449 text_data *d = (text_data *) calloc (1, sizeof (*d));
452 fprintf (stderr, "%s: textclient: init\n", progname);
457 if (get_boolean_resource (dpy, "usePty", "UsePty"))
463 "%s: no pty support on this system; using a pipe instead.\n",
468 d->subproc_relaunch_delay =
469 get_integer_resource (dpy, "relaunchDelay", "Time");
470 if (d->subproc_relaunch_delay < 1)
471 d->subproc_relaunch_delay = 1;
472 d->subproc_relaunch_delay *= 1000;
475 d->meta_sends_esc_p = get_boolean_resource (dpy, "metaSendsESC", "Boolean");
476 d->swap_bs_del_p = get_boolean_resource (dpy, "swapBSDEL", "Boolean");
478 d->program = get_string_resource (dpy, "program", "Program");
482 /* Kludge for MacOS standalone mode: see OSX/SaverRunner.m. */
484 const char *s = getenv ("XSCREENSAVER_STANDALONE");
485 if (s && *s && strcmp(s, "0"))
488 d->program = strdup (getenv ("SHELL"));
490 fprintf (stderr, "%s: textclient: standalone: %s\n",
491 progname, d->program);
504 textclient_close (text_data *d)
507 fprintf (stderr, "%s: textclient: free\n", progname);
514 XtRemoveTimeOut (d->pipe_timer);
516 memset (d, 0, sizeof (*d));
521 textclient_getc (text_data *d)
523 XtAppContext app = XtDisplayToApplicationContext (d->dpy);
526 if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
527 XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
529 if (d->out_buffer && *d->out_buffer)
531 ret = *d->out_buffer;
534 else if (d->input_available_p && d->pipe)
537 int n = read (fileno (d->pipe), (void *) s, 1);
545 fprintf (stderr, "%s: textclient: waitpid %d\n",
548 waitpid (d->pid, NULL, 0);
554 if (d->out_column > 0)
557 fprintf (stderr, "%s: textclient: adding blank line at EOF\n",
560 d->out_buffer = "\r\n\r\n";
565 d->input_available_p = False;
568 if (ret == '\r' || ret == '\n')
575 fprintf (stderr, "%s: textclient: getc: %d\n", progname, ret);
577 fprintf (stderr, "%s: textclient: getc: %03o\n", progname, ret);
579 fprintf (stderr, "%s: textclient: getc: '%c'\n", progname, (char) ret);
586 /* The interpretation of the ModN modifiers is dependent on what keys
587 are bound to them: Mod1 does not necessarily mean "meta". It only
588 means "meta" if Meta_L or Meta_R are bound to it. If Meta_L is on
589 Mod5, then Mod5 is the one that means Meta. Oh, and Meta and Alt
590 aren't necessarily the same thing. Icepicks in my forehead!
593 do_icccm_meta_key_stupidity (Display *dpy)
595 unsigned int modbits = 0;
598 XModifierKeymap *modmap = XGetModifierMapping (dpy);
599 for (i = 3; i < 8; i++)
600 for (j = 0; j < modmap->max_keypermod; j++)
602 int code = modmap->modifiermap[i * modmap->max_keypermod + j];
605 if (code == 0) continue;
606 syms = XGetKeyboardMapping (dpy, code, 1, &nsyms);
607 for (k = 0; k < nsyms; k++)
608 if (syms[k] == XK_Meta_L || syms[k] == XK_Meta_R ||
609 syms[k] == XK_Alt_L || syms[k] == XK_Alt_R)
613 XFreeModifiermap (modmap);
614 # endif /* HAVE_COCOA */
619 /* Returns a mask of the bit or bits of a KeyPress event that mean "meta".
622 meta_modifier (text_data *d)
624 if (!d->meta_done_once)
626 /* Really, we are supposed to recompute this if a KeymapNotify
627 event comes in, but fuck it. */
628 d->meta_done_once = True;
629 d->meta_mask = do_icccm_meta_key_stupidity (d->dpy);
631 fprintf (stderr, "%s: textclient: ICCCM Meta is 0x%08X\n",
632 progname, d->meta_mask);
640 textclient_putc (text_data *d, XKeyEvent *k)
644 XLookupString (k, (char *) &c, 1, &keysym, &d->compose);
645 if (c != 0 && d->pipe)
647 if (!d->swap_bs_del_p) ;
648 else if (c == 127) c = 8;
649 else if (c == 8) c = 127;
651 /* If meta was held down, send ESC, or turn on the high bit. */
652 if (k->state & meta_modifier (d))
654 if (d->meta_sends_esc_p)
655 fputc ('\033', d->pipe);
662 k->type = 0; /* don't interpret this event defaultly. */
665 fprintf (stderr, "%s: textclient: putc '%c'\n", progname, (char) c);
673 #endif /* !USE_IPHONE -- whole file */