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 	/// Type used for window IDs.
86 	static if (haveX11)
87 		alias Window = deimos.X11.X.Window;
88 	else
89 		alias Window = uint;
90 
91 	/// Return a screen capture of the given window.
92 	auto captureWindow(Window window)
93 	{
94 		static if (haveX11)
95 		{
96 			auto g = getWindowGeometry(window);
97 			return captureWindowRect(window, Rect!int(0, 0, g.w, g.h));
98 		}
99 		else
100 		{
101 			auto result = execute(["import", "-window", text(window), "bmp:-"]);
102 			enforce(result.status == 0, "ImageMagick import failed");
103 			return result.output.parseBMP!BGR();
104 		}
105 	}
106 
107 	/// Return a screen capture of some sub-rectangle of the given window.
108 	void captureWindowRect(Window window, Rect!int r, ref Image!BGRA image)
109 	{
110 		static if (haveX11)
111 		{
112 			auto dpy = getDisplay();
113 			auto ximage = XGetImage(dpy, window, r.x0, r.y0, r.w, r.h, AllPlanes, ZPixmap).xEnforce("XGetImage");
114 			scope(exit) XDestroyImage(ximage);
115 
116 			enforce(ximage.format == ZPixmap, "Wrong image format (expected ZPixmap)");
117 			enforce(ximage.bits_per_pixel == 32, "Wrong image bits_per_pixel (expected 32)");
118 
119 			alias COLOR = BGRA;
120 			ImageRef!COLOR(ximage.width, ximage.height, ximage.chars_per_line, cast(PlainStorageUnit!COLOR*) ximage.data).copy(image);
121 		}
122 		else
123 			assert(false, "TODO");
124 	}
125 
126 	/// ditto
127 	auto captureWindowRect(Window window, Rect!int r)
128 	{
129 		Image!BGRA image;
130 		captureWindowRect(window, r, image);
131 		return image;
132 	}
133 
134 	/// Return a capture of some sub-rectangle of the screen.
135 	auto captureRect(Rect!int r, ref Image!BGRA image)
136 	{
137 		static if (haveX11)
138 		{
139 			auto dpy = getDisplay();
140 			return captureWindowRect(RootWindow(dpy, DefaultScreen(dpy)), r, image);
141 		}
142 		else
143 			assert(false, "TODO");
144 	}
145 
146 	/// ditto
147 	auto captureRect(Rect!int r)
148 	{
149 		Image!BGRA image;
150 		captureRect(r, image);
151 		return image;
152 	}
153 
154 	/// Read a single pixel.
155 	auto getPixel(int x, int y)
156 	{
157 		static if (haveX11)
158 		{
159 			static Image!BGRA r;
160 			captureRect(Rect!int(x, y, x+1, y+1), r);
161 			return r[0, 0];
162 		}
163 		else
164 			assert(false, "TODO");
165 	}
166 
167 	/// Find a window using its name.
168 	/// Throws an exception if there are no results, or there is more than one match.
169 	Window findWindowByName(string name)
170 	{
171 		// TODO haveX11
172 		auto result = execute(["xdotool", "search", "--name", "^" ~ escapeRE(name) ~ "$"]);
173 		enforce(result.status == 0, "xdotool failed");
174 		return result.output.chomp.to!Window;
175 	}
176 
177 	/// Obtain a window's coordinates on the screen.
178 	static if (haveX11)
179 	Rect!int getWindowGeometry(Window window)
180 	{
181 		auto dpy = getDisplay();
182 		Window child;
183 		XWindowAttributes xwa;
184 		XGetWindowAttributes(dpy, window, &xwa).xEnforce("XGetWindowAttributes");
185 		XTranslateCoordinates(dpy, window, XRootWindow(dpy, 0), xwa.x, xwa.y, &xwa.x, &xwa.y, &child).xEnforce("XTranslateCoordinates");
186 		return Rect!int(xwa.x, xwa.y, xwa.x + xwa.width, xwa.y + xwa.height);
187 	}
188 
189 	private float ease(float t, float speed)
190 	{
191 		import std.math : pow, abs;
192 		speed = 0.3f + speed * 0.4f;
193 		t = t * 2 - 1;
194 		t = (1-pow(1-abs(t), 1/speed)) * sign(t);
195 		t = (t + 1) / 2;
196 		return t;
197 	}
198 
199 	static if (haveX11)
200 	private T xEnforce(T)(T cond, string msg)
201 	{
202 		return enforce(cond, msg);
203 	}
204 
205 	/// Smoothly move the mouse from one coordinate to another.
206 	void easeMousePos(int x0, int y0, int x1, int y1, Duration duration)
207 	{
208 		auto xSpeed = uniform01!float;
209 		auto ySpeed = uniform01!float;
210 
211 		auto start = MonoTime.currTime();
212 		auto end = start + duration;
213 		while (true)
214 		{
215 			auto now = MonoTime.currTime();
216 			if (now >= end)
217 				break;
218 			float t = 1f * (now - start).total!"hnsecs" / duration.total!"hnsecs";
219 			setMousePos(
220 				x0 + cast(int)(ease(t, xSpeed) * (x1 - x0)),
221 				y0 + cast(int)(ease(t, ySpeed) * (y1 - y0)),
222 			);
223 			Thread.sleep(1.msecs);
224 		}
225 		x0 = x1;
226 		y0 = y1;
227 
228 		setMousePos(x1, y1);
229 	}
230 
231 	/// Send a mouse button press or release.
232 	void mouseButton(int button, bool down)
233 	{
234 		// TODO haveX11
235 		enforce(spawnProcess(["xdotool", down ? "mousedown" : "mouseup", text(button)]).wait() == 0, "xdotool failed");
236 	}
237 }
238 
239 version (Windows)
240 {
241 	// TODO
242 }