Skip to main content

Lightweight execution and bidirectional communication for native AutoHotkey code and scripts; full language support.

Project description

ahkUnwrapped

Program in Python; use AutoHotkey for its simplification and mastery of the Windows API.

Features

  • Lightweight single module:

    • 170 lines of AutoHotkey.
  • Performance is a feature.

  • Zero wrappers or abstractions.

  • GPL licensed to bake-in AutoHotkey.

  • Test suite for all types (primitives).

  • Supports exceptions and hard exits.

  • Execute arbitrary AHK or load from files.

  • Errors for unsupported values (NaN Inf \0).

  • Warnings for loss of data (maximum 6 decimal places).

  • Supports PyInstaller for single .exe bundles.

  • Full language support; all of AutoHotkey:

    • Easy mouse and keyboard events.
    • Detect and manipulate windows.
    • WinAPI calls with minimal code.
    • Rapid GUI layouts.

How it Works

Each Script launches an AutoHotkey.exe process with framework and user code passed via stdin. The framework listens for windows messages from Python and responds via stdout.

Usage

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

from ahkunwrapped import Script

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

from ahkunwrapped import Script

ahk = Script('''
LuckyMinimize(winTitle) {
  global myVar
  myVar := 7

  Clipboard := "You minimized: " winTitle
  WinMinimize, % winTitle
}
''')

ahk.call('LuckyMinimize', "ahk_class Notepad")  # built-in commands can be used from functions
print("Lucky number", ahk.get('myVar'))

from pathlib import Path
from ahkunwrapped import Script

ahk = Script.from_file(Path('my_msg.ahk'))  # load from a file
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 from OnMessage() for speed.
AutoHotkey's #Warn is special and will apply to both standalone and from-Python execution, unless you add/remove it dynamically.

call(proc, ...) is for performance, to avoid receiving a large unneeded result.
get(var) set(var, val) are shorthand for 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 OnMessage().
This is necessary if AhkCantCallOutInInputSyncCallError is thrown, generally from some uses of ComObjCreate().
This is slower (except with very large data), but still fast and unlikely to bottleneck.

Example event loop

See bottom of AHK script for 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)


ahk = Script.from_file(Path('example.ahk'), format_dict=globals())


def main() -> None:
    ts = 0
    while True:
        exit_code = ahk.poll()
        if exit_code:
            sys.exit(exit_code)

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

            event = ahk.get('event')
            if event:
                ahk.set('event', '')
                on_event(event)
        except AhkExitException:
            print("Graceful exit.")
            sys.exit(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
    SetTimer, RemoveToolTip, % s * 1000
}

RemoveToolTip:
    SetTimer, RemoveToolTip, off
    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}}

Example PyInstaller spec (single .exe)

example.spec:

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

import ahkunwrapped

a = Analysis(['example.py'], datas=[
        (Path(ahkunwrapped.__file__).parent / 'lib', 'lib'),
        ('example.ahk', '.'),
    ]
)
pyz = PYZ(a.pure)
exe = EXE(pyz, a.scripts, a.binaries, a.datas, name='my-example', upx=True, console=False)

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.0.1.tar.gz (13.1 kB view hashes)

Uploaded Source

Built Distribution

ahkunwrapped-2.0.1-py3-none-any.whl (613.0 kB view hashes)

Uploaded Python 3

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