/* xscreensaver, Copyright (c) 2012-2020 Jamie Zawinski <jwz@jwz.org>
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* Running programs under a pipe or pty and returning bytes from them.
* Uses these X resources:
* program: What to run. Usually "xscreensaver-text".
* relaunchDelay: secs How long after the command dies before restarting.
* usePty: bool Whether to run the command interactively.
* metaSendsESC: bool Whether to send Alt-x as ESC x in pty-mode.
* swapBSDEL: bool Swap Backspace and Delete in pty-mode.
* On iOS and Android, textclient-mobile.c is used instead.
#if !defined(HAVE_IPHONE) && !defined(HAVE_ANDROID) /* whole file */
# include <X11/keysymdef.h>
# include <X11/Intrinsic.h>
# include <fcntl.h> /* for O_RDWR */
# ifdef HAVE_SYS_TERMIOS_H
# include <sys/termios.h>
#endif /* HAVE_FORKPTY */
extern const char *progname
;
int pix_w
, pix_h
, char_w
, char_h
;
Time subproc_relaunch_delay
;
subproc_cb (XtPointer closure
, int *source
, XtInputId
*id
)
text_data
*d
= (text_data
*) closure
;
if (! d
->input_available_p
)
fprintf (stderr
, "%s: textclient: input available\n", progname
);
d
->input_available_p
= True
;
(! ((c >= 'a' && c <= 'z') || \
(c >= 'A' && c <= 'Z') || \
(c >= '0' && c <= '9') || \
c == '.' || c == '_' || c == '-' || c == '+' || c == '/'))
escape_str (char *s
, const char *src
)
if (BACKSLASH(c
)) *s
++ = '\\';
/* Let's see if we're able to fork and exec at all. Thanks, macOS.
static Bool done
= False
;
sprintf (buf
, "%s: textclient: selftest: couldn't fork", progname
);
char * const av
[] = { "/bin/sh", "-c", "true", 0 };
exit (1); /* exits child fork */
/* Busy-loops are bad mmmmkayyyy */
tv
.tv_usec
= 100000L; /* 0.1 sec */
for (i
= 0; i
< 50; i
++) { /* 5 sec max */
pid_t pid2
= waitpid (pid
, &status
, 0);
(void) select (0, 0, 0, 0, &tv
);
fprintf (stderr
, "%s: selftest: textclient: status %d\n",
fprintf (stderr
, "%s: textclient: selftest ok\n", progname
);
static void start_timer (text_data
*d
);
launch_text_generator (text_data
*d
)
XtAppContext app
= XtDisplayToApplicationContext (d
->dpy
);
const char *oprogram
= d
->program
;
size_t oprogram_size
= strlen(oprogram
);
/* /bin/sh on OS X 10.10 wipes out the PATH. */
const char *path
= getenv("PATH");
size_t cmd_capacity
= (oprogram_size
+ strlen(path
)) * 2 + 100;
char *cmd
= s
= malloc (cmd_capacity
);
strcpy (s
, "export PATH=");
s
= escape_str (s
, path
);
char *cmd
= s
= malloc ((strlen(oprogram
)) * 2 + 100);
if (!d
->out_buffer
|| !*d
->out_buffer
)
d
->out_buffer
= "Can't exec; Gatekeeper problem?\r\n\r\n";
/* Kludge! Special-case "xscreensaver-text" to tell it how wide
the screen is. We used to do this by just always feeding
`program' through sprintf() and setting the default value to
"xscreensaver-text --cols %d", but that makes things blow up
if someone ever uses a --program that includes a % anywhere.
len
= 17; /* strlen("xscreensaver-text") */
if (oprogram_size
>= len
&&
!memcmp (oprogram
, "xscreensaver-text", len
) &&
(oprogram
[len
] == ' ' || !oprogram
[len
]))
/* strstr is sloppy here. Technically, we should be parsing the command
line to identify flags and their arguments. This will blow up if one
of those pesky end users could set .textLiteral to "--cols".
if (d
->char_w
&& !strstr (oprogram
, "--cols "))
sprintf (s
, " --cols %d", d
->char_w
);
if (d
->max_lines
&& !strstr (oprogram
, "--lines "))
sprintf (s
, " --lines %d", d
->max_lines
);
/* Also special-case "xscreensaver-text" to specify the text content on
the command line. defaults(1) on macOS doesn't know about the default
screenhack resources that don't make it into the
~/Library/Preferences/ByHost/org.jwz.xscreensaver.*.plist.
char *text_mode_flag
= " --date";
char *text_mode
= get_string_resource (d
->dpy
, "textMode", "String");
if (!strcmp (text_mode
, "1") || !strcmp (text_mode
, "literal"))
text_mode_flag
= " --text";
value_res
= "textLiteral";
else if (!strcmp (text_mode
, "2") || !strcmp (text_mode
, "file"))
text_mode_flag
= " --file";
else if (!strcmp (text_mode
, "3") || !strcmp (text_mode
, "url"))
text_mode_flag
= " --url";
else if (!strcmp (text_mode
, "4") || !strcmp (text_mode
, "program"))
text_mode_flag
= " --program";
value_res
= "textProgram";
strcpy (s
, text_mode_flag
);
char *value
= get_string_resource (d
->dpy
, value_res
, "");
cmd
= realloc(cmd
, cmd_capacity
+ strlen(value
) * 2);
s
= escape_str(s
, value
);
fprintf (stderr
, "%s: textclient: launch %s: %s\n", progname
,
(d
->pty_p
? "pty" : "pipe"), cmd
);
if (getenv ("MallocScribble"))
/* This is here to stop me from wasting my time trying to answer
this question the next time I forget about it. */
fprintf (stderr
, "%s: WARNING: forkpty hates 'Enable Guard Malloc'\n",
if ((d
->pid
= forkpty(&fd
, NULL
, NULL
, &ws
)) < 0)
sprintf (buf
, "%.100s: forkpty", progname
);
/* This is the child fork. */
if (putenv ("TERM=vt100"))
fprintf (stderr
, "%s: textclient: execvp:", progname
);
fprintf (stderr
, " %s", av
[j
]);
sprintf (buf
, "%.100s: %.100s", progname
, oprogram
);
/* This is the parent fork. */
d
->pipe
= fdopen (fd
, "r+");
XtAppAddInput (app
, fileno (d
->pipe
),
(XtPointer
) (XtInputReadMask
| XtInputExceptMask
),
subproc_cb
, (XtPointer
) d
);
fprintf (stderr
, "%s: textclient: pid = %d\n", progname
, d
->pid
);
#endif /* HAVE_FORKPTY */
/* don't mess up controlling terminal on "-pipe -program tcsh". */
static int protected_stdin_p
= 0;
if (! protected_stdin_p
) {
open ("/dev/null", O_RDWR
); /* re-allocate fd 0 */
if ((d
->pipe
= popen (cmd
, "r")))
XtAppAddInput (app
, fileno (d
->pipe
),
(XtPointer
) (XtInputReadMask
| XtInputExceptMask
),
subproc_cb
, (XtPointer
) d
);
fprintf (stderr
, "%s: textclient: popen\n", progname
);
sprintf (buf
, "%.100s: %.100s", progname
, cmd
);
relaunch_generator_timer (XtPointer closure
, XtIntervalId
*id
)
text_data
*d
= (text_data
*) closure
;
/* if (!d->pipe_timer) abort(); */
fprintf (stderr
, "%s: textclient: launch timer fired\n", progname
);
launch_text_generator (d
);
start_timer (text_data
*d
)
XtAppContext app
= XtDisplayToApplicationContext (d
->dpy
);
fprintf (stderr
, "%s: textclient: relaunching in %d\n", progname
,
(int) d
->subproc_relaunch_delay
);
XtRemoveTimeOut (d
->pipe_timer
);
XtAppAddTimeOut (app
, d
->subproc_relaunch_delay
,
relaunch_generator_timer
,
close_pipe (text_data
*d
)
fprintf (stderr
, "%s: textclient: kill %d\n", progname
, d
->pid
);
XtRemoveInput (d
->pipe_id
);
fprintf (stderr
, "%s: textclient: pclose\n", progname
);
textclient_reshape (text_data
*d
,
# if defined(HAVE_FORKPTY) && defined(TIOCSWINSZ)
d
->max_lines
= max_lines
;
fprintf (stderr
, "%s: textclient: reshape: %dx%d, %dx%d\n", progname
,
pix_w
, pix_h
, char_w
, char_h
);
/* Tell the sub-process that the screen size has changed. */
ioctl (fileno (d
->pipe
), TIOCSWINSZ
, &ws
);
fprintf (stderr
, "%s: textclient: SIGWINCH\n", progname
);
# endif /* HAVE_FORKPTY && TIOCSWINSZ */
/* If we're running xscreensaver-text, then kill and restart it any
time the window is resized so that it gets an updated --cols arg
right away. But if we're running something else, leave it alone.
if (!strcmp (d
->program
, "xscreensaver-text"))
fprintf (stderr
, "%s: textclient: reshape relaunch\n", progname
);
d
->input_available_p
= False
;
textclient_open (Display
*dpy
)
text_data
*d
= (text_data
*) calloc (1, sizeof (*d
));
fprintf (stderr
, "%s: textclient: init\n", progname
);
if (get_boolean_resource (dpy
, "usePty", "UsePty"))
"%s: no pty support on this system; using a pipe instead.\n",
d
->subproc_relaunch_delay
=
get_integer_resource (dpy
, "relaunchDelay", "Time");
if (d
->subproc_relaunch_delay
< 1)
d
->subproc_relaunch_delay
= 1;
d
->subproc_relaunch_delay
*= 1000;
d
->meta_sends_esc_p
= get_boolean_resource (dpy
, "metaSendsESC", "Boolean");
d
->swap_bs_del_p
= get_boolean_resource (dpy
, "swapBSDEL", "Boolean");
d
->program
= get_string_resource (dpy
, "program", "Program");
/* Kludge for MacOS standalone mode: see OSX/SaverRunner.m. */
const char *s
= getenv ("XSCREENSAVER_STANDALONE");
if (s
&& *s
&& strcmp(s
, "0"))
d
->program
= strdup (getenv ("SHELL"));
fprintf (stderr
, "%s: textclient: standalone: %s\n",
textclient_close (text_data
*d
)
fprintf (stderr
, "%s: textclient: free\n", progname
);
XtRemoveTimeOut (d
->pipe_timer
);
memset (d
, 0, sizeof (*d
));
textclient_getc (text_data
*d
)
XtAppContext app
= XtDisplayToApplicationContext (d
->dpy
);
if (XtAppPending (app
) & (XtIMTimer
|XtIMAlternateInput
))
XtAppProcessEvent (app
, XtIMTimer
|XtIMAlternateInput
);
if (d
->out_buffer
&& *d
->out_buffer
)
else if (d
->input_available_p
&& d
->pipe
)
int n
= read (fileno (d
->pipe
), (void *) s
, 1);
fprintf (stderr
, "%s: textclient: waitpid %d\n",
waitpid (d
->pid
, NULL
, 0);
fprintf (stderr
, "%s: textclient: adding blank line at EOF\n",
d
->out_buffer
= "\r\n\r\n";
d
->input_available_p
= False
;
if (ret
== '\r' || ret
== '\n')
fprintf (stderr
, "%s: textclient: getc: %d\n", progname
, ret
);
fprintf (stderr
, "%s: textclient: getc: %03o\n", progname
, ret
);
fprintf (stderr
, "%s: textclient: getc: '%c'\n", progname
, (char) ret
);
/* The interpretation of the ModN modifiers is dependent on what keys
are bound to them: Mod1 does not necessarily mean "meta". It only
means "meta" if Meta_L or Meta_R are bound to it. If Meta_L is on
Mod5, then Mod5 is the one that means Meta. Oh, and Meta and Alt
aren't necessarily the same thing. Icepicks in my forehead!
do_icccm_meta_key_stupidity (Display
*dpy
)
unsigned int modbits
= 0;
XModifierKeymap
*modmap
= XGetModifierMapping (dpy
);
for (j
= 0; j
< modmap
->max_keypermod
; j
++)
int code
= modmap
->modifiermap
[i
* modmap
->max_keypermod
+ j
];
syms
= XGetKeyboardMapping (dpy
, code
, 1, &nsyms
);
for (k
= 0; k
< nsyms
; k
++)
if (syms
[k
] == XK_Meta_L
|| syms
[k
] == XK_Meta_R
||
syms
[k
] == XK_Alt_L
|| syms
[k
] == XK_Alt_R
)
XFreeModifiermap (modmap
);
/* Returns a mask of the bit or bits of a KeyPress event that mean "meta".
meta_modifier (text_data
*d
)
/* Really, we are supposed to recompute this if a KeymapNotify
event comes in, but fuck it. */
d
->meta_done_once
= True
;
d
->meta_mask
= do_icccm_meta_key_stupidity (d
->dpy
);
fprintf (stderr
, "%s: textclient: ICCCM Meta is 0x%08X\n",
textclient_putc (text_data
*d
, XKeyEvent
*k
)
XLookupString (k
, (char *) &c
, 1, &keysym
, &d
->compose
);
else if (c
== 127) c
= 8;
else if (c
== 8) c
= 127;
/* If meta was held down, send ESC, or turn on the high bit. */
if (k
->state
& meta_modifier (d
))
k
->type
= 0; /* don't interpret this event defaultly. */
fprintf (stderr
, "%s: textclient: putc '%c'\n", progname
, (char) c
);
#endif /* !HAVE_IPHONE -- whole file */