Read-side support for the incremental selection transfer mode
[sgt/utils] / xcopy / xcopy.c
1 /*
2 * xcopy: quickly pipe text data into, or out of, the primary X
3 * selection
4 */
5
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <stdarg.h>
10 #include <math.h>
11 #include <errno.h>
12 #include <assert.h>
13
14 #include <X11/X.h>
15 #include <X11/Xlib.h>
16 #include <X11/Xutil.h>
17 #include <X11/Xatom.h>
18
19 int init_X(void);
20 void run_X(void);
21 void done_X(void);
22 void full_redraw(void);
23 void do_paste(Window window, Atom property, int Delete);
24
25 char *pname; /* program name */
26
27 void error (char *fmt, ...);
28
29 /* set from command-line parameters */
30 char *display = NULL;
31 enum { STRING, CTEXT, UTF8, TARGETS, TIMESTAMP, CUSTOM } mode = STRING;
32 int use_clipboard = False;
33 char *custom_seltype = NULL;
34 int fork_when_writing = True;
35
36 /* selection data */
37 char *seltext;
38 int sellen, selsize;
39 #define SELDELTA 16384
40
41 /* functional parameters */
42 int reading; /* read instead of writing? */
43 int convert_to_ctext = True; /* Xmb convert to compound text? */
44 int verbose;
45
46 const char usagemsg[] =
47 "usage: xcopy [ -r ] [ -u | -c ] [ -C ]\n"
48 "where: -r read X selection and print on stdout\n"
49 " no -r read stdin and store in X selection\n"
50 " -u work with UTF8_STRING type selections\n"
51 " -c work with COMPOUND_TEXT type selections\n"
52 " -C suppress automatic conversion to COMPOUND_TEXT\n"
53 " -b read the CLIPBOARD rather than the PRIMARY selection\n"
54 " -t get the list of targets available to retrieve\n"
55 " -T get the time stamp of the selection contents\n"
56 " -a atom get an arbitrary form of the selection data\n"
57 " -F do not fork in write mode\n"
58 " -v proceed verbosely when reading selection\n"
59 " also: xcopy --version report version number\n"
60 " xcopy --help display this help text\n"
61 " xcopy --licence display the (MIT) licence text\n"
62 ;
63
64 void usage(void) {
65 fputs(usagemsg, stdout);
66 }
67
68 const char licencemsg[] =
69 "xcopy is copyright 2001-2004,2008 Simon Tatham.\n"
70 "\n"
71 "Permission is hereby granted, free of charge, to any person\n"
72 "obtaining a copy of this software and associated documentation files\n"
73 "(the \"Software\"), to deal in the Software without restriction,\n"
74 "including without limitation the rights to use, copy, modify, merge,\n"
75 "publish, distribute, sublicense, and/or sell copies of the Software,\n"
76 "and to permit persons to whom the Software is furnished to do so,\n"
77 "subject to the following conditions:\n"
78 "\n"
79 "The above copyright notice and this permission notice shall be\n"
80 "included in all copies or substantial portions of the Software.\n"
81 "\n"
82 "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n"
83 "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n"
84 "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n"
85 "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\n"
86 "BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n"
87 "ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n"
88 "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
89 "SOFTWARE.\n"
90 ;
91
92 void licence(void) {
93 fputs(licencemsg, stdout);
94 }
95
96 void version(void) {
97 #define SVN_REV "$Revision$"
98 char rev[sizeof(SVN_REV)];
99 char *p, *q;
100
101 strcpy(rev, SVN_REV);
102
103 for (p = rev; *p && *p != ':'; p++);
104 if (*p) {
105 p++;
106 while (*p && isspace(*p)) p++;
107 for (q = p; *q && *q != '$'; q++);
108 if (*q) *q = '\0';
109 printf("xcopy revision %s\n", p);
110 } else {
111 printf("xcopy: unknown version\n");
112 }
113 }
114
115 int main(int ac, char **av) {
116 int n;
117 int eventloop;
118
119 pname = *av;
120
121 /* parse the command line arguments */
122 while (--ac > 0) {
123 char *p = *++av;
124
125 if (!strcmp(p, "-display") || !strcmp(p, "-disp")) {
126 if (!av[1])
127 error ("option `%s' expects a parameter", p);
128 display = *++av, --ac;
129 } else if (!strcmp(p, "-r")) {
130 reading = True;
131 } else if (!strcmp(p, "-u")) {
132 mode = UTF8;
133 } else if (!strcmp(p, "-c")) {
134 mode = CTEXT;
135 } else if (!strcmp(p, "-C")) {
136 convert_to_ctext = False;
137 } else if (!strcmp(p, "-b")) {
138 use_clipboard = True;
139 } else if (!strcmp(p, "-t")) {
140 mode = TARGETS;
141 } else if (!strcmp(p, "-T")) {
142 mode = TIMESTAMP;
143 } else if (!strcmp(p, "-a")) {
144 if (--ac > 0)
145 custom_seltype = *++av;
146 else
147 error ("expected an argument to `-a'");
148 mode = CUSTOM;
149 } else if (!strcmp(p, "-F")) {
150 fork_when_writing = False;
151 } else if (!strcmp(p, "-v")) {
152 verbose = True;
153 } else if (!strcmp(p, "--help")) {
154 usage();
155 return 0;
156 } else if (!strcmp(p, "--version")) {
157 version();
158 return 0;
159 } else if (!strcmp(p, "--licence") || !strcmp(p, "--license")) {
160 licence();
161 return 0;
162 } else if (*p=='-') {
163 error ("unrecognised option `%s'", p);
164 } else {
165 error ("no parameters required");
166 }
167 }
168
169 if (!reading) {
170 if (mode == TARGETS || mode == TIMESTAMP || mode == CUSTOM) {
171 error ("%s not supported in writing mode; use -r",
172 (mode == TARGETS ? "-t" :
173 mode == TIMESTAMP ? "-T" :
174 /* mode == CUSTOM ? */ "-a"));
175 }
176 }
177
178 if (!reading) {
179 seltext = malloc(SELDELTA);
180 if (!seltext)
181 error ("out of memory");
182 selsize = SELDELTA;
183 sellen = 0;
184 do {
185 n = fread(seltext+sellen, 1, selsize-sellen, stdin);
186 sellen += n;
187 if (sellen >= selsize) {
188 seltext = realloc(seltext, selsize += SELDELTA);
189 if (!seltext)
190 error ("out of memory");
191 }
192 } while (n > 0);
193 if (sellen == selsize) {
194 seltext = realloc(seltext, selsize += SELDELTA);
195 if (!seltext)
196 error ("out of memory");
197 }
198 seltext[sellen] = '\0';
199 }
200
201 eventloop = init_X();
202 if (!reading && fork_when_writing) {
203 /*
204 * If we are writing the selection, we must go into the
205 * background now.
206 */
207 int pid = fork();
208 if (pid < 0) {
209 error("unable to fork: %s", strerror(errno));
210 } else if (pid > 0) {
211 /*
212 * we are the parent; just exit
213 */
214 return 0;
215 }
216 /*
217 * we are the child
218 */
219 close(0);
220 close(1);
221 close(2);
222 chdir("/");
223 }
224 if (eventloop)
225 run_X();
226 done_X();
227 return 0;
228 }
229
230 /* handle errors */
231
232 void error (char *fmt, ...) {
233 va_list ap;
234 char errbuf[200];
235
236 done_X();
237 va_start (ap, fmt);
238 vsprintf (errbuf, fmt, ap);
239 va_end (ap);
240 fprintf (stderr, "%s: %s\n", pname, errbuf);
241 exit (1);
242 }
243
244 /* begin the X interface */
245
246 char *lcasename = "xcopy";
247 char *ucasename = "XCopy";
248
249 Display *disp = NULL;
250 Window ourwin = None;
251 Atom compound_text_atom, targets_atom, timestamp_atom, atom_atom, integer_atom;
252 Atom multiple_atom, atom_pair_atom, incr_atom;
253 int screen, wwidth, wheight;
254
255 Atom strtype = XA_STRING;
256 Atom sel_atom = XA_PRIMARY;
257 Atom expected_type = (Atom)None;
258 int expected_format = 8;
259
260 static const char *translate_atom(Display *disp, Atom atom)
261 {
262 if (atom == None)
263 return "None";
264 else
265 return XGetAtomName(disp, atom);
266 }
267
268 /*
269 * Returns TRUE if we need to enter an event loop, FALSE otherwise.
270 */
271 int init_X(void) {
272 Window root;
273 int x = 0, y = 0, width = 512, height = 128;
274 int i, got = 0;
275 XWMHints wm_hints;
276 XSizeHints size_hints;
277 XClassHint class_hints;
278 XTextProperty textprop;
279 XGCValues gcv;
280
281 /* open the X display */
282 disp = XOpenDisplay (display);
283 if (!disp)
284 error ("unable to open display");
285
286 targets_atom = XInternAtom(disp, "TARGETS", False);
287 timestamp_atom = XInternAtom(disp, "TIMESTAMP", False);
288 atom_atom = XInternAtom(disp, "ATOM", False);
289 atom_pair_atom = XInternAtom(disp, "ATOM_PAIR", False);
290 multiple_atom = XInternAtom(disp, "MULTIPLE", False);
291 integer_atom = XInternAtom(disp, "INTEGER", False);
292 incr_atom = XInternAtom(disp, "INCR", False);
293 if (mode == UTF8) {
294 strtype = XInternAtom(disp, "UTF8_STRING", False);
295 } else if (mode == CTEXT) {
296 strtype = XInternAtom(disp, "COMPOUND_TEXT", False);
297 } else if (mode == TARGETS) {
298 strtype = targets_atom;
299 expected_type = atom_atom;
300 expected_format = 32;
301 } else if (mode == TIMESTAMP) {
302 strtype = timestamp_atom;
303 expected_type = integer_atom;
304 expected_format = 32;
305 } else if (mode == CUSTOM) {
306 strtype = XInternAtom(disp, custom_seltype, True);
307 if (!strtype)
308 error ("atom '%s' does not exist on the server", custom_seltype);
309 expected_format = 0;
310 }
311 if (use_clipboard) {
312 sel_atom = XInternAtom(disp, "CLIPBOARD", False);
313 }
314
315 /* get the screen and root-window */
316 screen = DefaultScreen (disp);
317 root = RootWindow (disp, screen);
318
319 x = y = 0;
320 width = height = 10; /* doesn't really matter */
321
322 /* actually create the window */
323 ourwin = XCreateSimpleWindow (disp, root, x, y, width, height,0,
324 BlackPixel(disp, screen),
325 WhitePixel(disp, screen));
326
327 /* resource class name */
328 class_hints.res_name = lcasename;
329 class_hints.res_class = ucasename;
330 XSetClassHint (disp, ourwin, &class_hints);
331
332 /* do selection fiddling */
333 if (reading) {
334 /*
335 * We are reading the selection. Call XConvertSelection to
336 * request transmission of the selection data in the
337 * appropriate format; the X event loop will then wait to
338 * receive the data from the selection owner. Also we need to
339 * make sure we receive PropertyNotify events, for INCR
340 * transfers.
341 *
342 * If there is no selection owner, look in the cut buffer
343 * property on the root window.
344 */
345 XSelectInput(disp, ourwin, PropertyChangeMask);
346 if (XGetSelectionOwner(disp, sel_atom) == None) {
347 /* No primary selection, so use the cut buffer. */
348 if (verbose)
349 fprintf(stderr, "no selection owner: trying cut buffer\n");
350 if (strtype == XA_STRING)
351 do_paste(DefaultRootWindow(disp), XA_CUT_BUFFER0, True);
352 return False;
353 } else {
354 Atom sel_property = XInternAtom(disp, "VT_SELECTION", False);
355 if (verbose)
356 fprintf(stderr, "calling XConvertSelection: selection=%s"
357 " target=%s property=%s requestor=%08lx\n",
358 translate_atom(disp, sel_atom),
359 translate_atom(disp, strtype),
360 translate_atom(disp, sel_property),
361 ourwin);
362 XConvertSelection(disp, sel_atom, strtype,
363 sel_property, ourwin, CurrentTime);
364 return True;
365 }
366 } else {
367 /*
368 * We are writing to the selection, so we establish
369 * ourselves as selection owner. Also place the data in
370 * CUT_BUFFER0, if it isn't of an exotic type (cut buffers
371 * can only take ordinary string data, it turns out).
372 */
373 XSetSelectionOwner (disp, sel_atom, ourwin, CurrentTime);
374 if (XGetSelectionOwner (disp, sel_atom) != ourwin)
375 error ("unable to obtain primary X selection");
376 compound_text_atom = XInternAtom(disp, "COMPOUND_TEXT", False);
377 if (strtype == XA_STRING) {
378 /*
379 * ICCCM-required cut buffer initialisation.
380 */
381 XChangeProperty(disp, root, XA_CUT_BUFFER0,
382 XA_STRING, 8, PropModeAppend, "", 0);
383 XChangeProperty(disp, root, XA_CUT_BUFFER1,
384 XA_STRING, 8, PropModeAppend, "", 0);
385 XChangeProperty(disp, root, XA_CUT_BUFFER2,
386 XA_STRING, 8, PropModeAppend, "", 0);
387 XChangeProperty(disp, root, XA_CUT_BUFFER3,
388 XA_STRING, 8, PropModeAppend, "", 0);
389 XChangeProperty(disp, root, XA_CUT_BUFFER4,
390 XA_STRING, 8, PropModeAppend, "", 0);
391 XChangeProperty(disp, root, XA_CUT_BUFFER5,
392 XA_STRING, 8, PropModeAppend, "", 0);
393 XChangeProperty(disp, root, XA_CUT_BUFFER6,
394 XA_STRING, 8, PropModeAppend, "", 0);
395 XChangeProperty(disp, root, XA_CUT_BUFFER7,
396 XA_STRING, 8, PropModeAppend, "", 0);
397 /*
398 * Rotate the cut buffers and add our text in CUT_BUFFER0.
399 */
400 XRotateBuffers(disp, 1);
401 XStoreBytes(disp, seltext, sellen);
402 }
403 return True;
404 }
405 }
406
407 Atom convert_sel_inner(Window requestor, Atom target, Atom property) {
408 if (target == strtype) {
409 XChangeProperty (disp, requestor, property, strtype,
410 8, PropModeReplace, seltext, sellen);
411 return property;
412 } else if (target == compound_text_atom && convert_to_ctext) {
413 XTextProperty tp;
414 XmbTextListToTextProperty (disp, &seltext, 1,
415 XCompoundTextStyle, &tp);
416 XChangeProperty (disp, requestor, property, target,
417 tp.format, PropModeReplace,
418 tp.value, tp.nitems);
419 return property;
420 } else if (target == targets_atom) {
421 Atom targets[16];
422 int len = 0;
423 targets[len++] = timestamp_atom;
424 targets[len++] = targets_atom;
425 targets[len++] = multiple_atom;
426 targets[len++] = strtype;
427 if (strtype != compound_text_atom && convert_to_ctext)
428 targets[len++] = compound_text_atom;
429 XChangeProperty (disp, requestor, property,
430 atom_atom, 32, PropModeReplace,
431 (unsigned char *)targets, len);
432 return property;
433 } else if (target == timestamp_atom) {
434 Time rettime = CurrentTime;
435 XChangeProperty (disp, requestor, property,
436 integer_atom, 32, PropModeReplace,
437 (unsigned char *)&rettime, 1);
438 return property;
439 } else {
440 return None;
441 }
442 }
443
444 Atom convert_sel_outer(Window requestor, Atom target, Atom property) {
445 if (target == multiple_atom) {
446 /*
447 * Support for the MULTIPLE selection type, since it's
448 * specified as required in the ICCCM. Completely
449 * untested, though, because I have no idea of what X
450 * application might implement it for me to test against.
451 */
452
453 int size = SELDELTA;
454 Atom actual_type;
455 int actual_format, i;
456 long nitems, bytes_after, nread;
457 unsigned char *data;
458 Atom *adata;
459
460 if (property == (Atom)None)
461 return None; /* ICCCM says this isn't allowed */
462
463 /*
464 * Fetch the requestor's window property giving a list of
465 * selection requests.
466 */
467 while (XGetWindowProperty(disp, requestor, property, 0, size,
468 False, AnyPropertyType, &actual_type,
469 &actual_format, &nitems, &bytes_after,
470 (unsigned char **) &data) == Success &&
471 nitems * (actual_format / 8) == size) {
472 XFree(data);
473 size *= 3 / 2;
474 }
475
476 if (actual_type != atom_pair_atom || actual_format != 32) {
477 XFree(data);
478 return None;
479 }
480
481 adata = (Atom *)data;
482
483 for (i = 0; i+1 < nitems; i += 2) {
484 if (adata[i+1] != (Atom)None) /* ICCCM says this isn't allowed */
485 adata[i+1] = convert_sel_inner(requestor, adata[i],
486 adata[i+1]);
487 }
488
489 XChangeProperty (disp, requestor, property,
490 atom_pair_atom, 32, PropModeReplace,
491 data, nitems);
492
493 XFree(data);
494
495 return property;
496 } else {
497 if (property == (Atom)None)
498 property = target; /* ICCCM says this is a sensible default */
499 return convert_sel_inner(requestor, target, property);
500 }
501 }
502
503 void run_X(void) {
504 XEvent ev, e2;
505
506 while (1) {
507 XNextEvent (disp, &ev);
508 if (reading) {
509 switch (ev.type) {
510 case SelectionNotify:
511 if (verbose)
512 fprintf(stderr, "got SelectionNotify: requestor=%08lx "
513 "selection=%s target=%s property=%s\n",
514 ev.xselection.requestor,
515 translate_atom(disp, ev.xselection.selection),
516 translate_atom(disp, ev.xselection.target),
517 translate_atom(disp, ev.xselection.property));
518
519 if (ev.xselection.property != None)
520 do_paste(ev.xselection.requestor,
521 ev.xselection.property, False);
522 return;
523 }
524 } else {
525 switch (ev.type) {
526 case SelectionClear:
527 /* Selection has been cleared by another app. */
528 return;
529 case SelectionRequest:
530 e2.xselection.type = SelectionNotify;
531 e2.xselection.requestor = ev.xselectionrequest.requestor;
532 e2.xselection.selection = ev.xselectionrequest.selection;
533 e2.xselection.target = ev.xselectionrequest.target;
534 e2.xselection.time = ev.xselectionrequest.time;
535 e2.xselection.property =
536 convert_sel_outer(ev.xselectionrequest.requestor,
537 ev.xselectionrequest.target,
538 ev.xselectionrequest.property);
539 XSendEvent (disp, ev.xselectionrequest.requestor,
540 False, 0, &e2);
541 }
542 }
543 }
544 }
545
546 void done_X(void) {
547 int i;
548
549 if (ourwin != None)
550 XDestroyWindow (disp, ourwin);
551 if (disp)
552 XCloseDisplay (disp);
553 }
554
555 void do_paste(Window window, Atom property, int cutbuffer) {
556 Atom actual_type;
557 int actual_format, i;
558 long nitems, bytes_after, nread;
559 unsigned char *data;
560 int incremental = False;
561 XEvent ev;
562
563 nread = 0;
564 while (XGetWindowProperty(disp, window, property, nread / 4, SELDELTA,
565 !cutbuffer, AnyPropertyType, &actual_type,
566 &actual_format, &nitems, &bytes_after,
567 (unsigned char **) &data) == Success) {
568 if (verbose)
569 fprintf(stderr, "got %ld items of %d-byte data, type=%s;"
570 " %ld to go\n", nitems, actual_format,
571 translate_atom(disp, actual_type), bytes_after);
572
573 /*
574 * ICCCM 2.7.2: if we receive data with the type atom set to
575 * INCR, it indicates that the actual data will arrive in
576 * multiple chunks, terminating with a zero-length one.
577 * Between each pair, we must wait for a PropertyNotify event
578 * which tells us that the next chunk has arrived.
579 */
580 if (actual_type == incr_atom && !cutbuffer) {
581 incremental = True;
582 /*
583 * Immediately wait for the first chunk of real data.
584 */
585 do {
586 XMaskEvent(disp, PropertyChangeMask, &ev);
587 } while (ev.xproperty.state != PropertyNewValue);
588 /*
589 * And loop straight back round to read it.
590 */
591 continue;
592 }
593
594 if (nitems > 0) {
595 /*
596 * We expect all returned chunks of data to be
597 * multiples of 4 bytes (because we can only request
598 * the subsequent starting offset in 4-byte
599 * increments). Of course you can store an odd number
600 * of bytes in a selection, so this can't be the case
601 * every time XGetWindowProperty returns; but it
602 * should be the case every time it returns _and there
603 * is more data to come_.
604 *
605 * Hence, whenever XGetWindowProperty returns, we
606 * verify that the size of the data returned _last_
607 * time was divisible by 4.
608 */
609 if ((nread & 3) != 0) {
610 error("unexpected data size: %d read (not a multiple"
611 " of 4), but more to come", nread);
612 }
613
614 if (expected_type != (Atom)None && actual_type != expected_type) {
615 const char *expout = translate_atom(disp, expected_type);
616 const char *gotout = translate_atom(disp, actual_type);
617 error("unexpected data type: expected %s, got %s",
618 expout, gotout);
619 }
620 if (expected_format && expected_format != actual_format) {
621 error("unexpected data format: expected %d-bit, got %d-bit",
622 expected_format, actual_format);
623 }
624 }
625
626 if (nitems > 0) {
627 if (mode == TARGETS) {
628 assert(actual_format == 32);
629 int i;
630 for (i = 0; i < nitems; i++) {
631 Atom x = ((Atom *)data)[i];
632 printf("%s\n", translate_atom(disp, x));
633 }
634 } else if (mode == TIMESTAMP) {
635 assert(actual_format == 32);
636 Time x = ((Time *)data)[0];
637 printf("%lu\n", (unsigned long)x);
638 } else {
639 fwrite(data, actual_format / 8, nitems, stdout);
640 nread += nitems * actual_format / 8;
641 }
642 }
643 XFree(data);
644 if (bytes_after == 0) {
645 /*
646 * We've come to the end of the property we're reading.
647 */
648 if (incremental) {
649 /*
650 * For an incremental transfer, this means we wait for
651 * another property to be dumped in the same place on
652 * our window, and loop round again reading that. The
653 * exception is if the total length of the property we
654 * got was zero, which signals the end.
655 */
656 if (nread == 0 && nitems == 0)
657 break; /* all done */
658
659 /* otherwise wait for the next chunk */
660 do {
661 XMaskEvent(disp, PropertyChangeMask, &ev);
662 } while (ev.xproperty.state != PropertyNewValue);
663
664 /* which we read from the beginning */
665 nread = 0;
666 } else {
667 break;
668 }
669 }
670 }
671 }