Skip to content

Commit

Permalink
Merge pull request #2 from spyoungtech/develop
Browse files Browse the repository at this point in the history
v0.1.1
  • Loading branch information
spyoungtech authored Nov 27, 2018
2 parents 8d4d709 + 02b3244 commit 550dae7
Show file tree
Hide file tree
Showing 16 changed files with 597 additions and 99 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# ahk

A (proof-of-concept) Python wrapper around AHK.
A Python wrapper around AHK.

# Usage

```python
from ahk import AHK
ahk = AHK()
ahk.move_mouse(x=100, y=100, speed=10) # blocks until mouse finishes moving
ahk.mouse_move(x=100, y=100, speed=10) # blocks until mouse finishes moving
print(ahk.mouse_position) # (100, 100)
```

Expand All @@ -32,6 +32,8 @@ ahk = AHK(executable_path=r'C:\ProgramFiles\AutoHotkey\AutoHotkey.exe')

# Development

Right now this is just an idea. It may not even be a particularly good one.
Right now this is just an exploration of an idea. It may not even be a particularly good idea.

Not much is implemented right now, but the vision is to provide additional interfaces that mirror the core functionality from the AHK API in a Pythonic way.
There's still a bit to be done in the way of implementation.

The vision is to provide additional interfaces that implement the most important parts of the AHK API in a Pythonic way.
2 changes: 1 addition & 1 deletion ahk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from ahk.autohotkey import AHK
__all__ = ['AHK']
__all__ = ['AHK']
94 changes: 4 additions & 90 deletions ahk/autohotkey.py
Original file line number Diff line number Diff line change
@@ -1,92 +1,6 @@
from tempfile import NamedTemporaryFile
import os
import ast
import subprocess
import shutil
from textwrap import dedent
import time
from contextlib import suppress
import logging
from ahk.mouse import MouseMixin
from ahk.window import WindowMixin

class AHK(object):
def __init__(self, executable_path: str='', keep_scripts: bool=False):
"""

:param executable_path: the path to the AHK executable. Defaults to environ['AHK_PATH'] otherwise tries 'AutoHotkeyA32.exe'
:param keep_scripts:
:raises RuntimeError: if AHK executable is not provided and cannot be found in environment variables or PATH
"""
self.speed = 2
self.keep_scripts = bool(keep_scripts)
executable_path = executable_path or os.environ.get('AHK_PATH') or shutil.which('AutoHotkey.exe') or shutil.which('AutoHotkeyA32.exe')
self.executable_path = executable_path

def _run_script(self, script_path, **kwargs):
result = subprocess.run([self.executable_path, script_path], stdin=None, stderr=None, stdout=subprocess.PIPE, **kwargs)
return result.stdout.decode()

def run_script(self, script_text:str, delete=None, blocking=True, **runkwargs):
if blocking is False:
script_text = script_text.lstrip('#Persistent')
if delete is None:
delete = not self.keep_scripts
try:
with NamedTemporaryFile(mode='w', delete=False) as temp_script:
temp_script.write(script_text)
result = self._run_script(temp_script.name)
except Exception as e:
logging.critical('Something went terribly wrong: %s', e)
result = None
finally:
if delete:
with suppress(OSError):
os.remove(temp_script.name)
return result

def _mouse_position(self):
return dedent('''
#Persistent
MouseGetPos, xpos, ypos
s .= Format("({}, {})", xpos, ypos)
FileAppend, %s%, *
ExitApp
''')

@property
def mouse_position(self):
response = self.run_script(self._mouse_position())
return ast.literal_eval(response)

@mouse_position.setter
def mouse_position(self, position):
x, y = position
self.move_mouse(x=x, y=y, speed=0, relative=False)

def _move_mouse(self, x=None, y=None, speed=None, relative=False):
if x is None and y is None:
raise ValueError('Position argument(s) missing. Must provide x and/or y coordinates')
if speed is None:
speed = self.speed
if relative and (x is None or y is None):
x = x or 0
y = y or 0
elif not relative and (x is None or y is None):
posx, posy = self.mouse_position
x = x or posx
y = y or posy

if relative:
relative = ', R'
else:
relative = ''
script = dedent(f'''\
#Persistent
MouseMove, {x}, {y} , {speed}{relative}
ExitApp
''')
return script

def move_mouse(self, *args, **kwargs):
script = self._move_mouse(*args, **kwargs)
response = self.run_script(script_text=script)
return response or None
class AHK(WindowMixin, MouseMixin):
pass
160 changes: 160 additions & 0 deletions ahk/directives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from types import SimpleNamespace


class DirectiveMeta(type):
"""
Overrides __str__ so directives with no arguments can be used without instantiation
Overrides __hash__ to make objects 'unique' based upon a hash of the str representation
"""
def __str__(cls):
return f"#{cls.__name__}"

