/*  slrnface - feeble attempt at delivering X-Faces to slrn and mutt users
 *  Copyright (C) 2000, 2001, 2002  Drazen Kacar
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <X11/Xlib.h>
#include <X11/Intrinsic.h>
#include <X11/cursorfont.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <ctype.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#ifdef __linux
#include <sys/time.h>
#include <sys/ioctl.h>
#include <termio.h>
#else
#include <termios.h>
#endif

#define countof(x) (sizeof(x)/sizeof(x[0]))

extern void uncompface (char *);

extern int errno;

/*
 * The values in optvalues[] array are just fallbacks if appropriate
 * X resources are not found. You can set them here if you like, but
 * you should really use X resources for that. Command line parameters
 * should also work.
 */

typedef enum {
   val_int,
   val_color
} OptionType;

struct optval {
   OptionType type;
   union {
      unsigned long intval;
      char *string;
   } value;
};

struct optval optvalues[] = {
   { val_int,   { 0 }},
   { val_int,   { 1 }},
   { val_int,   { 0 }},
   { val_int,   { 2 }},
   { val_int,   { 0 }},
   { val_color, { 0 }},
   { val_color, { 0 }},
   { val_color, { 0 }}
};

#define X_OFFSET_CHAR	(optvalues[0].value.intval)
#define Y_OFFSET_CHAR	(optvalues[1].value.intval)
#define X_OFFSET_PIX	(optvalues[2].value.intval)
#define Y_OFFSET_PIX	(optvalues[3].value.intval)
#define XFACE_PAD	(optvalues[4].value.intval)

/* Very unconsistent */

#define INK		(optvalues[5])
#define PAPER		(optvalues[6])
#define PADCOLOR	(optvalues[7])

XrmOptionDescRec my_options[] = {
   { "-xOffsetChar", "*slrnface.xOffsetChar", XrmoptionSepArg, NULL },
   { "-yOffsetChar", "*slrnface.yOffsetChar", XrmoptionSepArg, NULL },
   { "-xOffsetPix",  "*slrnface.xOffsetPix",  XrmoptionSepArg, NULL },
   { "-yOffsetPix",  "*slrnface.yOffsetPix",  XrmoptionSepArg, NULL },
   { "-XFacePad",    "*slrnface.XFacePad",    XrmoptionSepArg, NULL },
   { "-ink",	     "*slrnface.ink",	      XrmoptionSepArg, NULL },
   { "-paper",	     "*slrnface.paper",	      XrmoptionSepArg, NULL },
   { "-padColor",    "*slrnface.padColor",    XrmoptionSepArg, NULL }
};

/*
 * MAXRESOURCESIZE should be the size of the longest resource string in the
 * above array. It can be a bit larger, of course. If you're adding
 * resources, make sure it's not smaller. Wouldn't it be nice if preprocessor
 * was able to calculate that?
 */

#define MAXRESOURCESIZE 30

Display *d = NULL;
Window win = 0, winid;		/* win is X-Face window, winid is the parent */
int winchild;
Cursor cursor_move;
int x_pos, y_pos;		/* X-Face window position */
Colormap cmap;
GC gc;
XGCValues gcvals;
unsigned long gcmask;
unsigned long pad_color;
Pixmap bitmap = 0;
int fifo_fd, term_fd;
struct winsize winsz;

int is_mapped = 0;		/* Whether X-Face window is mapped */
int buffered_face_len = -1;	/* Length of last X-Face data */
int pixmap_stale = 1;		/* If set, X-Face data in the buffer doesn't
				   match pixmap, ie. pixmap needs to be
				   regenerated. */
int face_cleared = 1;		/* If set, the last command told us to clear
				   X-Face. We'll still keep the pixmap
				   and the buffer data (if we had any), just
				   in case the next X-Face will be the same,
				   but this means we'll not show X-Face
				   until we get a command with the new
				   X-Face data. */
char last_face[8000];		/* Buffer for X-Face data. We keep the last
				   one here for comparison. If the new X-Face
				   turns out to be the same as the previous
				   one, we can save some round trips. */

struct pollfd fifo_pollfd;

/*
 * Flags for signals. We can't use bits in the same variable because bit
 * operations are not atomic.
 */

