/*
 * xsel -- manipulate the X selection
 * Copyright (C) 2001 Conrad Parker <conrad@vergenet.net>
 *
 * 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
 * implied warranty.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <sys/time.h>
#include <setjmp.h>
#include <signal.h>
#include <X11/StringDefs.h>
#include <X11/Xlib.h>
#include <X11/Intrinsic.h>
#include <X11/Xatom.h>

#include "xsel.h"


/* The name we were invoked as (argv[0]) */
static char * progname;

/* Verbosity level for debugging */
static int debug_level = DEBUG_LEVEL;

/* Our X Display and Window */
static Display * display;
static Window window;

/* Maxmimum request size supported by this X server */
static long max_req;

/* Our timestamp for all operations */
static Time timestamp;

static Atom timestamp_atom; /* The TIMESTAMP atom */
static Atom multiple_atom; /* The MULTIPLE atom */
static Atom targets_atom; /* The TARGETS atom */
static Atom delete_atom; /* The DELETE atom */
static Atom incr_atom; /* The INCR atom */
static Atom null_atom; /* The NULL atom */
static Atom text_atom; /* The TEXT atom */
static Atom utf8_atom; /* The UTF8 atom */
static Atom compound_text_atom; /* The COMPOUND_TEXT atom */

/* Number of selection targets served by this.
 * (MULTIPLE, INCR, TARGETS, TIMESTAMP, DELETE, TEXT, UTF8_STRING and STRING)
 * NB. We do not currently serve COMPOUND_TEXT; we can retrieve it but do not
 * perform charset conversion.
 */
#define MAX_NUM_TARGETS 8
static int NUM_TARGETS;
static Atom supported_targets[MAX_NUM_TARGETS];

/* do_follow: Follow mode for output */
static Boolean do_follow = False;

/* nodaemon: Disable daemon mode if True. */
static Boolean no_daemon = False;

/* logfile: name of file to log error messages to when detached */
static char logfile[MAXFNAME];

/* fstat() on stdin and stdout */
static struct stat in_statbuf, out_statbuf;

static int total_input = 0;
static int current_alloc = 0;

static long timeout = 0;
static struct itimerval timer;

static int saved_argc;
static char ** saved_argv;

/*
 * usage ()
 *
 * print usage information.
 */
static void
usage (void)
{
  printf ("Usage: xsel [options]\n");
  printf ("Manipulate the X selection.\n\n");
  printf ("By default the current selection is output and not modified if both\n");
  printf ("standard input and standard output are terminals (ttys).  Otherwise,\n");
  printf ("the current selection is output if standard output is not a terminal\n");
  printf ("(tty), and the selection is set from standard input if standard input\n");
  printf ("is not a terminal (tty). If any input or output options are given then\n");
  printf ("the program behaves only in the requested mode.\n\n");
  printf ("If both input and output is required then the previous selection is\n");
  printf ("output before being replaced by the contents of standard input.\n\n");
  printf ("Input options\n");
  printf ("  -a, --append          Append standard input to the selection\n");
  printf ("  -f, --follow          Append to selection as standard input grows\n");
  printf ("  -i, --input           Read standard input into the selection\n\n");
  printf ("Output options\n");
  printf ("  -o, --output          Write the selection to standard output\n\n");
  printf ("Action options\n");
  printf ("  -c, --clear           Clear the selection\n");
  printf ("  -d, --delete          Request that the selection be cleared and that\n");
  printf ("                        the application owning it delete its contents\n\n");
  printf ("Selection options\n");
  printf ("  -p, --primary         Operate on the PRIMARY selection (default)\n");
  printf ("  -s, --secondary       Operate on the SECONDARY selection\n");
  printf ("  -b, --clipboard       Operate on the CLIPBOARD selection\n\n");
  printf ("  -k, --keep            Do not modify the selections, but make the PRIMARY\n");
  printf ("                        and SECONDARY selections persist even after the\n");
  printf ("                        programs they were selected in exit.\n");
  printf ("  -x, --exchange        Exchange the PRIMARY and SECONDARY selections\n\n");
  printf ("X options\n");
  printf ("  --display displayname\n");
  printf ("                        Specify the connection to the X server\n");
  printf ("  -t ms, --selectionTimeout ms\n");
  printf ("                        Specify the timeout in milliseconds within which the\n");
  printf ("                        selection must be retrieved. A value of 0 (zero)\n");
  printf ("                        specifies no timeout (default)\n\n");
  printf ("Miscellaneous options\n");
  printf ("  -l, --logfile         Specify file to log errors to when detached.\n");
  printf ("  -n, --nodetach        Do not detach from the controlling terminal. Without\n");
  printf ("                        this option, xsel will fork to become a background\n");
  printf ("                        process in input, exchange and keep modes.\n\n");
  printf ("  -h, --help            Display this help and exit\n");
  printf ("  -v, --verbose         Print informative messages\n");
  printf ("  --version             Output version information and exit\n\n");
  printf ("Please report bugs to <conrad@vergenet.net>.\n");
}

/*
 * exit_err (fmt)
 *
 * Print a formatted error message and errno information to stderr,
 * then exit with return code 1.
 */
static void
exit_err (const char * fmt, ...)
{
  va_list ap;
  int errno_save;
  char buf[MAXLINE];
  int n;

  errno_save = errno;

  va_start (ap, fmt);

  snprintf (buf, MAXLINE, "%s: ", progname);
  n = strlen (buf);

  vsnprintf (buf+n, MAXLINE-n, fmt, ap);
  n = strlen (buf);

  snprintf (buf+n, MAXLINE-n, ": %s\n", strerror (errno_save));

  fflush (stdout); /* in case stdout and stderr are the same */
  fputs (buf, stderr);
  fflush (NULL);

  va_end (ap);
  exit (1);
}

/*
 * print_err (fmt)
 *
 * Print a formatted error message to stderr.
 */
static void
print_err (const char * fmt, ...)
{
  va_list ap;
  int errno_save;
  char buf[MAXLINE];
  int n;

  errno_save = errno;

  va_start (ap, fmt);

  snprintf (buf, MAXLINE, "%s: ", progname);
  n = strlen (buf);

  vsnprintf (buf+n, MAXLINE-n, fmt, ap);
  n = strlen (buf);

  fflush (stdout); /* in case stdout and stderr are the same */
  fputs (buf, stderr);
  fputc ('\n', stderr);
  fflush (NULL);

  va_end (ap);
}

/*
 * print_debug (level, fmt)
 *
 * Print a formatted debugging message of level 'level' to stderr
 */
#define print_debug(x,y...) {if (x <= debug_level) print_err (y);}

/*
 * get_atom_name (atom)
 *
 * Returns a string with a printable name for the Atom 'atom'.
 */
static char *
get_atom_name (Atom atom)
{
  if (atom == None) return "None";
  if (atom == XA_STRING) return "STRING";
  if (atom == XA_PRIMARY) return "PRIMARY";
  if (atom == XA_SECONDARY) return "SECONDARY";
  if (atom == timestamp_atom) return "TIMESTAMP";
  if (atom == multiple_atom) return "MULTIPLE";
  if (atom == targets_atom) return "TARGETS";
  if (atom == delete_atom) return "DELETE";
  if (atom == incr_atom) return "INCR";
  if (atom == null_atom) return "NULL";
  if (atom == text_atom) return "TEXT";
  if (utf8_atom!=XA_STRING && atom == utf8_atom) return "UTF8_STRING";
  if (atom == XInternAtom (display, "XSEL_DATA", True)) return "XSEL_DATA";

  return "<unknown atom>";
}

/*
 * debug_property (level, requestor, property, target, length)
 *
 * Print debugging information (at level 'level') about a property received.
 */
static void
debug_property (int level, Window requestor, Atom property, Atom target,
                int length)
{
  print_debug (level, "Got window property: requestor 0x%x, property 0x%x, target 0x%x %s, length %d bytes", requestor, property, target, get_atom_name (target), length);
}

