Verbose mode, for yet more selection-debugging usefulness.
[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;
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 if (mode == UTF8) {
293 strtype = XInternAtom(disp, "UTF8_STRING", False);
294 } else if (mode == CTEXT) {
295 strtype = XInternAtom(disp, "COMPOUND_TEXT", False);
296 } else if (mode == TARGETS) {
297 strtype = targets_atom;
298 expected_type = atom_atom;
299 expected_format = 32;
300 } else if (mode == TIMESTAMP) {
301 strtype = timestamp_atom;
302 expected_type = integer_atom;
303 expected_format = 32;
304 } else if (mode == CUSTOM) {
305 strtype = XInternAtom(disp, custom_seltype, True);
306 if (!strtype)
307 error ("atom '%s' does not exist on the server", custom_seltype);
308 expected_format = 0;
309 }
310 if (use_clipboard) {
311 sel_atom = XInternAtom(disp, "CLIPBOARD", False);
312 }
313
314 /* get the screen and root-window */
315 screen = DefaultScreen (disp);
316 root = RootWindow (disp, screen);
317
318 x = y = 0;
319 width = height = 10; /* doesn't really matter */
320
321 /* actually create the window */
322 ourwin = XCreateSimpleWindow (disp, root, x, y, width, height,0,
323 BlackPixel(disp, screen),
324 WhitePixel(disp, screen));
325
326 /* resource class name */
327 class_hints.res_name = lcasename;
328 class_hints.res_class = ucasename;
329 XSetClassHint (disp, ourwin, &class_hints);
330
331 /* do selection fiddling */
332 if (reading) {
333 /*
334 * We are reading the selection, so we must FIXME.
335 */
336 if (XGetSelectionOwner(disp, sel_atom) == None) {
337 /* No primary selection, so use the cut buffer. */
338 if (verbose)
339 fprintf(stderr, "no selection owner: trying cut buffer\n");
340 if (strtype == XA_STRING)
341 do_paste(DefaultRootWindow(disp), XA_CUT_BUFFER0, False);
342 return False;
343 } else {
344 Atom sel_property = XInternAtom(disp, "VT_SELECTION", False);
345 if (verbose)
346 fprintf(stderr, "calling XConvertSelection: selection=%s"
347 " target=%s property=%s requestor=%08lx\n",
348 translate_atom(disp, sel_atom),
349 translate_atom(disp, strtype),
350 translate_atom(disp, sel_property),
351 ourwin);
352 XConvertSelection(disp, sel_atom, strtype,
353 sel_property, ourwin, CurrentTime);
354 return True;
355 }
356 } else {
357 /*
358 * We are writing to the selection, so we establish
359 * ourselves as selection owner. Also place the data in
360 * CUT_BUFFER0, if it isn't of an exotic type (cut buffers
361 * can only take ordinary string data, it turns out).
362 */
363 XSetSelectionOwner (disp, sel_atom, ourwin, CurrentTime);
364 if (XGetSelectionOwner (disp, sel_atom) != ourwin)
365 error ("unable to obtain primary X selection");
366 compound_text_atom = XInternAtom(disp, "COMPOUND_TEXT", False);
367 if (strtype == XA_STRING) {
368 /*
369 * ICCCM-required cut buffer initialisation.
370 */
371 XChangeProperty(disp, root, XA_CUT_BUFFER0,
372 XA_STRING, 8, PropModeAppend, "", 0);
373 XChangeProperty(disp, root, XA_CUT_BUFFER1,
374 XA_STRING, 8, PropModeAppend, "", 0);
375 XChangeProperty(disp, root, XA_CUT_BUFFER2,
376 XA_STRING, 8, PropModeAppend, "", 0);
377 XChangeProperty(disp, root, XA_CUT_BUFFER3,
378 XA_STRING, 8, PropModeAppend, "", 0);
379 XChangeProperty(disp, root, XA_CUT_BUFFER4,
380 XA_STRING, 8, PropModeAppend, "", 0);
381 XChangeProperty(disp, root, XA_CUT_BUFFER5,
382 XA_STRING, 8, PropModeAppend, "", 0);
383 XChangeProperty(disp, root, XA_CUT_BUFFER6,
384 XA_STRING, 8, PropModeAppend, "", 0);
385 XChangeProperty(disp, root, XA_CUT_BUFFER7,
386 XA_STRING, 8, PropModeAppend, "", 0);
387 /*
388 * Rotate the cut buffers and add our text in CUT_BUFFER0.
389 */
390 XRotateBuffers(disp, 1);
391 XStoreBytes(disp, seltext, sellen);
392 }
393 return True;
394 }
395 }
396
397 Atom convert_sel_inner(Window requestor, Atom target, Atom property) {
398 if (target == strtype) {
399 XChangeProperty (disp, requestor, property, strtype,
400 8, PropModeReplace, seltext, sellen);
401 return property;
402 } else if (target == compound_text_atom && convert_to_ctext) {
403 XTextProperty tp;
404 XmbTextListToTextProperty (disp, &seltext, 1,
405 XCompoundTextStyle, &tp);
406 XChangeProperty (disp, requestor, property, target,
407 tp.format, PropModeReplace,
408 tp.value, tp.nitems);
409 return property;
410 } else if (target == targets_atom) {
411 Atom targets[16];
412 int len = 0;
413 targets[len++] = timestamp_atom;
414 targets[len++] = targets_atom;
415 targets[len++] = multiple_atom;
416 targets[len++] = strtype;
417 if (strtype != compound_text_atom && convert_to_ctext)
418 targets[len++] = compound_text_atom;
419 XChangeProperty (disp, requestor, property,
420 atom_atom, 32, PropModeReplace,
421 (unsigned char *)targets, len);
422 return property;
423 } else if (target == timestamp_atom) {
424 Time rettime = CurrentTime;
425 XChangeProperty (disp, requestor, property,
426 integer_atom, 32, PropModeReplace,
427 (unsigned char *)&rettime, 1);
428 return property;
429 } else {
430 return None;
431 }
432 }
433
434 Atom convert_sel_outer(Window requestor, Atom target, Atom property) {
435 if (target == multiple_atom) {
436 /*
437 * Support for the MULTIPLE selection type, since it's
438 * specified as required in the ICCCM. Completely
439 * untested, though, because I have no idea of what X
440 * application might implement it for me to test against.
441 */
442
443 int size = SELDELTA;
444 Atom actual_type;
445 int actual_format, i;
446 long nitems, bytes_after, nread;
447 unsigned char *data;
448 Atom *adata;
449
450 if (property == (Atom)None)
451 return None; /* ICCCM says this isn't allowed */
452
453 /*
454 * Fetch the requestor's window property giving a list of
455 * selection requests.
456 */
457 while (XGetWindowProperty(disp, requestor, property, 0, size,
458 False, AnyPropertyType, &actual_type,
459 &actual_format, &nitems, &bytes_after,
460 (unsigned char **) &data) == Success &&
461 nitems * (actual_format / 8) == size) {
462 XFree(data);
463 size *= 3 / 2;
464 }
465
466 if (actual_type != atom_pair_atom || actual_format != 32) {
467 XFree(data);
468 return None;
469 }
470
471 adata = (Atom *)data;
472
473 for (i = 0; i+1 < nitems; i += 2) {
474 if (adata[i+1] != (Atom)None) /* ICCCM says this isn't allowed */
475 adata[i+1] = convert_sel_inner(requestor, adata[i],
476 adata[i+1]);
477 }
478
479 XChangeProperty (disp, requestor, property,
480 atom_pair_atom, 32, PropModeReplace,
481 data, nitems);
482
483 XFree(data);
484
485 return property;
486 } else {
487 if (property == (Atom)None)
488 property = target; /* ICCCM says this is a sensible default */
489 return convert_sel_inner(requestor, target, property);
490 }
491 }
492
493 void run_X(void) {
494 XEvent ev, e2;
495
496 while (1) {
497 XNextEvent (disp, &ev);
498 if (reading) {
499 switch (ev.type) {
500 case SelectionNotify:
501 if (verbose)
502 fprintf(stderr, "got SelectionNotify: requestor=%08lx "
503 "selection=%s target=%s property=%s\n",
504 ev.xselection.requestor,
505 translate_atom(disp, ev.xselection.selection),
506 translate_atom(disp, ev.xselection.target),
507 translate_atom(disp, ev.xselection.property));
508
509 if (ev.xselection.property != None)
510 do_paste(ev.xselection.requestor,
511 ev.xselection.property, True);
512 return;
513 }
514 } else {
515 switch (ev.type) {
516 case SelectionClear:
517 /* Selection has been cleared by another app. */
518 return;
519 case SelectionRequest:
520 e2.xselection.type = SelectionNotify;
521 e2.xselection.requestor = ev.xselectionrequest.requestor;
522 e2.xselection.selection = ev.xselectionrequest.selection;
523 e2.xselection.target = ev.xselectionrequest.target;
524 e2.xselection.time = ev.xselectionrequest.time;
525 e2.xselection.property =
526 convert_sel_outer(ev.xselectionrequest.requestor,
527 ev.xselectionrequest.target,
528 ev.xselectionrequest.property);
529 XSendEvent (disp, ev.xselectionrequest.requestor,
530 False, 0, &e2);
531 }
532 }
533 }
534 }
535
536 void done_X(void) {
537 int i;
538
539 if (ourwin != None)
540 XDestroyWindow (disp, ourwin);
541 if (disp)
542 XCloseDisplay (disp);
543 }
544
545 void do_paste(Window window, Atom property, int Delete) {
546 Atom actual_type;
547 int actual_format, i;
548 long nitems, bytes_after, nread;
549 unsigned char *data;
550
551 nread = 0;
552 while (XGetWindowProperty(disp, window, property, nread / 4, SELDELTA,
553 Delete, AnyPropertyType, &actual_type,
554 &actual_format, &nitems, &bytes_after,
555 (unsigned char **) &data) == Success) {
556 if (verbose)
557 fprintf(stderr, "got %ld items of %d-byte data, type=%s;"
558 " %ld to go\n", nitems, actual_format,
559 translate_atom(disp, actual_type), bytes_after);
560
561 if (nitems > 0) {
562 /*
563 * We expect all returned chunks of data to be
564 * multiples of 4 bytes (because we can only request
565 * the subsequent starting offset in 4-byte
566 * increments). Of course you can store an odd number
567 * of bytes in a selection, so this can't be the case
568 * every time XGetWindowProperty returns; but it
569 * should be the case every time it returns _and there
570 * is more data to come_.
571 *
572 * Hence, whenever XGetWindowProperty returns, we
573 * verify that the size of the data returned _last_
574 * time was divisible by 4.
575 */
576 if ((nread & 3) != 0) {
577 error("unexpected data size: %d read (not a multiple"
578 " of 4), but more to come", nread);
579 }
580
581 if (expected_type != (Atom)None && actual_type != expected_type) {
582 const char *expout = translate_atom(disp, expected_type);
583 const char *gotout = translate_atom(disp, actual_type);
584 error("unexpected data type: expected %s, got %s",
585 expout, gotout);
586 }
587 if (expected_format && expected_format != actual_format) {
588 error("unexpected data format: expected %d-bit, got %d-bit",
589 expected_format, actual_format);
590 }
591 }
592
593 if (nitems > 0) {
594 if (mode == TARGETS) {
595 assert(actual_format == 32);
596 int i;
597 for (i = 0; i < nitems; i++) {
598 Atom x = ((Atom *)data)[i];
599 printf("%s\n", translate_atom(disp, x));
600 }
601 } else if (mode == TIMESTAMP) {
602 assert(actual_format == 32);
603 Time x = ((Time *)data)[0];
604 printf("%lu\n", (unsigned long)x);
605 } else {
606 fwrite(data, actual_format / 8, nitems, stdout);
607 nread += nitems * actual_format / 8;
608 }
609 }
610 XFree(data);
611 if (nitems == 0)
612 break;
613 }
614 }