main uint/cterm / libmaus / source / maus_x11.c
  1#include <limits.h>
  2#include <stdbool.h>
  3#include <stdlib.h>
  4#include <string.h>
  5#include <sys/shm.h>
  6#include <sys/ipc.h>
  7
  8#include <X11/Xlib.h>
  9#include <X11/Xutil.h>
 10#include <X11/keysym.h>
 11#include <X11/cursorfont.h>
 12#include <X11/extensions/XShm.h>
 13
 14#include "maus.h"
 15#include "maus_input.h"
 16#include "maus_x11.h"
 17#include "utils.h"
 18
 19typedef struct {
 20	KeySym  x11;
 21	MausKey maus;
 22} KeyMapEntry;
 23
 24static bool fb_create(Maus* mw);
 25static bool fb_create_shm(Maus* mw);
 26/* TODO? static bool fb_create_ximage(Maus* mw); */
 27static void fb_destroy(Maus* mw);
 28static bool handle_event(XEvent* xev, MausEvent* ev, Maus* mw);
 29static MausKey keysym_to_mauskey(KeySym sym);
 30static MausMouseButton mouse_button_to_maus(int btn);
 31static int xerr(Display* dpy, XErrorEvent* ev);
 32
 33static const KeyMapEntry keymap[] = {
 34	{ XK_BackSpace,    MAUS_KEY_BACKSPACE }, { XK_Tab,              MAUS_KEY_TAB },
 35	{ XK_Return,       MAUS_KEY_ENTER },     { XK_Escape,           MAUS_KEY_ESCAPE },
 36	{ XK_apostrophe,   MAUS_KEY_APOSTROPHE },{ XK_comma,            MAUS_KEY_COMMA },
 37	{ XK_space,        MAUS_KEY_SPACE },     { XK_minus,            MAUS_KEY_MINUS },
 38	{ XK_period,       MAUS_KEY_PERIOD },    { XK_slash,            MAUS_KEY_SLASH },
 39	{ XK_semicolon,    MAUS_KEY_SEMICOLON }, { XK_equal,            MAUS_KEY_EQUAL },
 40	{ XK_Delete,       MAUS_KEY_DELETE },    { XK_Left,             MAUS_KEY_LEFT },
 41	{ XK_Right,        MAUS_KEY_RIGHT },     { XK_Up,               MAUS_KEY_UP },
 42	{ XK_Down,         MAUS_KEY_DOWN },      { XK_Home,             MAUS_KEY_HOME },
 43	{ XK_End,          MAUS_KEY_END },       { XK_Page_Up,          MAUS_KEY_PAGE_UP },
 44	{ XK_Page_Down,    MAUS_KEY_PAGE_DOWN }, { XK_Insert,           MAUS_KEY_INSERT },
 45	{ XK_Shift_L,      MAUS_KEY_SHIFT_L },   { XK_Shift_R,          MAUS_KEY_SHIFT_R },
 46	{ XK_Control_L,    MAUS_KEY_CONTROL_L }, { XK_Control_R,        MAUS_KEY_CONTROL_R },
 47	{ XK_Alt_L,        MAUS_KEY_ALT_L },     { XK_Alt_R,            MAUS_KEY_ALT_R },
 48	{ XK_Super_L,      MAUS_KEY_SUPER_L },   { XK_Super_R,          MAUS_KEY_SUPER_R },
 49	{ XK_Caps_Lock,    MAUS_KEY_CAPS_LOCK }, { XK_Num_Lock,         MAUS_KEY_NUM_LOCK },
 50	{ XK_Pause,        MAUS_KEY_PAUSE },     { XK_Menu,             MAUS_KEY_MENU },
 51	{ XK_KP_Add,       MAUS_KEY_KP_ADD },    { XK_KP_Enter,         MAUS_KEY_KP_ENTER },
 52	{ XK_bracketright, MAUS_KEY_RIGHT_BRACKET }, { XK_grave,        MAUS_KEY_GRAVE },
 53	{ XK_Scroll_Lock,  MAUS_KEY_SCROLL_LOCK },   { XK_Print,        MAUS_KEY_PRINT_SCREEN },
 54	{ XK_KP_Decimal,   MAUS_KEY_KP_DECIMAL },    { XK_KP_Divide,    MAUS_KEY_KP_DIVIDE },
 55	{ XK_KP_Multiply,  MAUS_KEY_KP_MULTIPLY },   { XK_KP_Subtract,  MAUS_KEY_KP_SUBTRACT },
 56	{ XK_bracketleft,  MAUS_KEY_LEFT_BRACKET },  { XK_backslash,    MAUS_KEY_BACKSLASH },
 57	{ XK_KP_Equal,     MAUS_KEY_KP_EQUAL },
 58};
 59
 60static int xerrored;
 61
 62static bool fb_create(Maus* mw)
 63{
 64	MausBackend* be = &mw->backend;
 65	be->image = NULL;
 66	be->shmat = false;
 67	be->shm.shmid = -1;
 68	be->shm.shmaddr = NULL;
 69	mw->fb = NULL;
 70	mw->stride = 0;
 71
 72	return fb_create_shm(mw);
 73}
 74
 75/* allocate and store a new shm for the framebuffer */
 76static bool fb_create_shm(Maus* mw)
 77{
 78	MausBackend* be = &mw->backend;
 79	if (!XShmQueryExtension(be->display))
 80		return false;
 81
 82	int scr = DefaultScreen(be->display);
 83	int depth = DefaultDepth(be->display, scr);
 84	Visual* vis = DefaultVisual(be->display, scr);
 85	if (!vis) {
 86		maus_log(stderr, "could not get default visual");
 87		return false;
 88	}
 89
 90	be->image = XShmCreateImage(
 91		be->display, vis, depth, ZPixmap, NULL,
 92		&be->shm, mw->width, mw->height
 93	);
 94	if (!be->image) {
 95		maus_log(stderr, "could not create XImage (shm)");
 96		return false;
 97	}
 98
 99	if (be->image->bits_per_pixel != 32) {
100		XDestroyImage(be->image);
101		be->image = NULL;
102		maus_log(stderr, "XImage not 32bpp");
103		return false;
104	}
105
106	size_t size = be->image->bytes_per_line * be->image->height;
107	be->shm.shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
108	if (be->shm.shmid < 0) {
109		XDestroyImage(be->image);
110		be->image = NULL;
111		maus_log(stderr, "shmget() failed");
112		return false;
113	}
114
115	be->shm.shmaddr = shmat(be->shm.shmid, NULL, 0);
116	if (be->shm.shmaddr == (char*)-1) {
117		shmctl(be->shm.shmid, IPC_RMID, NULL);
118		XDestroyImage(be->image);
119		be->image = NULL;
120		maus_log(stderr, "shmat() failed");
121		return false;
122	}
123
124	be->shm.readOnly = False;
125	be->image->data = be->shm.shmaddr;
126
127	xerrored = 0;
128	int (*old_xerr)(Display*, XErrorEvent*) = XSetErrorHandler(xerr);
129
130	if (!XShmAttach(be->display, &be->shm))
131		xerrored = 1;
132
133	XSync(be->display, False);
134	XSetErrorHandler(old_xerr);
135	if (xerrored) {
136		shmdt(be->shm.shmaddr);
137		shmctl(be->shm.shmid, IPC_RMID, NULL);
138		XDestroyImage(be->image);
139
140		be->image = NULL;
141		mw->fb = NULL;
142		be->shm.shmaddr = NULL;
143		be->shm.shmid = -1;
144		maus_log(stderr, "XShmAttach failed");
145		return false;
146	}
147
148	shmctl(be->shm.shmid, IPC_RMID, NULL);
149
150	be->shmat = true;
151	mw->fb = (uint32_t*) be->image->data;
152	mw->stride = be->image->bytes_per_line / sizeof(uint32_t);
153
154	return true;
155}
156
157static void fb_destroy(Maus* mw)
158{
159	MausBackend* be = &mw->backend;
160	Display* dpy = be->display;
161	if (be->shmat) {
162		XShmDetach(dpy, &be->shm);
163		XSync(dpy, False);
164		be->shmat = false;
165	}
166
167	if (be->image) {
168		XDestroyImage(be->image);
169		be->image = NULL;
170	}
171
172	if (be->shm.shmaddr && be->shm.shmaddr != (char*) -1) {
173		shmdt(be->shm.shmaddr);
174		be->shm.shmaddr = NULL;
175	}
176
177	/* normally already removed after attach but
178	   can check anyways */
179	if (be->shm.shmid >= 0) {
180		shmctl(be->shm.shmid, IPC_RMID, NULL);
181		be->shm.shmid = -1;
182	}
183
184	mw->fb = NULL;
185	mw->stride = 0;
186}
187
188static bool handle_event(XEvent* xev, MausEvent* ev, Maus* mw)
189{
190	MausBackend* be = &mw->backend;
191	uint32_t code = 0;
192	unsigned int mb;
193	MausMouseButton mbtype;
194
195	switch (xev->type) {
196		case Expose:
197			if (xev->xexpose.count != 0)
198				return false;
199			ev->type = MAUS_EV_REDRAW;
200			return true;
201
202		case ClientMessage:
203			if ((Atom)xev->xclient.data.l[0] == be->atoms[MAUS_ATOM_WM_DELETE_WINDOW]) {
204				ev->type = MAUS_EV_CLOSE;
205				return true;
206			}
207			break;
208
209		case MappingNotify:
210			XRefreshKeyboardMapping(&xev->xmapping);
211			return true;
212			break;
213
214		case KeyPress: {
215			ev->type = MAUS_EV_KEY;
216			code = xev->xkey.keycode;
217			ev->key.code = code;
218			ev->key.pressed = true;
219			if (code < MAUS_KEYCODE_LAST)
220			        mw->key_codes[code] = true;
221
222			/* grab the raw, base sym */
223			KeySym sym = XLookupKeysym(&xev->xkey, 0);
224			ev->key.key = keysym_to_mauskey(sym);
225			if (ev->key.key != MAUS_KEY_NONE)
226			        mw->key_syms[ev->key.key] = true;
227
228			char buf[8] = {0};
229			int len = XLookupString(&xev->xkey, buf, sizeof(buf), NULL, NULL);
230			if (len > 0)
231			        ev->key.text = (buf[0] == '\r') ? '\n' : buf[0];
232			else
233			        ev->key.text = 0;
234
235			return true;
236		}
237
238		case KeyRelease: {
239			ev->type = MAUS_EV_KEY;
240			code = xev->xkey.keycode;
241			ev->key.code = code;
242			ev->key.pressed = false;
243			ev->key.text = 0;
244
245			if (code < MAUS_KEYCODE_LAST)
246			        mw->key_codes[code] = false;
247
248			/* unset base sym set on `KeyPress` */
249			KeySym sym = XLookupKeysym(&xev->xkey, 0);
250			ev->key.key = keysym_to_mauskey(sym);
251			if (ev->key.key != MAUS_KEY_NONE)
252			        mw->key_syms[ev->key.key] = false;
253
254			return true;
255		}
256
257		case ButtonPress:
258			ev->type = MAUS_EV_MOUSE_BUTTON;
259			mb = xev->xbutton.button;
260			mbtype = mouse_button_to_maus(mb);
261			ev->mouse.button.button = mbtype;
262			ev->mouse.button.pressed = true;
263			mw->mouse_buttons[mbtype] = true;
264			return true;
265
266		case ButtonRelease:
267			ev->type = MAUS_EV_MOUSE_BUTTON;
268			mb = xev->xbutton.button;
269			mbtype = mouse_button_to_maus(mb);
270			ev->mouse.button.button = mbtype;
271			ev->mouse.button.pressed = false;
272			mw->mouse_buttons[mbtype] = false;
273			return true;
274
275		case MotionNotify:
276			ev->type = MAUS_EV_MOUSE_MOTION;
277			ev->mouse.motion.x = xev->xmotion.x;
278			ev->mouse.motion.y = xev->xmotion.y;
279			return true;
280
281		case ConfigureNotify:
282			ev->type = MAUS_EV_RESIZE;
283			ev->resize.width = xev->xconfigure.width;
284			ev->resize.height = xev->xconfigure.height;
285			return true;
286
287		case SelectionRequest: {
288			XSelectionRequestEvent* req = &xev->xselectionrequest;
289			XEvent rsp; /* response */
290
291			Atom utf8_string = XInternAtom(be->display, "UTF8_STRING", False);
292			Atom clipboard = XInternAtom(be->display, "CLIPBOARD", False);
293
294			rsp.type = SelectionNotify;
295			rsp.xselection.display = req->display;
296			rsp.xselection.requestor = req->requestor;
297			rsp.xselection.selection = req->selection;
298			rsp.xselection.target = req->target;
299			rsp.xselection.property = None; 
300			rsp.xselection.time = req->time;
301
302			/* assign clipboard text to requesting programs
303			   window property */
304			if (req->selection == clipboard &&
305			    req->target == utf8_string &&
306			    mw->clipboard) {
307			        XChangeProperty(
308					req->display, req->requestor,
309					req->property, utf8_string, 8,
310					PropModeReplace, (unsigned char*)mw->clipboard, 
311					strlen(mw->clipboard)
312			 	);
313				rsp.xselection.property = req->property; 
314			}
315
316			/* send reply back to the prog requesting paste */
317			XSendEvent(req->display, req->requestor, False, 0, &rsp);
318			XFlush(be->display);
319
320			return false; /* skip maus polling it */
321		}
322	}
323
324	return false;
325}
326
327static MausKey keysym_to_mauskey(KeySym sym)
328{
329	if (sym >= 0x20 && sym <= 0x7E)
330		return (MausKey)sym;
331
332	if (sym >= XK_F1 && sym <= XK_F12)
333		return MAUS_KEY_F1 + (sym - XK_F1);
334	if (sym >= XK_KP_0 && sym <= XK_KP_9)
335		return MAUS_KEY_KP_0 + (sym - XK_KP_0);
336
337	for (size_t i = 0; i < sizeof(keymap)/ sizeof(keymap[0]); i++) {
338		if (keymap[i].x11 == sym)
339			return keymap[i].maus;
340	}
341
342	return MAUS_KEY_NONE;
343}
344
345static MausMouseButton mouse_button_to_maus(int btn)
346{
347	switch (btn) {
348		case Button1: return MAUS_MOUSE_BUTTON_LEFT;
349		case Button2: return MAUS_MOUSE_BUTTON_MIDDLE;
350		case Button3: return MAUS_MOUSE_BUTTON_RIGHT;
351		case Button4: return MAUS_MOUSE_BUTTON_SCROLL_UP;
352		case Button5: return MAUS_MOUSE_BUTTON_SCROLL_DOWN;
353		default:      return MAUS_MOUSE_BUTTON_NONE;
354	}
355}
356
357static int xerr(Display* dpy, XErrorEvent* ev)
358{
359	(void) dpy;
360	(void) ev;
361	xerrored = 1;
362	return 0;
363}
364
365void maus_clipboard_set_text(Maus* mw, const char* text)
366{
367	MausBackend* be = &mw->backend;
368	if (!be->display || be->win == None)
369		return;
370
371	/* clear old entry */
372	if (mw->clipboard) {
373		free(mw->clipboard);
374		mw->clipboard = NULL;
375	}
376
377	if (text)
378		mw->clipboard = maus_strdup(text);
379
380	Atom clipboard = XInternAtom(be->display, "CLIPBOARD", False);
381	XSetSelectionOwner(be->display, clipboard, be->win, CurrentTime);
382	XFlush(be->display);
383}
384
385char* maus_clipboard_get_text(Maus* mw)
386{
387	MausBackend* be = &mw->backend;
388	Atom clipboard = XInternAtom(be->display, "CLIPBOARD", False);
389	Atom utf8_string = XInternAtom(be->display, "UTF8_STRING", False);
390	Atom maus_clipboard_prop = XInternAtom(be->display, "MAUS_CLIPBOARD_PROP", False);
391
392	/* fetch clipboard data to `maus_clipboard_prop`
393	   window property */
394	XConvertSelection(
395		be->display, clipboard, utf8_string,
396		maus_clipboard_prop, be->win, CurrentTime
397	);
398	XFlush(be->display);
399
400	/* check for selection response */
401	XEvent xev;
402	bool ok = false;
403	for (int timeout = 0; timeout < 10000; timeout++) { 
404		if (XCheckTypedWindowEvent(be->display, be->win, SelectionNotify, &xev)) {
405			if (xev.xselection.property != None)
406				ok = true;
407			break;
408		}
409	}
410	if (!ok)
411		return NULL;
412
413	/* read string off property */
414	Atom actual_type;
415	int actual_fmt;
416	unsigned long nitems;
417	unsigned long bytes_after;
418	unsigned char* prop_data = NULL;
419	XGetWindowProperty(
420		be->display, be->win, maus_clipboard_prop, 0,LONG_MAX,
421		True, utf8_string, &actual_type, &actual_fmt, &nitems,
422		&bytes_after, &prop_data
423	);
424
425	char* res = NULL;
426	if (prop_data) {
427		res = maus_strdup((char*)prop_data);
428		XFree(prop_data);
429	}
430
431	return res;
432}
433
434void maus_close(Maus* mw)
435{
436	MausBackend* be = &mw->backend;
437	fb_destroy(mw);
438	if (mw->bfb) {
439		free(mw->bfb);
440		mw->bfb = NULL;
441	}
442	if (mw->clipboard) {
443		free(mw->clipboard);
444		mw->clipboard = NULL;
445	}
446	if (be->win != None) {
447		(void) maus_close_window(mw);
448	}
449	if (be->display)
450		XCloseDisplay(be->display);
451}
452
453bool maus_close_window(Maus* mw)
454{
455	MausBackend* be = &mw->backend;
456	if (be->gc) {
457		XFreeGC(be->display, be->gc);
458		be->gc = 0;
459	}
460
461	if (be->win != None) {
462		XUnmapWindow(be->display, be->win);
463		XDestroyWindow(be->display, be->win);
464	}
465
466	return true;
467}
468
469bool maus_create_window(Maus* mw)
470{
471	MausBackend* be = &mw->backend;
472	Display* dpy = be->display;
473	Window* win = &be->win;
474
475	*win = XCreateSimpleWindow(
476		dpy, be->root,
477		mw->x, mw->y,
478		mw->width, mw->height,
479		0u, 0u, 0u
480	);
481
482	if (*win == None)
483		return false;
484
485	XSelectInput(dpy, *win,
486		ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask |
487		ButtonReleaseMask | PointerMotionMask | StructureNotifyMask
488	);
489	/* no black flashing when resizing */
490	XSetWindowBackgroundPixmap(dpy, *win, None);
491
492	/* atoms */
493	be->atoms[MAUS_ATOM_WM_DELETE_WINDOW] = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
494
495	XSetWMProtocols(dpy, *win, &be->atoms[MAUS_ATOM_WM_DELETE_WINDOW], 1);
496	XStoreName(dpy, *win, mw->title);
497
498	XMapWindow(dpy, *win);
499	XFlush(dpy);
500
501	be->gc = XCreateGC(dpy, *win, 0, NULL);
502	if (!be->gc) {
503		maus_log(stderr, "failed to create window GC");
504		maus_close_window(mw);
505		return false;
506	}
507
508	return true;
509}
510
511void maus_clear(Maus* mw, MausColor col)
512{
513	uint32_t col_up = MAUS_UNPACK_COL(col);
514	uint32_t pxs = mw->height * mw->width;
515	for (uint32_t i = 0; i < pxs; i++)
516		mw->bfb[i] = col_up;
517}
518
519Maus* maus_init(const char* title, int x, int y, int width, int height)
520{
521	if (MAUS_WARN_BACKEND_AUTO_SEL)
522		maus_log(stderr, "backend auto-selected: X11");
523
524	Display* d = XOpenDisplay(NULL);
525	if (!d) {
526		maus_die("failed to open X11 display");
527		return NULL;
528	}
529
530	Maus* mw = calloc(1, sizeof(Maus));
531	MausBackend* be = &mw->backend;
532	be->display = d;
533	be->root = DefaultRootWindow(d);
534	be->win = None;
535	mw->frame_time_last = maus_get_time_ns();
536	mw->title = title;
537	mw->width = width;
538	mw->height = height;
539	mw->x = x;
540	mw->y = y;
541	mw->fb = NULL;
542
543	be->gc = 0;
544	be->image = NULL;
545	be->shmat = false;
546	be->shm.shmid = -1;
547	be->shm.shmaddr = NULL;
548	if (!fb_create(mw)) {
549		maus_log(stderr, "failed to create framebuffer");
550		maus_close(mw);
551		free(mw);
552		return NULL;
553	}
554	mw->bfb = calloc(mw->stride * mw->height, sizeof(uint32_t));
555	if (!mw->bfb) {
556		maus_log(stderr, "failed to allocate back buffer");
557		maus_close(mw);
558		free(mw);
559		return NULL;
560	}
561
562	return mw;
563}
564
565bool maus_event_poll(Maus* mw, MausEvent* ev)
566{
567	MausBackend* be = &mw->backend;
568	XEvent xev;
569	while (XPending(be->display)) {
570		XNextEvent(be->display, &xev);
571		ev->type = MAUS_EV_NONE;
572		if (handle_event(&xev, ev, mw))
573			return true;
574
575	}
576
577	return false;
578}
579
580void maus_event_wait(Maus* mw, MausEvent* ev)
581{
582	MausBackend* be = &mw->backend;
583	XEvent xev;
584	for (;;) {
585		XNextEvent(be->display, &xev);
586		ev->type = MAUS_EV_NONE;
587		if (handle_event(&xev, ev, mw))
588			return;
589	}
590}
591
592void maus_present(Maus* mw)
593{
594	MausBackend* be = &mw->backend;
595	if (!be->image || be->win == None)
596		return;
597
598	uint32_t bytes = mw->stride * mw->height * sizeof(uint32_t);
599	memcpy(mw->fb, mw->bfb, bytes);
600
601	XShmPutImage(
602		be->display, be->win, be->gc, be->image,
603		0, 0, 0, 0, mw->width, mw->height, False
604	);
605
606	XFlush(be->display);
607}
608
609bool maus_resize(Maus* mw, uint32_t width, uint32_t height)
610{
611	MausBackend* be = &mw->backend;
612	if (width == 0 || height == 0 ||
613	    (mw->width == width && mw->height == height))
614		return false;
615
616	fb_destroy(mw);
617	if (mw->bfb) {
618		free(mw->bfb);
619		mw->bfb = NULL;
620	}
621
622	mw->width = width;
623	mw->height = height;
624
625	XSync(be->display, False);
626	XFlush(be->display);
627
628	if (!fb_create(mw))
629		return false;
630
631	mw->bfb = calloc(mw->stride * mw->height, sizeof(uint32_t));
632	if (!mw->bfb)
633		return false;
634
635	return true;
636}
637
638void maus_cur_set_mode(Maus* mw, MausCursorState state)
639{
640	MausBackend* be = &mw->backend;
641	Display* dpy = be->display;
642	Window win = be->win;
643
644	if (!dpy) {
645		maus_log(stderr, "display is NULL");
646		return;
647	}
648	if (win == None) {
649		maus_log(stderr, "win is None");
650		return;
651	}
652
653	/* show cursor */
654	if (state == MAUS_CURSOR_STATE_VISIBLE) {
655		XUndefineCursor(dpy, win);
656		XFlush(dpy);
657		return;
658	}
659
660	/* hide cursor */
661	if (state == MAUS_CURSOR_STATE_HIDDEN) {
662		Pixmap blank_pm = XCreatePixmap(dpy, win, 1, 1, 1);
663		XColor black = {.red=0, .green=0, .blue=0};
664		Cursor blank_cur = XCreatePixmapCursor(
665			dpy, blank_pm, blank_pm, &black, &black, 0, 0
666		);
667		XFreePixmap(dpy, blank_pm);
668
669		XDefineCursor(dpy, win, blank_cur);
670		XFreeCursor(dpy, blank_cur);
671		XFlush(dpy);
672		return;
673	}
674
675	/* lock cursor */
676	if (state == MAUS_CURSOR_STATE_LOCKED) {
677		Mask mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
678		int grab = XGrabPointer(
679			dpy, win, True, mask, GrabModeAsync,
680			GrabModeAsync, win, None, CurrentTime
681		);
682
683		if (grab != GrabSuccess) {
684			maus_log(stderr, "failed to grab mouse pointer");
685			return;
686		}
687
688		/* TODO keep position just snap to window dimensions */
689		XWarpPointer(dpy, None, win, 0, 0, 0, 0, mw->width/2, mw->height/2);
690		XFlush(dpy);
691		return;
692	}
693
694	/* unlock cursor */
695	if (state == MAUS_CURSOR_STATE_FREE) {
696		XUngrabPointer(dpy, CurrentTime);
697		XFlush(dpy);
698		return;
699	}
700}
701