volatile sig_atomic_t got_sigterm = 0, got_sigstop = 0, got_sigcont = 0;
volatile sig_atomic_t got_sigwinch = 0;

/* Signal handlers */

/* ARGSUSED */
void
handle_sigterm (int sig)
{
   got_sigterm = 1;
}

/* ARGSUSED */
void
handle_sigstop (int sig)
{
   got_sigstop = 1;
}

/* ARGSUSED */
void
handle_sigcont (int sig)
{
   got_sigcont = 1;
}

/* ARGSUSED */
void
handle_sigwinch (int sig)
{
   got_sigwinch = 1;
}

void
cleanup (void)
{
   if (d)
   {
      if (bitmap)
	 XFreePixmap (d, bitmap);
      if (win)
      {
	 XFreeCursor (d, cursor_move);
	 XDestroyWindow (d, win);
      }
      XCloseDisplay (d);
   }
}

void
suspend_me (void)
{
   if (is_mapped)
   {
      XUnmapWindow (d, win);
      XFlush (d);
   }
   got_sigstop = 0;
   /*
    * And we'll proceed to sleep in poll(). In case we get a command through
    * the FIFO we'll process it, so something might show up on the screen.
    * I'm not sure if this is a bug or a feature. :-)
    */
}

void
restart_me (void)
{
   if (is_mapped)
   {
      XMapWindow (d, win);
      XFlush (d);
   }
   got_sigcont = 0;
}

/*
 * We don't know if we got top level window id or an id for a child
 * window. A child window typically won't have WM_CLASS set, so we need
 * to traverse up the window hierarchy until we find a window with that
 * property.
 *
 * Input:  w    - window id where the search starts
 *	   hint - pointer to XClassHint structure
 * Output: 0 - WM_CLASS not found
 *	   1 - WM_CLASS found on window w
 *	   2 - WM_CLASS found on one of w's parents
 *
 *         If WM_CLASS was found, it's stored in h.
 */

int
get_class_hint (Window w, XClassHint *hint)
{
   Window root, parent, *children;
   unsigned int nchld;
   int childwin = 0;

   do {
      if (XGetClassHint (d, w, hint))
	 return childwin + 1;
      if (!XQueryTree (d, w, &root, &parent, &children, &nchld))
         return 0;
      if (children)
	 XFree (children);
      childwin = 1;
      w = parent;
   } while (parent != root);
   return 0;
}

/*
 * Calculate the position at which X-Face window should be placed.
 *
 * Input:  global variables X_OFFSET_CHAR, X_OFFSET_PIX, Y_OFFSET_CHAR,
 *         Y_OFFSET_PIX and XFACE_PAD
 * Output: global variables x_pos and y_pos
 * Uses:   global struct winsz and variables winid and winchild
 */

void
get_face_position (void)
{
   /*
    * The problem with terminals is that they might have scrollbars on
    * the unknown side. But if we detected WM_CLASS on a parent window,
    * we can reasonably assume that our window id is just the text window
    * and nothing else.
    */

   if (!winsz.ws_xpixel || !winsz.ws_ypixel)	/* Bastards... */
   {
      Window root_return;
      int x_return, y_return;
      unsigned width_return, height_return;
      unsigned border_width_return;
      unsigned depth_return;

      XGetGeometry (d, winid, &root_return, &x_return, &y_return,
	    	    &width_return, &height_return, &border_width_return,
		    &depth_return);

      if (winchild > 1)
	 x_pos = width_return - X_OFFSET_CHAR * (width_return / winsz.ws_col)
	     		      - X_OFFSET_PIX;
      else
      {
	 /*
	  * First try to substract scrollbar and hope the rest are
	  * just characters on the screen.
	  */
         width_return -= X_OFFSET_PIX;
         x_pos = width_return - X_OFFSET_CHAR * (width_return / winsz.ws_col);
      }
      y_pos = Y_OFFSET_CHAR * (height_return / winsz.ws_row) + Y_OFFSET_PIX;
   }
   else
   {
      x_pos = winsz.ws_xpixel - X_OFFSET_CHAR * (winsz.ws_xpixel / winsz.ws_col)
      			      - X_OFFSET_PIX;
      y_pos = Y_OFFSET_CHAR * (winsz.ws_ypixel / winsz.ws_row) + Y_OFFSET_PIX;
   }
   x_pos -= 48 + XFACE_PAD;	/* X-Face window width. */
}

