Skip to main content

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

Project description

Simple and complete

ahkUnwrapped is the complete AutoHotkey v2 available from Python. It is not a collection of wrapper functions introducing another layer of abstraction you need to memorize. AutoHotkey already does that for the Windows API, providing simplicity, and I find that to be enough. Let's not go backwards and add complexity.

Instead, ahkUnwrapped achieves its integration with low-latency interprocess communication. We inject a small communication framework into otherwise normal AutoHotkey scripts that permit us to call functions and monitor variables from Python.

Batteries included

We've the privilege of bundling AutoHotkey64.exe under terms of the GPL, though you can provide your own. You can immediately call built-in AutoHotkey functions entirely scriptless, or embed a simple script within Python, or supply a separate file.

Other features

  • Minimal latency.
  • Works with multithreading/multiprocessing.
  • Hypothesis powered testing of data types.
  • Separate startup sections for independent script use and development.
  • Works from PyInstaller and embedded Python environments.
  • Complete exception handling:
    • Errors for unsupported values (NaN Inf \0).
    • Unhandled AHK exceptions carry over to Python.
  • Special care for:
    • Persistent tray icon visibility settings.
    • Descendant process handling.
    • Unexpected exit handling.
  • Bidirectional primitive types.
    • Types persist Python → AutoHotkey.
    • Optionally, AutoHotkey → Python with (..., t=type).
  • Dot syntax for object properties and methods.
    • Direct access to UI object members.
    • Arrays with .Push, .Get, etc.
    • Maps with .Set, .Get, etc.

Get started

> uv add ahkunwrapped

call(name, ...) f(name, ..., t=type)
get(var, t=type) set(var, val)

You can immediately interact with standard AutoHotkey functions and variables:

from ahkunwrapped import Script

ahk = Script()
ahk.set('A_Clipboard', "Hello from Python!")

is_notepad_active = ahk.f('WinActive', "ahk_class Notepad", t=bool)
if not is_notepad_active:
    ahk.call('Run', "notepad.exe")

You can write a script inline:

from ahkunwrapped import Script

ahk = Script('''
Startup() {
  global myVar := 0
}

LuckyMinimize(winTitle) {
  global myVar := 7
  WinMinimize(winTitle)
}
''')

print(ahk.get('myVar'))
ahk.call('LuckyMinimize', "ahk_class Notepad")
lucky_num = ahk.get('myVar', t=int)

Or load it from a file:

from pathlib import Path
from ahkunwrapped import Script

ahk = Script.from_file(Path('hello.ahk'))
ahk.call('Hello', "World!")

hello.ahk:

; global directives
#Warn
#SingleInstance

; AHK-only startup section
A_ScriptName := "AutoHotkey"
Hello("test")
return

; Python-only startup section
Startup() {
  A_ScriptName := "Python"
}

Hello(text) {
  MsgBox("Hello " text)
}

Usage

call(name, ...) f(name, ..., t=type)
Execute a standalone function or a dot-notated object method (e.g., myObj.Method).

  • call is for performance, to avoid receiving a large unneeded result.
  • f returns the result, optionally type-asserted with t= (otherwise the union float | int | bool | str).

get(var, t=type) set(var, val)
Shorthand for accessing built-ins like A_Clipboard, or global variables and dot-notated properties (e.g., myObj.prop.subProp).

call_main(...) f_main(...)
Execute on AutoHotkey's main thread instead of the OnMessage() listener.
This avoids AhkCantCallOutInInputSyncCallError in constrained threading contexts, but has higher latency.

Example event loop with hotkeys

example.py:

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

import schedule

from ahkunwrapped import Script, AhkExitException

choice = None
HOTKEY_SEND_CHOICE = 'F2'


class Event(IntEnum):
    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(__file__).parent / 'example.ahk', format_dict=globals())


def main() -> None:
    print("Scroll your mousewheel up and down in Notepad.")
    schedule.every(10).seconds.do(print_time)

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

            event = ahk.get('event', t=int)  # contains `ahk.poll()`
            if event >= 0:
                ahk.set('event', -1)
                on_event(event)

            schedule.run_pending()
            time.sleep(0.1)
    except AhkExitException as e:
        sys.exit(e.args[0])


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


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

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

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


if __name__ == '__main__':
    main()

example.ahk:

#Warn
#SingleInstance

Notify("Standalone script test!")
return

Startup() {
    global event := -1
}

Notify(text, duration := 2000) {
    ToolTip(text)

    static RemoveToolTip() {
        ToolTip()
        global event := {{Event.CLEAR_CHOICE}}
    }
    SetTimer(RemoveToolTip, -duration)  ; negative for non-repeating
}

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

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

PyInstaller (single .exe or folder)

example.spec:

from PyInstaller.utils.hooks import collect_data_files

import ahkunwrapped

a = Analysis(
    ['example.py'],                               # Python file
    datas=collect_data_files('ahkunwrapped') + [
        ('example.ahk', '.'),                     # AutoHotkey script (if using `Script.from_file()`)
    ]
)
pyz = PYZ(a.pure)

name = 'my-example'                               # used below

# for onefile
#exe = EXE(pyz, a.scripts, a.binaries, a.datas, name=name, upx=True, console=False)
# for onedir
exe = EXE(pyz, a.scripts, exclude_binaries=True, name=name, upx=True, console=False)
dir = COLLECT(exe, a.binaries, a.datas, name=name)

PyInstaller folder considerations

import os
from pathlib import Path

from ahkunwrapped import Script

# Works both in and out of PyInstaller
CUR_DIR = Path(__file__).parent

# Windows needs a consistent exe path to remember tray icon visibility
LOCALAPP_DIR = Path(os.environ['LOCALAPPDATA'] / 'pyinstaller-example')

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

# ...

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-3.0.3.tar.gz (675.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ahkunwrapped-3.0.3-py3-none-any.whl (657.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: ahkunwrapped-3.0.3.tar.gz
  • Upload date:
  • Size: 675.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ahkunwrapped-3.0.3.tar.gz
Algorithm Hash digest
SHA256 5934644d741ff94a8aa7359a4aa614ca7599709991877e5483641fcafea830b9
MD5 3e40def861ff29bf1ff4693d4bc6d0fc
BLAKE2b-256 3f971f5f16e5592b23c36d1d42a526e5925c85f8dddde1e1a20e3377fcbede6e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: ahkunwrapped-3.0.3-py3-none-any.whl
  • Upload date:
  • Size: 657.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ahkunwrapped-3.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 78b111db53d9d0c63290e859779ecd497537a809c3513dfe74ba7d6e1cfbe8db
MD5 ee51b01c9eef5a098d6f08adf838669c
BLAKE2b-256 2018e9ffe1f6b80893c03f0b12860fb74976fda748dfe71e815fe7ed949fac8a

See more details on using hashes here.

Supported by

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