#include <math.h>
#include <errno.h>
#include <assert.h>
+#include <ctype.h>
+
+#include <unistd.h>
#include <X11/X.h>
+#include <X11/Intrinsic.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
enum { STRING, CTEXT, UTF8, TARGETS, TIMESTAMP, CUSTOM } mode = STRING;
int use_clipboard = False;
char *custom_seltype = NULL;
+int fork_when_writing = True;
+int use_cutbuffers = True;
+int use_outgoing_incr = True;
+int sel_delta = 16384;
/* selection data */
char *seltext;
int sellen, selsize;
-#define SELDELTA 16384
+
+/* incremental transfers still pending when we return to the event loop */
+struct incr {
+ Window window;
+ Atom property;
+ Atom type;
+ int format;
+ unsigned char *data;
+ size_t size;
+} *incrs = NULL;
+int nincrs = 0, incrsize = 0;
/* functional parameters */
int reading; /* read instead of writing? */
int convert_to_ctext = True; /* Xmb convert to compound text? */
+int verbose;
const char usagemsg[] =
"usage: xcopy [ -r ] [ -u | -c ] [ -C ]\n"
- "where: -r read X selection and print on stdout\n"
- " no -r read stdin and store in X selection\n"
- " -u work with UTF8_STRING type selections\n"
- " -c work with COMPOUND_TEXT type selections\n"
- " -C suppress automatic conversion to COMPOUND_TEXT\n"
- " -b read the CLIPBOARD rather than the PRIMARY selection\n"
- " -t get the list of targets available to retrieve\n"
+ "modes: -r read X selection and print on stdout\n"
+ " no -r write to X selection from stdin\n"
+ " read: -t get the list of targets available to retrieve\n"
" -T get the time stamp of the selection contents\n"
" -a atom get an arbitrary form of the selection data\n"
+ " -v verbosely report progress in reading selection\n"
+ "write: -F remain in foreground until selection reclaimed\n"
+ " -I do not attempt incremental transfers (INCR)\n"
+ " both: -u work with UTF8_STRING type selections\n"
+ " -c work with COMPOUND_TEXT type selections\n"
+ " -C suppress automatic conversion to COMPOUND_TEXT\n"
+ " -b use the CLIPBOARD rather than the PRIMARY selection\n"
+ " -d size transfer data in chunks of at most <size> (default 16384)\n"
+ " -B do not use root window cut buffers\n"
" also: xcopy --version report version number\n"
" xcopy --help display this help text\n"
" xcopy --licence display the (MIT) licence text\n"
}
const char licencemsg[] =
- "xcopy is copyright 2001-2004 Simon Tatham.\n"
+ "xcopy is copyright 2001-2004,2008 Simon Tatham.\n"
"\n"
"Permission is hereby granted, free of charge, to any person\n"
"obtaining a copy of this software and associated documentation files\n"
mode = TARGETS;
} else if (!strcmp(p, "-T")) {
mode = TIMESTAMP;
+ } else if (!strcmp(p, "-I")) {
+ use_outgoing_incr = False;
+ } else if (!strcmp(p, "-B")) {
+ use_cutbuffers = False;
} else if (!strcmp(p, "-a")) {
if (--ac > 0)
custom_seltype = *++av;
else
error ("expected an argument to `-a'");
mode = CUSTOM;
+ } else if (!strcmp(p, "-d")) {
+ if (--ac > 0)
+ sel_delta = atoi(*++av);
+ else
+ error ("expected an argument to `-d'");
+ } else if (!strcmp(p, "-F")) {
+ fork_when_writing = False;
+ } else if (!strcmp(p, "-v")) {
+ verbose = True;
} else if (!strcmp(p, "--help")) {
usage();
return 0;
}
if (!reading) {
- seltext = malloc(SELDELTA);
+ seltext = malloc(sel_delta);
if (!seltext)
error ("out of memory");
- selsize = SELDELTA;
+ selsize = sel_delta;
sellen = 0;
do {
n = fread(seltext+sellen, 1, selsize-sellen, stdin);
sellen += n;
if (sellen >= selsize) {
- seltext = realloc(seltext, selsize += SELDELTA);
+ seltext = realloc(seltext, selsize += sel_delta);
if (!seltext)
error ("out of memory");
}
} while (n > 0);
if (sellen == selsize) {
- seltext = realloc(seltext, selsize += SELDELTA);
+ seltext = realloc(seltext, selsize += sel_delta);
if (!seltext)
error ("out of memory");
}
}
eventloop = init_X();
- if (!reading) {
+ if (!reading && fork_when_writing) {
/*
* If we are writing the selection, we must go into the
* background now.
Display *disp = NULL;
Window ourwin = None;
-Atom compound_text_atom, targets_atom;
+Atom compound_text_atom, targets_atom, timestamp_atom, atom_atom, integer_atom;
+Atom multiple_atom, atom_pair_atom, incr_atom;
int screen, wwidth, wheight;
Atom strtype = XA_STRING;
Atom expected_type = (Atom)None;
int expected_format = 8;
+static const char *translate_atom(Display *disp, Atom atom)
+{
+ if (atom == None)
+ return "None";
+ else
+ return XGetAtomName(disp, atom);
+}
+
/*
* Returns TRUE if we need to enter an event loop, FALSE otherwise.
*/
error ("unable to open display");
targets_atom = XInternAtom(disp, "TARGETS", False);
- if (!targets_atom)
- error ("unable to get TARGETS property");
+ timestamp_atom = XInternAtom(disp, "TIMESTAMP", False);
+ atom_atom = XInternAtom(disp, "ATOM", False);
+ atom_pair_atom = XInternAtom(disp, "ATOM_PAIR", False);
+ multiple_atom = XInternAtom(disp, "MULTIPLE", False);
+ integer_atom = XInternAtom(disp, "INTEGER", False);
+ incr_atom = XInternAtom(disp, "INCR", False);
if (mode == UTF8) {
strtype = XInternAtom(disp, "UTF8_STRING", False);
- if (!strtype)
- error ("unable to get UTF8_STRING property");
} else if (mode == CTEXT) {
strtype = XInternAtom(disp, "COMPOUND_TEXT", False);
- if (!strtype)
- error ("unable to get COMPOUND_TEXT property");
} else if (mode == TARGETS) {
strtype = targets_atom;
- expected_type = XInternAtom(disp, "ATOM", False);
+ expected_type = atom_atom;
expected_format = 32;
} else if (mode == TIMESTAMP) {
- strtype = XInternAtom(disp, "TIMESTAMP", False);
+ strtype = timestamp_atom;
+ expected_type = integer_atom;
expected_format = 32;
} else if (mode == CUSTOM) {
- strtype = XInternAtom(disp, custom_seltype, False);
+ strtype = XInternAtom(disp, custom_seltype, True);
+ if (!strtype)
+ error ("atom '%s' does not exist on the server", custom_seltype);
expected_format = 0;
}
if (use_clipboard) {
sel_atom = XInternAtom(disp, "CLIPBOARD", False);
- if (!sel_atom)
- error ("unable to get CLIPBOARD property");
}
/* get the screen and root-window */
/* do selection fiddling */
if (reading) {
/*
- * We are reading the selection, so we must FIXME.
+ * We are reading the selection. Call XConvertSelection to
+ * request transmission of the selection data in the
+ * appropriate format; the X event loop will then wait to
+ * receive the data from the selection owner. Also we need to
+ * make sure we receive PropertyNotify events, for INCR
+ * transfers.
+ *
+ * If there is no selection owner, look in the cut buffer
+ * property on the root window.
*/
- if (XGetSelectionOwner(disp, sel_atom) == None) {
- /* No primary selection, so use the cut buffer. */
- if (strtype == XA_STRING)
- do_paste(DefaultRootWindow(disp), XA_CUT_BUFFER0, False);
- return False;
- } else {
+ XSelectInput(disp, ourwin, PropertyChangeMask);
+ if (XGetSelectionOwner(disp, sel_atom) != None) {
Atom sel_property = XInternAtom(disp, "VT_SELECTION", False);
+ if (verbose)
+ fprintf(stderr, "calling XConvertSelection: selection=%s"
+ " target=%s property=%s requestor=%08lx\n",
+ translate_atom(disp, sel_atom),
+ translate_atom(disp, strtype),
+ translate_atom(disp, sel_property),
+ ourwin);
XConvertSelection(disp, sel_atom, strtype,
sel_property, ourwin, CurrentTime);
return True;
+ } else if (use_cutbuffers) {
+ /* No primary selection, so use the cut buffer. */
+ if (verbose)
+ fprintf(stderr, "no selection owner: trying cut buffer\n");
+ if (strtype == XA_STRING)
+ do_paste(DefaultRootWindow(disp), XA_CUT_BUFFER0, True);
+ return False;
+ } else {
+ /* Last fallback: do nothing. */
+ return False;
}
} else {
/*
- * We are writing to the selection, so we establish
- * ourselves as selection owner. Also place the data in
- * CUT_BUFFER0, if it isn't of an exotic type (cut buffers
- * can only take ordinary string data, it turns out).
+ * We are writing to the selection, so we establish ourselves
+ * as selection owner.
+ *
+ * Also place the data in CUT_BUFFER0, if it isn't of an
+ * exotic type (cut buffers can only take ordinary string
+ * data, it turns out) or bigger than our maximum chunk size.
*/
XSetSelectionOwner (disp, sel_atom, ourwin, CurrentTime);
if (XGetSelectionOwner (disp, sel_atom) != ourwin)
- error ("unable to obtain primary X selection\n");
+ error ("unable to obtain primary X selection");
compound_text_atom = XInternAtom(disp, "COMPOUND_TEXT", False);
- if (strtype == XA_STRING) {
+ if (strtype == XA_STRING && sellen <= sel_delta && use_cutbuffers) {
/*
* ICCCM-required cut buffer initialisation.
*/
+ static const unsigned char emptystring[] = {0};
XChangeProperty(disp, root, XA_CUT_BUFFER0,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER1,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER2,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER3,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER4,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER5,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER6,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
XChangeProperty(disp, root, XA_CUT_BUFFER7,
- XA_STRING, 8, PropModeAppend, "", 0);
+ XA_STRING, 8, PropModeAppend, emptystring, 0);
/*
* Rotate the cut buffers and add our text in CUT_BUFFER0.
*/
}
}
+void write_data(Window requestor, Atom property, Atom type, int format,
+ void *vdata, size_t size)
+{
+ int bformat = format / 8; /* bytes per element */
+ unsigned char *data = (unsigned char *)vdata;
+ XEvent ev;
+
+ if (!use_outgoing_incr || size * bformat <= sel_delta) {
+ XChangeProperty(disp, requestor, property, type, format,
+ PropModeReplace, data, size);
+ } else {
+ /*
+ * For large data, an incremental transfer as per ICCCM 2.7.2.
+ */
+ Cardinal totalsize = size * bformat;
+ Cardinal sent, thissize;
+
+ /*
+ * We're going to need PropertyNotify events on the target
+ * window to tell us when to send the next chunk.
+ */
+ XSelectInput(disp, requestor, PropertyChangeMask);
+
+ /*
+ * Start by sending a single 32-bit word with type INCR giving
+ * the total size in bytes.
+ */
+ XChangeProperty(disp, requestor, property, incr_atom, 32,
+ PropModeReplace, (unsigned char *)&totalsize, 1);
+
+ /*
+ * Now set up an entry in our list of ongoing incremental
+ * transfers, so that whenever that property is deleted, we'll
+ * send the next batch.
+ */
+ if (nincrs >= incrsize) {
+ incrsize = nincrs * 9 / 8 + 16;
+ incrs = realloc(incrs, incrsize * sizeof(*incrs));
+ if (!incrs)
+ error ("out of memory");
+ }
+ incrs[nincrs].window = requestor;
+ incrs[nincrs].property = property;
+ incrs[nincrs].type = type;
+ incrs[nincrs].format = format;
+ incrs[nincrs].size = totalsize;
+ incrs[nincrs].data = malloc(totalsize);
+ if (!incrs[nincrs].data)
+ error("out of memory");
+ memcpy(incrs[nincrs].data, data, size);
+ nincrs++;
+ }
+}
+
+Atom convert_sel_inner(Window requestor, Atom target, Atom property) {
+ if (target == strtype) {
+ write_data(requestor, property, strtype, 8, seltext, sellen);
+ return property;
+ } else if (target == compound_text_atom && convert_to_ctext) {
+ XTextProperty tp;
+ XmbTextListToTextProperty(disp, &seltext, 1, XCompoundTextStyle, &tp);
+ write_data(requestor, property, target, tp.format, tp.value,tp.nitems);
+ return property;
+ } else if (target == targets_atom) {
+ Atom targets[16];
+ int len = 0;
+ targets[len++] = timestamp_atom;
+ targets[len++] = targets_atom;
+ targets[len++] = multiple_atom;
+ targets[len++] = strtype;
+ if (strtype != compound_text_atom && convert_to_ctext)
+ targets[len++] = compound_text_atom;
+ write_data(requestor, property, atom_atom, 32, targets, len);
+ return property;
+ } else if (target == timestamp_atom) {
+ Time rettime = CurrentTime;
+ write_data(requestor, property, integer_atom, 32, &rettime, 1);
+ return property;
+ } else {
+ return None;
+ }
+}
+
+Atom convert_sel_outer(Window requestor, Atom target, Atom property) {
+ /*
+ * ICCCM 2.2 says that obsolete clients requesting the selection
+ * request may not specify a property name under which they want
+ * the data written to their window; selection owners are
+ * encouraged to support such clients by reusing the selection
+ * target name as the property.
+ */
+ if (property == None)
+ property = target;
+
+ if (target == multiple_atom) {
+ /*
+ * Support for the MULTIPLE selection type, since it's
+ * specified as required in the ICCCM. Completely
+ * untested, though, because I have no idea of what X
+ * application might implement it for me to test against.
+ */
+
+ int size = sel_delta;
+ Atom actual_type;
+ int actual_format, i;
+ unsigned long nitems, bytes_after, nread;
+ unsigned char *data;
+ Atom *adata;
+
+ if (property == (Atom)None)
+ return None; /* ICCCM says this isn't allowed */
+
+ /*
+ * Fetch the requestor's window property giving a list of
+ * selection requests.
+ */
+ while (XGetWindowProperty(disp, requestor, property, 0, size,
+ False, AnyPropertyType, &actual_type,
+ &actual_format, &nitems, &bytes_after,
+ (unsigned char **) &data) == Success &&
+ nitems * (actual_format / 8) == size) {
+ XFree(data);
+ size *= 3 / 2;
+ }
+
+ if (actual_type != atom_pair_atom || actual_format != 32) {
+ XFree(data);
+ return None;
+ }
+
+ adata = (Atom *)data;
+
+ for (i = 0; i+1 < (long)nitems; i += 2) {
+ if (adata[i+1] != (Atom)None) /* ICCCM says this isn't allowed */
+ adata[i+1] = convert_sel_inner(requestor, adata[i],
+ adata[i+1]);
+ }
+
+ XChangeProperty (disp, requestor, property,
+ atom_pair_atom, 32, PropModeReplace,
+ data, nitems);
+
+ XFree(data);
+
+ return property;
+ } else {
+ if (property == (Atom)None)
+ property = target; /* ICCCM says this is a sensible default */
+ return convert_sel_inner(requestor, target, property);
+ }
+}
+
void run_X(void) {
XEvent ev, e2;
+ int i, j;
while (1) {
XNextEvent (disp, &ev);
if (reading) {
switch (ev.type) {
case SelectionNotify:
+ if (verbose)
+ fprintf(stderr, "got SelectionNotify: requestor=%08lx "
+ "selection=%s target=%s property=%s\n",
+ ev.xselection.requestor,
+ translate_atom(disp, ev.xselection.selection),
+ translate_atom(disp, ev.xselection.target),
+ translate_atom(disp, ev.xselection.property));
+
if (ev.xselection.property != None)
do_paste(ev.xselection.requestor,
- ev.xselection.property, True);
+ ev.xselection.property, False);
return;
}
} else {
+ int have_ownership = True;
+
switch (ev.type) {
case SelectionClear:
/* Selection has been cleared by another app. */
- return;
+ have_ownership = False;
+ break;
case SelectionRequest:
- e2.xselection.type = SelectionNotify;
- e2.xselection.requestor = ev.xselectionrequest.requestor;
- e2.xselection.selection = ev.xselectionrequest.selection;
- e2.xselection.target = ev.xselectionrequest.target;
- e2.xselection.time = ev.xselectionrequest.time;
- if (ev.xselectionrequest.target == strtype) {
- XChangeProperty (disp, ev.xselectionrequest.requestor,
- ev.xselectionrequest.property, strtype,
- 8, PropModeReplace, seltext, sellen);
- e2.xselection.property = ev.xselectionrequest.property;
- } else if (ev.xselectionrequest.target == compound_text_atom &&
- convert_to_ctext) {
- XTextProperty tp;
- XmbTextListToTextProperty (disp, &seltext, 1,
- XCompoundTextStyle, &tp);
- XChangeProperty (disp, ev.xselectionrequest.requestor,
- ev.xselectionrequest.property,
- ev.xselectionrequest.target,
- tp.format, PropModeReplace,
- tp.value, tp.nitems);
- e2.xselection.property = ev.xselectionrequest.property;
- } else if (ev.xselectionrequest.target == targets_atom) {
- Atom targets[2];
- int len = 0;
- targets[len++] = strtype;
- if (strtype != compound_text_atom && convert_to_ctext)
- targets[len++] = compound_text_atom;
- XChangeProperty (disp, ev.xselectionrequest.requestor,
- ev.xselectionrequest.property,
- ev.xselectionrequest.target,
- 32, PropModeReplace,
- (unsigned char *)targets, len);
- } else {
- e2.xselection.property = None;
+ if (have_ownership) {
+ e2.xselection.type = SelectionNotify;
+ e2.xselection.requestor = ev.xselectionrequest.requestor;
+ e2.xselection.selection = ev.xselectionrequest.selection;
+ e2.xselection.target = ev.xselectionrequest.target;
+ e2.xselection.time = ev.xselectionrequest.time;
+ e2.xselection.property =
+ convert_sel_outer(ev.xselectionrequest.requestor,
+ ev.xselectionrequest.target,
+ ev.xselectionrequest.property);
+ XSendEvent (disp, ev.xselectionrequest.requestor,
+ False, 0, &e2);
+ }
+ break;
+ case PropertyNotify:
+ for (i = j = 0; i < nincrs; i++) {
+ int keep = True;
+ if (incrs[i].window == ev.xproperty.window &&
+ incrs[i].property == ev.xproperty.atom &&
+ ev.xproperty.state == PropertyDelete) {
+ size_t thissize = incrs[i].size;
+ if (thissize > sel_delta)
+ thissize = sel_delta;
+
+ XChangeProperty(disp,
+ incrs[i].window, incrs[i].property,
+ incrs[i].type, incrs[i].format,
+ PropModeReplace, incrs[i].data,
+ thissize / (incrs[i].format/8));
+
+ if (thissize == 0) {
+ /*
+ * If we've just sent a zero-length block,
+ * the incremental transfer is over and we
+ * should delete this entry.
+ */
+ keep = False;
+ }
+
+ incrs[i].data += thissize;
+ incrs[i].size -= thissize;
+ }
+ if (keep) {
+ if (j != i)
+ incrs[j] = incrs[i];
+ j++;
+ }
}
- XSendEvent (disp, ev.xselectionrequest.requestor, False, 0, &e2);
+ nincrs = j;
+ break;
}
+ if (nincrs == 0 && !have_ownership)
+ return;
}
}
}
XCloseDisplay (disp);
}
-void do_paste(Window window, Atom property, int Delete) {
+void do_paste(Window window, Atom property, int cutbuffer) {
Atom actual_type;
int actual_format, i;
- long nitems, bytes_after, nread;
+ unsigned long nitems, bytes_after, nread;
unsigned char *data;
+ int incremental = False;
+ XEvent ev;
nread = 0;
- while (XGetWindowProperty(disp, window, property, nread / 4, SELDELTA,
- Delete, AnyPropertyType, &actual_type,
+ while (XGetWindowProperty(disp, window, property, nread / 4, sel_delta / 4,
+ !cutbuffer, AnyPropertyType, &actual_type,
&actual_format, &nitems, &bytes_after,
(unsigned char **) &data) == Success) {
+ if (verbose)
+ fprintf(stderr, "got %ld items of %d-byte data, type=%s;"
+ " %ld to go\n", nitems, actual_format,
+ translate_atom(disp, actual_type), bytes_after);
+
+ /*
+ * ICCCM 2.7.2: if we receive data with the type atom set to
+ * INCR, it indicates that the actual data will arrive in
+ * multiple chunks, terminating with a zero-length one.
+ * Between each pair, we must wait for a PropertyNotify event
+ * which tells us that the next chunk has arrived.
+ */
+ if (actual_type == incr_atom && !cutbuffer) {
+ incremental = True;
+ /*
+ * Immediately wait for the first chunk of real data.
+ */
+ do {
+ XMaskEvent(disp, PropertyChangeMask, &ev);
+ } while (ev.xproperty.state != PropertyNewValue);
+ /*
+ * And loop straight back round to read it.
+ */
+ continue;
+ }
+
if (nitems > 0) {
/*
* We expect all returned chunks of data to be
*/
if ((nread & 3) != 0) {
error("unexpected data size: %d read (not a multiple"
- " of 4), but more to come\n", nread);
+ " of 4), but more to come", nread);
}
if (expected_type != (Atom)None && actual_type != expected_type) {
- char *expout = XGetAtomName(disp, expected_type);
- char *gotout = (actual_type == (Atom)None ? "None" :
- XGetAtomName(disp, actual_type));
- error("unexpected data type: expected %s, got %s\n",
+ const char *expout = translate_atom(disp, expected_type);
+ const char *gotout = translate_atom(disp, actual_type);
+ error("unexpected data type: expected %s, got %s",
expout, gotout);
}
if (expected_format && expected_format != actual_format) {
- error("unexpected data format: expected %d-bit, got %d-bit\n",
+ error("unexpected data format: expected %d-bit, got %d-bit",
expected_format, actual_format);
}
}
int i;
for (i = 0; i < nitems; i++) {
Atom x = ((Atom *)data)[i];
- printf("%s\n", XGetAtomName(disp, x));
+ printf("%s\n", translate_atom(disp, x));
}
} else if (mode == TIMESTAMP) {
assert(actual_format == 32);
}
}
XFree(data);
- if (nitems == 0)
- break;
+ if (bytes_after == 0) {
+ /*
+ * We've come to the end of the property we're reading.
+ */
+ if (incremental) {
+ /*
+ * For an incremental transfer, this means we wait for
+ * another property to be dumped in the same place on
+ * our window, and loop round again reading that. The
+ * exception is if the total length of the property we
+ * got was zero, which signals the end.
+ */
+ if (nread == 0 && nitems == 0)
+ break; /* all done */
+
+ /* otherwise wait for the next chunk */
+ do {
+ XMaskEvent(disp, PropertyChangeMask, &ev);
+ } while (ev.xproperty.state != PropertyNewValue);
+
+ /* which we read from the beginning */
+ nread = 0;
+ } else {
+ break;
+ }
+ }
}
}