/*
 * xs_malloc (size)
 *
 * Malloc wrapper. Always returns a successful allocation. Exits if the
 * allocation didn't succeed.
 */
static void *
xs_malloc (size_t size)
{
  void * ret;

  if ((ret = malloc (size)) == NULL) {
    exit_err ("malloc error");
  }

  return ret;
}

/*
 * xs_strdup (s)
 *
 * strdup wrapper for unsigned char *
 */
#define xs_strdup(s) ((unsigned char *) strdup ((const char *)s))

/*
 * xs_strlen (s)
 *
 * strlen wrapper for unsigned char *
 */
#define xs_strlen(s) (strlen ((const char *) s))

/*
 * xs_strncpy (s)
 *
 * strncpy wrapper for unsigned char *
 */
#define xs_strncpy(dest,src,n) (strncpy ((char *)dest, (const char *)src, n))

/*
 * get_homedir ()
 *
 * Get the user's home directory.
 */
static char *
get_homedir (void)
{
  uid_t uid;
  char * username, * homedir;
  struct passwd * pw;

  if ((homedir = getenv ("HOME")) != NULL) {
    return homedir;
  }

  /* else ... go hunting for it */
  uid = getuid ();

  username = getenv ("LOGNAME");
  if (!username) username = getenv ("USER");

  if (username) {
    pw = getpwnam (username);
    if (pw && pw->pw_uid == uid) goto gotpw;
  }

  pw = getpwuid (uid);

gotpw:

  if (!pw) {
    exit_err ("error retrieving passwd entry");
  }

  homedir = strdup (pw->pw_dir);

  return homedir;
}

/*
 * become_daemon ()
 *
 * Perform the required procedure to become a daemon process, as
 * outlined in the Unix programming FAQ:
 * http://www.steve.org.uk/Reference/Unix/faq_2.html#SEC16
 * and open a logfile.
 */
static void
become_daemon (void)
{
  pid_t pid;
  int null_r_fd, null_w_fd, log_fd;
  char * homedir;

  if (no_daemon) return;

  homedir = get_homedir ();

  /* Check that we can open a logfile before continuing */

  /* If the user has specified a --logfile, use that ... */
  if (logfile[0] == '\0') {
    /* ... otherwise use the default logfile */
    snprintf (logfile, MAXFNAME, "%s/.xsel.log", homedir);
  }

  /* Make sure to create the logfile with sane permissions */
  log_fd = open (logfile, O_WRONLY|O_APPEND|O_CREAT, 0600);
  if (log_fd == -1) {
    exit_err ("error opening logfile %s for writing", logfile);
  }
  print_debug (D_INFO, "opened logfile %s", logfile);

  if ((pid = fork()) == -1) {
    exit_err ("error forking");
  } else if (pid > 0) {
    _exit (0);
  }

  if (setsid () == -1) {
    exit_err ("setsid error");
  }

  if ((pid = fork()) == -1) {
    exit_err ("error forking");
  } else if (pid > 0) {
    _exit (0);
  }

  umask (0);

  if (chdir (homedir) == -1) {
    print_debug (D_WARN, "Could not chdir to %s\n", homedir);
    if (chdir ("/") == -1) {
      exit_err ("Error chdir to /");
    }
  }

  /* dup2 /dev/null on stdin unless following input */
  if (!do_follow) {
    null_r_fd = open ("/dev/null", O_RDONLY);
    if (null_r_fd == -1) {
      exit_err ("error opening /dev/null for reading");
    }
    if (dup2 (null_r_fd, 0) == -1) {
      exit_err ("error duplicating /dev/null on stdin");
    }
  }

  /* dup2 /dev/null on stdout */
  null_w_fd = open ("/dev/null", O_WRONLY|O_APPEND);
  if (null_w_fd == -1) {
    exit_err ("error opening /dev/null for writing");
  }
  if (dup2 (null_w_fd, 1) == -1) {
    exit_err ("error duplicating /dev/null on stdout");
  }

  /* dup2 logfile on stderr */
  if (dup2 (log_fd, 2) == -1) {
    exit_err ("error duplicating logfile %s on stderr", logfile);
  }
}

/*
 * get_timestamp ()
 *
 * Get the current X server time.
 *
 * This is done by doing a zero-length append to a random property of the
 * window, and checking the time on the subsequent PropertyNotify event.
 *
 * PRECONDITION: the window must have PropertyChangeMask set.
 */
static Time
get_timestamp (void)
{
  XEvent event;

  XChangeProperty (display, window, XA_WM_NAME, XA_STRING, 8,
                   PropModeAppend, NULL, 0);

  while (1) {
    XNextEvent (display, &event);

    if (event.type == PropertyNotify)
      return event.xproperty.time;
  }
}

/*
 * SELECTION RETRIEVAL
 * ===================
 *
 * The following functions implement retrieval of an X selection,
 * optionally within a user-specified timeout.
 *
 *
 * Selection timeout handling.
 * ---------------------------
 *
 * The selection retrieval can time out if no response is received within
 * a user-specified time limit. In order to ensure we time the entire
 * selection retrieval, we use an interval timer and catch SIGVTALRM.
 * [Calling select() on the XConnectionNumber would only provide a timeout
 * to the first XEvent.]
 */

/* The jmp_buf to longjmp out of the signal handler */
static sigjmp_buf env_alrm;

/*
 * alarm_handler (sig)
 *
 * Signal handler for catching SIGVTALRM.
 */
static void
alarm_handler (int sig)
{
  siglongjmp (env_alrm, 1);
}

/*
 * get_append_property ()
 *
 * Get a window property and append its data to a buffer at a given offset
 * pointed to by *offset. 'offset' is modified by this routine to point to
 * the end of the data.
 *
 * Returns True if more data is available for receipt.
 *
 * If an error is encountered, the buffer is free'd.
 */
static Boolean
get_append_property (XSelectionEvent * xsl, unsigned char ** buffer,
                     int * offset, int * alloc)
{
  unsigned char * ptr;
  Atom target;
  int format;
  unsigned long bytesafter, length;
  unsigned char * value;

  XGetWindowProperty (xsl->display, xsl->requestor, xsl->property,
                      0L, 1000000, True, (Atom)AnyPropertyType,
                      &target, &format, &length, &bytesafter, &value);

  debug_property (D_TRACE, xsl->requestor, xsl->property, target, length);

  if (target != XA_STRING) {
    print_debug (D_OBSC, "target %s not XA_STRING in get_append_property()",
                 get_atom_name (target));
    free (*buffer);
    *buffer = NULL;
    return False;
  } else if (length == 0) {
    /* A length of 0 indicates the end of the transfer */
    print_debug (D_TRACE, "Got zero length property; end of INCR transfer");
    return False;
  } else if (format == 8) {
    if ((unsigned long)*offset + length > (unsigned long)*alloc) {
      *alloc = *offset + length;
      if ((*buffer = realloc (*buffer, *alloc)) == NULL) {
        exit_err ("realloc error");
      }
    }
    ptr = *buffer + *offset;
    xs_strncpy (ptr, value, length);
    *offset += length;
    print_debug (D_TRACE, "Appended %d bytes to buffer\n", length);
  } else {
    print_debug (D_WARN, "Retrieved non-8-bit data\n");
  }

  return True;
}


/*
 * wait_incr_selection (selection)
 *
 * Retrieve a property of target type INCR. Perform incremental retrieval
 * and return the resulting data.
 */
