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 }