Package: ahkunwrapped
ahkUnwrappedI wanted to automate Windows with the coverage and simplicity of thecompleteAutoHotkey API, yet code in Python, so I createdahkUnwrapped.AutoHotkey already abstracts the Windows API, so another layer to introduce complexity and slowdowns is undesirable.Instead, we bundle and bridgeAutoHotkey.exe, sending your initial scriptvia stdinwith minimal boilerplate to listen forwindow messagesfrom Python and respondvia stdout.FeaturesAllof AutoHotkey!Execute arbitrary AHK code or load scripts.Hypothesispowered testing of convoluted unicode, et al.Warnings for loss of precision (maximum 6 decimal places).Errors for unsupported values (NaNInf\0).Unhandled AHK exceptions carry over to Python.Won't explode when used from multiple threads.Separate auto-execute sections to ease scripting.SupportsPyInstallerforonefile/onedirinstallations.Special care for:Descriptive errors with accurate line numbers.PersistentWindows notification areasettings.Unexpected exit handling.Minimal latency.Get started> pip install ahkunwrappedcall(proc, ...)f(func, ...)get(var)set(var, val)fromahkunwrappedimportScriptahk=Script()# built-in functions are directly callableisNotepadActive=ahk.f('WinActive','ahk_class Notepad')# built-in variables (and user globals) can be set directlyahk.set('Clipboard',"Copied text!")print(isNotepadActive)fromahkunwrappedimportScriptahk=Script('''LuckyMinimize(winTitle) {global myVarmyVar := 7WinMinimize, % winTitleClipboard := "You minimized: " winTitle}''')ahk.call('LuckyMinimize','ahk_class Notepad')print("Lucky number",ahk.get('myVar'))frompathlibimportPathfromahkunwrappedimportScriptahk=Script.from_file(Path('my_msg.ahk'))ahk.call('MyMsg',"Wooo!")my_msg.ahk:; auto-execute section when ran standalone#SingleInstanceforce#WarnAutoExec(); we can call this if we wantMyMsg("test our function")return; auto-execute section when ran from PythonAutoExec(){SetBatchLines,100ms; slow our code to reduce CPU}MyMsg(text){MsgBox,%text}Settings fromAutoExec()willstill applyeven though we execute directly from the message listening thread for speed.(AutoHotkey's#Warnis special and will apply to both standalone and from-Python execution, unless you add/remove it dynamically.)Usagecall(proc, ...)is for performance, to avoid receiving a large unneeded result.get(var)set(var, val)are shorthand for accessing global variables andbuilt-inslikeA_TimeIdle.f(func, ...)get(var)will inferfloatandint(base-16 beginning with0x) 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 theOnMessage()listener.
This avoidsAhkCantCallOutInInputSyncCallError, e.g. from some uses ofComObjCreate().
This is slower (except with very large data), but still fast and unlikely to bottleneck.Event loop with hotkeysimportsysimporttimefromdatetimeimportdatetimefromenumimportEnumfrompathlibimportPathfromahkunwrappedimportScript,AhkExitExceptionchoice=NoneHOTKEY_SEND_CHOICE='F2'classEvent(Enum):QUIT,SEND_CHOICE,CLEAR_CHOICE,CHOOSE_MONTH,CHOOSE_DAY=range(5)# format_dict= so we can use {{VARIABLE}} within example.ahkahk=Script.from_file(Path('example.ahk'),format_dict=globals())defmain()->None:print("Scroll your mousewheel in Notepad.")ts=0whileTrue:try:# ahk.poll()  # detect exit, but all ahk functions include thiss_elapsed=time.time()-tsifs_elapsed>=60:ts=time.time()print_minute()event=ahk.get('event')# contains ahk.poll()ifevent:ahk.set('event','')on_event(event)exceptAhkExitExceptionasex:sys.exit(ex.args[0])time.sleep(0.01)defprint_minute()->None:print(f"It is now{datetime.now().time()}")defon_event(event:str)->None:globalchoicedefget_choice()->str:returnchoiceordatetime.now().strftime('%#I:%M %p')ifevent==str(Event.QUIT):ahk.exit()ifevent==str(Event.CLEAR_CHOICE):choice=Noneifevent==str(Event.SEND_CHOICE):ahk.call('Send',f'{get_choice()}')ifevent==str(Event.CHOOSE_MONTH):choice=datetime.now().strftime('%b')ahk.call('ToolTip',f"Month is{get_choice()},{HOTKEY_SEND_CHOICE}to insert.")ifevent==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#WarnToolTip("Standalone script test!")returnAutoExec(){globaleventevent:=""SendMode,input}Send(text){Send,%text}ToolTip(text,s:=2){ToolTip,%text; negative for non-repeatingSetTimer,RemoveToolTip,%s*-1000}RemoveToolTip:ToolTip,event={{Event.CLEAR_CHOICE}}returnMouseIsOver(winTitle){MouseGetPos,,,winIdresult:=WinExist(winTitle" ahk_id "winId)returnresult}#IfWinActive("ahk_class Notepad"){{HOTKEY_SEND_CHOICE}}::event={{Event.SEND_CHOICE}}^Q::event={{Event.QUIT}}#IfMouseIsOver("ahk_class Notepad")WheelUp::event={{Event.CHOOSE_MONTH}}WheelDown::event={{Event.CHOOSE_DAY}}PyInstaller (single.exeor folder)example.spec:# -*- mode: python -*-frompathlibimportPathimportahkunwrappeda=Analysis(['example.py'],datas=[(Path(ahkunwrapped.__file__).parent/'lib','lib'),# required('example.ahk','.'),])pyz=PYZ(a.pure)# for onefileexe=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 considerationsexample.py:frompathlibimportPathfromahkunwrappedimportScript# tray icon visibility settings rely on consistent exe pathsLOCALAPP_DIR=Path(os.getenv('LOCALAPPDATA')/'pyinstaller-example')# working directory is different between onefile and onedir modes#  https://pyinstaller.readthedocs.io/en/stable/runtime-information.htmlCUR_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.icoMenu,Tray,Icon; unhide}; ...