static unsigned char *
wait_incr_selection (Atom selection, XSelectionEvent * xsl, int init_alloc)
{
  XEvent event;
  unsigned char * incr_base = NULL, * incr_ptr = NULL;
  int incr_alloc = 0, incr_xfer = 0;
  Boolean wait_prop = True;

  print_debug (D_TRACE, "Initialising incremental retrieval of at least %d bytes\n", init_alloc);

  /* Take an interest in the requestor */
  XSelectInput (xsl->display, xsl->requestor, PropertyChangeMask);

  incr_alloc = init_alloc;
  incr_base = xs_malloc (incr_alloc);
  incr_ptr = incr_base;

  print_debug (D_TRACE, "Deleting property that informed of INCR transfer");
  XDeleteProperty (xsl->display, xsl->requestor, xsl->property);

  print_debug (D_TRACE, "Waiting on PropertyNotify events");
  while (wait_prop) {
    XNextEvent (xsl->display, &event);

    switch (event.type) {
    case PropertyNotify:
      if (event.xproperty.state != PropertyNewValue) break;

      wait_prop = get_append_property (xsl, &incr_base, &incr_xfer,
                                       &incr_alloc);
      break;
    default:
      break;
    }
  }

  /* when zero length found, finish up & delete last */
  XDeleteProperty (xsl->display, xsl->requestor, xsl->property);

  print_debug (D_TRACE, "Finished INCR retrieval");

  return incr_base;
}

/*
 * wait_selection (selection, request_target)
 *
 * Block until we receive a SelectionNotify event, and return its
 * contents; or NULL in the case of a deletion or error. This assumes we
 * have already called XConvertSelection, requesting a string (explicitly
 * XA_STRING) or deletion (delete_atom).
 */
static unsigned char *
wait_selection (Atom selection, Atom request_target)
{
  XEvent event;
  Atom target;
  int format;
  unsigned long bytesafter, length;
  unsigned char * value, * retval = NULL;
  Boolean keep_waiting = True;

  while (keep_waiting) {
    XNextEvent (display, &event);

    switch (event.type) {
    case SelectionNotify:
      if (event.xselection.selection != selection) break;

      if (event.xselection.property == None) {
        print_debug (D_WARN, "Conversion refused");
        value = NULL;
        keep_waiting = False;
      } else if (event.xselection.property == null_atom &&
                 request_target == delete_atom) {
      } else {
	XGetWindowProperty (event.xselection.display,
			    event.xselection.requestor,
			    event.xselection.property, 0L, 1000000,
			    False, (Atom)AnyPropertyType, &target,
			    &format, &length, &bytesafter, &value);

        debug_property (D_TRACE, event.xselection.requestor,
                        event.xselection.property, target, length);

        if (request_target == delete_atom && value == NULL) {
          keep_waiting = False;
        } else if (target == incr_atom) {
          /* Handle INCR transfers */
          retval = wait_incr_selection (selection, &event.xselection,
                                        *(int *)value);
          keep_waiting = False;
        } else if (target != utf8_atom && target != XA_STRING &&
                   target != compound_text_atom &&
                   request_target != delete_atom) {
          /* Report non-TEXT atoms */
          print_debug (D_WARN, "Selection (type %s) is not a string.",
                       get_atom_name (target));
          free (retval);
          retval = NULL;
          keep_waiting = False;
        } else {
          retval = xs_strdup (value);
          XFree (value);
          keep_waiting = False;
        }

        XDeleteProperty (event.xselection.display,
                         event.xselection.requestor,
                         event.xselection.property);

      }
      break;
    default:
      break;
    }
  }

  /* Now that we've received the SelectionNotify event, clear any
   * remaining timeout. */
  if (timeout > 0) {
    setitimer (ITIMER_VIRTUAL, (struct itimerval *)0, (struct itimerval *)0);
  }

  return retval;
}

/*
 * get_selection (selection, request_target)
 *
 * Retrieves the specified selection and returns its value.
 *
 * If a non-zero timeout is specified then set a virtual interval
 * timer. Return NULL and print an error message if the timeout
 * expires before the selection has been retrieved.
 */
static unsigned char *
get_selection (Atom selection, Atom request_target)
{
  Atom prop;
  unsigned char * retval;

  prop = XInternAtom (display, "XSEL_DATA", False);
  XConvertSelection (display, selection, request_target, prop, window,
                     timestamp);
  XSync (display, False);

  if (timeout > 0) {
    if (signal (SIGVTALRM, alarm_handler) == SIG_ERR) {
      exit_err ("error setting timeout handler");
    }

    timer.it_interval.tv_sec = 0;
    timer.it_interval.tv_usec = timeout;
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = timeout;

    if (sigsetjmp (env_alrm, 0) == 0) {
      setitimer (ITIMER_VIRTUAL, &timer, (struct itimerval *)0);
      retval = wait_selection (selection, request_target);
    } else {
      print_debug (D_WARN, "selection timed out");
      retval = NULL;
    }
  } else {
    retval = wait_selection (selection, request_target);
  }

  return retval;
}

/*
 * get_selection_text (Atom selection)
 *
 * Retrieve a text selection. First attempt to retrieve it as UTF_STRING,
 * and if that fails attempt to retrieve it as a plain XA_STRING.
 *
 * NB. Before implementing this, an attempt was made to query TARGETS and
 * request UTF8_STRING only if listed there, as described in:
 * http://www.pps.jussieu.fr/~jch/software/UTF8_STRING/UTF8_STRING.text
 * However, that did not seem to work reliably when tested against various
 * applications (eg. Mozilla Firefox). This method is of course more
 * reliable.
 */
static unsigned char *
get_selection_text (Atom selection)
{
  unsigned char * retval;

  if ((retval = get_selection (selection, utf8_atom)) == NULL)
    retval = get_selection (selection, XA_STRING);

  return retval;
}


/*
 * SELECTION SETTING
 * =================
 *
 * The following functions allow a given selection to be set, appended to
 * or cleared, or to exchange the primary and secondary selections.
 */

/*
 * copy_sel (s)
 *
 * Copy a string into a new selection buffer, and intitialise
 * current_alloc and total_input to exactly its length.
 */
static unsigned char *
copy_sel (unsigned char * s)
{
  unsigned char * new_sel = NULL;

  new_sel = xs_strdup (s);
  current_alloc = total_input = xs_strlen (s);

  return new_sel;
}

/*
 * read_input (read_buffer, do_select)
 *
 * Read input from stdin into the specified read_buffer.
 *
 * read_buffer must have been dynamically allocated before calling this
 * function, or be NULL. Input is read until end-of-file is reached, and
 * read_buffer will be reallocated to accomodate the entire contents of
 * the input. read_buffer, which may have been reallocated, is returned
 * upon completion.
 *
 * If 'do_select' is True, this function will first check if any data
 * is available for reading, and return immediately if not.
 */
static unsigned char *
read_input (unsigned char * read_buffer, Boolean do_select)
{
  int insize = in_statbuf.st_blksize;
  unsigned char * new_buffer = NULL;
  int d, fatal = 0, nfd;
  ssize_t n;
  fd_set fds;
  struct timeval select_timeout;

  if (do_select) {
try_read:
    /* Check if data is available for reading -- if not, return immediately */
    FD_ZERO (&fds);
    FD_SET (0, &fds);

    select_timeout.tv_sec = (time_t)0;
    select_timeout.tv_usec = (time_t)0;

    nfd = select (1, &fds, NULL, NULL, &select_timeout);
    if (nfd == -1) {
      if (errno == EINTR) goto try_read;
      else exit_err ("select error");
    } else if (nfd == 0) {
      print_debug (D_TRACE, "No data available for reading");
      return read_buffer;
    }
  }

  do {
    /* check if buffer is full */
    if (current_alloc == total_input) {
      if ((d = (current_alloc % insize)) != 0) current_alloc += (insize-d);
      current_alloc *= 2;
      new_buffer = realloc (read_buffer, current_alloc);
      if (new_buffer == NULL) {
        exit_err ("realloc error");
      }
      read_buffer = new_buffer;
    }

    /* read the remaining data, up to the optimal block length */
    n = read (0, &read_buffer[total_input],
              MIN(current_alloc - total_input, insize));
    if (n == -1) {
      switch (errno) {
      case EAGAIN:
      case EINTR:
        break;
      default:
        perror ("read error");
        fatal = 1;
        break;
      }
    }
    total_input += n;
  } while (n != 0 && !fatal);

  read_buffer[total_input] = '\0';

  print_debug (D_TRACE, "Accumulated %d bytes input", total_input);

  return read_buffer;
}

