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 }