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 <ae@cy.md>
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 import ae.utils.regex : escapeRE;
29 
30 version (linux)
31 {
32 	/// Are X11 bindings available?
33 	enum haveX11 = is(typeof({ import deimos.X11.X; }));
34 	//version = HAVE_X11;
35 
36 	static if (haveX11)
37 	{
38 		pragma(lib, "X11");
39 
40 		import deimos.X11.X;
41 		import deimos.X11.Xlib;
42 		import deimos.X11.Xutil;
43 	}
44 
45 	import std.conv;
46 	import std.process;
47 
48 	static if (haveX11)
49 	{
50 		private static Display* dpy;
51 
52 		/// Get X Display, connecting to the X server first
53 		/// if that hasn't been done yet in this thread.
54 		/// The connection is closed automatically on thread exit.
55 		Display* getDisplay()
56 		{
57 			if (!dpy)
58 				dpy = XOpenDisplay(null);
59 			enforce(dpy, "Can't open display!");
60 			return dpy;
61 		}
62 		static ~this()
63 		{
64 			if (dpy)
65 				XCloseDisplay(dpy);
66 			dpy = null;
67 		}
68 	}
69 
70 	/// Move the mouse cursor.
71 	void setMousePos(int x, int y)
72 	{
73 		static if (haveX11)
74 		{
75 			auto dpy = getDisplay();
76 			auto rootWindow = XRootWindow(dpy, 0);
77 			XSelectInput(dpy, rootWindow, KeyReleaseMask);
78 			XWarpPointer(dpy, None, rootWindow, 0, 0, 0, 0, x, y);
79 			XFlush(dpy);
80 		}
81 		else
82 			enforce(spawnProcess(["xdotool", "mousemove", text(x), text(y)]).wait() == 0, "xdotool failed");
83 	}
84 
85 	int[2] getMousePos()
86 	{
87 		// TODO static if (haveX11)
88 		import ae.sys.cmd : query;
89 		import std.algorithm.searching : skipOver;
90 		auto s = query(["xdotool", "getmouselocation"]);
91 		auto t = s.split();
92 		enforce(t.length == 4);
93 		enforce(t[0].skipOver("x:"));
94 		enforce(t[1].skipOver("y:"));
95 		return [t[0].to!int, t[1].to!int];
96 	}
97 
98 	/// Type used for window IDs.
99 	static if (haveX11)
100 		alias Window = deimos.X11.X.Window;
101 	else
102 		alias Window = uint;
103 
104 	/// Return a screen capture of the given window.
105 	auto captureWindow(Window window)
106 	{
107 		static if (haveX11)
108 		{
109 			auto g = getWindowGeometry(window);
110 			return captureWindowRect(window, Rect!int(0, 0, g.w, g.h));
111 		}
112 		else
113 		{
114 			auto result = execute(["import", "-window", text(window), "bmp:-"]);
115 			enforce(result.status == 0, "ImageMagick import failed");
116 			return result.output.parseBMP!BGR();
117 		}
118 	}
119 
120 	/// Return a screen capture of some sub-rectangle of the given window.
121 	void captureWindowRect(Window window, Rect!int r, ref Image!BGRA image)
122 	{
123 		static if (haveX11)
124 		{
125 			auto dpy = getDisplay();
126 			auto ximage = XGetImage(dpy, window, r.x0, r.y0, r.w, r.h, AllPlanes, ZPixmap).xEnforce("XGetImage");
127 			scope(exit) XDestroyImage(ximage);
128 
129 			enforce(ximage.format == ZPixmap, "Wrong image format (expected ZPixmap)");
130 			enforce(ximage.bits_per_pixel == 32, "Wrong image bits_per_pixel (expected 32)");
131 
132 			alias COLOR = BGRA;
133 			ImageRef!COLOR(ximage.width, ximage.height, ximage.chars_per_line, cast(PlainStorageUnit!COLOR*) ximage.data).copy(image);
134 		}
135 		else
136 			assert(false, "TODO");
137 	}
138 
139 	/// ditto
140 	auto captureWindowRect(Window window, Rect!int r)
141 	{
142 		Image!BGRA image;
143 		captureWindowRect(window, r, image);
144 		return image;
145 	}
146 
147 	/// Return a capture of some sub-rectangle of the screen.
148 	auto captureRect(Rect!int r, ref Image!BGRA image)
149 	{
150 		static if (haveX11)
151 		{
152 			auto dpy = getDisplay();
153 			return captureWindowRect(RootWindow(dpy, DefaultScreen(dpy)), r, image);
154 		}
155 		else
156 			assert(false, "TODO");
157 	}
158 
159 	/// ditto
160 	auto captureRect(Rect!int r)
161 	{
162 		Image!BGRA image;
163 		captureRect(r, image);
164 		return image;
165 	}
166 
167 	/// Read a single pixel.
168 	auto getPixel(int x, int y)
169 	{
170 		static if (haveX11)
171 		{
172 			static Image!BGRA r;
173 			captureRect(Rect!int(x, y, x+1, y+1), r);
174 			return r[0, 0];
175 		}
176 		else
177 			assert(false, "TODO");
178 	}
179 
180 	/// Find a window using its name.
181 	/// Throws an exception if there are no results, or there is more than one match.
182 	Window findWindowByName(string name)
183 	{
184 		// TODO haveX11
185 		auto result = execute(["xdotool", "search", "--name", "^" ~ escapeRE(name) ~ "$"]);
186 		enforce(result.status == 0, "xdotool failed");
187 		return result.output.chomp.to!Window;
188 	}
189 
190 	/// Obtain a window's coordinates on the screen.
191 	static if (haveX11)
192 	Rect!int getWindowGeometry(Window window)
193 	{
194 		auto dpy = getDisplay();
195 		Window child;
196 		XWindowAttributes xwa;
197 		XGetWindowAttributes(dpy, window, &xwa).xEnforce("XGetWindowAttributes");
198 		XTranslateCoordinates(dpy, window, XRootWindow(dpy, 0), xwa.x, xwa.y, &xwa.x, &xwa.y, &child).xEnforce("XTranslateCoordinates");
199 		return Rect!int(xwa.x, xwa.y, xwa.x + xwa.width, xwa.y + xwa.height);
200 	}
201 
202 	private float ease(float t, float speed)
203 	{
204 		import std.math : pow, abs;
205 		speed = 0.3f + speed * 0.4f;
206 		t = t * 2 - 1;
207 		t = (1-pow(1-abs(t), 1/speed)) * sign(t);
208 		t = (t + 1) / 2;
209 		return t;
210 	}
211 
212 	static if (haveX11)
213 	private T xEnforce(T)(T cond, string msg)
214 	{
215 		return enforce(cond, msg);
216 	}
217 
218 	/// Smoothly move the mouse from one coordinate to another.
219 	void easeMousePos(int x0, int y0, int x1, int y1, Duration duration)
220 	{
221 		auto xSpeed = uniform01!float;
222 		auto ySpeed = uniform01!float;
223 
224 		auto start = MonoTime.currTime();
225 		auto end = start + duration;
226 		while (true)
227 		{
228 			auto now = MonoTime.currTime();
229 			if (now >= end)
230 				break;
231 			float t = 1f * (now - start).total!"hnsecs" / duration.total!"hnsecs";
232 			setMousePos(
233 				x0 + cast(int)(ease(t, xSpeed) * (x1 - x0)),
234 				y0 + cast(int)(ease(t, ySpeed) * (y1 - y0)),
235 			);
236 			Thread.sleep(1.msecs);
237 		}
238 		x0 = x1;
239 		y0 = y1;
240 
241 		setMousePos(x1, y1);
242 	}
243 
244 	/// Send a mouse button press or release.
245 	void mouseButton(int button, bool down)
246 	{
247 		// TODO haveX11
248 		enforce(spawnProcess(["xdotool", down ? "mousedown" : "mouseup", text(button)]).wait() == 0, "xdotool failed");
249 	}
250 }
251 
252 version (Windows)
253 {
254 	// TODO
255 }