/*
 * initialise_read (read_buffer)
 *
 * Initialises the read_buffer and the state variable current_alloc.
 * read_buffer is reallocated to accomodate either the entire input
 * if stdin is a regular file, or at least one block of input otherwise.
 * If the supplied read_buffer is NULL, a new buffer will be allocated.
 */
static unsigned char *
initialise_read (unsigned char * read_buffer)
{
  int insize = in_statbuf.st_blksize;
  unsigned char * new_buffer = NULL;

  if (S_ISREG (in_statbuf.st_mode)) {
    current_alloc += in_statbuf.st_size;
  } else {
    current_alloc += insize;
  }

  if ((new_buffer = realloc (read_buffer, current_alloc)) == NULL) {
    exit_err ("realloc error");
  }

  read_buffer = new_buffer;

  return read_buffer;
}

/* Forward declaration of refuse_all_incr () */
static void
refuse_all_incr (void);

/*
 * handle_x_errors ()
 *
 * XError handler.
 */
static int
handle_x_errors (Display * display, XErrorEvent * eev)
{
  char err_buf[MAXLINE];

  /* Make sure to send a refusal to all waiting INCR requests
   * and delete the corresponding properties. */
  if (eev->error_code == BadAlloc) refuse_all_incr ();

  XGetErrorText (display, eev->error_code, err_buf, MAXLINE);
  exit_err (err_buf);

  return 0;
}

/*
 * clear_selection (selection)
 *
 * Clears the specified X selection 'selection'. This requests that no
 * process should own 'selection'; thus the X server will respond to
 * SelectionRequests with an empty property and we don't need to leave
 * a daemon hanging around to service this selection.
 */
static void
clear_selection (Atom selection)
{
  XSetSelectionOwner (display, selection, None, timestamp);
  /* Call XSync to ensure this operation completes before program
   * termination, especially if this is all we are doing. */
  XSync (display, False);
}

/*
 * own_selection (selection)
 *
 * Requests ownership of the X selection. Returns True if ownership was
 * granted, and False otherwise.
 */
static Boolean
own_selection (Atom selection)
{
  Window owner;

  XSetSelectionOwner (display, selection, window, timestamp);
  /* XGetSelectionOwner does a round trip to the X server, so there is
   * no need to call XSync here. */
  owner = XGetSelectionOwner (display, selection);
  if (owner != window) {
    return False;
  } else {
    XSetErrorHandler (handle_x_errors);
    return True;
  }
}


static IncrTrack * incrtrack_list = NULL;

/*
 * add_incrtrack (it)
 *
 * Add 'it' to the head of incrtrack_list.
 */
static void
add_incrtrack (IncrTrack * it)
{
  if (incrtrack_list) {
    incrtrack_list->prev = it;
  }
  it->prev = NULL;
  it->next = incrtrack_list;
  incrtrack_list = it;
}

/*
 * remove_incrtrack (it)
 *
 * Remove 'it' from incrtrack_list.
 */
static void
remove_incrtrack (IncrTrack * it)
{
  if (it->prev) {
    it->prev->next = it->next;
  }
  if (it->next) {
    it->next->prev = it->prev;
  }

  if (incrtrack_list == it) {
    incrtrack_list = it->next;
  }
}

/*
 * fresh_incrtrack ()
 *
 * Create a new incrtrack, and add it to incrtrack_list.
 */
static IncrTrack *
fresh_incrtrack (void)
{
  IncrTrack * it;

  it = xs_malloc (sizeof (IncrTrack));
  add_incrtrack (it);

  return it;
}

/*
 * trash_incrtrack (it)
 *
 * Remove 'it' from incrtrack_list, and free it.
 */
static void
trash_incrtrack (IncrTrack * it)
{
  remove_incrtrack (it);
  free (it);
}

/*
 * find_incrtrack (atom)
 *
 * Find the IncrTrack structure within incrtrack_list pertaining to 'atom',
 * if it exists.
 */
static IncrTrack *
find_incrtrack (Atom atom)
{
  IncrTrack * iti;

  for (iti = incrtrack_list; iti; iti = iti->next) {
    if (atom == iti->property) return iti;
  }

  return NULL;
}

/* Forward declaration of handle_multiple() */
static HandleResult
handle_multiple (Display * display, Window requestor, Atom property,
                 unsigned char * sel, Atom selection, Time time,
                 MultTrack * mparent);

/* Forward declaration of process_multiple() */
static HandleResult
process_multiple (MultTrack * mt, Boolean do_parent);

/*
 * confirm_incr (it)
 *
 * Confirm the selection request of ITER tracked by 'it'.
 */
static void
notify_incr (IncrTrack * it, HandleResult hr)
{
  XSelectionEvent ev;

  /* Call XSync here to make sure any BadAlloc errors are caught before
   * confirming the conversion. */
  XSync (it->display, False);

  print_debug (D_TRACE, "Confirming conversion");

  /* Prepare a SelectionNotify event to send, placing the selection in the
   * requested property. */
  ev.type = SelectionNotify;
  ev.display = it->display;
  ev.requestor = it->requestor;
  ev.selection = it->selection;
  ev.time = it->time;
  ev.target = it->target;

  if (hr & HANDLE_ERR) ev.property = None;
  else ev.property = it->property;

  XSendEvent (display, ev.requestor, False,
              (unsigned long)NULL, (XEvent *)&ev);
}

/*
 * refuse_all_incr ()
 *
 * Refuse all INCR transfers in progress. ASSUMES that this is called in
 * response to an error, and that the program is about to bail out;
 * ie. incr_track is not cleaned out.
 */
static void
refuse_all_incr (void)
{
  IncrTrack * it;

  for (it = incrtrack_list; it; it = it->next) {
    XDeleteProperty (it->display, it->requestor, it->property);
    notify_incr (it, HANDLE_ERR);
    /* Don't bother trashing and list-removing these; we are about to
     * bail out anyway. */
  }
}

/*
 * complete_incr (it)
 *
 * Finish off an INCR retrieval. If it was part of a multiple, continue
 * that; otherwise, send confirmation that this completed.
 */
static void
complete_incr (IncrTrack * it, HandleResult hr)
{
  MultTrack * mparent = it->mparent;

  if (mparent) {
    trash_incrtrack (it);
    process_multiple (mparent, True);
  } else {
    notify_incr (it, hr);
    trash_incrtrack (it);
  }
}

/*
 * notify_multiple (mt, hr)
 *
 * Confirm the selection request initiated with MULTIPLE tracked by 'mt'.
 */
static void
notify_multiple (MultTrack * mt, HandleResult hr)
{
  XSelectionEvent ev;

  /* Call XSync here to make sure any BadAlloc errors are caught before
   * confirming the conversion. */
  XSync (mt->display, False);

  /* Prepare a SelectionNotify event to send, placing the selection in the
   * requested property. */
  ev.type = SelectionNotify;
  ev.display = mt->display;
  ev.requestor = mt->requestor;
  ev.selection = mt->selection;
  ev.time = mt->time;
  ev.target = multiple_atom;

  if (hr & HANDLE_ERR) ev.property = None;
  else ev.property = mt->property;

  XSendEvent (display, ev.requestor, False,
              (unsigned long)NULL, (XEvent *)&ev);
}

/*
 * complete_multiple (mt, do_parent, hr)
 *
 * Complete a MULTIPLE transfer. Iterate to its parent MULTIPLE if
 * 'do_parent' is true. If there is not parent MULTIPLE, send notification
 * of its completion with status 'hr'.
 */