/*
 * We need to allocate black and white colors instead of using
 * BlackPixelOfScreen and WhitePixelOfScreen macros because we'll want to
 * deallocate them in case we get a command to change one of the colors.
 * Using macros and trying to deallocate would result in BadAccess.
 */

unsigned long
alloc_named_color (char *name)
{
   XColor col;

   if (XParseColor (d, cmap, name, &col) && XAllocColor (d, cmap, &col))
      return col.pixel;
   /*
    * XXX This isn't kosher in the general case, but we're currently
    * using this only for black & white, so it's not going to fail.
    */
   return 0;
}

void
parse_resource(int idx, char *resource)
{
   if (optvalues[idx].type == val_int)
      optvalues[idx].value.intval = strtol (resource, NULL, 0);
   else	/* val_color */
   {
      XColor col;

      if (XParseColor (d, cmap, resource, &col)
	  && XAllocColor (d, cmap, &col))
      {
	 optvalues[idx].value.intval = col.pixel;
	 /*
	  * Change type to val_int, so we know the value is set here.
	  */
	 optvalues[idx].type = val_int;
      }
   }
}

void
force_cmd_line (int argc, char **argv)
{
   int i, j;

   for (i = 1; i < argc; ++i)
   {
      if (*argv[i] != '-')
	 continue;
      for (j = 0; j < countof(my_options); ++j)
	 if (!strcmp (argv[i], my_options[j].option))
	 {
	    if (argv[i + 1])
	    {
	       parse_resource (j, argv[i + 1]);
	       i++;
	    }
	    break;
	 }
   }
}

