"""
Highlevel scripting API, requires xautomation to be installed
"""
import time
import os
import subprocess
import tempfile
import imghdr
import struct
[docs]
class PatternNotFound(Exception):
"""Exception raised by functions"""
pass
# numeric representation of the mouse buttons. For use in visgrep.
LEFT = 1
"""Left mouse button"""
MIDDLE = 2
"""Middle mouse button"""
RIGHT = 3
"""Right mouse button"""
[docs]
def visgrep(scr: str, pat: str, tolerance: int = 0) -> int:
"""
Usage: C{visgrep(scr: str, pat: str, tolerance: int = 0) -> int}
Visual grep of scr for pattern pat.
Requires xautomation (http://hoopajoo.net/projects/xautomation.html).
Usage: C{visgrep("screen.png", "pat.png")}
@param scr: path of PNG image to be grepped.
@param pat: path of pattern image (PNG) to look for in scr.
@param tolerance: An integer ≥ 0 to specify the level of tolerance for 'fuzzy' matches.
@raise ValueError: Raised if tolerance is negative or not convertable to int
@raise PatternNotFound: Raised if C{pat} not found.
@raise FileNotFoundError: Raised if either file is not found
@returns: Coordinates of the topleft point of the match, if any. Raises L{PatternNotFound} exception otherwise.
"""
tol = int(tolerance)
if tol < 0:
raise ValueError("tolerance must be ≥ 0.")
with open(scr), open(pat):
pass
with tempfile.NamedTemporaryFile() as f:
subprocess.call(['png2pat', pat], stdout=f)
# don't use check_call, some versions (1.05) have a missing return statement in png2pat.c so the exit status ≠ 0
f.flush()
os.fsync(f.fileno())
vg = subprocess.Popen(['visgrep', '-t' + str(tol), scr, f.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = vg.communicate()
coord_str = out[0].decode().split(' ')[0].split(',')
try:
coord = [int(coord_str[0]), int(coord_str[1])]
except (ValueError, IndexError) as e:
raise PatternNotFound(str([x.decode() for x in out]) + '\n\t' + repr(e))
return coord
[docs]
def get_png_dim(filepath: str) -> int:
"""
Usage: C{get_png_dim(filepath:str) -> (int)}
Finds the dimension of a PNG.
@param filepath: file path of the PNG.
@returns: (width, height).
@raise Exception: Raised if the file is not a png
"""
if not imghdr.what(filepath) == 'png':
raise Exception("not PNG")
head = open(filepath, 'rb').read(24)
return struct.unpack('!II', head[16:24])
[docs]
def mouse_move(x: int, y: int, display: str=''):
"""
Moves the mouse using xte C{mousemove} from xautomation
@param x: x location to move the mouse to
@param y: y location to move the mouse to
@param display: X display to pass to C{xte}
"""
subprocess.call(['xte', '-x', display, "mousemove {} {}".format(int(x), int(y))])
[docs]
def mouse_rmove(x: int, y: int, display: str=''):
"""
Moves the mouse using xte C{mousermove} command from xautomation
@param x: x location to move the mouse to
@param y: y location to move the mouse to
@param display: X display to pass to C{xte}
"""
subprocess.call(['xte', '-x', display, "mousermove {} {}".format(int(x), int(y))])
[docs]
def mouse_click(button: int, display: str=''):
"""
Clicks the mouse in the current location using xte C{mouseclick} from xautomation
@param button: Which button signal to send from the mouse
@param display: X display to pass to C{xte}
"""
subprocess.call(['xte', '-x', display, "mouseclick {}".format(int(button))])
[docs]
def mouse_pos():
"""
Returns the current location of the mouse.
@returns: Returns the mouse location in a C{list}
"""
tmp = subprocess.check_output("xmousepos").decode().split()
return list(map(int, tmp))[:2]
[docs]
def click_on_pat(pat: str, mousebutton: int=1, offset: (float, float)=None, tolerance: int=0, restore_pos: bool=False) -> None:
"""
Requires C{imagemagick}, C{xautomation}, C{xwd}.
Click on a pattern at a specified offset (x,y) in percent of the pattern dimension. x is the horizontal distance from the top left corner, y is the vertical distance from the top left corner. By default, the offset is (50,50), which means that the center of the pattern will be clicked at.
@param pat: path of pattern image (PNG) to click on.
@param mousebutton: mouse button number used for the click
@param offset: offset from the top left point of the match. (float,float)
@param tolerance: An integer ≥ 0 to specify the level of tolerance for 'fuzzy' matches. If negative or not convertible to int, raises ValueError.
@param restore_pos: return to the initial mouse position after the click.
@raises: L{PatternNotFound}: Raised when the pattern is not found on the screen
"""
x0, y0 = mouse_pos()
move_to_pat(pat, offset, tolerance)
mouse_click(mousebutton)
if restore_pos:
mouse_move(x0, y0)
[docs]
def move_to_pat(pat: str, offset: (float, float)=None, tolerance: int=0) -> None:
"""See L{click_on_pat}"""
with tempfile.NamedTemporaryFile() as f:
subprocess.call('''
xwd -root -silent -display :0 |
convert xwd:- png:''' + f.name, shell=True)
loc = visgrep(f.name, pat, tolerance)
pat_size = get_png_dim(pat)
if offset is None:
x, y = [l + ps//2 for l, ps in zip(loc, pat_size)]
else:
x, y = [l + ps*(off/100) for off, l, ps in zip(offset, loc, pat_size)]
mouse_move(x, y)
[docs]
def acknowledge_gnome_notification():
"""
Moves mouse pointer to the bottom center of the screen and clicks on it.
"""
x0, y0 = mouse_pos()
mouse_move(10000, 10000) # TODO: What if the screen is larger? Loop until mouse position does not change anymore?
x, y = mouse_pos()
mouse_rmove(-x/2, 0)
mouse_click(LEFT)
time.sleep(.2)
mouse_move(x0, y0)