static void
complete_multiple (MultTrack * mt, Boolean do_parent, HandleResult hr)
{
  MultTrack * mparent = mt->mparent;

  if (mparent) {
    free (mt);
    if (do_parent) process_multiple (mparent, True);
  } else {
    notify_multiple (mt, hr);
    free (mt);
  }
}

/*
 * change_property (display, requestor, property, target, format, mode,
 *                  data, nelements)
 *
 * Wrapper to XChangeProperty that performs INCR transfer if required and
 * returns status of entire transfer.
 */
static HandleResult
change_property (Display * display, Window requestor, Atom property,
                 Atom target, int format, int mode,
                 unsigned char * data, int nelements,
                 Atom selection, Time time, MultTrack * mparent)
{
  XSelectionEvent ev;
  int nr_bytes;
  IncrTrack * it;

  print_debug (D_TRACE, "change_property ()");

  nr_bytes = nelements * format / 8;

  if (nr_bytes <= max_req) {
    print_debug (D_TRACE, "data within maximum request size");
    XChangeProperty (display, requestor, property, target, format, mode,
                     data, nelements);

    return HANDLE_OK;
  }

  /* else */
  print_debug (D_TRACE, "large data transfer");


  /* Send a SelectionNotify event of type INCR */
  ev.type = SelectionNotify;
  ev.display = display;
  ev.requestor = requestor;
  ev.selection = selection;
  ev.time = time;
  ev.target = incr_atom; /* INCR */
  ev.property = property;

  XSelectInput (ev.display, ev.requestor, PropertyChangeMask);

  XChangeProperty (ev.display, ev.requestor, ev.property, incr_atom, 32,
                   PropModeReplace, (unsigned char *)&nr_bytes, 1);

  XSendEvent (display, requestor, False,
              (unsigned long)NULL, (XEvent *)&ev);

  /* Set up the IncrTrack to track this */
  it = fresh_incrtrack ();

  it->mparent = mparent;
  it->state = S_INCR_1;
  it->display = display;
  it->requestor = requestor;
  it->property = property;
  it->selection = selection;
  it->time = time;
  it->target = target;
  it->format = format;
  it->data = data;
  it->nelements = nelements;
  it->offset = 0;

  /* Maximum nr. of elements that can be transferred in one go */
  it->max_elements = max_req * 8 / format;

  /* Nr. of elements to transfer in this instance */
  it->chunk = MIN (it->max_elements, it->nelements - it->offset);

  /* Wait for that property to get deleted */
  print_debug (D_TRACE, "Waiting on intial property deletion (%s)",
               get_atom_name (it->property));

  return HANDLE_INCOMPLETE;
}

static HandleResult
incr_stage_1 (IncrTrack * it)
{
  /* First pass: PropModeReplace, from data, size chunk */
  print_debug (D_TRACE, "Writing first chunk (%d bytes) (target 0x%x %s) to property 0x%x of requestor 0x%x", it->chunk, it->target, get_atom_name(it->target), it->property, it->requestor);
  XChangeProperty (it->display, it->requestor, it->property, it->target,
                   it->format, PropModeReplace, it->data, it->chunk);

  it->offset += it->chunk;

  /* wait for PropertyNotify events */
  print_debug (D_TRACE, "Waiting on subsequent deletions ...");

  it->state = S_INCR_2;

  return HANDLE_INCOMPLETE;
}

static HandleResult
incr_stage_2 (IncrTrack * it)
{
  it->chunk = MIN (it->max_elements, it->nelements - it->offset);

  if (it->chunk <= 0) {

    /* Now write zero-length data to the property */
    XChangeProperty (it->display, it->requestor, it->property, it->target,
                     it->format, PropModeAppend, NULL, 0);
    it->state = S_NULL;
    print_debug (D_TRACE, "Set si to state S_NULL");
    return HANDLE_OK;
  } else {
    print_debug (D_TRACE, "Writing chunk (%d bytes) to property",
                 it->chunk);
    XChangeProperty (it->display, it->requestor, it->property, it->target,
                     it->format, PropModeAppend, it->data+it->offset,
                     it->chunk);
    it->offset += it->chunk;
    print_debug (D_TRACE, "%d bytes remaining",
                 it->nelements - it->offset);
    return HANDLE_INCOMPLETE;
  }
}


/*
 * handle_timestamp (display, requestor, property)
 *
 * Handle a TIMESTAMP request.
 */
static HandleResult
handle_timestamp (Display * display, Window requestor, Atom property,
                  Atom selection, Time time, MultTrack * mparent)
{
  return
    change_property (display, requestor, property, XA_INTEGER, 32,
                     PropModeReplace, (unsigned char *)&timestamp, 1,
                     selection, time, mparent);
}

/*
 * handle_targets (display, requestor, property)
 *
 * Handle a TARGETS request.
 */
static HandleResult
handle_targets (Display * display, Window requestor, Atom property,
                Atom selection, Time time, MultTrack * mparent)
{
  Atom * targets_cpy;

  targets_cpy = malloc (sizeof (supported_targets));
  memcpy (targets_cpy, supported_targets, sizeof (supported_targets));

  return
    change_property (display, requestor, property, XA_ATOM, 32,
                     PropModeReplace, (unsigned char *)targets_cpy,
                     NUM_TARGETS, selection, time, mparent);
}

/*
 * handle_string (display, requestor, property, sel)
 *
 * Handle a STRING request; setting 'sel' as the data
 */
static HandleResult
handle_string (Display * display, Window requestor, Atom property,
               unsigned char * sel, Atom selection, Time time,
               MultTrack * mparent)
{
  return
    change_property (display, requestor, property, XA_STRING, 8,
                     PropModeReplace, sel, xs_strlen(sel),
                     selection, time, mparent);
}

/*
 * handle_utf8_string (display, requestor, property, sel)
 *
 * Handle a UTF8_STRING request; setting 'sel' as the data
 */
static HandleResult
handle_utf8_string (Display * display, Window requestor, Atom property,
                    unsigned char * sel, Atom selection, Time time,
                    MultTrack * mparent)
{
  return
    change_property (display, requestor, property, utf8_atom, 8,
                     PropModeReplace, sel, xs_strlen(sel),
                     selection, time, mparent);
}

/*
 * handle_delete (display, requestor, property)
 *
 * Handle a DELETE request.
 */
static HandleResult
handle_delete (Display * display, Window requestor, Atom property)
{
  XChangeProperty (display, requestor, property, null_atom, 0,
                   PropModeReplace, NULL, 0);

  return DID_DELETE;
}

/*
 * process_multiple (mt, do_parent)
 *
 * Iterate through a MultTrack until it completes, or until one of its
 * entries initiates an interated selection.
 *
 * If 'do_parent' is true, and the actions proscribed in 'mt' are
 * completed during the course of this call, then process_multiple
 * is iteratively called on mt->mparent.
 */