void
setup (int *argc, char **argv)
{
   int i, free_fifo = 0, status;
   char *winenv, *fifo_file, *home;
   XWindowAttributes winattrs;
   XSetWindowAttributes winsetattrs;
   unsigned long winmask;
   struct stat inode;
   XClassHint terminal;

   /* See if we can connect to the display */
   if (!(d = XOpenDisplay (NULL)))
      exit (1);

   /* Then close because we'll fork. We'll reopen it in the child. Hopefully
      it won't run away. */
   XCloseDisplay (d);

   /* Terminal Window ID. We must have this. */
   if (!(winenv = getenv ("WINDOWID")) || !(winid = strtol (winenv, NULL, 0)))
      exit (2);

   /* Stupid method for determining controlling terminal. It should be
      one of those. But it doesn't have to. FIXME */
   for (i = 0; i < 3; ++i)
      if (isatty (term_fd = i))
	 break;
   if (i == 3)
      exit (3);
   ioctl (term_fd, TIOCGWINSZ, &winsz);

   /* We could work without this, but we're just picky. */
   if (!winsz.ws_row || !winsz.ws_col)
      exit (4);

   /* See if we got FIFO name as the argument. */
   fifo_file = NULL;
   for (i = 1; i < *argc && argv[i]; i += 2)
   {
      if (*argv[i] != '-')
      {
	 fifo_file = argv[i];
	 break;
      }
   }
   if (!fifo_file)
   {
      /* FIFO not in argument; construct the name */
      struct utsname u;
      size_t maxlen;

      if (!(home = getenv ("HOME")))
      {
	 struct passwd *pw;

	 pw = getpwuid (geteuid ());
	 if (!pw || !(home = pw->pw_dir))
	    exit (5);
      }
      if (uname (&u) < 0)
	 exit (5);
      
      maxlen = strlen (home) + sizeof ("/.slrnfaces/") + strlen (u.nodename)
	       + 30;
      fifo_file = malloc (maxlen);
      if (!fifo_file)
	 exit (5);
      free_fifo = 1;
      if (snprintf(fifo_file, maxlen, "%s/.slrnfaces/%s.%ld", home,
	           u.nodename, (long) getppid ()) < 0)
	 exit (5);
   }

   /*
    * The FIFO should have been created by the parent. If it doesn't exist
    * we'll create it here, but it's a bad omen. If the file exists, but
    * is not a FIFO, we need to return an error now, because we won't have
    * the chance later. Parent could block waiting for us or waiting on a
    * FIFO without a reader. We can't allow that. This whole show would have
    * been unnecessary if slang could open a pipe. A simple, unnamed,
    * unidirectional pipe. Is that too much to ask?
    */

   /* Open non-blocking FIFO now and remove that flag after the fork. */
   if (stat (fifo_file, &inode))
      if (errno == ENOENT)
      {
	 if (mkfifo (fifo_file, 0600))
	    exit (5);
      }
      else
	 exit (5);
   else
      if (!S_ISFIFO (inode.st_mode))
	 exit (5);
   if ((fifo_fd = open (fifo_file, O_RDONLY | O_NONBLOCK)) < 0)
      exit (5);
   if (free_fifo)
      free (fifo_file);

   switch (fork ())
   {
      case -1: exit (6);
      case  0: break;	 /* Child. Just continue. */
      default: exit (0); /* Parent. Return success and make its parent happy. */
   }

   fcntl (fifo_fd, F_SETFL, 0);

   /*
    * Fill this here. The structure will be used for the normal processing
    * as well, which is why it's a global thing.
    */

   fifo_pollfd.fd = fifo_fd;
   fifo_pollfd.events = POLLIN;

   /* Sync with the parent. */

   do {
      status = poll (&fifo_pollfd, 1, -1);
   } while (status == -1 && errno == EINTR);

   if (!(d = XOpenDisplay (NULL)))
      exit (1);

   /* Get colormap, preferably the one used by the parent terminal window. */

   if (XGetWindowAttributes (d, winid, &winattrs) == Success)
      cmap = winattrs.colormap;
   else
      cmap = DefaultColormap (d, DefaultScreen (d));

   /* X Resources stuff. Suggested by a user's demented mind. */

   winchild = get_class_hint (winid, &terminal);
   if (winchild)
   {
      /*
       * It seems there's a lot of idiots who are putting dots in the name
       * property. Examples are gnome-terminal and Eterm. If you run into
       * this problem, quote them Xlib manual for me:

         Note that the name set in this property may differ from the name
	 set as WM_NAME. That is, WM_NAME specifies what should be
	 displayed in the title bar and, therefore, can contain temporal
	 information (for example, the name of a file currently in an
	 editor's buffer). On the other hand, the name specified as part
	 of WM_CLASS is the formal name of the application that should be
	 used when retrieving the application's resources from the
	 resource database.

       * We'll try to work around the brain damage by parsing command
       * line parameters directly, so at least that will work.
       */

      if (strpbrk (terminal.res_name, ".*?")
	  || strpbrk (terminal.res_class, ".*?"))
      {
	 force_cmd_line (*argc, argv);
      }
      else
      {
	 XtAppContext app_context;
	 XrmDatabase res;
	 size_t name_len, class_len;
	 int i;
	 char *name_str, *class_str;
	 char *name_start, *class_start;
	 char *type_str;
	 XrmValue value;

	 XtToolkitInitialize ();
	 app_context = XtCreateApplicationContext ();
	 XtDisplayInitialize (app_context, d, terminal.res_name,
			      terminal.res_class, my_options,
			      countof (my_options), argc, argv);
	 res = XtScreenDatabase (DefaultScreenOfDisplay (d));
	 name_len = strlen (terminal.res_name);
	 name_str = malloc (name_len + MAXRESOURCESIZE);
	 memcpy (name_str, terminal.res_name, name_len);
	 name_str[name_len] = '.';
	 name_start = name_str + name_len + 1;
	 class_len = strlen (terminal.res_class);
	 class_str = malloc (class_len + MAXRESOURCESIZE);
	 memcpy (class_str, terminal.res_class, class_len);
	 class_str[class_len] = '.';
	 class_start = class_str + class_len + 1;

	 for (i = 0; i < countof (my_options); ++i)
	 {
	    strcpy (name_start, my_options[i].specifier + 1);
	    strcpy (class_start, my_options[i].specifier + 1);

	    if(XrmGetResource(res, name_str, class_str, &type_str,
		     	      &value) == True)
	    {
	       /* We don't have generic strings yet, so this will do. */

	       char *resource = malloc (value.size + 1);

	       memcpy (resource, value.addr, value.size);
	       resource[value.size] = 0;
	       parse_resource (i, resource);
	       free (resource);
	    }
	 }

	 free (name_str);
	 free (class_str);
	 XrmDestroyDatabase (res);
      }
      XFree (terminal.res_name);
      XFree (terminal.res_class);
   }
   else
      force_cmd_line (*argc, argv);

   /* Calculate X-Face window position. */

   get_face_position ();

   /* Set up background color and create our window. */

   if (PADCOLOR.type == val_color)
      pad_color = alloc_named_color ("black");
   else
      pad_color = PADCOLOR.value.intval;

   winsetattrs.background_pixel = pad_color;
   winsetattrs.win_gravity = NorthEastGravity;
   winsetattrs.colormap = cmap;
   winmask = CWBackPixel | CWWinGravity | CWColormap;
   win = XCreateWindow (d, winid, x_pos, y_pos, 48 + XFACE_PAD, 48,
	   		0, CopyFromParent, CopyFromParent, CopyFromParent,
			winmask, &winsetattrs);

   /* Then set up colors in the GC. */

   if (INK.type == val_color)
      gcvals.foreground = alloc_named_color ("black");
   else
      gcvals.foreground = INK.value.intval;

   if (PAPER.type == val_color)
      gcvals.background = alloc_named_color ("white");
   else
      gcvals.background = PAPER.value.intval;

   gcvals.graphics_exposures = False;
   gcmask = GCForeground | GCBackground | GCGraphicsExposures;

   gc = XCreateGC(d, win, gcmask, &gcvals);

   /* And the last piece of crap. */

   cursor_move = XCreateFontCursor (d, XC_fleur);
   XGrabButton (d, Button1, AnyModifier, win, False,
	        ButtonPressMask | ButtonReleaseMask | Button1MotionMask,
	        GrabModeSync, GrabModeAsync, win, cursor_move);
   XSelectInput (d, win, ExposureMask | StructureNotifyMask);

   /* We are done. Amazing. */
}

