Skip to main content

Bundled and bridged AutoHotkey for full native code execution from Python.

Project description

ahkUnwrapped

I wanted to automate Windows with the coverage and simplicity of the complete AutoHotkey API, yet code in Python, so I created ahkUnwrapped.

AutoHotkey already abstracts the Windows API, so another layer to introduce complexity and slowdowns is undesirable.

Instead, we bundle and bridge AutoHotkey.exe, sending your initial script via stdin with minimal boilerplate to listen for window messages from Python and respond via stdout.

Features

  • All of AutoHotkey!
  • Execute arbitrary AHK code or load scripts.
  • Hypothesis powered testing of convoluted unicode, et al.
  • Warnings for loss of precision (maximum 6 decimal places).
  • Errors for unsupported values (NaN Inf \0).
  • Unhandled AHK exceptions carry over to Python.
  • Won't explode when used from multiple threads.
  • Separate auto-execute sections to ease scripting.
  • Supports PyInstaller for onefile/onedir installations.
  • Special care for:
    • Descriptive errors with accurate line numbers.
    • Persistent Windows notification area settings.
    • Unexpected exit handling.
    • Minimal latency.

Get started

> pip install ahkunwrapped

call(proc, ...) f(func, ...) get(var) set(var, val)

from ahkunwrapped import Script

ahk = Script()
# built-in functions are directly callable
isNotepadActive = ahk.f('WinActive', 'ahk_class Notepad')
# built-in variables (and user globals) can be set directly
ahk.set('Clipboard', "Copied text!")
print(isNotepadActive)

from ahkunwrapped import Script

ahk = Script('''
LuckyMinimize(winTitle) {
  global myVar
  myVar := 7
  
  WinMinimize, % winTitle
  Clipboard := "You minimized: " winTitle
}
''')

ahk.call('LuckyMinimize', 'ahk_class Notepad')
print("Lucky number", ahk.get('myVar'))

from pathlib import Path
from ahkunwrapped import Script

ahk = Script.from_file(Path('my_msg.ahk'))
ahk.call('MyMsg', "Wooo!")

my_msg.ahk:

; auto-execute section when ran standalone
#SingleInstance force
#Warn
AutoExec()                   ; we can call this if we want
MyMsg("test our function")
return

; auto-execute section when ran from Python
AutoExec() {
  SetBatchLines, 100ms       ; slow our code to reduce CPU
}

MyMsg(text) {
  MsgBox, % text
}