static HandleResult
process_multiple (MultTrack * mt, Boolean do_parent)
{
  HandleResult retval = HANDLE_OK;
  int i;

  if (!mt) return retval;

  for (; mt->index < mt->length; mt->index += 2) {
    i = mt->index;
    if (mt->atoms[i] == timestamp_atom) {
      retval |= handle_timestamp (mt->display, mt->requestor, mt->atoms[i+1],
                                  mt->selection, mt->time, mt);
    } else if (mt->atoms[i] == targets_atom) {
      retval |= handle_targets (mt->display, mt->requestor, mt->atoms[i+1],
                                mt->selection, mt->time, mt);
    } else if (mt->atoms[i] == multiple_atom) {
      retval |= handle_multiple (mt->display, mt->requestor, mt->atoms[i+1],
                                 mt->sel, mt->selection, mt->time, mt);
    } else if (mt->atoms[i] == XA_STRING || mt->atoms[i] == text_atom) {
      retval |= handle_string (mt->display, mt->requestor, mt->atoms[i+1],
                               mt->sel, mt->selection, mt->time, mt);
    } else if (mt->atoms[i] == utf8_atom) {
      retval |= handle_utf8_string (mt->display, mt->requestor, mt->atoms[i+1],
                                    mt->sel, mt->selection, mt->time, mt);
    } else if (mt->atoms[i] == delete_atom) {
      retval |= handle_delete (mt->display, mt->requestor, mt->atoms[i+1]);
    } else if (mt->atoms[i] == None) {
      /* the only other thing we know to handle is None, for which we
       * do nothing. This block is, like, __so__ redundant. Welcome to
       * Over-engineering 101 :) This comment is just here to keep the
       * logic documented and separate from the 'else' block. */
    } else {
      /* for anything we don't know how to handle, we fail the conversion
       * by setting this: */
      mt->atoms[i] = None;
    }

    /* If any of the conversions failed, signify this by setting that
     * atom to None ...*/
    if (retval & HANDLE_ERR) {
      mt->atoms[i] = None;
    }
    /* ... but don't propogate HANDLE_ERR */
    retval &= (~HANDLE_ERR);

    if (retval & HANDLE_INCOMPLETE) break;
  }

  if ((retval & HANDLE_INCOMPLETE) == 0) {
    complete_multiple (mt, do_parent, retval);
  }

  return retval;
}

/*
 * continue_incr (it)
 *
 * Continue an incremental transfer of IncrTrack * it.
 *
 * NB. If the incremental transfer was part of a multiple request, this
 * function calls process_multiple with do_parent=True because it is
 * assumed we are continuing an interrupted ITER, thus we must continue
 * the multiple as its original handler did not complete.
 */
static HandleResult
continue_incr (IncrTrack * it)
{
  HandleResult retval = HANDLE_OK;

  if (it->state == S_INCR_1) {
    retval = incr_stage_1 (it);
  } else if (it->state == S_INCR_2) {
    retval = incr_stage_2 (it);
  }

  /* If that completed the INCR, deal with completion */
  if ((retval & HANDLE_INCOMPLETE) == 0) {
    complete_incr (it, retval);
  }

  return retval;
}

/*
 * handle_multiple (display, requestor, property, sel, selection, time)
 *
 * Handle a MULTIPLE request; possibly setting 'sel' if any STRING
 * requests are processed within it. Return value has DID_DELETE bit set
 * if any delete requests are processed.
 *
 * NB. This calls process_multiple with do_parent=False because it is
 * assumed we are "handling" the multiple request on behalf of a
 * multiple already in progress, or (more likely) directly off a
 * SelectionRequest event.
 */
static HandleResult
handle_multiple (Display * display, Window requestor, Atom property,
                 unsigned char * sel, Atom selection, Time time,
                 MultTrack * mparent)
{
  MultTrack * mt;
  int format;
  unsigned long bytesafter;
  HandleResult retval = HANDLE_OK;

  mt = xs_malloc (sizeof (MultTrack));

  XGetWindowProperty (display, requestor, property, 0L, 1000000,
                      False, (Atom)AnyPropertyType, &mt->property,
                      &format, &mt->length, &bytesafter,
                      (unsigned char **)&mt->atoms);

  /* Make sure we got the Atom list we want */
  if (format != 32) return HANDLE_OK;


  mt->mparent = mparent;
  mt->display = display;
  mt->requestor = requestor;
  mt->sel = sel;
  mt->selection = selection;
  mt->time = time;
  mt->index = 0;

  retval = process_multiple (mt, False);

  return retval;
}

/*
 * handle_selection_request (event, sel)
 *
 * Processes a SelectionRequest event 'event' and replies to its
 * sender appropriately, eg. with the contents of the string 'sel'.
 * Returns False if a DELETE request is processed, indicating to
 * the calling function to delete the corresponding selection.
 * Returns True otherwise.
 */
static Boolean
handle_selection_request (XEvent event, unsigned char * sel)
{
  XSelectionRequestEvent * xsr = &event.xselectionrequest;
  XSelectionEvent ev;
  HandleResult hr = HANDLE_OK;
  Boolean retval = True;

  print_debug (D_TRACE, "handle_selection_request, property=0x%x (%s), target=0x%x (%s)",
               xsr->property, get_atom_name (xsr->property),
               xsr->target, get_atom_name (xsr->target));

  /* Prepare a SelectionNotify event to send, either as confirmation of
   * placing the selection in the requested property, or as notification
   * that this could not be performed. */
  ev.type = SelectionNotify;
  ev.display = xsr->display;
  ev.requestor = xsr->requestor;
  ev.selection = xsr->selection;
  ev.time = xsr->time;
  ev.target = xsr->target;

  if (xsr->property == None && ev.target != multiple_atom) {
      /* Obsolete requestor */
      xsr->property = xsr->target;
  }

  if (ev.time != CurrentTime && ev.time < timestamp) {
    /* If the time is outside the period we have owned the selection,
     * which is any time later than timestamp, or if the requested target
     * is not a string, then refuse the SelectionRequest. NB. Some broken
     * clients don't set a valid timestamp, so we have to check against
     * CurrentTime here. */
    ev.property = None;
  } else if (ev.target == timestamp_atom) {
    /* Return timestamp used to acquire ownership if target is TIMESTAMP */
    ev.property = xsr->property;
    hr = handle_timestamp (ev.display, ev.requestor, ev.property,
                           ev.selection, ev.time, NULL);
  } else if (ev.target == targets_atom) {
    /* Return a list of supported targets (TARGETS)*/
    ev.property = xsr->property;
    hr = handle_targets (ev.display, ev.requestor, ev.property,
                         ev.selection, ev.time, NULL);
  } else if (ev.target == multiple_atom) {
    if (xsr->property == None) { /* Invalid MULTIPLE request */
      ev.property = None;
    } else {
      /* Handle MULTIPLE request */
      hr = handle_multiple (ev.display, ev.requestor, ev.property, sel,
                            ev.selection, ev.time, NULL);
    }
  } else if (ev.target == XA_STRING || ev.target == text_atom) {
    /* Received STRING or TEXT request */
    ev.property = xsr->property;
    hr = handle_string (ev.display, ev.requestor, ev.property, sel,
                        ev.selection, ev.time, NULL);
  } else if (ev.target == utf8_atom) {
    /* Received UTF8_STRING request */
    ev.property = xsr->property;
    hr = handle_utf8_string (ev.display, ev.requestor, ev.property, sel,
                             ev.selection, ev.time, NULL);
  } else if (ev.target == delete_atom) {
    /* Received DELETE request */
    ev.property = xsr->property;
    hr = handle_delete (ev.display, ev.requestor, ev.property);
    retval = False;
  } else {
    /* Cannot convert to requested target. This includes most non-string
     * datatypes, and INSERT_SELECTION, INSERT_PROPERTY */
    ev.property = None;
  }

  /* Return False if a DELETE was processed */
  retval = (hr & DID_DELETE) ? False : True;

  /* If there was an error in the transfer, it should be refused */
  if (hr & HANDLE_ERR) {
    print_debug (D_TRACE, "Error in transfer");
    ev.property = None;
  }

  if ((hr & HANDLE_INCOMPLETE) == 0) {
    if (ev.property == None) {print_debug (D_TRACE, "Refusing conversion");}
    else { print_debug (D_TRACE, "Confirming conversion");}

    XSendEvent (display, ev.requestor, False,
                (unsigned long)NULL, (XEvent *)&ev);

    /* If we return False here, we may quit immediately, so sync out the
     * X queue. */
    if (!retval) XSync (display, False);
  }

  return retval;
}

/*
 * set_selection (selection, sel)
 *
 * Takes ownership of the selection 'selection', then loops waiting for
 * its SelectionClear or SelectionRequest events.
 *
 * Handles SelectionRequest events, first checking for additional
 * input if the user has specified 'follow' mode. Returns when a
 * SelectionClear event is received for the specified selection.
 */
