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