def __hash__(self):
return hash(str(self))

def __eq__(cls, other):
return str(cls) == other


class Directive(SimpleNamespace, metaclass=DirectiveMeta):
"""
Simple directive class
They are designed to be hashable and comparable with string equivalent of AHK directive.
Directives that don't require arguments do not need to be instantiated.
"""
def __init__(self, **kwargs):
super().__init__(name=self.name, **kwargs)
self._kwargs = kwargs

@property
def name(self):
return self.__class__.__name__

def __str__(self):
if self._kwargs:
arguments = ' '.join(str(value) for key, value in self._kwargs.items())
else:
arguments = ''
return f"#{self.name} {arguments}".rstrip()

def __eq__(self, other):
return str(self) == other

def __hash__(self):
return hash(str(self))


class AllowSameLineComments(Directive):
pass


class ClipboardTimeout(Directive):
def __init__(self, milliseconds=0, **kwargs):
kwargs['milliseconds'] = milliseconds
super().__init__(**kwargs)


class ErrorStdOut(Directive):
pass


class HotKeyInterval(ClipboardTimeout):
pass


class HotKeyModifierTimeout(HotKeyInterval):
pass


class Include(Directive):
def __init__(self, include_name, **kwargs):
kwargs['include_name'] = include_name
super().__init__(**kwargs)


class IncludeAgain(Include):
pass


class InputLevel(Directive):
def __init__(self, level, **kwargs):
kwargs['level'] = level
super().__init__(**kwargs)


class InstallKeybdHook(Directive):
pass


class InstallMouseHook(Directive):
pass


class KeyHistory(Directive):
def __init__(self, limit=40, **kwargs):
kwargs['limit'] = limit
super().__init__(**kwargs)


class MaxHotkeysPerInterval(Directive):
def __init__(self, value, **kwargs):
kwargs['value'] = value
super().__init__(**kwargs)


class MaxMem(Directive):
def __init__(self, megabytes: int, **kwargs):
if megabytes < 1:
raise ValueError('megabytes cannot be less than 1')
if megabytes > 4095:
raise ValueError('megabytes cannot exceed 4095')
kwargs['megabytes'] = megabytes
super().__init__(**kwargs)


class MaxThreads(Directive):
def __init__(self):
raise NotImplemented


class MaxThreadsBuffer(Directive):
def __init__(self):
raise NotImplemented


class MaxThreadsPerHotkey(Directive):
def __init__(self):
raise NotImplemented


class MenuMaskKey(Directive):
def __init__(self):
raise NotImplemented


class NoEnv(Directive):
pass


class NoTrayIcon(Directive):
pass


class Persistent(Directive):
pass


class SingleInstance(Directive):
pass


class UseHook(Directive):
pass


class Warn(Directive):
pass


class WinActivateForce(Directive):
pass
69 changes: 69 additions & 0 deletions ahk/mouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from ahk.utils import make_script
from ahk.script import ScriptEngine
import ast


class MouseMixin(ScriptEngine):
def __init__(self, mouse_speed=2, mode=None, **kwargs):
if mode is None:
mode = 'Screen'
self.mode = mode
self._mouse_speed = mouse_speed
super().__init__(**kwargs)

@property
def mouse_speed(self):
if callable(self._mouse_speed):
return self._mouse_speed()
else:
return self._mouse_speed


def _mouse_position(self, mode=None):
if mode is None:
mode = self.mode
return make_script(f'''
CoordMode, Mouse, {mode}
MouseGetPos, xpos, ypos
s .= Format("({{}}, {{}})", xpos, ypos)
FileAppend, %s%, *
''')

@property
def mouse_position(self):
response = self.run_script(self._mouse_position())
return ast.literal_eval(response)

@mouse_position.setter
def mouse_position(self, position):
x, y = position
self.mouse_move(x=x, y=y, speed=0, relative=False)

def _mouse_move(self, x=None, y=None, speed=None, relative=False, mode=None):
if x is None and y is None:
raise ValueError('Position argument(s) missing. Must provide x and/or y coordinates')
if speed is None:
speed = self.mouse_speed
if mode is None:
mode = self.mode
if relative and (x is None or y is None):
x = x or 0
y = y or 0
elif not relative and (x is None or y is None):
posx, posy = self.mouse_position
x = x or posx
y = y or posy

if relative:
relative = ', R'
else:
relative = ''
script = make_script(f'''
CoordMode Mouse, {mode}
MouseMove, {x}, {y} , {speed}{relative}
''')
return script

def mouse_move(self, *args, **kwargs):
script = self._mouse_move(*args, **kwargs)
self.run_script(script_text=script)
Loading

0 comments on commit 550dae7

Please sign in to comment.