#define MAXCOLORLEN 40

unsigned long
chk_alloc_color (char *color, unsigned long pixel, unsigned long *pixel_return)
{
   char *p, *s, clr[MAXCOLORLEN];
   XColor col;

   p = color;
   while (*p != '\n' && isspace (*(unsigned char *)p))
      ++p;
   if (*p == '\n')
      return 0;
   for (s = clr; !isspace (*(unsigned char *)p) && s < clr + MAXCOLORLEN;
	++p, ++s)
      *s = *p;
   if (s == clr + MAXCOLORLEN)
      return 0;
   *s = 0;

   if (!XParseColor (d, cmap, clr, &col) || !XAllocColor (d, cmap, &col))
      return 0;
   XFreeColors (d, cmap, &pixel, 1, 0);
   if (col.pixel == pixel)
      return 0;
   *pixel_return = col.pixel;
   return 1;
}

int
chk_get_naturalnum (char *buf)
{
   long num;
   char *endptr;

   errno = 0;
   num = strtol (buf, &endptr, 0);
   if (!errno && num >= 0 && *endptr == '\n' && num < INT_MAX)
      return num;
   else
      return -1;
}

typedef enum {
   xface_eof,		/* EOF or error on FIFO */
   xface_noop,		/* Do nothing */
   xface_flush,		/* Flush the display */
   xface_unmap,		/* Unmap window */
   xface_map,		/* Map window */
   xface_display	/* Map window and paint X-Face */
} XFaceState;

/* Read data from FIFO and return state. Reimplementing stdio is always fun. */