Settings from AutoExec() will still apply even though we execute directly from the message listening thread for speed.
(AutoHotkey's #Warn is special and will apply to both standalone and from-Python execution, unless you add/remove it dynamically.)

Usage

call(proc, ...) is for performance, to avoid receiving a large unneeded result.

get(var) set(var, val) are shorthand for accessing global variables and built-ins like A_TimeIdle.

f(func, ...) get(var) will infer float and int (base-16 beginning with 0x) like AutoHotkey.

f_raw(func, ...) get_raw(var) will return the raw string as-stored.

call_main(proc, ...) f_main(func, ...) f_raw_main(func, ...) will execute on AutoHotkey's main thread instead of the OnMessage() listener. This avoids AhkCantCallOutInInputSyncCallError, e.g. from some uses of ComObjCreate(). This is slower (except with very large data), but still fast and unlikely to bottleneck.

Event loop with hotkeys

import sys
import time
from datetime import datetime
from enum import Enum
from pathlib import Path

from ahkunwrapped import Script, AhkExitException

choice = None
HOTKEY_SEND_CHOICE = 'F2'


class Event(Enum):
  QUIT, SEND_CHOICE, CLEAR_CHOICE, CHOOSE_MONTH, CHOOSE_DAY = range(5)


# format_dict= so we can use {{VARIABLE}} within example.ahk
ahk = Script.from_file(Path('example.ahk'), format_dict=globals())


def main() -> None:
  print("Scroll your mousewheel in Notepad.")

  ts = 0
  while True:
    try:
      # ahk.poll()  # detect exit, but all ahk functions include this

      s_elapsed = time.time() - ts
      if s_elapsed >= 60:
        ts = time.time()
        print_minute()

      event = ahk.get('event')  # contains ahk.poll()
      if event:
        ahk.set('event', '')
        on_event(event)
    except AhkExitException as ex:
      sys.exit(ex.args[0])
    time.sleep(0.01)


def print_minute() -> None:
  print(f"It is now {datetime.now().time()}")


def on_event(event: str) -> None:
  global choice

  def get_choice() -> str:
    return choice or datetime.now().strftime('%#I:%M %p')

  if event == str(Event.QUIT):
    ahk.exit()
  if event == str(Event.CLEAR_CHOICE):
    choice = None
  if event == str(Event.SEND_CHOICE):
    ahk.call('Send', f'{get_choice()} ')
  if event == str(Event.CHOOSE_MONTH):
    choice = datetime.now().strftime('%b')
    ahk.call('ToolTip', f"Month is {get_choice()}, {HOTKEY_SEND_CHOICE} to insert.")
  if event == str(Event.CHOOSE_DAY):
    choice = datetime.now().strftime('%#d')
    ahk.call('ToolTip', f"Day is {get_choice()}, {HOTKEY_SEND_CHOICE} to insert.")


if __name__ == '__main__':
  main()

example.ahk:

#SingleInstance, force
#Warn
ToolTip("Standalone script test!")
return

AutoExec() {
    global event
    event := ""
    SendMode, input
}

Send(text) {
    Send, % text
}

ToolTip(text, s := 2) {
    ToolTip, % text
    ; negative for non-repeating
    SetTimer, RemoveToolTip, % s * -1000
}

RemoveToolTip:
    ToolTip,
    event = {{Event.CLEAR_CHOICE}}
return

MouseIsOver(winTitle) {
    MouseGetPos,,, winId
    result := WinExist(winTitle " ahk_id " winId)
    return result
}

#If WinActive("ahk_class Notepad")
{{HOTKEY_SEND_CHOICE}}::event = {{Event.SEND_CHOICE}}
^Q::event = {{Event.QUIT}}
#If MouseIsOver("ahk_class Notepad")
WheelUp::event = {{Event.CHOOSE_MONTH}}
WheelDown::event = {{Event.CHOOSE_DAY}}

PyInstaller (single .exe or folder)

example.spec:

# -*- mode: python -*-
from pathlib import Path

import ahkunwrapped

a = Analysis(
  ['example.py'],
  datas=[
    (Path(ahkunwrapped.__file__).parent / 'lib', 'lib'),  # required
    ('example.ahk', '.'),
  ]
)
pyz = PYZ(a.pure)

# for onefile
exe = EXE(pyz, a.scripts, a.binaries, a.datas, name='my-example', upx=True, console=False)

# for onedir
# exe = EXE(pyz, a.scripts, exclude_binaries=True, name='my-example', upx=True, console=False)
# dir = COLLECT(exe, a.binaries, a.datas, name='my-example-folder')

Folder considerations

example.py:

from pathlib import Path

from ahkunwrapped import Script

# tray icon visibility settings rely on consistent exe paths
LOCALAPP_DIR = Path(os.getenv('LOCALAPPDATA') / 'pyinstaller-example')

# working directory is different between onefile and onedir modes
#  https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
CUR_DIR = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))

ahk = Script.from_file(CUR_DIR / 'example.ahk', format_dict=globals(), execute_from=LOCALAPP_DIR)

# ...

example.ahk:

AutoExec() {
  Menu, Tray, Icon, {{CUR_DIR}}\black.ico
  Menu, Tray, Icon  ; unhide
}

; ...

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ahkunwrapped-2.2.1.tar.gz (663.1 kB view details)

Uploaded Source

Built Distribution

ahkunwrapped-2.2.1-py3-none-any.whl (661.7 kB view details)

Uploaded Python 3

File details

Details for the file ahkunwrapped-2.2.1.tar.gz.

File metadata

  • Download URL: ahkunwrapped-2.2.1.tar.gz
  • Upload date:
  • Size: 663.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.8.0 pkginfo/1.8.3 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.12 tqdm/4.64.1 importlib-metadata/4.8.3 keyring/23.4.1 rfc3986/1.5.0 colorama/0.4.5 CPython/3.6.8

File hashes

Hashes for ahkunwrapped-2.2.1.tar.gz
Algorithm Hash digest
SHA256 8e5e63543f62f459864c01d752b135c2ec94a25dead1a5901f84e3cb278b094b
MD5 70809e967dd5675498bb57b4476e1c83
BLAKE2b-256 145153daee32bd33c103ef77b8ace4022fdcdc644758722eafa92cc3a51c6e65

See more details on using hashes here.

File details

Details for the file ahkunwrapped-2.2.1-py3-none-any.whl.

File metadata

  • Download URL: ahkunwrapped-2.2.1-py3-none-any.whl
  • Upload date:
  • Size: 661.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.8.0 pkginfo/1.8.3 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.12 tqdm/4.64.1 importlib-metadata/4.8.3 keyring/23.4.1 rfc3986/1.5.0 colorama/0.4.5 CPython/3.6.8

File hashes

Hashes for ahkunwrapped-2.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9d4eef97092b133fca6e1ec6d72475aacfeb538707b8512ef25d1627fb74a2b4
MD5 1bcf4cdfd1f3e51083813f9a3197ea98
BLAKE2b-256 ad34540a5041b27d545bf3edf88783ab1a11d04bb22b42d40cfda762410caf05

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page