""" Abstracts the capturing and interfacing of applications """
import os
import re
import time
import pyperclip
import platform
import subprocess
from .RegionMatching import Region
from .SettingsDebug import Debug
if platform.system() == "Windows":
from .PlatformManagerWindows import PlatformManagerWindows
PlatformManager = PlatformManagerWindows() # No other input managers built yet
elif platform.system() == "Darwin":
from .PlatformManagerDarwin import PlatformManagerDarwin
PlatformManager = PlatformManagerDarwin()
else:
# Avoid throwing an error if it's just being imported for documentation purposes
if not os.environ.get('READTHEDOCS') == 'True':
raise NotImplementedError("Lackey is currently only compatible with Windows and OSX.")
# Python 3 compatibility
try:
basestring
except NameError:
basestring = str
[docs]class App(object):
""" Allows apps to be selected by title, PID, or by starting an
application directly. Can address individual windows tied to an
app.
For more information, see `Sikuli's App documentation <http://sikulix-2014.readthedocs.io/en/latest/appclass.html#App>`_.
"""
def __init__(self, identifier=None):
self._pid = None
self._search = identifier
self._title = ""
self._exec = ""
self._params = ""
self._process = None
self._devnull = None
self._defaultScanRate = 0.1
self.proc = None
# Replace class methods with instance methods
self.focus = self._focus_instance
self.close = self._close_instance
self.open = self._open_instance
# Process `identifier`
if isinstance(identifier, int):
# `identifier` is a PID
Debug.log(3, "Creating App by PID ({})".format(identifier))
self._pid = identifier
elif isinstance(identifier, basestring):
# `identifier` is either part of a window title
# or a command line to execute. If it starts with a "+",
# launch it immediately. Otherwise, store it until open() is called.
Debug.log(3, "Creating App by string ({})".format(identifier))
launchNow = False
if identifier.startswith("+"):
# Should launch immediately - strip the `+` sign and continue
launchNow = True
identifier = identifier[1:]
# Check if `identifier` is an executable commmand
# Possible formats:
# Case 1: notepad.exe C:\sample.txt
# Case 2: "C:\Program Files\someprogram.exe" -flag
# Extract hypothetical executable name
if identifier.startswith('"'):
executable = identifier[1:].split('"')[0]
params = identifier[len(executable)+2:].split(" ") if len(identifier) > len(executable) + 2 else []
else:
executable = identifier.split(" ")[0]
params = identifier[len(executable)+1:].split(" ") if len(identifier) > len(executable) + 1 else []
# Check if hypothetical executable exists
if self._which(executable) is not None:
# Found the referenced executable
self._exec = executable
self._params = params
# If the command was keyed to execute immediately, do so.
if launchNow:
self.open()
else:
# No executable found - treat as a title instead. Try to capture window.
self._title = identifier
self.open()
else:
self._pid = -1 # Unrecognized identifier, setting to empty app
self._pid = self.getPID() # Confirm PID is an active process (sets to -1 otherwise)
def _which(self, program):
""" Private method to check if an executable exists
Shamelessly stolen from http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
"""
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
[docs] @classmethod
def pause(cls, waitTime):
time.sleep(waitTime)
[docs] @classmethod
def focus(cls, appName):
""" Searches for exact text, case insensitive, anywhere in the window title.
Brings the matching window to the foreground.
As a class method, accessible as `App.focus(appName)`. As an instance method,
accessible as `App(appName).focus()`.
"""
app = cls(appName)
return app.focus()
def _focus_instance(self):
""" In instances, the ``focus()`` classmethod is replaced with this instance method. """
if self._title:
Debug.log(3, "Focusing app with title like ({})".format(self._title))
PlatformManager.focusWindow(PlatformManager.getWindowByTitle(re.escape(self._title)))
if self.getPID() == -1:
self.open()
elif self._pid and self._pid != -1:
Debug.log(3, "Focusing app with pid ({})".format(self._pid))
PlatformManager.focusWindow(PlatformManager.getWindowByPID(self._pid))
return self
[docs] @classmethod
def close(cls, appName):
""" Closes the process associated with the specified app.
As a class method, accessible as `App.class(appName)`.
As an instance method, accessible as `App(appName).close()`.
"""
return cls(appName).close()
def _close_instance(self):
if self._process:
self._process.terminate()
self._devnull.close()
elif self.getPID() != -1:
PlatformManager.killProcess(self.getPID())
[docs] @classmethod
def open(self, executable):
""" Runs the specified command and returns an App linked to the generated PID.
As a class method, accessible as `App.open(executable_path)`.
As an instance method, accessible as `App(executable_path).open()`.
"""
return App(executable).open()
def _open_instance(self, waitTime=0):
if self._exec != "":
# Open from an executable + parameters
self._devnull = open(os.devnull, 'w')
self._process = subprocess.Popen([self._exec] + self._params, shell=False, stderr=self._devnull, stdout=self._devnull)
self._pid = self._process.pid
elif self._title != "":
# Capture an existing window that matches self._title
self._pid = PlatformManager.getWindowPID(
PlatformManager.getWindowByTitle(
re.escape(self._title)))
time.sleep(waitTime)
return self
[docs] @classmethod
def focusedWindow(cls):
""" Returns a Region corresponding to whatever window is in the foreground """
x, y, w, h = PlatformManager.getWindowRect(PlatformManager.getForegroundWindow())
return Region(x, y, w, h)
[docs] def getWindow(self):
""" Returns the title of the main window of the currently open app.
Returns an empty string if no match could be found.
"""
if self.getPID() != -1:
return PlatformManager.getWindowTitle(PlatformManager.getWindowByPID(self.getPID()))
else:
return ""
[docs] def getName(self):
""" Returns the short name of the app as shown in the process list """
return PlatformManager.getProcessName(self.getPID())
[docs] def getPID(self):
""" Returns the PID for the associated app
(or -1, if no app is associated or the app is not running)
"""
if self._pid is not None:
if not PlatformManager.isPIDValid(self._pid):
self._pid = -1
return self._pid
return -1
[docs] def hasWindow(self):
""" Returns True if the process has a window associated, False otherwise """
return PlatformManager.getWindowByPID(self.getPID()) is not None
[docs] def waitForWindow(self, seconds=5):
timeout = time.time() + seconds
while True:
window_region = self.window()
if window_region is not None or time.time() < timeout:
break
time.sleep(0.5)
return window_region
[docs] def window(self, windowNum=0):
""" Returns the region corresponding to the specified window of the app.
Defaults to the first window found for the corresponding PID.
"""
if self._pid == -1:
return None
x,y,w,h = PlatformManager.getWindowRect(PlatformManager.getWindowByPID(self._pid, windowNum))
return Region(x,y,w,h).clipRegionToScreen()
[docs] def setUsing(self, params):
self._params = params.split(" ")
def __repr__(self):
""" Returns a string representation of the app """
return "[{pid}:{executable} ({windowtitle})] {searchtext}".format(pid=self._pid, executable=self.getName(), windowtitle=self.getWindow(), searchtext=self._search)
[docs] def isRunning(self, waitTime=0):
""" If PID isn't set yet, checks if there is a window with the specified title. """
waitUntil = time.time() + waitTime
while True:
if self.getPID() > 0:
return True
else:
self._pid = PlatformManager.getWindowPID(PlatformManager.getWindowByTitle(re.escape(self._title)))
# Check if we've waited long enough
if time.time() > waitUntil:
break
else:
time.sleep(self._defaultScanRate)
return self.getPID() > 0
[docs] def isValid(self):
return (os.path.isfile(self._exec) or self.getPID() > 0)
[docs] @classmethod
def getClipboard(cls):
""" Gets the contents of the clipboard (as classmethod) """
return pyperclip.paste()
[docs] @classmethod
def setClipboard(cls, contents):
""" Sets the contents of the clipboard (as classmethod) """
return pyperclip.copy(contents)