1 /**
2  * Control, screen-scrape, and send input to other
3  * graphical programs.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <vladimir@thecybershadow.net>
13  */
14 
15 module ae.sys.sendinput;
16 
17 import core.thread;
18 import core.time;
19 
20 import std.exception;
21 import std.random;
22 import std.string;
23 
24 import ae.utils.graphics.color;
25 import ae.utils.graphics.image;
26 import ae.utils.geometry : Rect;
27 import ae.utils.math;
28 
29 version (linux)
30 {
31 	enum haveX11 = is(typeof({ import deimos.X11.X; }));
32 	//version = HAVE_X11;
33 
34 	static if (haveX11)
35 	{
36 		pragma(lib, "X11");
37 
38 		import deimos.X11.X;
39 		import deimos.X11.Xlib;
40 		import deimos.X11.Xutil;
41 	}
42 
43 	import std.conv;
44 	import std.process;
45 
46 	static if (haveX11)
47 	{
48 		private static Display* dpy;
49 		Display* getDisplay()
50 		{
51 			if (!dpy)
52 				dpy = XOpenDisplay(null);
53 			enforce(dpy, "Can't open display!");
54 			return dpy;
55 		}
56 		static ~this()
57 		{
58 			if (dpy)
59 				XCloseDisplay(dpy);
60 			dpy = null;
61 		}
62 	}
63 
64 	void setMousePos(int x, int y)
65 	{
66 		static if (haveX11)
67 		{
68 			auto dpy = getDisplay();
69 			auto rootWindow = XRootWindow(dpy, 0);
70 			XSelectInput(dpy, rootWindow, KeyReleaseMask);
71 			XWarpPointer(dpy, None, rootWindow, 0, 0, 0, 0, x, y);
72 			XFlush(dpy);
73 		}
74 		else
75 			enforce(spawnProcess(["xdotool", "mousemove", text(x), text(y)]).wait() == 0, "xdotool failed");
76 	}
77 
78 	static if (haveX11)
79 		alias Window = deimos.X11.X.Window;
80 	else
81 		alias Window = uint;
82 
83 	auto captureWindow(Window window)
84 	{
85 		static if (haveX11)
86 		{
87 			auto g = getWindowGeometry(window);
88 			return captureWindowRect(window, Rect!int(0, 0, g.w, g.h));
89 		}
90 		else
91 		{
92 			auto result = execute(["import", "-window", text(window), "bmp:-"]);
93 			enforce(result.status == 0, "ImageMagick import failed");
94 			return result.output.parseBMP!BGR();
95 		}
96 	}
97 
98 	void captureWindowRect(Window window, Rect!int r, ref Image!BGRA image)
99 	{
100 		static if (haveX11)
101 		{
102 			auto dpy = getDisplay();
103 			auto ximage = XGetImage(dpy, window, r.x0, r.y0, r.w, r.h, AllPlanes, ZPixmap).xEnforce("XGetImage");
104 			scope(exit) XDestroyImage(ximage);
105 
106 			enforce(ximage.format == ZPixmap, "Wrong image format (expected ZPixmap)");
107 			enforce(ximage.bits_per_pixel == 32, "Wrong image bits_per_pixel (expected 32)");
108 
109 			alias COLOR = BGRA;
110 			ImageRef!COLOR(ximage.width, ximage.height, ximage.chars_per_line, cast(PlainStorageUnit!COLOR*) ximage.data).copy(image);
111 		}
112 		else
113 			assert(false, "TODO");
114 	}
115 
116 	auto captureWindowRect(Window window, Rect!int r)
117 	{
118 		Image!BGRA image;
119 		captureWindowRect(window, r, image);
120 		return image;
121 	}
122 
123 	auto captureRect(Rect!int r, ref Image!BGRA image)
124 	{
125 		static if (haveX11)
126 		{
127 			auto dpy = getDisplay();
128 			return captureWindowRect(RootWindow(dpy, DefaultScreen(dpy)), r, image);
129 		}
130 		else
131 			assert(false, "TODO");
132 	}
133 
134 	auto captureRect(Rect!int r)
135 	{
136 		Image!BGRA image;
137 		captureRect(r, image);
138 		return image;
139 	}
140 
141 	auto getPixel(int x, int y)
142 	{
143 		static if (haveX11)
144 		{
145 			static Image!BGRA r;
146 			captureRect(Rect!int(x, y, x+1, y+1), r);
147 			return r[0, 0];
148 		}
149 		else
150 			assert(false, "TODO");
151 	}
152 
153 	Window findWindowByName(string name)
154 	{
155 		// TODO haveX11
156 		auto result = execute(["xdotool", "search", "--name", name]);
157 		enforce(result.status == 0, "xdotool failed");
158 		return result.output.chomp.to!Window;
159 	}
160 
161 	static if (haveX11)
162 	Rect!int getWindowGeometry(Window window)
163 	{
164 		auto dpy = getDisplay();
165 		Window child;
166 		XWindowAttributes xwa;
167 		XGetWindowAttributes(dpy, window, &xwa).xEnforce("XGetWindowAttributes");
168 		XTranslateCoordinates(dpy, window, XRootWindow(dpy, 0), xwa.x, xwa.y, &xwa.x, &xwa.y, &child).xEnforce("XTranslateCoordinates");
169 		return Rect!int(xwa.x, xwa.y, xwa.x + xwa.width, xwa.y + xwa.height);
170 	}
171 
172 	float ease(float t, float speed)
173 	{
174 		import std.math : pow, abs;
175 		speed = 0.3f + speed * 0.4f;
176 		t = t * 2 - 1;
177 		t = (1-pow(1-abs(t), 1/speed)) * sign(t);
178 		t = (t + 1) / 2;
179 		return t;
180 	}
181 
182 	static if (haveX11)
183 	T xEnforce(T)(T cond, string msg)
184 	{
185 		return enforce(cond, msg);
186 	}
187 
188 	void easeMousePos(int x0, int y0, int x1, int y1, Duration duration)
189 	{
190 		auto xSpeed = uniform01!float;
191 		auto ySpeed = uniform01!float;
192 
193 		auto start = MonoTime.currTime();
194 		auto end = start + duration;
195 		while (true)
196 		{
197 			auto now = MonoTime.currTime();
198 			if (now >= end)
199 				break;
200 			float t = 1f * (now - start).total!"hnsecs" / duration.total!"hnsecs";
201 			setMousePos(
202 				x0 + cast(int)(ease(t, xSpeed) * (x1 - x0)),
203 				y0 + cast(int)(ease(t, ySpeed) * (y1 - y0)),
204 			);
205 			Thread.sleep(1.msecs);
206 		}
207 		x0 = x1;
208 		y0 = y1;
209 
210 		setMousePos(x1, y1);
211 	}
212 
213 	void mouseButton(int button, bool down)
214 	{
215 		// TODO haveX11
216 		enforce(spawnProcess(["xdotool", down ? "mousedown" : "mouseup", text(button)]).wait() == 0, "xdotool failed");
217 	}
218 }
219 
220 version (Windows)
221 {
222 	// TODO
223 }