XFaceState
read_fifo (void)
{
   int n, status, refresh_gc = 0, refresh_pad = 0, reposition_face = 0;
   size_t toread;
   int state, skip_junk = 0;
   char *command, *bufstart, *bufend, *bufp;
   int xoffsetchar = -1, yoffsetchar = -1, xoffsetpix = -1, yoffsetpix = -1;
   char buf[8000];

   do {
begin:
      toread = sizeof (buf);
      bufstart = command = buf;

readmore:
      do {
	 n = read (fifo_fd, bufstart, toread);
      } while (n < 0 && errno == EINTR);

      if (n <= 0)
	 return xface_eof;

      bufend = bufstart + n;
      bufp = bufstart;
      state = xface_noop;
      while (bufp < bufend)
      {
	 /* Find the end of the command. */
	 while (bufp < bufend && *bufp != '\n')
	    ++bufp;
	 if (*bufp != '\n')
	 {
	    /* Partial command in the buffer. Try to read some more. */
	    toread = sizeof(buf) - (bufend - buf);
	    if (toread)
	    {
	       bufstart = bufend;
	       goto readmore;
	    }
	    if (command == buf)
	    {
	       /*
		* The command doesn't fit in the buffer. It must be
		* some junk, so discard it and resync on newline.
		*/
	       skip_junk = 1;
	       goto begin;
	    }
	    /*
	     * Copy what we have to the beginning of the buffer and read
	     * some more.
	     */
	    memmove (buf, command, bufend - command);
	    bufstart = buf + (bufend - command);
	    toread = sizeof (buf) - (bufend - command) + 1;
	    goto readmore;
	 }

	 /*
	  * See if we were supposed to skip the junk. This can happen
	  * only with the first chunk in the buffer.
	  */

	 if (skip_junk)
	 {
	    skip_junk = 0;
	    if (bufend == bufp + 1)
	       goto begin;

	    /* The next command needs our attention. */
	    command = ++bufp;
	    continue;
	 }

	 if (!memcmp (command, "clear\n", sizeof "clear"))
	 {
	    state = xface_unmap;
	    face_cleared = 1;
	 }
	 else if (!memcmp (command, "suppress\n", sizeof "suppress"))
	    state = xface_unmap;
	 else if (!memcmp (command, "show\n", sizeof "show"))
	 {
	    if (!face_cleared)
	       if (pixmap_stale)
		  state = xface_display;
	       else
		  state = xface_map;
	 }
	 else if (!memcmp (command, "xface ", sizeof "xface"))
	 {
	    int face_len;
	    char *face_start;

	    face_start = command + sizeof "xface";
	    face_len = bufp - face_start;
	    /*
	     * We need to do all sanity checking here because of supress
	     * command. Our caller cannot afford to ignore certain kinds
	     * of data because it wouldn't know what the previous good
	     * data was.
	     *
	     * We are ignoring X-Faces longer than 2000 bytes because
	     * uncompface() uses the same buffer for input and output and
	     * it's concievable that this might lead to buffer overruns,
	     * since there's no way to pass the buffer size to it.
	     *
	     * TODO: mmap a chunk of memory somewhere, make sure there is
	     * a gap above it, catch SIGSEGV temporarily and do uncompressing
	     * there. It's the only safe way.
	     */
	    if (face_len <= 2000)
	    {
	       if (face_len == buffered_face_len
		   && !memcmp (face_start, last_face, face_len))
		  if (pixmap_stale)
		     state = xface_display;
		  else
		     state = xface_map;
	       else
	       {
		  memcpy (last_face, face_start, face_len);
		  pixmap_stale = 1;
		  buffered_face_len = face_len;
		  state = xface_display;
	       }
	       face_cleared = 0;
	    }
	 }
	 else if (!memcmp (command, "ink ", sizeof "ink"))
	 {
	    unsigned long pixel;

	    if (chk_alloc_color (command + sizeof "ink", gcvals.foreground,
		     		 &pixel))
	    {
	       gcvals.foreground = pixel;
	       refresh_gc = 1;
	    }
	 }
	 else if (!memcmp (command, "paper ", sizeof "paper"))
	 {
	    unsigned long pixel;

	    if (chk_alloc_color (command + sizeof "paper", gcvals.background,
		     		 &pixel))
	    {
	       gcvals.background = pixel;
	       refresh_gc = 1;
	    }
	 }
	 else if (!memcmp (command, "padcolor ", sizeof "padcolor"))
	 {
	    unsigned long pixel;

	    if (chk_alloc_color (command + sizeof "padcolor", pad_color,
		     		 &pixel))
	    {
	       pad_color = pixel;
	       refresh_pad = 1;
	    }
	 }
	 else if (!memcmp (command, "xoffsetchar ", sizeof "xoffsetchar"))
	 {
	    xoffsetchar = chk_get_naturalnum (command + sizeof "xoffsetchar");
	    if (xoffsetchar >= 0)
	       reposition_face = 1;
	 }
	 else if (!memcmp (command, "yoffsetchar ", sizeof "yoffsetchar"))
	 {
	    yoffsetchar = chk_get_naturalnum (command + sizeof "yoffsetchar");
	    if (yoffsetchar >= 0)
	       reposition_face = 1;
	 }
	 else if (!memcmp (command, "xoffsetpix ", sizeof "xoffsetpix"))
	 {
	    xoffsetpix = chk_get_naturalnum (command + sizeof "xoffsetpix");
	    if (xoffsetpix >= 0)
	       reposition_face = 1;
	 }
	 else if (!memcmp (command, "yoffsetpix ", sizeof "yoffsetpix"))
	 {
	    yoffsetpix = chk_get_naturalnum (command + sizeof "yoffsetpix");
	    if (yoffsetpix >= 0)
	       reposition_face = 1;
	 }

	 /* Otherwise it's unrecognized command. Just skip it. */

	 command = ++bufp;
      }

      /*
       * Wait 10 milliseconds in case the parent has something else to send.
       * The user is not likely to notice this pause and we might avoid
       * certain amount of processing and roundtrips.
       */
      do {
	 status = poll (&fifo_pollfd, 1, 10);
      } while (status == -1 && errno == EINTR);
   } while (status > 0);

   if (refresh_gc)
   {
      XChangeGC (d, gc, gcmask, &gcvals);
      pixmap_stale = 1;
      if (!face_cleared)
	 state = xface_display;
   }
   if (refresh_pad)
   {
      XSetWindowBackground (d, win, pad_color);
      XClearArea (d, win, 0, 0, XFACE_PAD, 48, False);
      if (state == xface_noop)
	 state = xface_flush;
   }
   if (reposition_face)
   {
      if (got_sigwinch)
      {
         ioctl (term_fd, TIOCGWINSZ, &winsz);
	 got_sigwinch = 0;
      }

      if (xoffsetchar >= 0)
	 X_OFFSET_CHAR = xoffsetchar;
      if (yoffsetchar >= 0)
	 Y_OFFSET_CHAR = yoffsetchar;
      if (xoffsetpix >= 0)
	 X_OFFSET_PIX = xoffsetpix;
      if (yoffsetpix >= 0)
	 Y_OFFSET_PIX = yoffsetpix;

      get_face_position ();
      XMoveWindow (d, win, x_pos, y_pos);
      if (state == xface_noop)
	 state = xface_flush;
   }

   return state;
}