static void
set_selection (Atom selection, unsigned char * sel)
{
  XEvent event;
  IncrTrack * it;
  
  if (own_selection (selection) == False) return;

  for (;;) {
    XNextEvent (display, &event);
    
    switch (event.type) {
    case SelectionClear:
      if (event.xselectionclear.selection == selection) return;
      break;
    case SelectionRequest:
      if (event.xselectionrequest.selection != selection) break;

      if (do_follow)
        sel = read_input (sel, True);
      
      if (!handle_selection_request (event, sel)) return;
      
      break;
    case PropertyNotify:
      if (event.xproperty.state != PropertyDelete) break;

      it = find_incrtrack (event.xproperty.atom);

      if (it != NULL) {
        continue_incr (it);
      }

      break;
    default:
      break;
    }
  }
}

/*
 * set_selection__daemon (selection, sel)
 *
 * Creates a daemon process to handle selection requests for the
 * specified selection 'selection', to respond with selection text 'sel'.
 * If 'sel' is an empty string (NULL or "") then no daemon process is
 * created and the specified selection is cleared instead.
 */
static void
set_selection__daemon (Atom selection, unsigned char * sel)
{
  if (empty_string (sel)) {
    clear_selection (selection);
    return;
  }

  become_daemon ();

  set_selection (selection, sel);
}

/*
 * set_selection_pair (sel_p, sel_s)
 *
 * Handles SelectionClear and SelectionRequest events for both the
 * primary and secondary selections. Returns once SelectionClear events
 * have been received for both selections. Responds to SelectionRequest
 * events for the primary selection with text 'sel_p' and for the
 * secondary selection with text 'sel_s'.
 */
static void
set_selection_pair (unsigned char * sel_p, unsigned char * sel_s)
{
  XEvent event;
  IncrTrack * it;
  
  if (sel_p) {
    if (own_selection (XA_PRIMARY) == False)
      free_string (sel_p);
  } else {
    clear_selection (XA_PRIMARY);
  }

  if (sel_s) {
    if (own_selection (XA_SECONDARY) == False)
      free_string (sel_s);
  } else {
    clear_selection (XA_SECONDARY);
  }

  for (;;) {
    XNextEvent (display, &event);
    
    switch (event.type) {
    case SelectionClear:
      if (event.xselectionclear.selection == XA_PRIMARY) {
        free_string (sel_p);
        if (sel_s == NULL) return;
      } else if (event.xselectionclear.selection == XA_SECONDARY) {
        free_string (sel_s);
        if (sel_p == NULL) return;
      }
      break;
    case SelectionRequest:
      if (event.xselectionrequest.selection == XA_PRIMARY) {
        if (!handle_selection_request (event, sel_p)) {
          free_string (sel_p);
          if (sel_s == NULL) return;
        }
      } else if (event.xselectionrequest.selection == XA_SECONDARY) {
        if (!handle_selection_request (event, sel_s)) {
          free_string (sel_s);
          if (sel_p == NULL) return;
        }
      }
      break;
    case PropertyNotify:
      if (event.xproperty.state != PropertyDelete) break;

      it = find_incrtrack (event.xproperty.atom);

      if (it != NULL) {
        continue_incr (it);
      }
      break;
    default:
      break;
    }
  }
}

/*
 * set_selection_pair__daemon (sel_p, sel_s)
 *
 * Creates a daemon process to handle selection requests for both the
 * primary and secondary selections with texts 'sel_p' and 'sel_s'
 * respectively.
 *
 * If both 'sel_p' and 'sel_s' are empty strings (NULL or "") then no
 * daemon process is created, and both selections are cleared instead.
 */
static void
set_selection_pair__daemon (unsigned char * sel_p, unsigned char * sel_s)
{
  if (empty_string (sel_p) && empty_string (sel_s)) {
    clear_selection (XA_PRIMARY);
    clear_selection (XA_SECONDARY);
    return;
  }

  become_daemon ();

  set_selection_pair (sel_p, sel_s);
}

/*
 * keep_selections ()
 *
 * Takes ownership of both the primary and secondary selections. The current
 * selection texts are retrieved and a new daemon process is created to
 * handle both selections unmodified.
 */
static void
keep_selections (void)
{
  unsigned char * text1, * text2;

  text1 = get_selection_text (XA_PRIMARY);
  text2 = get_selection_text (XA_SECONDARY);

  set_selection_pair__daemon (text1, text2);
}

/*
 * exchange_selections ()
 *
 * Exchanges the primary and secondary selections. The current selection
 * texts are retrieved and a new daemon process is created to handle both
 * selections with their texts exchanged.
 */
static void
exchange_selections (void)
{
  unsigned char * text1, * text2;

  text1 = get_selection_text (XA_PRIMARY);
  text2 = get_selection_text (XA_SECONDARY);

  set_selection_pair__daemon (text2, text1);
}

/*
 * free_saved_argv ()
 *
 * atexit function for freeing argv, after it has been relocated to the
 * heap.
 */
static void
free_saved_argv (void)
{
  int i;

  for (i=0; i < saved_argc; i++) {
    free (saved_argv[i]);
  }
  free (saved_argv);
}

/*
 * expand_argv (&argc, &argv)
 *
 * Explodes single letter options so that the argument parser can see
 * all of them. Relocates argv and all arguments to the heap.
 */
static void 
expand_argv(int * argc, char **argv[])
{
  int i, new_i, arglen, new_argc = *argc;
  char ** new_argv;
  char * arg;
 
  /* Calculate new argc */
  for (i = 0; i < *argc; i++) {
    arglen = strlen((*argv)[i]);
    /* An option we need to expand? */
    if ((arglen > 2) && (*argv)[i][0] == '-' && (*argv)[i][1] != '-')
      new_argc += arglen-2;
  }

  /* Allocate new_argv */
  new_argv = xs_malloc (new_argc * sizeof(char *));

  /* Copy args into new argv */
  for (i = 0, new_i = 0; i < *argc; i++) {
    arglen = strlen((*argv)[i]);
   
    /* An option we need to expand? */
    if ((arglen > 2)
	&& (*argv)[i][0] == '-' && (*argv)[i][1] != '-') {
      /* Make each letter a new argument. */

      char * c = ((*argv)[i] + 1);
     
      while (*c != '\0') {
	arg = xs_malloc(sizeof(char) * 3);
	arg[0] = '-';
	arg[1] = *c;
	arg[2] = '\0';
        new_argv[new_i++] = arg;
        c++;
      }
    } else {
      /* Simply copy the argument pointer to new_argv */
      new_argv[new_i++] = strdup ((*argv)[i]);
    }
  }

  /* Set the expected return values */
  *argc = new_argc;
  *argv = new_argv;

  /* Save the new argc, argv values and free them on exit */
  saved_argc = new_argc;
  saved_argv = new_argv;
  atexit (free_saved_argv);
}

/*
 * main (argc, argv)
 * =================
 *
 * Parse user options and set behaviour.
 *
 * By default the current selection is output and not modified if both
 * standard input and standard output are terminals (ttys). Otherwise,
 * the current selection is output if standard output is not a terminal
 * (tty), and the selection is set from standard input if standard input
 * is not a terminal (tty). If any input or output options are given then
 * the program behaves only in the requested mode.
 *
 * If both input and output is required then the previous selection is
 * output before being replaced by the contents of standard input.
 */
