#Region ;**** Directives created by AutoIt3Wrapper_GUI **** #AutoIt3Wrapper_Icon=icons\key_bindings.ico #AutoIt3Wrapper_Compile_Both=y #AutoIt3Wrapper_UseX64=y #AutoIt3Wrapper_Res_Comment=https://shrum.net/sean/code/hotrkeyz #AutoIt3Wrapper_Res_Description=Hotrkeyz #AutoIt3Wrapper_Res_Fileversion=2.0.0.18 #AutoIt3Wrapper_Res_Fileversion_AutoIncrement=y #AutoIt3Wrapper_Res_ProductName=Hotrkeyz #AutoIt3Wrapper_Res_ProductVersion=00 #AutoIt3Wrapper_Res_CompanyName=Sean Shrum #AutoIt3Wrapper_Res_LegalCopyright=Sean Shrum #EndRegion ;**** Directives created by AutoIt3Wrapper_GUI **** ; =================================================================== ; Hotrkeyz – Canonical, Persistent Combo Launcher ; =================================================================== #include #include #include #include #include ; =================================================================== ; DUPLICATE INSTANCE CHECK ; =================================================================== If UBound(ProcessList(@ScriptName)) > 2 Then Exit ; =================================================================== ; VERSION / BUILD STAMP FORMALIZATION ; =================================================================== If @Compiled Then $sAppVer = FileGetVersion(@ScriptFullPath) Else $sAppVer = "DEVELOPMENT" EndIf $aAppBuild = FileGetTime(@ScriptFullPath) $sAppBuild = $aAppBuild[0] & "." & $aAppBuild[1] & "." & $aAppBuild[2] & " @ " & $aAppBuild[3] & ":" & $aAppBuild[4] & ":" & $aAppBuild[5] ; =================================================================== ; GLOBALS ; =================================================================== Global Const $g_sScriptDir = @ScriptDir Global Const $g_sRegBase = "HKCU\Software\shrum.net\hotrkeyz" Global $g_sCombosDir = RegRead($g_sRegBase, "CombosDir") ; If registry empty, default to script dir If $g_sCombosDir = "" Then $g_sCombosDir = @ScriptDir & "\combos" ; Canonical key order (expanded) Global $aKeyOrder = [ _ "Ctrl","LWin","RWin","Alt","Shift", _ "F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12", _ "F13","F14","F15","F16","F17","F18","F19","F20","F21","F22","F23","F24", _ "Insert","Delete","Home","End","PgUp","PgDn","Up","Down","Left","Right", _ "PrintScreen","ScrollLock","Pause", _ "Enter","NUMPADENTER", _ "A","B","C","D","E","F","G","H","I","J","K","L","M", _ "N","O","P","Q","R","S","T","U","V","W","X","Y","Z", _ "0","1","2","3","4","5","6","7","8","9", _ "-", "=", "[", "]", "\", ";", "'", ",", ".", "/", "`", _ "VOLUME_MUTE","VOLUME_DOWN","VOLUME_UP", _ "MEDIA_NEXT","MEDIA_PREV","MEDIA_STOP","MEDIA_PLAY_PAUSE" _ ] Global $aWindowsReservedCombos = [ _ "Alt + Esc", _ "Alt + Tab", _ "Alt + Shift + Esc", _ "Alt + Shift + PrintScreen", _ "Alt + Shift + Tab", _ "Ctrl + A", _ "Ctrl + C", _ "Ctrl + V", _ "Ctrl + X", _ "Ctrl + Esc", _ "Ctrl + Alt + Delete", _ "Ctrl + Shift + Esc", _ "Ctrl + Win + F", _ "Win + A", _ "Win + B", _ "Win + D", _ "Win + E", _ "Win + F", _ "Win + H", _ "Win + I", _ "Win + K", _ "Win + L", _ "Win + M", _ "Win + O", _ "Win + P", _ "Win + R", _ "Win + S", _ "Win + U", _ "Win + V", _ "Win + W", _ "Win + X", _ "Win + Pause", _ "Win + Break", _ "Win + Tab", _ "Win + Esc", _ "Win + PrtScr", _ "Win + Up", _ "Win + Down", _ "Win + Left", _ "Win + Right", _ "Win + Plus", _ "Win + Minus", _ "Win + Shift + M", _ "Win + Shift + S", _ "Win + Shift + Left", _ "Win + Shift + Right", _ "Win + Ctrl + F", _ "Win + Ctrl + D", _ "Win + Ctrl + Q", _ "Win + Ctrl + Shift + B" _ ] ; State Global $g_aCombos[0] Global $g_sActiveCombo = "" Global $g_bWasPressing = False ; =================================================================== ; TRAY SETUP ; =================================================================== Opt("TrayMenuMode", 3) Opt("TrayOnEventMode", 1) Global $idNew = TrayCreateItem("New...") TrayItemSetState (-1, 517) Global $idCombos = TrayCreateItem("Combos") Global $idReload = TrayCreateItem("Reload") TrayCreateItem("") Global $idHome = TrayCreateItem("Homepage") Global $idNews = TrayCreateItem("News") Global $idSupport = TrayCreateItem("Support") Global $idUpdate = TrayCreateItem("Update") TrayCreateItem("") Global $idExit = TrayCreateItem("Exit") TrayItemSetOnEvent($idNew, "_Tray_New") TrayItemSetOnEvent($idCombos, "_Tray_Combos") TrayItemSetOnEvent($idReload, "_Tray_Reload") TrayItemSetOnEvent($idHome, "_Tray_Home") TrayItemSetOnEvent($idNews, "_Tray_News") TrayItemSetOnEvent($idSupport, "_Tray_Support") TrayItemSetOnEvent($idUpdate, "_Tray_Update") TrayItemSetOnEvent($idExit, "_Tray_Exit") TraySetOnEvent(-1, "_Tray_New") TraySetToolTip("Hotrkeyz") TraySetState() If FileExists(@ScriptDir & @ScriptName & ".ico") Then TraySetIcon(@ScriptDir & @ScriptName & ".ico") ; =================================================================== ; STARTUP ; =================================================================== TrayTip("A persistent KB combo monitor & app launcher","By Sean Shrum :: https://sean.shrum.net",5) _ReloadCombos() If UBound($g_aCombos) = 0 Then Local $iAns = MsgBox($MB_ICONINFORMATION + $MB_YESNO, "Hotrkeyz", _ "No Hotrkeyz are defined." & @CRLF & "Define one now?") If $iAns = $IDYES Then _NewCombo() EndIf ; =================================================================== ; MAIN LOOP ; =================================================================== While 1 _MonitorKeys() Sleep(40) WEnd ; =================================================================== ; TRAY HANDLERS ; =================================================================== Func _Tray_New() _NewCombo() EndFunc Func _Tray_Combos() ShellExecute($g_sCombosDir) EndFunc Func _Tray_Reload() _ReloadCombos() TrayTip("Hotrkeyz", "All combos reloaded.", 3) EndFunc Func _Tray_Home() ShellExecute("https://shrum.net/sean/code/hotrkeyz") EndFunc Func _Tray_News() ShellExecute("https://shrum.net/cats.php?dir=/updates&search=hotrkeyz") EndFunc Func _Tray_Support() ShellExecute("https://shrum.net/cats.php?dir=/sean/code/hotrkeyz&body=/sean/support.shtml") EndFunc Func _Tray_Update() Local $sURL = "http://shrum.net/sean/code/hotrkeyz/hotrkeyz.au3" Local $sData = InetRead($sURL, 1) If @error Or $sData = "" Then MsgBox($MB_ICONWARNING, "Hotrkeyz", "Unable to check for updates.") Return EndIf Local $sText = BinaryToString($sData) Local $aLines = StringSplit($sText, @CRLF, 1) Local $sRemoteVer = "" For $i = 1 To $aLines[0] If StringInStr($aLines[$i], "#AutoIt3Wrapper_Res_Fileversion") Then Local $sLine = $aLines[$i] Local $iPos = StringInStr($sLine, "=") $sRemoteVer = StringStripWS(StringTrimLeft($sLine, $iPos), 3) $sRemoteVer = StringReplace($sRemoteVer, '"', "") ExitLoop EndIf Next If $sRemoteVer = "" Then MsgBox($MB_ICONWARNING, "Hotrkeyz", "Could not find version info.") Return EndIf If $sRemoteVer > $sAppVer Then Local $iAns = MsgBox($MB_ICONINFORMATION + $MB_YESNO, "Hotrkeyz", _ "New version available: " & $sRemoteVer & @CRLF & _ "Go to homepage to download newer version?") If $iAns = $IDYES Then _Tray_Home() Else MsgBox($MB_ICONINFORMATION, "Hotrkeyz", "You are running the latest version.") EndIf EndFunc Func _Tray_Exit() Exit EndFunc ; =================================================================== ; COMBO MANAGEMENT ; =================================================================== Func _EnsureCombosFolder() ; If registry entry missing, fallback to script dir If $g_sCombosDir = "" Then $g_sCombosDir = @ScriptDir & "\combos" RegWrite($g_sRegBase, "CombosDir", "REG_SZ", $g_sCombosDir) EndIf ; If folder exists, nothing to do If FileExists($g_sCombosDir) Then Return ; Folder missing ? prompt user Local $iAns = MsgBox($MB_ICONQUESTION + $MB_YESNO, "Hotrkeyz", _ "The Hotrkeyz combos location is either undefined or missing:" & @CRLF & @CRLF & _ $g_sCombosDir & @CRLF & @CRLF & _ "Is the default location for the Hotrkeyz app. Do you want to specify a different location?") If $iAns = $IDNO Then ; Use script directory as fallback $g_sCombosDir = @ScriptDir & "\combos" DirCreate($g_sCombosDir) RegWrite($g_sRegBase, "CombosDir", "REG_SZ", $g_sCombosDir) Return EndIf ; Let user choose a folder Local $sNew = FileSelectFolder("Select location of combos folder", @ScriptDir) If @error Or $sNew = "" Then ; User cancelled ? fallback to script dir $g_sCombosDir = @ScriptDir & "\combos" Else $g_sCombosDir = $sNew & "\combos" EndIf ; Create folder if needed DirCreate($g_sCombosDir) ; Save new path to registry RegWrite($g_sRegBase, "CombosDir", "REG_SZ", $g_sCombosDir) EndFunc Func _ReloadCombos() _EnsureCombosFolder() Local $aList = _FileListToArray($g_sCombosDir, "*", $FLTA_FOLDERS) If @error Then ReDim $g_aCombos[0] Return EndIf Local $aOut[0] For $i = 1 To $aList[0] _ArrayAdd($aOut, $aList[$i]) Next $g_aCombos = $aOut EndFunc Func _NewCombo() Local $sCombo = _CaptureCombo() If $sCombo = "" Then Return $sCombo = _CanonicalizeCombo($sCombo) If _IsWindowsReservedCombo($sCombo) Then MsgBox($MB_ICONWARNING, "Hotrkeyz", _ "This key combination is reserved by Windows and cannot be used." & @CRLF & _ "Please choose a different combination.") _NewCombo() Return EndIf Local $sFolder = $g_sCombosDir & "\" & $sCombo If FileExists($sFolder) Then Local $iAns = MsgBox($MB_ICONINFORMATION + $MB_YESNO, "Hotrkeyz", _ "This combo already exists:" & @CRLF & @CRLF & $sCombo & @CRLF & @CRLF & _ "Open this combo folder?") If $iAns = $IDYES Then ShellExecute($sFolder) Else _NewCombo() EndIf Return EndIf DirCreate($sFolder) DirCreate($sFolder & "\ShellExecute") DirCreate($sFolder & "\ShellExecuteWait") FileWrite($sFolder & "\_Apps and links placed here will not run", "") FileWrite($sFolder & "\_Place them into the subfolders here", "") FileWrite($sFolder & "\_'ShellExecute' launches ALL alphabetically at the same time", "") FileWrite($sFolder & "\_'ShellExecuteWait' launches 1 at a time alphabetically, til app terminate", "") ShellExecute($sFolder) _ReloadCombos() EndFunc ; =================================================================== ; KEY MONITORING (persistent until all keys released) ; =================================================================== Func _MonitorKeys() Local $aPressed = _GetPressedKeys() ; DEBUG: show raw pressed keys If UBound($aPressed) > 0 Then ConsoleWrite(_ArrayToString($aPressed, ", ") & @CRLF) EndIf Local $sCurrent = _CanonicalizeCombo(_ArrayToString($aPressed, " + ")) If $sCurrent <> "" Then $g_bWasPressing = True $g_sActiveCombo = $sCurrent Return EndIf ; no keys pressed If $g_bWasPressing Then _TriggerCombo($g_sActiveCombo) EndIf $g_bWasPressing = False $g_sActiveCombo = "" EndFunc Func _TriggerCombo($sCombo) If $sCombo = "" Then Return ; user combos For $i = 0 To UBound($g_aCombos) - 1 If $sCombo = $g_aCombos[$i] Then _RunCombo($sCombo) ExitLoop EndIf Next EndFunc Func _RunCombo($sCombo) Local $sBase = $g_sCombosDir & "\" & $sCombo Local $sSE = $sBase & "\ShellExecute" Local $sSEW = $sBase & "\ShellExecuteWait" If FileExists($sSE) Then Local $aFiles = _FileListToArray($sSE, "*", $FLTA_FILES) If Not @error Then _ArraySort($aFiles, 0, 1) For $i = 1 To $aFiles[0] ShellExecute($sSE & "\" & $aFiles[$i]) Next EndIf EndIf If FileExists($sSEW) Then Local $aFiles2 = _FileListToArray($sSEW, "*", $FLTA_FILES) If Not @error Then _ArraySort($aFiles2, 0, 1) For $j = 1 To $aFiles2[0] ShellExecuteWait($sSEW & "\" & $aFiles2[$j]) Next EndIf EndIf EndFunc ; =================================================================== ; COMBO CAPTURE (persistent until all keys released) ; =================================================================== Func _CaptureCombo() Local $hGUI = GUICreate("New Hotrkeyz Combo", 400, 100) GUICtrlCreateLabel("Press and hold your desired key combination then release all keys to confirm.", 10, 10, 380, 40) Local $idLbl = GUICtrlCreateLabel("Current: ", 10, 60, 380, 20) GUISetState(@SW_SHOW, $hGUI) While 1 Local $bWasPressing = False Local $sActive = "" Local $sFinal = "" ; Inner loop: capture one attempt While 1 Local $msg = GUIGetMsg() ; User clicked the X button ? cancel If $msg = $GUI_EVENT_CLOSE Then GUIDelete($hGUI) Return "" EndIf Local $aPressed = _GetPressedKeys() Local $sCurrent = _CanonicalizeCombo(_ArrayToString($aPressed, " + ")) GUICtrlSetData($idLbl, "Current: " & $sCurrent) If $sCurrent <> "" Then $bWasPressing = True $sActive = $sCurrent Else If $bWasPressing Then $sFinal = $sActive ExitLoop ; end of one attempt EndIf $bWasPressing = False EndIf Sleep(40) WEnd ; Reject empty or single-key combos If $sFinal = "" Then ContinueLoop Local $aParts = StringSplit($sFinal, " + ", 2) If UBound($aParts) < 2 Then MsgBox($MB_ICONWARNING, "Hotrkeyz", _ "Single-key combinations are not allowed." & @CRLF & _ "Please press at least two keys.") ContinueLoop EndIf ; Valid combo ? return it GUIDelete($hGUI) Return $sFinal WEnd EndFunc ; =================================================================== ; KEY HELPERS ; =================================================================== Func _CanonicalizeCombo($sCombo) If $sCombo = "" Then Return "" Local $aSplit = StringSplit($sCombo, " + ", 2) Local $aOut[0] For $i = 0 To UBound($aKeyOrder) - 1 For $j = 0 To UBound($aSplit) - 1 If $aSplit[$j] = $aKeyOrder[$i] Then _ArrayAdd($aOut, $aSplit[$j]) EndIf Next Next Return _ArrayToString($aOut, " + ") EndFunc Func _GetPressedKeys() Local $aOut[0] For $i = 0 To UBound($aKeyOrder) - 1 If _IsKeyDown($aKeyOrder[$i]) Then _ArrayAdd($aOut, $aKeyOrder[$i]) EndIf Next Return $aOut EndFunc Func _IsKeyDown($sKey) Return BitAND(_WinAPI_GetAsyncKeyState(_KeyToVK($sKey)), 0x8000) <> 0 EndFunc Func _KeyToVK($sKey) Switch $sKey ; ============================ ; MODIFIERS ; ============================ Case "Ctrl" Return 0x11 Case "Shift" Return 0x10 Case "Alt" Return 0x12 Case "LWin" Return 0x5B Case "RWin" Return 0x5C ; ============================ ; NAVIGATION ; ============================ Case "Insert" Return 0x2D Case "Delete" Return 0x2E Case "Home" Return 0x24 Case "End" Return 0x23 Case "PgUp" Return 0x21 Case "PgDn" Return 0x22 Case "Up" Return 0x26 Case "Down" Return 0x28 Case "Left" Return 0x25 Case "Right" Return 0x27 ; ============================ ; SYSTEM KEYS ; ============================ Case "PrintScreen" Return 0x2C Case "ScrollLock" Return 0x91 ; ============================ ; ENTER DISTINCTION ; ============================ Case "Enter" Return 0x0D Case "NUMPADENTER" Return 0x0C ; VK_SEPARATOR ; ============================ ; FUNCTION KEYS ; ============================ Case "F1" To "F24" Return 0x70 + (Int(StringTrimLeft($sKey, 1)) - 1) ; ============================ ; DIGITS (TOP ROW) ; ============================ Case "0" To "9" Return Asc($sKey) ; ============================ ; LETTERS (A–Z) ; ============================ Case "A" To "Z" Return Asc($sKey) ; ============================ ; PUNCTUATION / OEM KEYS ; ============================ Case "-" Return 0xBD Case "=" Return 0xBB Case "[" Return 0xDB Case "]" Return 0xDD Case "\" Return 0xDC Case ";" Return 0xBA Case "'" Return 0xDE Case "," Return 0xBC Case "." Return 0xBE Case "/" Return 0xBF Case "`" Return 0xC0 ; ============================ ; MULTIMEDIA KEYS ; ============================ Case "VOLUME_MUTE" Return 0xAD Case "VOLUME_DOWN" Return 0xAE Case "VOLUME_UP" Return 0xAF Case "MEDIA_NEXT" Return 0xB0 Case "MEDIA_PREV" Return 0xB1 Case "MEDIA_STOP" Return 0xB2 Case "MEDIA_PLAY_PAUSE" Return 0xB3 EndSwitch Return 0 EndFunc Func _WinAPI_GetAsyncKeyState($vKey) Local $ret = DllCall("user32.dll", "short", "GetAsyncKeyState", "int", $vKey) If @error Then Return 0 Return $ret[0] EndFunc Func _IsWindowsReservedCombo($sCombo) For $i = 0 To UBound($aWindowsReservedCombos) - 1 If $sCombo = $aWindowsReservedCombos[$i] Then Return True Next Return False EndFunc