char ls[] = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };

int
process_command (void)
{
   int i, n;
   unsigned char *datap;
   unsigned char data[288];
   char uncompfacebuf[15000];

   switch (read_fifo ())
   {
      case xface_eof:

	 return 1;

      case xface_unmap:

	 if (is_mapped)
	 {
	    XUnmapWindow (d, win);
	    is_mapped = 0;
	    XFlush (d);
	 }
	 break;

      case xface_map:

	 if (!is_mapped)
	 {
	    XMapWindow (d, win);
	    is_mapped = 1;
	    XFlush (d);
	 }
	 break;

      case xface_display:

	 memcpy (uncompfacebuf, last_face, buffered_face_len);
	 uncompfacebuf[buffered_face_len] = 0;
	 pixmap_stale = 0;
	 uncompface (uncompfacebuf);

	 /* Are we alive? No buffer overruns? Fuck. */

	 for (i = 0, n = 0, datap = data; i < 48; ++i)
	 {
	    int i1, i2, i3, len;

	    sscanf (uncompfacebuf + n, "%i,%i,%i,%n", &i1, &i2, &i3, &len);
	    n += len;

	    /* Who invented that all-bits-are-reversed format? */

	    *datap++ = ls[i1 >> 12 & 15] | ls[i1 >> 8 & 15] << 4;
	    *datap++ = ls[i1 >> 4 & 15]  | ls[i1 & 15] << 4;
	    *datap++ = ls[i2 >> 12 & 15] | ls[i2 >> 8 & 15] << 4;
	    *datap++ = ls[i2 >> 4 & 15]  | ls[i2 & 15] << 4;
	    *datap++ = ls[i3 >> 12 & 15] | ls[i3 >> 8 & 15] << 4;
	    *datap++ = ls[i3 >> 4 & 15]  | ls[i3 & 15] << 4;
	 }

	 if (bitmap)
	    XFreePixmap (d, bitmap);
	 bitmap = XCreateBitmapFromData (d, win, (char *) data, 48, 48);
	 if (is_mapped)
	    XCopyPlane(d, bitmap, win, gc, 0, 0, 48, 48, XFACE_PAD, 0, 1);
	 else
	    XMapWindow (d, win); /* It will be filled on expose. */
	 XFlush (d);
	 break;

      case xface_flush:

	 XFlush (d);
   }

   return 0;
}