int
main(int argc, char *argv[])
{
  Boolean show_version = False;
  Boolean show_help = False;
  Boolean do_append = False, do_clear = False;
  Boolean do_keep = False, do_exchange = False;
  Boolean do_input = False, do_output = False;
  Boolean dont_input = True, dont_output = False;
  Boolean want_clipboard = False, do_delete = False;
  Window root;
  Atom selection = XA_PRIMARY, test_atom;
  int black;
  int i, s=0;
  unsigned char * old_sel = NULL, * new_sel = NULL;
  char * display_name = NULL;
  long timeout_ms = 0L;

  progname = argv[0];

  /* Specify default behaviour based on input and output file types */
  if (isatty(0) && isatty(1)) {
    /* Interactive mode: both stdin and stdout are ttys */
    do_input = False; dont_input = True;
    do_output = False; dont_output = False;
  } else if (!isatty(0) && !isatty(1)) {
    /* Scripted: both stdin and stdout are NOT ttys */
    do_input = False; dont_input = True;
    do_output = True; dont_output = False;
  } else {
    /* Interactive, pipelined: one of stdin or stdout is a tty */
    do_input = !isatty(0); dont_input = !do_input;
    do_output = !isatty(1); dont_output = !do_output;
  }

#define OPT(s) (strcmp (argv[i], (s)) == 0)

  /* Expand argv array before parsing to uncombine arguments. */
  expand_argv(&argc, &argv);

  /* Parse options; modify behaviour according to user-specified options */
  for (i=1; i < argc; i++) {
    if (OPT("--help") || OPT("-h")) {
      show_help = True;
    } else if (OPT("--version")) {
      show_version = True;
    } else if (OPT("--verbose") || OPT("-v")) {
      debug_level++;
    } else if (OPT("--append") || OPT("-a")) {
      do_append = True;
      dont_output = True;
    } else if (OPT("--input") || OPT("-i")) {
      do_input = True;
      dont_output = True;
    } else if (OPT("--clear") || OPT("-c")) {
      do_clear = True;
      dont_output = True;
    } else if (OPT("--output") || OPT("-o")) {
      do_output = True;
      dont_input = True;
    } else if (OPT("--follow") || OPT("-f")) {
      do_follow = True;
      dont_output = True;
    } else if (OPT("--primary") || OPT("-p")) {
      selection = XA_PRIMARY;
    } else if (OPT("--secondary") || OPT("-s")) {
      selection = XA_SECONDARY;
    } else if (OPT("--clipboard") || OPT("-b")) {
      want_clipboard = True;
    } else if (OPT("--keep") || OPT("-k")) {
      do_keep = True;
    } else if (OPT("--exchange") || OPT("-x")) {
      do_exchange = True;
    } else if (OPT("--display")) {
      i++; if (i >= argc) goto usage_err;
      display_name = argv[i];
    } else if (OPT("--selectionTimeout") || OPT("-t")) {
      i++; if (i >= argc) goto usage_err;
      timeout_ms = strtol(argv[i], (char **)NULL, 10);
      if (timeout_ms < 0) timeout_ms = 0;
    } else if (OPT("--nodetach") || OPT("-n")) {
      no_daemon = True;
    } else if (OPT("--delete") || OPT("-d")) {
      do_delete = True;
      dont_output = True;
    } else if (OPT("--logfile") || OPT("-l")) {
      i++; if (i >= argc) goto usage_err;
      strncpy (logfile, argv[i], MAXFNAME);
    } else {
      goto usage_err;
    }
  }

  if (show_version) {
    printf ("xsel version " VERSION " by " AUTHOR "\n");
  }

  if (show_help) {
    usage ();
  }

  if (show_version || show_help) {
    exit (0);
  }

  if (fstat (0, &in_statbuf) == -1) {
    exit_err ("fstat error on stdin");
  }
  if (fstat (1, &out_statbuf) == -1) {
    exit_err ("fstat error on stdout");
  }

  if (S_ISDIR(in_statbuf.st_mode)) {
    exit_err ("-: Is a directory\n");
  }
  if (S_ISDIR(out_statbuf.st_mode)) {
    exit_err ("stdout: Is a directory\n");
  }

  timeout = timeout_ms * 1000;

  display = XOpenDisplay (display_name);
  if (display==NULL) {
    exit_err ("Can't open display: %s\n", display_name);
  }
  root = XDefaultRootWindow (display);
  
  /* Create an unmapped window for receiving events */
  black = BlackPixel (display, DefaultScreen (display));
  window = XCreateSimpleWindow (display, root, 0, 0, 1, 1, 0, black, black);

  print_debug (D_INFO, "Window id: 0x%x (unmapped)", window);

  /* Get a timestamp */
  XSelectInput (display, window, PropertyChangeMask);
  timestamp = get_timestamp ();

  print_debug (D_OBSC, "Timestamp: %lu", timestamp);

  /* Get the maximum incremental selection size in bytes */
  /*max_req = MAX_SELECTION_INCR (display);*/
  max_req = 4000;

  print_debug (D_OBSC, "Maximum request size: %ld bytes", max_req);

  /* Consistency check */
  test_atom = XInternAtom (display, "PRIMARY", False);
  if (test_atom != XA_PRIMARY)
    print_debug (D_WARN, "XA_PRIMARY not named \"PRIMARY\"\n");
  test_atom = XInternAtom (display, "SECONDARY", False);
  if (test_atom != XA_SECONDARY)
    print_debug (D_WARN, "XA_SECONDARY not named \"SECONDARY\"\n");

  NUM_TARGETS=0;

  /* Get the TIMESTAMP atom */
  timestamp_atom = XInternAtom (display, "TIMESTAMP", False);
  supported_targets[s++] = timestamp_atom;
  NUM_TARGETS++;

  /* Get the MULTIPLE atom */
  multiple_atom = XInternAtom (display, "MULTIPLE", False);
  supported_targets[s++] = multiple_atom;
  NUM_TARGETS++;

  /* Get the TARGETS atom */
  targets_atom = XInternAtom (display, "TARGETS", False);
  supported_targets[s++] = targets_atom;
  NUM_TARGETS++;

  /* Get the DELETE atom */
  delete_atom = XInternAtom (display, "DELETE", False);
  supported_targets[s++] = delete_atom;
  NUM_TARGETS++;

  /* Get the INCR atom */
  incr_atom = XInternAtom (display, "INCR", False);
  supported_targets[s++] = incr_atom;
  NUM_TARGETS++;

  /* Get the NULL atom */
  null_atom = XInternAtom (display, "NULL", False);
  NUM_TARGETS++;

  /* Get the TEXT atom */
  text_atom = XInternAtom (display, "TEXT", False);
  supported_targets[s++] = text_atom;
  NUM_TARGETS++;

  /* Get the UTF8_STRING atom */
  utf8_atom = XInternAtom (display, "UTF8_STRING", True);
  if(utf8_atom != None) {
    supported_targets[s++] = utf8_atom;
    NUM_TARGETS++;
  } else {
    utf8_atom = XA_STRING;
  }

  supported_targets[s++] = XA_STRING;
  NUM_TARGETS++;

  /* Get the COMPOUND_TEXT atom.
   * NB. We do not currently serve COMPOUND_TEXT; we can retrieve it but
   * do not perform charset conversion.
   */
  compound_text_atom = XInternAtom (display, "COMPOUND_TEXT", False);

  /* handle selection keeping and exit if so */
  if (do_keep) {
    keep_selections ();
    _exit (0);
  }

  /* handle selection exchange and exit if so */
  if (do_exchange) {
    exchange_selections ();
    _exit (0);
  }

  /* Find the "CLIPBOARD" selection if required */
  if (want_clipboard) {
    selection = XInternAtom (display, "CLIPBOARD", False);
  }

  /* handle output modes */
  if (do_output || !dont_output) {
    /* Get the current selection */
    old_sel = get_selection_text (selection);
    if (old_sel) printf ("%s", old_sel);
  }

  /* handle input and clear modes */
  if (do_delete) {
    get_selection (selection, delete_atom);
  } else if (do_clear) {
    clear_selection (selection);
  }
  else if (do_input || !dont_input) {
    if (do_append) {
      if (!old_sel) old_sel = get_selection_text (selection);
      new_sel = copy_sel (old_sel);
    }
    new_sel = initialise_read (new_sel);
    new_sel = read_input (new_sel, False);
    set_selection__daemon (selection, new_sel);
  }
  
  exit (0);

usage_err:
  usage ();
  exit (0);
}