void
process_xevent (void)
{
   static int x_pointer, y_pointer;
   static int grab = AnyButton;
   int x, y;
   XEvent event;

   while (XPending (d))
   {
      XNextEvent (d, &event);

      switch (event.type)
      {
	 case MapNotify:
	 case Expose:

	    is_mapped = 1;
	    XCopyPlane (d, bitmap, win, gc, 0, 0, 48, 48, XFACE_PAD, 0, 1);
	    XFlush (d);
	    break;

	 case ButtonPress:

	    switch (((XButtonEvent *) &event)->button)
	    {
	       case Button1:
		  if (grab == AnyButton)
		  {
		     grab = Button1;
		     x_pointer = ((XButtonEvent *) &event)->x_root;
		     y_pointer = ((XButtonEvent *) &event)->y_root;
		  }
		  XAllowEvents (d, SyncPointer, CurrentTime);
		  break;
	    }
	    break;

	 case ButtonRelease:

	    if(grab == ((XButtonEvent *) &event)->button)
	       grab = AnyButton;
	    XAllowEvents (d, AsyncPointer, CurrentTime);
	    break;

	 case MotionNotify:

	    x = ((XMotionEvent *) &event)->x_root;
	    y = ((XMotionEvent *) &event)->y_root;
	    x_pos += x - x_pointer;
	    y_pos += y - y_pointer;
	    x_pointer = x;
	    y_pointer = y;
	    XMoveWindow (d, win, x_pos, y_pos);
	    break;

	 case GravityNotify:

	    x = ((XGravityEvent *) &event)->x;
	    y = ((XGravityEvent *) &event)->y;
	    if (grab != AnyButton)
	    {
	       x_pointer += x - x_pos;
	       y_pointer += y - y_pos;
	    }
	    x_pos = x;
	    y_pos = y;
	    break;
      }
   }
}

int
main (int argc, char **argv)
{
   struct pollfd p[2];
   struct sigaction sigact;

   /*
    * We don't need setlocale() call, because nothing here depends on the
    * locale. The only effect it might have is loading and initializing
    * one or more shared libraries. We don't need that.
    *
    * setlocale (LC_ALL, "");
    *
    */

   setup (&argc, argv);	/* child returns, FIFO set and ready, window created */

   /*
    * We'll ignore Ctrl-C because our action should depend on parent's
    * action. If the parent decides to exit it will close the pipe and
    * then we'll exit as well. If it doesn't exit, we are not supposed
    * to exit either. So we need to ignore this signal.
    */

   signal (SIGINT, SIG_IGN);

   /* But we need to handle these. */

   sigemptyset (&sigact.sa_mask);
   sigact.sa_flags = SA_RESTART;	/* Not that it matters. */

   sigact.sa_handler = handle_sigterm;
   sigaction (SIGTERM, &sigact, NULL);

   sigact.sa_handler = handle_sigstop;
   sigaction (SIGTSTP, &sigact, NULL);

   sigact.sa_handler = handle_sigcont;
   sigaction (SIGCONT, &sigact, NULL);

   sigact.sa_handler = handle_sigwinch;
   sigaction (SIGWINCH, &sigact, NULL);

   /* Set up poll struct. */

   p[0].fd = ConnectionNumber (d);
   p[1].fd = fifo_fd;
   p[0].events = p[1].events = POLLIN;

   while (1)
   {
      int status;

      status = poll (p, 2, -1);

      if (status < 0)
      {
	 /* A signal was caught, most likely. */

	 if (got_sigterm)
	    break;
	 if (got_sigstop)
	    suspend_me ();
	 if (got_sigcont)
	    restart_me ();
      }
      else
      {
	 if ((p[0].revents | p[1].revents) & (POLLERR | POLLHUP | POLLNVAL))
	    break;
	 if (p[0].revents)
	    process_xevent ();
	 if (p[1].revents)
	    if (process_command ())
	       break;
      }
   }

   cleanup ();

   return 0;
}
