diff --git a/AutoHotkey64.exe b/AutoHotkey64.exe
index d38aecc..50822a4 100644
Binary files a/AutoHotkey64.exe and b/AutoHotkey64.exe differ
diff --git a/UX/Templates/Minimal for v2.ahk b/UX/Templates/Minimal for v2.ahk
index c59bfef..93ca0cf 100644
--- a/UX/Templates/Minimal for v2.ahk
+++ b/UX/Templates/Minimal for v2.ahk
@@ -1,6 +1,6 @@
-/*
-[NewScriptTemplate]
-Description = Just #Requires v2.0
-*/
-#Requires AutoHotkey v2.0
-
+/*
+[NewScriptTemplate]
+Description = Just #Requires v2.0
+*/
+#Requires AutoHotkey v2.0
+
diff --git a/UX/WindowSpy.ahk b/UX/WindowSpy.ahk
index c3923ce..9b918e3 100644
--- a/UX/WindowSpy.ahk
+++ b/UX/WindowSpy.ahk
@@ -1,244 +1,244 @@
-;
-; Window Spy for AHKv2
-;
-
-#Requires AutoHotkey v2.0
-
-#NoTrayIcon
-#SingleInstance Ignore
-SetWorkingDir A_ScriptDir
-CoordMode "Pixel", "Screen"
-
-Global oGui
-
-WinSpyGui()
-
-WinSpyGui() {
- Global oGui
-
- try TraySetIcon "inc\spy.ico"
- DllCall("shell32\SetCurrentProcessExplicitAppUserModelID", "wstr", "AutoHotkey.WindowSpy")
-
- oGui := Gui("AlwaysOnTop Resize MinSize +DPIScale","Window Spy for AHKv2")
- oGui.OnEvent("Close",WinSpyClose)
- oGui.OnEvent("Size",WinSpySize)
-
- oGui.Add("Text",,"Window Title, Class and Process:")
- oGui.Add("Checkbox","yp xp+200 w120 Right vCtrl_FollowMouse","Follow Mouse").Value := 1
- oGui.Add("Edit","xm w320 r5 ReadOnly -Wrap vCtrl_Title")
- oGui.Add("Text",,"Mouse Position")
- oGui.Add("Edit","w320 r4 ReadOnly vCtrl_MousePos")
- oGui.Add("Text","w320 vCtrl_CtrlLabel",(txtFocusCtrl := "Focused Control") ":")
- oGui.Add("Edit","w320 r4 ReadOnly vCtrl_Ctrl")
- oGui.Add("Text",,"Active Window Postition:")
- oGui.Add("Edit","w320 r2 ReadOnly vCtrl_Pos")
- oGui.Add("Text",,"Status Bar Text:")
- oGui.Add("Edit","w320 r2 ReadOnly vCtrl_SBText")
- oGui.Add("Checkbox","vCtrl_IsSlow","Slow TitleMatchMode")
- oGui.Add("Text",,"Visible Text:")
- oGui.Add("Edit","w320 r2 ReadOnly vCtrl_VisText")
- oGui.Add("Text",,"All Text:")
- oGui.Add("Edit","w320 r2 ReadOnly vCtrl_AllText")
- oGui.Add("Text","w320 r1 vCtrl_Freeze",(txtNotFrozen := "(Hold Ctrl or Shift to suspend updates)"))
-
- oGui.Show("NoActivate")
- WinGetClientPos(&x_temp, &y_temp2,,,"ahk_id " oGui.hwnd)
-
- ; oGui.horzMargin := x_temp*96//A_ScreenDPI - 320 ; now using oGui.MarginX
-
- oGui.txtNotFrozen := txtNotFrozen ; create properties for futur use
- oGui.txtFrozen := "(Updates suspended)"
- oGui.txtMouseCtrl := "Control Under Mouse Position"
- oGui.txtFocusCtrl := txtFocusCtrl
-
- SetTimer Update, 250
-}
-
-WinSpySize(GuiObj, MinMax, Width, Height) {
- Global oGui
-
- If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is
- return
-
- SetTimer Update, (MinMax=0)?250:0 ; suspend updates on minimize
-
- ctrlW := Width - (oGui.MarginX * 2) ; ctrlW := Width - horzMargin
- list := "Title,MousePos,Ctrl,Pos,SBText,VisText,AllText,Freeze"
- Loop Parse list, ","
- oGui["Ctrl_" A_LoopField].Move(,,ctrlW)
-}
-
-WinSpyClose(GuiObj) {
- ExitApp
-}
-
-Update() { ; timer, no params
- Try TryUpdate() ; Try
-}
-
-TryUpdate() {
- Global oGui
-
- If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is
- return
-
- Ctrl_FollowMouse := oGui["Ctrl_FollowMouse"].Value
- CoordMode "Mouse", "Screen"
- MouseGetPos &msX, &msY, &msWin, &msCtrl, 2 ; get ClassNN and hWindow
- actWin := WinExist("A")
-
- if (Ctrl_FollowMouse) {
- curWin := msWin, curCtrl := msCtrl
- WinExist("ahk_id " curWin) ; updating LastWindowFound?
- } else {
- curWin := actWin
- curCtrl := ControlGetFocus() ; get focused control hwnd from active win
- }
- curCtrlClassNN := ""
- Try curCtrlClassNN := ControlGetClassNN(curCtrl)
-
- t1 := WinGetTitle(), t2 := WinGetClass()
- if (curWin = oGui.hwnd || t2 = "MultitaskingViewFrame") { ; Our Gui || Alt-tab
- UpdateText("Ctrl_Freeze", oGui.txtFrozen)
- return
- }
-
- UpdateText("Ctrl_Freeze", oGui.txtNotFrozen)
- t3 := WinGetProcessName(), t4 := WinGetPID()
-
- WinDataText := t1 "`n" ; ZZZ
- . "ahk_class " t2 "`n"
- . "ahk_exe " t3 "`n"
- . "ahk_pid " t4 "`n"
- . "ahk_id " curWin
-
- UpdateText("Ctrl_Title", WinDataText)
- CoordMode "Mouse", "Window"
- MouseGetPos &mrX, &mrY
- CoordMode "Mouse", "Client"
- MouseGetPos &mcX, &mcY
- mClr := PixelGetColor(msX,msY,"RGB")
- mClr := SubStr(mClr, 3)
-
- mpText := "Screen:`t" msX ", " msY "`n"
- . "Window:`t" mrX ", " mrY "`n"
- . "Client:`t" mcX ", " mcY " (default)`n"
- . "Color:`t" mClr " (Red=" SubStr(mClr, 1, 2) " Green=" SubStr(mClr, 3, 2) " Blue=" SubStr(mClr, 5) ")"
-
- UpdateText("Ctrl_MousePos", mpText)
-
- UpdateText("Ctrl_CtrlLabel", (Ctrl_FollowMouse ? oGui.txtMouseCtrl : oGui.txtFocusCtrl) ":")
-
- if (curCtrl) {
- ctrlTxt := ControlGetText(curCtrl)
- WinGetClientPos(&sX, &sY, &sW, &sH, curCtrl)
- ControlGetPos &cX, &cY, &cW, &cH, curCtrl
-
- cText := "ClassNN:`t" curCtrlClassNN "`n"
- . "Text:`t" textMangle(ctrlTxt) "`n"
- . "Screen:`tx: " sX "`ty: " sY "`tw: " sW "`th: " sH "`n"
- . "Client`tx: " cX "`ty: " cY "`tw: " cW "`th: " cH
- } else
- cText := ""
-
- UpdateText("Ctrl_Ctrl", cText)
- wX := "", wY := "", wW := "", wH := ""
- WinGetPos &wX, &wY, &wW, &wH, "ahk_id " curWin
- WinGetClientPos(&wcX, &wcY, &wcW, &wcH, "ahk_id " curWin)
-
- wText := "Screen:`tx: " wX "`ty: " wY "`tw: " wW "`th: " wH "`n"
- . "Client:`tx: " wcX "`ty: " wcY "`tw: " wcW "`th: " wcH
-
- UpdateText("Ctrl_Pos", wText)
- sbTxt := ""
-
- Loop {
- ovi := ""
- Try ovi := StatusBarGetText(A_Index)
- if (ovi = "")
- break
- sbTxt .= "(" A_Index "):`t" textMangle(ovi) "`n"
- }
-
- sbTxt := SubStr(sbTxt,1,-1) ; StringTrimRight, sbTxt, sbTxt, 1
- UpdateText("Ctrl_SBText", sbTxt)
- bSlow := oGui["Ctrl_IsSlow"].Value ; GuiControlGet, bSlow,, Ctrl_IsSlow
-
- if (bSlow) {
- DetectHiddenText False
- ovVisText := WinGetText() ; WinGetText, ovVisText
- DetectHiddenText True
- ovAllText := WinGetText() ; WinGetText, ovAllText
- } else {
- ovVisText := WinGetTextFast(false)
- ovAllText := WinGetTextFast(true)
- }
-
- UpdateText("Ctrl_VisText", ovVisText)
- UpdateText("Ctrl_AllText", ovAllText)
-}
-
-; ===========================================================================================
-; WinGetText ALWAYS uses the "slow" mode - TitleMatchMode only affects
-; WinText/ExcludeText parameters. In "fast" mode, GetWindowText() is used
-; to retrieve the text of each control.
-; ===========================================================================================
-WinGetTextFast(detect_hidden) {
- controls := WinGetControlsHwnd()
-
- static WINDOW_TEXT_SIZE := 32767 ; Defined in AutoHotkey source.
-
- buf := Buffer(WINDOW_TEXT_SIZE * 2,0)
-
- text := ""
-
- Loop controls.Length {
- hCtl := controls[A_Index]
- if !detect_hidden && !DllCall("IsWindowVisible", "ptr", hCtl)
- continue
- if !DllCall("GetWindowText", "ptr", hCtl, "Ptr", buf.ptr, "int", WINDOW_TEXT_SIZE)
- continue
-
- text .= StrGet(buf) "`r`n" ; text .= buf "`r`n"
- }
- return text
-}
-
-; ===========================================================================================
-; Unlike using a pure GuiControl, this function causes the text of the
-; controls to be updated only when the text has changed, preventing periodic
-; flickering (especially on older systems).
-; ===========================================================================================
-UpdateText(vCtl, NewText) {
- Global oGui
- static OldText := {}
- ctl := oGui[vCtl], hCtl := Integer(ctl.hwnd)
-
- if (!oldText.HasProp(hCtl) Or OldText.%hCtl% != NewText) {
- ctl.Value := NewText
- OldText.%hCtl% := NewText
- }
-}
-
-textMangle(x) {
- elli := false
- if (pos := InStr(x, "`n"))
- x := SubStr(x, 1, pos-1), elli := true
- else if (StrLen(x) > 40)
- x := SubStr(x,1,40), elli := true
- if elli
- x .= " (...)"
- return x
-}
-
-suspend_timer() {
- Global oGui
- SetTimer Update, 0
- UpdateText("Ctrl_Freeze", oGui.txtFrozen)
-}
-
-~*Shift::
-~*Ctrl::suspend_timer()
-
-~*Ctrl up::
-~*Shift up::SetTimer Update, 250
+;
+; Window Spy for AHKv2
+;
+
+#Requires AutoHotkey v2.0
+
+#NoTrayIcon
+#SingleInstance Ignore
+SetWorkingDir A_ScriptDir
+CoordMode "Pixel", "Screen"
+
+Global oGui
+
+WinSpyGui()
+
+WinSpyGui() {
+ Global oGui
+
+ try TraySetIcon "inc\spy.ico"
+ DllCall("shell32\SetCurrentProcessExplicitAppUserModelID", "wstr", "AutoHotkey.WindowSpy")
+
+ oGui := Gui("AlwaysOnTop Resize MinSize +DPIScale","Window Spy for AHKv2")
+ oGui.OnEvent("Close",WinSpyClose)
+ oGui.OnEvent("Size",WinSpySize)
+
+ oGui.Add("Text",,"Window Title, Class and Process:")
+ oGui.Add("Checkbox","yp xp+200 w120 Right vCtrl_FollowMouse","Follow Mouse").Value := 1
+ oGui.Add("Edit","xm w320 r5 ReadOnly -Wrap vCtrl_Title")
+ oGui.Add("Text",,"Mouse Position")
+ oGui.Add("Edit","w320 r4 ReadOnly vCtrl_MousePos")
+ oGui.Add("Text","w320 vCtrl_CtrlLabel",(txtFocusCtrl := "Focused Control") ":")
+ oGui.Add("Edit","w320 r4 ReadOnly vCtrl_Ctrl")
+ oGui.Add("Text",,"Active Window Postition:")
+ oGui.Add("Edit","w320 r2 ReadOnly vCtrl_Pos")
+ oGui.Add("Text",,"Status Bar Text:")
+ oGui.Add("Edit","w320 r2 ReadOnly vCtrl_SBText")
+ oGui.Add("Checkbox","vCtrl_IsSlow","Slow TitleMatchMode")
+ oGui.Add("Text",,"Visible Text:")
+ oGui.Add("Edit","w320 r2 ReadOnly vCtrl_VisText")
+ oGui.Add("Text",,"All Text:")
+ oGui.Add("Edit","w320 r2 ReadOnly vCtrl_AllText")
+ oGui.Add("Text","w320 r1 vCtrl_Freeze",(txtNotFrozen := "(Hold Ctrl or Shift to suspend updates)"))
+
+ oGui.Show("NoActivate")
+ WinGetClientPos(&x_temp, &y_temp2,,,"ahk_id " oGui.hwnd)
+
+ ; oGui.horzMargin := x_temp*96//A_ScreenDPI - 320 ; now using oGui.MarginX
+
+ oGui.txtNotFrozen := txtNotFrozen ; create properties for futur use
+ oGui.txtFrozen := "(Updates suspended)"
+ oGui.txtMouseCtrl := "Control Under Mouse Position"
+ oGui.txtFocusCtrl := txtFocusCtrl
+
+ SetTimer Update, 250
+}
+
+WinSpySize(GuiObj, MinMax, Width, Height) {
+ Global oGui
+
+ If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is
+ return
+
+ SetTimer Update, (MinMax=0)?250:0 ; suspend updates on minimize
+
+ ctrlW := Width - (oGui.MarginX * 2) ; ctrlW := Width - horzMargin
+ list := "Title,MousePos,Ctrl,Pos,SBText,VisText,AllText,Freeze"
+ Loop Parse list, ","
+ oGui["Ctrl_" A_LoopField].Move(,,ctrlW)
+}
+
+WinSpyClose(GuiObj) {
+ ExitApp
+}
+
+Update() { ; timer, no params
+ Try TryUpdate() ; Try
+}
+
+TryUpdate() {
+ Global oGui
+
+ If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is
+ return
+
+ Ctrl_FollowMouse := oGui["Ctrl_FollowMouse"].Value
+ CoordMode "Mouse", "Screen"
+ MouseGetPos &msX, &msY, &msWin, &msCtrl, 2 ; get ClassNN and hWindow
+ actWin := WinExist("A")
+
+ if (Ctrl_FollowMouse) {
+ curWin := msWin, curCtrl := msCtrl
+ WinExist("ahk_id " curWin) ; updating LastWindowFound?
+ } else {
+ curWin := actWin
+ curCtrl := ControlGetFocus() ; get focused control hwnd from active win
+ }
+ curCtrlClassNN := ""
+ Try curCtrlClassNN := ControlGetClassNN(curCtrl)
+
+ t1 := WinGetTitle(), t2 := WinGetClass()
+ if (curWin = oGui.hwnd || t2 = "MultitaskingViewFrame") { ; Our Gui || Alt-tab
+ UpdateText("Ctrl_Freeze", oGui.txtFrozen)
+ return
+ }
+
+ UpdateText("Ctrl_Freeze", oGui.txtNotFrozen)
+ t3 := WinGetProcessName(), t4 := WinGetPID()
+
+ WinDataText := t1 "`n" ; ZZZ
+ . "ahk_class " t2 "`n"
+ . "ahk_exe " t3 "`n"
+ . "ahk_pid " t4 "`n"
+ . "ahk_id " curWin
+
+ UpdateText("Ctrl_Title", WinDataText)
+ CoordMode "Mouse", "Window"
+ MouseGetPos &mrX, &mrY
+ CoordMode "Mouse", "Client"
+ MouseGetPos &mcX, &mcY
+ mClr := PixelGetColor(msX,msY,"RGB")
+ mClr := SubStr(mClr, 3)
+
+ mpText := "Screen:`t" msX ", " msY "`n"
+ . "Window:`t" mrX ", " mrY "`n"
+ . "Client:`t" mcX ", " mcY " (default)`n"
+ . "Color:`t" mClr " (Red=" SubStr(mClr, 1, 2) " Green=" SubStr(mClr, 3, 2) " Blue=" SubStr(mClr, 5) ")"
+
+ UpdateText("Ctrl_MousePos", mpText)
+
+ UpdateText("Ctrl_CtrlLabel", (Ctrl_FollowMouse ? oGui.txtMouseCtrl : oGui.txtFocusCtrl) ":")
+
+ if (curCtrl) {
+ ctrlTxt := ControlGetText(curCtrl)
+ WinGetClientPos(&sX, &sY, &sW, &sH, curCtrl)
+ ControlGetPos &cX, &cY, &cW, &cH, curCtrl
+
+ cText := "ClassNN:`t" curCtrlClassNN "`n"
+ . "Text:`t" textMangle(ctrlTxt) "`n"
+ . "Screen:`tx: " sX "`ty: " sY "`tw: " sW "`th: " sH "`n"
+ . "Client`tx: " cX "`ty: " cY "`tw: " cW "`th: " cH
+ } else
+ cText := ""
+
+ UpdateText("Ctrl_Ctrl", cText)
+ wX := "", wY := "", wW := "", wH := ""
+ WinGetPos &wX, &wY, &wW, &wH, "ahk_id " curWin
+ WinGetClientPos(&wcX, &wcY, &wcW, &wcH, "ahk_id " curWin)
+
+ wText := "Screen:`tx: " wX "`ty: " wY "`tw: " wW "`th: " wH "`n"
+ . "Client:`tx: " wcX "`ty: " wcY "`tw: " wcW "`th: " wcH
+
+ UpdateText("Ctrl_Pos", wText)
+ sbTxt := ""
+
+ Loop {
+ ovi := ""
+ Try ovi := StatusBarGetText(A_Index)
+ if (ovi = "")
+ break
+ sbTxt .= "(" A_Index "):`t" textMangle(ovi) "`n"
+ }
+
+ sbTxt := SubStr(sbTxt,1,-1) ; StringTrimRight, sbTxt, sbTxt, 1
+ UpdateText("Ctrl_SBText", sbTxt)
+ bSlow := oGui["Ctrl_IsSlow"].Value ; GuiControlGet, bSlow,, Ctrl_IsSlow
+
+ if (bSlow) {
+ DetectHiddenText False
+ ovVisText := WinGetText() ; WinGetText, ovVisText
+ DetectHiddenText True
+ ovAllText := WinGetText() ; WinGetText, ovAllText
+ } else {
+ ovVisText := WinGetTextFast(false)
+ ovAllText := WinGetTextFast(true)
+ }
+
+ UpdateText("Ctrl_VisText", ovVisText)
+ UpdateText("Ctrl_AllText", ovAllText)
+}
+
+; ===========================================================================================
+; WinGetText ALWAYS uses the "slow" mode - TitleMatchMode only affects
+; WinText/ExcludeText parameters. In "fast" mode, GetWindowText() is used
+; to retrieve the text of each control.
+; ===========================================================================================
+WinGetTextFast(detect_hidden) {
+ controls := WinGetControlsHwnd()
+
+ static WINDOW_TEXT_SIZE := 32767 ; Defined in AutoHotkey source.
+
+ buf := Buffer(WINDOW_TEXT_SIZE * 2,0)
+
+ text := ""
+
+ Loop controls.Length {
+ hCtl := controls[A_Index]
+ if !detect_hidden && !DllCall("IsWindowVisible", "ptr", hCtl)
+ continue
+ if !DllCall("GetWindowText", "ptr", hCtl, "Ptr", buf.ptr, "int", WINDOW_TEXT_SIZE)
+ continue
+
+ text .= StrGet(buf) "`r`n" ; text .= buf "`r`n"
+ }
+ return text
+}
+
+; ===========================================================================================
+; Unlike using a pure GuiControl, this function causes the text of the
+; controls to be updated only when the text has changed, preventing periodic
+; flickering (especially on older systems).
+; ===========================================================================================
+UpdateText(vCtl, NewText) {
+ Global oGui
+ static OldText := {}
+ ctl := oGui[vCtl], hCtl := Integer(ctl.hwnd)
+
+ if (!oldText.HasProp(hCtl) Or OldText.%hCtl% != NewText) {
+ ctl.Value := NewText
+ OldText.%hCtl% := NewText
+ }
+}
+
+textMangle(x) {
+ elli := false
+ if (pos := InStr(x, "`n"))
+ x := SubStr(x, 1, pos-1), elli := true
+ else if (StrLen(x) > 40)
+ x := SubStr(x,1,40), elli := true
+ if elli
+ x .= " (...)"
+ return x
+}
+
+suspend_timer() {
+ Global oGui
+ SetTimer Update, 0
+ UpdateText("Ctrl_Freeze", oGui.txtFrozen)
+}
+
+~*Shift::
+~*Ctrl::suspend_timer()
+
+~*Ctrl up::
+~*Shift up::SetTimer Update, 250
diff --git a/UX/inc/CommandLineToArgs.ahk b/UX/inc/CommandLineToArgs.ahk
index 1083f27..209f961 100644
--- a/UX/inc/CommandLineToArgs.ahk
+++ b/UX/inc/CommandLineToArgs.ahk
@@ -1,12 +1,12 @@
-
-CommandLineToArgs(cmd) {
- argv := DllCall("shell32\CommandLineToArgvW", "wstr", cmd, 'int*', &narg:=0, "ptr")
- try {
- args := []
- Loop args.Capacity := narg
- args.Push(StrGet(NumGet(argv, (A_Index-1)*A_PtrSize, "ptr"), "UTF-16"))
- }
- finally
- DllCall("LocalFree", "ptr", argv)
- return args
-}
+
+CommandLineToArgs(cmd) {
+ argv := DllCall("shell32\CommandLineToArgvW", "wstr", cmd, 'int*', &narg:=0, "ptr")
+ try {
+ args := []
+ Loop args.Capacity := narg
+ args.Push(StrGet(NumGet(argv, (A_Index-1)*A_PtrSize, "ptr"), "UTF-16"))
+ }
+ finally
+ DllCall("LocalFree", "ptr", argv)
+ return args
+}
diff --git a/UX/inc/CreateAppShortcut.ahk b/UX/inc/CreateAppShortcut.ahk
index 9d0656f..1492a94 100644
--- a/UX/inc/CreateAppShortcut.ahk
+++ b/UX/inc/CreateAppShortcut.ahk
@@ -1,37 +1,37 @@
-CreateAppShortcut(linkFile, p) {
- ;target, args, description, aumid, uninst?
- lnk := ComObject('{00021401-0000-0000-C000-000000000046}' ; CLSID_ShellLink
- ,'{000214F9-0000-0000-C000-000000000046}') ; IID_IShellLink
-
- ComCall(20, lnk, 'wstr', p.target)
- ComCall(11, lnk, 'wstr', p.HasProp('args') ? p.args : "")
- ComCall(7, lnk, 'wstr', p.desc)
- if p.HasProp('icon')
- ComCall(17, lnk, 'wstr', p.icon, 'int', p.HasProp('iconIndex') ? p.iconIndex : 0)
-
- ; Set the System.AppUserModel.ID property via IPropertyStore
- props := ComObjQuery(lnk, '{886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99}')
- static PKEY_AppUserModel_ID := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 5)
- static PKEY_AppUserModel_UninstallCommand := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 37)
- setProp PKEY_AppUserModel_ID, p.aumid
- if p.HasProp('uninst')
- setProp PKEY_AppUserModel_UninstallCommand, p.uninst
-
- ; Save via IPersistFile
- pf := ComObjQuery(lnk, '{0000010B-0000-0000-C000-000000000046}')
- ComCall(6, pf, 'wstr', linkFile, 'int', true)
-
- setProp(key, value) {
- propvar := Buffer(24, 0), propref := ComValue(0x400C, propvar.ptr)
- propref[] := String(value)
- ComCall(6, props, 'ptr', key, 'ptr', propvar)
- propref[] := 0
- }
-
- PKEY(sguid, propID) {
- pk := Buffer(20)
- DllCall('ole32\IIDFromString', 'wstr', sguid, 'ptr', pk, 'hresult')
- NumPut('int', propID, pk, 16)
- return pk
- }
-}
+CreateAppShortcut(linkFile, p) {
+ ;target, args, description, aumid, uninst?
+ lnk := ComObject('{00021401-0000-0000-C000-000000000046}' ; CLSID_ShellLink
+ ,'{000214F9-0000-0000-C000-000000000046}') ; IID_IShellLink
+
+ ComCall(20, lnk, 'wstr', p.target)
+ ComCall(11, lnk, 'wstr', p.HasProp('args') ? p.args : "")
+ ComCall(7, lnk, 'wstr', p.desc)
+ if p.HasProp('icon')
+ ComCall(17, lnk, 'wstr', p.icon, 'int', p.HasProp('iconIndex') ? p.iconIndex : 0)
+
+ ; Set the System.AppUserModel.ID property via IPropertyStore
+ props := ComObjQuery(lnk, '{886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99}')
+ static PKEY_AppUserModel_ID := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 5)
+ static PKEY_AppUserModel_UninstallCommand := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 37)
+ setProp PKEY_AppUserModel_ID, p.aumid
+ if p.HasProp('uninst')
+ setProp PKEY_AppUserModel_UninstallCommand, p.uninst
+
+ ; Save via IPersistFile
+ pf := ComObjQuery(lnk, '{0000010B-0000-0000-C000-000000000046}')
+ ComCall(6, pf, 'wstr', linkFile, 'int', true)
+
+ setProp(key, value) {
+ propvar := Buffer(24, 0), propref := ComValue(0x400C, propvar.ptr)
+ propref[] := String(value)
+ ComCall(6, props, 'ptr', key, 'ptr', propvar)
+ propref[] := 0
+ }
+
+ PKEY(sguid, propID) {
+ pk := Buffer(20)
+ DllCall('ole32\IIDFromString', 'wstr', sguid, 'ptr', pk, 'hresult')
+ NumPut('int', propID, pk, 16)
+ return pk
+ }
+}
diff --git a/UX/inc/EnableUIAccess.ahk b/UX/inc/EnableUIAccess.ahk
index 8c6d2c7..79e6617 100644
--- a/UX/inc/EnableUIAccess.ahk
+++ b/UX/inc/EnableUIAccess.ahk
@@ -1,191 +1,212 @@
-EnableUIAccess(ExePath) {
- static CertName := "AutoHotkey"
- hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W
- , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE
- , "wstr", "Root", "ptr")
- if !hStore
- throw OSError()
- store := CertStore(hStore)
- ; Find or create certificate for signing.
- cert := CertContext()
- while (cert.ptr := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore
- , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING
- , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR
- , "wstr", CertName, "ptr", cert.ptr, "ptr"))
- && !(DllCall("Crypt32\CryptAcquireCertificatePrivateKey"
- , "ptr", cert, "uint", 1 ; CRYPT_ACQUIRE_CACHE_FLAG
- , "ptr", 0, "ptr*", 0, "uint*", &keySpec:=0, "ptr", 0)
- && (keySpec & 2)) { ; AT_SIGNATURE
- ; Keep looking for a certificate with a private key.
- }
- if !cert.ptr
- cert := EnableUIAccess_CreateCert(CertName, hStore)
- ; Set uiAccess attribute in manifest.
- EnableUIAccess_SetManifest(ExePath)
- ; Sign the file (otherwise uiAccess attribute is ignored).
- EnableUIAccess_SignFile(ExePath, cert, CertName)
-}
-
-EnableUIAccess_SetManifest(ExePath) {
- xml := ComObject("Msxml2.DOMDocument")
- xml.async := false
- xml.setProperty("SelectionLanguage", "XPath")
- xml.setProperty("SelectionNamespaces"
- , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' "
- . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'")
- if !xml.load("res://" ExePath "/#24/#1") ; Load current manifest
- throw Error("File or manifest not found",, ExePath)
-
- node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security"
- . "/v3:requestedPrivileges/v3:requestedExecutionLevel")
- if !node ; Not AutoHotkey?
- throw Error("Manifest is missing required elements")
-
- node.setAttribute("uiAccess", "true")
- xml := RTrim(xml.xml, "`r`n")
-
- data := Buffer(StrPut(xml, "utf-8") - 1)
- StrPut(xml, data, "utf-8")
-
- if !(hupd := DllCall("BeginUpdateResource", "str", ExePath, "int", false))
- throw OSError()
- r := DllCall("UpdateResource", "ptr", hupd, "ptr", 24, "ptr", 1
- , "ushort", 1033, "ptr", data, "uint", data.size)
-
- ; Retry loop to work around file locks (especially by antivirus)
- for delay in [0, 100, 500, 1000, 3500] {
- Sleep delay
- if DllCall("EndUpdateResource", "ptr", hupd, "int", !r) || !r
- return
- if !(A_LastError = 5 || A_LastError = 110) ; ERROR_ACCESS_DENIED || ERROR_OPEN_FAILED
- break
- }
- throw OSError(A_LastError, "EndUpdateResource")
-}
-
-EnableUIAccess_CreateCert(Name, hStore) {
- ; Here Name is used as the key container name.
- prov := CryptContext()
- if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov
- , "str", Name, "ptr", 0, "uint", 1, "uint", 0) { ; PROV_RSA_FULL=1, open existing=0
- if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov
- , "str", Name, "ptr", 0, "uint", 1, "uint", 8) ; PROV_RSA_FULL=1, CRYPT_NEWKEYSET=8
- throw OSError()
- if !DllCall("Advapi32\CryptGenKey", "ptr", prov
- , "uint", 2, "uint", 0x4000001, "ptr*", CryptKey()) ; AT_SIGNATURE=2, EXPORTABLE=..01
- throw OSError()
- }
-
- ; Here Name is used as the certificate subject and name.
- Loop 2 {
- if A_Index = 1
- pbName := cbName := 0
- else
- bName := Buffer(cbName), pbName := bName.ptr
- if !DllCall("Crypt32\CertStrToName", "uint", 1, "str", "CN=" Name
- , "uint", 3, "ptr", 0, "ptr", pbName, "uint*", &cbName, "ptr", 0) ; X509_ASN_ENCODING=1, CERT_X500_NAME_STR=3
- throw OSError()
- }
- cnb := Buffer(2*A_PtrSize), NumPut("ptr", cbName, "ptr", pbName, cnb)
-
- endTime := Buffer(16)
- DllCall("GetSystemTime", "ptr", endTime)
- NumPut("ushort", NumGet(endTime, "ushort") + 10, endTime) ; += 10 years
-
- if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate"
- , "ptr", prov, "ptr", cnb, "uint", 0, "ptr", 0
- , "ptr", 0, "ptr", 0, "ptr", endTime, "ptr", 0, "ptr")
- throw OSError()
- cert := CertContext(hCert)
-
- if !DllCall("Crypt32\CertAddCertificateContextToStore", "ptr", hStore
- , "ptr", hCert, "uint", 1, "ptr", 0) ; STORE_ADD_NEW=1
- throw OSError()
-
- return cert
-}
-
-EnableUIAccess_DeleteCertAndKey(Name) {
- ; This first call "acquires" the key container but also deletes it.
- DllCall("Advapi32\CryptAcquireContext", "ptr*", 0, "str", Name
- , "ptr", 0, "uint", 1, "uint", 16) ; PROV_RSA_FULL=1, CRYPT_DELETEKEYSET=16
- if !hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W
- , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE
- , "wstr", "Root", "ptr")
- throw OSError()
- store := CertStore(hStore)
- deleted := 0
- ; Multiple certificates might be created over time as keys become inaccessible.
- while p := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore
- , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING
- , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR
- , "wstr", Name, "ptr", 0, "ptr") {
- if !DllCall("Crypt32\CertDeleteCertificateFromStore", "ptr", p)
- throw OSError()
- deleted++
- }
- return deleted
-}
-
-class CryptPtrBase {
- __new(p:=0) => this.ptr := p
- __delete() => this.ptr && this.Dispose()
-}
-class CryptContext extends CryptPtrBase {
- Dispose() => DllCall("Advapi32\CryptReleaseContext", "ptr", this, "uint", 0)
-}
-class CertContext extends CryptPtrBase {
- Dispose() => DllCall("Crypt32\CertFreeCertificateContext", "ptr", this)
-}
-class CertStore extends CryptPtrBase {
- Dispose() => DllCall("Crypt32\CertCloseStore", "ptr", this, "uint", 0)
-}
-class CryptKey extends CryptPtrBase {
- Dispose() => DllCall("Advapi32\CryptDestroyKey", "ptr", this)
-}
-
-EnableUIAccess_SignFile(ExePath, CertCtx, Name) {
- file_info := struct( ; SIGNER_FILE_INFO
- "ptr", A_PtrSize*3, "ptr", StrPtr(ExePath))
- dwIndex := Buffer(4, 0) ; DWORD
- subject_info := struct( ; SIGNER_SUBJECT_INFO
- "ptr", A_PtrSize*4, "ptr", dwIndex.ptr, "ptr", SIGNER_SUBJECT_FILE:=1,
- "ptr", file_info.ptr)
- cert_store_info := struct( ; SIGNER_CERT_STORE_INFO
- "ptr", A_PtrSize*4, "ptr", CertCtx.ptr, "ptr", SIGNER_CERT_POLICY_CHAIN:=2)
- cert_info := struct( ; SIGNER_CERT
- "uint", 8+A_PtrSize*2, "uint", SIGNER_CERT_STORE:=2,
- "ptr", cert_store_info.ptr)
- authcode_attr := struct( ; SIGNER_ATTR_AUTHCODE
- "uint", 8+A_PtrSize*3, "int", false, "ptr", true, "ptr", StrPtr(Name))
- sig_info := struct( ; SIGNER_SIGNATURE_INFO
- "uint", 8+A_PtrSize*4, "uint", CALG_SHA1:=0x8004,
- "ptr", SIGNER_AUTHCODE_ATTR:=1, "ptr", authcode_attr.ptr)
-
- DllCall("MSSign32\SignerSign"
- , "ptr", subject_info, "ptr", cert_info, "ptr", sig_info
- , "ptr", 0, "ptr", 0, "ptr", 0, "ptr", 0, "hresult")
-
- struct(args*) => (
- args.Push(b := Buffer(args[2], 0)),
- NumPut(args*),
- b
- )
-}
-
-; Verifies a signed executable file. Returns 0 on success, or a standard OS error number.
-EnableUIAccess_Verify(ExePath) {
- wfi := Buffer(4*A_PtrSize) ; WINTRUST_FILE_INFO
- NumPut('ptr', wfi.size, 'ptr', StrPtr(ExePath), 'ptr', 0, 'ptr', 0, wfi)
-
- ; WINTRUST_ACTION_GENERIC_VERIFY_V2
- NumPut('int64', 0x11d0cd4400aac56b, 'int64', 0xee95c24fc000c28c, actionID := Buffer(16))
-
- wtd := Buffer(9*A_PtrSize+16) ; WINTRUST_DATA
- NumPut(
- 'ptr', wtd.Size, 'ptr', 0, 'ptr', 0, 'int', WTD_UI_NONE:=2, 'int', WTD_REVOKE_NONE:=0,
- 'ptr', WTD_CHOICE_FILE:=1, 'ptr', wfi.ptr, 'ptr', WTD_STATEACTION_VERIFY:=1,
- 'ptr', 0, 'ptr', 0, 'int', 0, 'int', 0, 'ptr', 0, wtd
- )
- return DllCall('wintrust\WinVerifyTrust', 'ptr', 0, 'ptr', actionID, 'ptr', wtd, 'int')
-}
+EnableUIAccess(ExePath) {
+ static CertName := "AutoHotkey"
+ hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W
+ , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE
+ , "wstr", "Root", "ptr")
+ if !hStore
+ throw OSError()
+ store := CertStore(hStore)
+ ; Find or create certificate for signing.
+ cert := CertContext()
+ while (cert.ptr := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore
+ , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING
+ , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR
+ , "wstr", CertName, "ptr", cert.ptr, "ptr"))
+ && !(DllCall("Crypt32\CryptAcquireCertificatePrivateKey"
+ , "ptr", cert, "uint", 5 ; CRYPT_ACQUIRE_CACHE_FLAG|CRYPT_ACQUIRE_COMPARE_KEY_FLAG
+ , "ptr", 0, "ptr*", 0, "uint*", &keySpec:=0, "ptr", 0)
+ && (keySpec & 2)) { ; AT_SIGNATURE
+ ; Keep looking for a certificate with a private key.
+ }
+ if !cert.ptr
+ cert := EnableUIAccess_CreateCert(CertName, hStore)
+ ; Set uiAccess attribute in manifest.
+ EnableUIAccess_SetManifest(ExePath)
+ ; Sign the file (otherwise uiAccess attribute is ignored).
+ EnableUIAccess_SignFile(ExePath, cert, CertName)
+}
+
+EnableUIAccess_SetManifest(ExePath) {
+ xml := ComObject("Msxml2.DOMDocument")
+ xml.async := false
+ xml.setProperty("SelectionLanguage", "XPath")
+ xml.setProperty("SelectionNamespaces"
+ , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' "
+ . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'")
+ try
+ if !xml.loadXML(EnableUIAccess_ReadManifest(ExePath))
+ throw Error("Invalid manifest")
+ catch as e
+ throw Error("Error loading manifest from " ExePath,, e.Message "`n @ " e.File ":" e.Line)
+
+
+ node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security"
+ . "/v3:requestedPrivileges/v3:requestedExecutionLevel")
+ if !node ; Not AutoHotkey?
+ throw Error("Manifest is missing required elements")
+
+ node.setAttribute("uiAccess", "true")
+ xml := RTrim(xml.xml, "`r`n")
+
+ data := Buffer(StrPut(xml, "utf-8") - 1)
+ StrPut(xml, data, "utf-8")
+
+ if !(hupd := DllCall("BeginUpdateResource", "str", ExePath, "int", false))
+ throw OSError()
+ r := DllCall("UpdateResource", "ptr", hupd, "ptr", 24, "ptr", 1
+ , "ushort", 1033, "ptr", data, "uint", data.size)
+
+ ; Retry loop to work around file locks (especially by antivirus)
+ for delay in [0, 100, 500, 1000, 3500] {
+ Sleep delay
+ if DllCall("EndUpdateResource", "ptr", hupd, "int", !r) || !r
+ return
+ if !(A_LastError = 5 || A_LastError = 110) ; ERROR_ACCESS_DENIED || ERROR_OPEN_FAILED
+ break
+ }
+ throw OSError(A_LastError, "EndUpdateResource")
+}
+
+EnableUIAccess_ReadManifest(ExePath) {
+ if !(hmod := DllCall("LoadLibraryEx", "str", ExePath, "ptr", 0, "uint", 2, "ptr"))
+ throw OSError()
+ try {
+ if !(hres := DllCall("FindResource", "ptr", hmod, "ptr", 1, "ptr", 24, "ptr"))
+ throw OSError()
+ size := DllCall("SizeofResource", "ptr", hmod, "ptr", hres, "uint")
+ if !(hglb := DllCall("LoadResource", "ptr", hmod, "ptr", hres, "ptr"))
+ throw OSError()
+ if !(pres := DllCall("LockResource", "ptr", hglb, "ptr"))
+ throw OSError()
+ return StrGet(pres, size, "utf-8")
+ }
+ finally
+ DllCall("FreeLibrary", "ptr", hmod)
+}
+
+EnableUIAccess_CreateCert(Name, hStore) {
+ ; Here Name is used as the key container name.
+ prov := CryptContext()
+ if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov
+ , "str", Name, "ptr", 0, "uint", 1, "uint", 0) { ; PROV_RSA_FULL=1, open existing=0
+ if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov
+ , "str", Name, "ptr", 0, "uint", 1, "uint", 8) ; PROV_RSA_FULL=1, CRYPT_NEWKEYSET=8
+ throw OSError()
+ if !DllCall("Advapi32\CryptGenKey", "ptr", prov
+ , "uint", 2, "uint", 0x4000001, "ptr*", CryptKey()) ; AT_SIGNATURE=2, EXPORTABLE=..01
+ throw OSError()
+ }
+
+ ; Here Name is used as the certificate subject and name.
+ Loop 2 {
+ if A_Index = 1
+ pbName := cbName := 0
+ else
+ bName := Buffer(cbName), pbName := bName.ptr
+ if !DllCall("Crypt32\CertStrToName", "uint", 1, "str", "CN=" Name
+ , "uint", 3, "ptr", 0, "ptr", pbName, "uint*", &cbName, "ptr", 0) ; X509_ASN_ENCODING=1, CERT_X500_NAME_STR=3
+ throw OSError()
+ }
+ cnb := Buffer(2*A_PtrSize), NumPut("ptr", cbName, "ptr", pbName, cnb)
+
+ endTime := Buffer(16)
+ DllCall("GetSystemTime", "ptr", endTime)
+ NumPut("ushort", NumGet(endTime, "ushort") + 10, endTime) ; += 10 years
+
+ if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate"
+ , "ptr", prov, "ptr", cnb, "uint", 0, "ptr", 0
+ , "ptr", 0, "ptr", 0, "ptr", endTime, "ptr", 0, "ptr")
+ throw OSError()
+ cert := CertContext(hCert)
+
+ if !DllCall("Crypt32\CertAddCertificateContextToStore", "ptr", hStore
+ , "ptr", hCert, "uint", 1, "ptr", 0) ; STORE_ADD_NEW=1
+ throw OSError()
+
+ return cert
+}
+
+EnableUIAccess_DeleteCertAndKey(Name) {
+ ; This first call "acquires" the key container but also deletes it.
+ DllCall("Advapi32\CryptAcquireContext", "ptr*", 0, "str", Name
+ , "ptr", 0, "uint", 1, "uint", 16) ; PROV_RSA_FULL=1, CRYPT_DELETEKEYSET=16
+ if !hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W
+ , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE
+ , "wstr", "Root", "ptr")
+ throw OSError()
+ store := CertStore(hStore)
+ deleted := 0
+ ; Multiple certificates might be created over time as keys become inaccessible.
+ while p := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore
+ , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING
+ , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR
+ , "wstr", Name, "ptr", 0, "ptr") {
+ if !DllCall("Crypt32\CertDeleteCertificateFromStore", "ptr", p)
+ throw OSError()
+ deleted++
+ }
+ return deleted
+}
+
+class CryptPtrBase {
+ __new(p:=0) => this.ptr := p
+ __delete() => this.ptr && this.Dispose()
+}
+class CryptContext extends CryptPtrBase {
+ Dispose() => DllCall("Advapi32\CryptReleaseContext", "ptr", this, "uint", 0)
+}
+class CertContext extends CryptPtrBase {
+ Dispose() => DllCall("Crypt32\CertFreeCertificateContext", "ptr", this)
+}
+class CertStore extends CryptPtrBase {
+ Dispose() => DllCall("Crypt32\CertCloseStore", "ptr", this, "uint", 0)
+}
+class CryptKey extends CryptPtrBase {
+ Dispose() => DllCall("Advapi32\CryptDestroyKey", "ptr", this)
+}
+
+EnableUIAccess_SignFile(ExePath, CertCtx, Name) {
+ file_info := struct( ; SIGNER_FILE_INFO
+ "ptr", A_PtrSize*3, "ptr", StrPtr(ExePath))
+ dwIndex := Buffer(4, 0) ; DWORD
+ subject_info := struct( ; SIGNER_SUBJECT_INFO
+ "ptr", A_PtrSize*4, "ptr", dwIndex.ptr, "ptr", SIGNER_SUBJECT_FILE:=1,
+ "ptr", file_info.ptr)
+ cert_store_info := struct( ; SIGNER_CERT_STORE_INFO
+ "ptr", A_PtrSize*4, "ptr", CertCtx.ptr, "ptr", SIGNER_CERT_POLICY_CHAIN:=2)
+ cert_info := struct( ; SIGNER_CERT
+ "uint", 8+A_PtrSize*2, "uint", SIGNER_CERT_STORE:=2,
+ "ptr", cert_store_info.ptr)
+ authcode_attr := struct( ; SIGNER_ATTR_AUTHCODE
+ "uint", 8+A_PtrSize*3, "int", false, "ptr", true, "ptr", StrPtr(Name))
+ sig_info := struct( ; SIGNER_SIGNATURE_INFO
+ "uint", 8+A_PtrSize*4, "uint", CALG_SHA1:=0x8004,
+ "ptr", SIGNER_AUTHCODE_ATTR:=1, "ptr", authcode_attr.ptr)
+
+ DllCall("MSSign32\SignerSign"
+ , "ptr", subject_info, "ptr", cert_info, "ptr", sig_info
+ , "ptr", 0, "ptr", 0, "ptr", 0, "ptr", 0, "hresult")
+
+ struct(args*) => (
+ args.Push(b := Buffer(args[2], 0)),
+ NumPut(args*),
+ b
+ )
+}
+
+; Verifies a signed executable file. Returns 0 on success, or a standard OS error number.
+EnableUIAccess_Verify(ExePath) {
+ wfi := Buffer(4*A_PtrSize) ; WINTRUST_FILE_INFO
+ NumPut('ptr', wfi.size, 'ptr', StrPtr(ExePath), 'ptr', 0, 'ptr', 0, wfi)
+
+ ; WINTRUST_ACTION_GENERIC_VERIFY_V2
+ NumPut('int64', 0x11d0cd4400aac56b, 'int64', 0xee95c24fc000c28c, actionID := Buffer(16))
+
+ wtd := Buffer(9*A_PtrSize+16) ; WINTRUST_DATA
+ NumPut(
+ 'ptr', wtd.Size, 'ptr', 0, 'ptr', 0, 'int', WTD_UI_NONE:=2, 'int', WTD_REVOKE_NONE:=0,
+ 'ptr', WTD_CHOICE_FILE:=1, 'ptr', wfi.ptr, 'ptr', WTD_STATEACTION_VERIFY:=1,
+ 'ptr', 0, 'ptr', 0, 'int', 0, 'int', 0, 'ptr', 0, wtd
+ )
+ return DllCall('wintrust\WinVerifyTrust', 'ptr', 0, 'ptr', actionID, 'ptr', wtd, 'int')
+}
diff --git a/UX/inc/GetGitHubReleaseAssetURL.ahk b/UX/inc/GetGitHubReleaseAssetURL.ahk
index 0f70228..739a009 100644
--- a/UX/inc/GetGitHubReleaseAssetURL.ahk
+++ b/UX/inc/GetGitHubReleaseAssetURL.ahk
@@ -1,26 +1,26 @@
-GetGitHubReleaseAssetURL(repo, ext:='.zip', release:='latest') {
- req := ComObject('Msxml2.XMLHTTP')
- req.open('GET', 'https://api.github.com/repos/' repo '/releases/' release, false)
- req.send()
- if req.status != 200
- throw Error(req.status ' - ' req.statusText, -1)
-
- res := JSON_parse(req.responseText)
- try
- assets := res.assets
- catch PropertyError
- throw Error(res.message, -1)
-
- loop assets.length {
- asset := assets.%A_Index-1%
- if SubStr(asset.name, -StrLen(ext)) = ext {
- return asset.browser_download_url
- }
- }
-
- JSON_parse(str) {
- htmlfile := ComObject('htmlfile')
- htmlfile.write('')
- return htmlfile.parentWindow.JSON.parse(str)
- }
+GetGitHubReleaseAssetURL(repo, ext:='.zip', release:='latest') {
+ req := ComObject('Msxml2.XMLHTTP')
+ req.open('GET', 'https://api.github.com/repos/' repo '/releases/' release, false)
+ req.send()
+ if req.status != 200
+ throw Error(req.status ' - ' req.statusText, -1)
+
+ res := JSON_parse(req.responseText)
+ try
+ assets := res.assets
+ catch PropertyError
+ throw Error(res.message, -1)
+
+ loop assets.length {
+ asset := assets.%A_Index-1%
+ if SubStr(asset.name, -StrLen(ext)) = ext {
+ return asset.browser_download_url
+ }
+ }
+
+ JSON_parse(str) {
+ htmlfile := ComObject('htmlfile')
+ htmlfile.write('')
+ return htmlfile.parentWindow.JSON.parse(str)
+ }
}
\ No newline at end of file
diff --git a/UX/inc/HashFile.ahk b/UX/inc/HashFile.ahk
index 615bc9a..a0a0944 100644
--- a/UX/inc/HashFile.ahk
+++ b/UX/inc/HashFile.ahk
@@ -1,96 +1,96 @@
-; HashFile by Deo
-; https://autohotkey.com/board/topic/66139-ahk-l-calculating-md5sha-checksum-from-file/
-; Modified for AutoHotkey v2 by lexikos.
-
-#Requires AutoHotkey v2.0-beta
-
-/*
-HASH types:
-1 - MD2
-2 - MD5
-3 - SHA
-4 - SHA256
-5 - SHA384
-6 - SHA512
-*/
-HashFile(filePath, hashType:=2)
-{
- static PROV_RSA_AES := 24
- static CRYPT_VERIFYCONTEXT := 0xF0000000
- static BUFF_SIZE := 1024 * 1024 ; 1 MB
- static HP_HASHVAL := 0x0002
- static HP_HASHSIZE := 0x0004
-
- switch hashType {
- case 1: hash_alg := (CALG_MD2 := 32769)
- case 2: hash_alg := (CALG_MD5 := 32771)
- case 3: hash_alg := (CALG_SHA := 32772)
- case 4: hash_alg := (CALG_SHA_256 := 32780)
- case 5: hash_alg := (CALG_SHA_384 := 32781)
- case 6: hash_alg := (CALG_SHA_512 := 32782)
- default: throw ValueError('Invalid hashType', -1, hashType)
- }
-
- f := FileOpen(filePath, "r")
- f.Pos := 0 ; Rewind in case of BOM.
-
- HCRYPTPROV() => {
- ptr: 0,
- __delete: this => this.ptr && DllCall("Advapi32\CryptReleaseContext", "Ptr", this, "UInt", 0)
- }
-
- if !DllCall("Advapi32\CryptAcquireContextW"
- , "Ptr*", hProv := HCRYPTPROV()
- , "Uint", 0
- , "Uint", 0
- , "Uint", PROV_RSA_AES
- , "UInt", CRYPT_VERIFYCONTEXT)
- throw OSError()
-
- HCRYPTHASH() => {
- ptr: 0,
- __delete: this => this.ptr && DllCall("Advapi32\CryptDestroyHash", "Ptr", this)
- }
-
- if !DllCall("Advapi32\CryptCreateHash"
- , "Ptr", hProv
- , "Uint", hash_alg
- , "Uint", 0
- , "Uint", 0
- , "Ptr*", hHash := HCRYPTHASH())
- throw OSError()
-
- read_buf := Buffer(BUFF_SIZE, 0)
-
- While (cbCount := f.RawRead(read_buf, BUFF_SIZE))
- {
- if !DllCall("Advapi32\CryptHashData"
- , "Ptr", hHash
- , "Ptr", read_buf
- , "Uint", cbCount
- , "Uint", 0)
- throw OSError()
- }
-
- if !DllCall("Advapi32\CryptGetHashParam"
- , "Ptr", hHash
- , "Uint", HP_HASHSIZE
- , "Uint*", &HashLen := 0
- , "Uint*", &HashLenSize := 4
- , "UInt", 0)
- throw OSError()
-
- bHash := Buffer(HashLen, 0)
- if !DllCall("Advapi32\CryptGetHashParam"
- , "Ptr", hHash
- , "Uint", HP_HASHVAL
- , "Ptr", bHash
- , "Uint*", &HashLen
- , "UInt", 0 )
- throw OSError()
-
- loop HashLen
- HashVal .= Format('{:02x}', (NumGet(bHash, A_Index-1, "UChar")) & 0xff)
-
- return HashVal
-}
+; HashFile by Deo
+; https://autohotkey.com/board/topic/66139-ahk-l-calculating-md5sha-checksum-from-file/
+; Modified for AutoHotkey v2 by lexikos.
+
+#Requires AutoHotkey v2.0-beta
+
+/*
+HASH types:
+1 - MD2
+2 - MD5
+3 - SHA
+4 - SHA256
+5 - SHA384
+6 - SHA512
+*/
+HashFile(filePath, hashType:=2)
+{
+ static PROV_RSA_AES := 24
+ static CRYPT_VERIFYCONTEXT := 0xF0000000
+ static BUFF_SIZE := 1024 * 1024 ; 1 MB
+ static HP_HASHVAL := 0x0002
+ static HP_HASHSIZE := 0x0004
+
+ switch hashType {
+ case 1: hash_alg := (CALG_MD2 := 32769)
+ case 2: hash_alg := (CALG_MD5 := 32771)
+ case 3: hash_alg := (CALG_SHA := 32772)
+ case 4: hash_alg := (CALG_SHA_256 := 32780)
+ case 5: hash_alg := (CALG_SHA_384 := 32781)
+ case 6: hash_alg := (CALG_SHA_512 := 32782)
+ default: throw ValueError('Invalid hashType', -1, hashType)
+ }
+
+ f := FileOpen(filePath, "r")
+ f.Pos := 0 ; Rewind in case of BOM.
+
+ HCRYPTPROV() => {
+ ptr: 0,
+ __delete: this => this.ptr && DllCall("Advapi32\CryptReleaseContext", "Ptr", this, "UInt", 0)
+ }
+
+ if !DllCall("Advapi32\CryptAcquireContextW"
+ , "Ptr*", hProv := HCRYPTPROV()
+ , "Uint", 0
+ , "Uint", 0
+ , "Uint", PROV_RSA_AES
+ , "UInt", CRYPT_VERIFYCONTEXT)
+ throw OSError()
+
+ HCRYPTHASH() => {
+ ptr: 0,
+ __delete: this => this.ptr && DllCall("Advapi32\CryptDestroyHash", "Ptr", this)
+ }
+
+ if !DllCall("Advapi32\CryptCreateHash"
+ , "Ptr", hProv
+ , "Uint", hash_alg
+ , "Uint", 0
+ , "Uint", 0
+ , "Ptr*", hHash := HCRYPTHASH())
+ throw OSError()
+
+ read_buf := Buffer(BUFF_SIZE, 0)
+
+ While (cbCount := f.RawRead(read_buf, BUFF_SIZE))
+ {
+ if !DllCall("Advapi32\CryptHashData"
+ , "Ptr", hHash
+ , "Ptr", read_buf
+ , "Uint", cbCount
+ , "Uint", 0)
+ throw OSError()
+ }
+
+ if !DllCall("Advapi32\CryptGetHashParam"
+ , "Ptr", hHash
+ , "Uint", HP_HASHSIZE
+ , "Uint*", &HashLen := 0
+ , "Uint*", &HashLenSize := 4
+ , "UInt", 0)
+ throw OSError()
+
+ bHash := Buffer(HashLen, 0)
+ if !DllCall("Advapi32\CryptGetHashParam"
+ , "Ptr", hHash
+ , "Uint", HP_HASHVAL
+ , "Ptr", bHash
+ , "Uint*", &HashLen
+ , "UInt", 0 )
+ throw OSError()
+
+ loop HashLen
+ HashVal .= Format('{:02x}', (NumGet(bHash, A_Index-1, "UChar")) & 0xff)
+
+ return HashVal
+}
diff --git a/UX/inc/README.txt b/UX/inc/README.txt
index a7f020c..2251389 100644
--- a/UX/inc/README.txt
+++ b/UX/inc/README.txt
@@ -1,3 +1,3 @@
-Scripts in this directory may be copied and used freely, but
-may be removed or modified without notice by any future release.
+Scripts in this directory may be copied and used freely, but
+may be removed or modified without notice by any future release.
Do not #include them directly; instead, create a copy.
\ No newline at end of file
diff --git a/UX/inc/ShellRun.ahk b/UX/inc/ShellRun.ahk
index b91bf06..b57c033 100644
--- a/UX/inc/ShellRun.ahk
+++ b/UX/inc/ShellRun.ahk
@@ -1,7 +1,7 @@
-; For documentation about the parameters, refer to:
-; https://learn.microsoft.com/en-us/windows/win32/shell/shell-shellexecute
-ShellRun(filePath, arguments?, directory?, operation?, show?) {
- static VT_UI4 := 0x13, SWC_DESKTOP := ComValue(VT_UI4, 0x8)
- ComObject("Shell.Application").Windows.Item(SWC_DESKTOP).Document.Application
- .ShellExecute(filePath, arguments?, directory?, operation?, show?)
+; For documentation about the parameters, refer to:
+; https://learn.microsoft.com/en-us/windows/win32/shell/shell-shellexecute
+ShellRun(filePath, arguments?, directory?, operation?, show?) {
+ static VT_UI4 := 0x13, SWC_DESKTOP := ComValue(VT_UI4, 0x8)
+ ComObject("Shell.Application").Windows.Item(SWC_DESKTOP).Document.Application
+ .ShellExecute(filePath, arguments?, directory?, operation?, show?)
}
\ No newline at end of file
diff --git a/UX/inc/bounce-v1.ahk b/UX/inc/bounce-v1.ahk
index 44cf087..fd01ed9 100644
--- a/UX/inc/bounce-v1.ahk
+++ b/UX/inc/bounce-v1.ahk
@@ -1,3 +1,3 @@
-; v1: includes the file from the script's directory.
-; v2: does nothing because the path is relative to this file.
+; v1: includes the file from the script's directory.
+; v2: does nothing because the path is relative to this file.
#include *i reload-v1.ahk
\ No newline at end of file
diff --git a/UX/inc/common.ahk b/UX/inc/common.ahk
index 0c93902..0a8939d 100644
--- a/UX/inc/common.ahk
+++ b/UX/inc/common.ahk
@@ -1,21 +1,21 @@
-A_AllowMainWindow := true
-if A_AhkPath != A_ScriptDir '\AutoHotkeyUX.exe' {
- ; Standalone, compiled or test mode: locate InstallDir via registry
- DirExist(ROOT_DIR := RegRead('HKCU\SOFTWARE\AutoHotkey', 'InstallDir', ""))
- || (ROOT_DIR := RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', ""))
-}
-if (ROOT_DIR ?? "") = "" || !DirExist(ROOT_DIR)
- Loop Files A_ScriptDir '\..', 'D'
- ROOT_DIR := A_LoopFileFullPath
-
-if !trace.Enabled := RegRead('HKCU\Software\AutoHotkey', 'Trace', false)
- trace.DefineProp 'call', {call: (*) => ''}
-
-#include config.ahk
-
-trace(s) {
- try
- FileAppend s "`n", "*"
- catch
- OutputDebug s "`n"
-}
+A_AllowMainWindow := true
+if A_AhkPath != A_ScriptDir '\AutoHotkeyUX.exe' {
+ ; Standalone, compiled or test mode: locate InstallDir via registry
+ DirExist(ROOT_DIR := RegRead('HKCU\SOFTWARE\AutoHotkey', 'InstallDir', ""))
+ || (ROOT_DIR := RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', ""))
+}
+if (ROOT_DIR ?? "") = "" || !DirExist(ROOT_DIR)
+ Loop Files A_ScriptDir '\..', 'D'
+ ROOT_DIR := A_LoopFileFullPath
+
+if !trace.Enabled := RegRead('HKCU\Software\AutoHotkey', 'Trace', false)
+ trace.DefineProp 'call', {call: (*) => ''}
+
+#include config.ahk
+
+trace(s) {
+ try
+ FileAppend s "`n", "*"
+ catch
+ OutputDebug s "`n"
+}
diff --git a/UX/inc/config.ahk b/UX/inc/config.ahk
index 97d2c46..26b5507 100644
--- a/UX/inc/config.ahk
+++ b/UX/inc/config.ahk
@@ -1,13 +1,13 @@
-
-; CONFIG_FILE_PATH := A_MyDocuments "\AutoHotkey\AutoHotkey.ini"
-CONFIG_KEY := 'HKCU\Software\AutoHotkey'
-
-ConfigRead(section, key, default) {
- ; return IniRead(CONFIG_FILE_PATH, section, key, default)
- return RegRead(CONFIG_KEY '\' section, key, default)
-}
-
-ConfigWrite(value, section, key) {
- ; IniWrite(value, CONFIG_FILE_PATH, section, key)
- RegWrite(value, 'REG_SZ', CONFIG_KEY '\' section, key)
-}
+
+; CONFIG_FILE_PATH := A_MyDocuments "\AutoHotkey\AutoHotkey.ini"
+CONFIG_KEY := 'HKCU\Software\AutoHotkey'
+
+ConfigRead(section, key, default) {
+ ; return IniRead(CONFIG_FILE_PATH, section, key, default)
+ return RegRead(CONFIG_KEY '\' section, key, default)
+}
+
+ConfigWrite(value, section, key) {
+ ; IniWrite(value, CONFIG_FILE_PATH, section, key)
+ RegWrite(value, 'REG_SZ', CONFIG_KEY '\' section, key)
+}
diff --git a/UX/inc/identify.ahk b/UX/inc/identify.ahk
index ab7cd61..4d6cee7 100644
--- a/UX/inc/identify.ahk
+++ b/UX/inc/identify.ahk
@@ -1,27 +1,27 @@
-#include identify_regex.ahk
-
-IdentifyBySyntax(code) {
- static identify_regex := get_identify_regex()
- p := 1, count_1 := count_2 := 0, version := marks := ''
- while (p := RegExMatch(code, identify_regex, &m, p)) {
- p += m.Len()
- if SubStr(m.mark,1,1) = 'v' {
- switch SubStr(m.mark,2,1) {
- case '1': count_1++
- case '2': count_2++
- }
- if !InStr(marks, m.mark)
- marks .= m.mark ' '
- }
- }
- if !(count_1 || count_2)
- return {v: 0, r: "no tell-tale matches"}
- ; Use a simple, cautious approach for now: select a version only if there were
- ; matches for exactly one version.
- if count_1 && count_2
- return {v: 0, r: Format(
- count_1 > count_2 ? "v1 {1}:{2} - {3}" : count_2 > count_1 ? "v2 {2}:{1} - {3}" : "? {1}:{2} - {3}",
- count_1, count_2, Trim(marks)
- )}
- return {v: count_1 ? 1 : 2, r: Trim(marks)}
-}
+#include identify_regex.ahk
+
+IdentifyBySyntax(code) {
+ static identify_regex := get_identify_regex()
+ p := 1, count_1 := count_2 := 0, version := marks := ''
+ while (p := RegExMatch(code, identify_regex, &m, p)) {
+ p += m.Len()
+ if SubStr(m.mark,1,1) = 'v' {
+ switch SubStr(m.mark,2,1) {
+ case '1': count_1++
+ case '2': count_2++
+ }
+ if !InStr(marks, m.mark)
+ marks .= m.mark ' '
+ }
+ }
+ if !(count_1 || count_2)
+ return {v: 0, r: "no tell-tale matches"}
+ ; Use a simple, cautious approach for now: select a version only if there were
+ ; matches for exactly one version.
+ if count_1 && count_2
+ return {v: 0, r: Format(
+ count_1 > count_2 ? "v1 {1}:{2} - {3}" : count_2 > count_1 ? "v2 {2}:{1} - {3}" : "? {1}:{2} - {3}",
+ count_1, count_2, Trim(marks)
+ )}
+ return {v: count_1 ? 1 : 2, r: Trim(marks)}
+}
diff --git a/UX/inc/identify_regex.ahk b/UX/inc/identify_regex.ahk
index 78500b6..952ed37 100644
--- a/UX/inc/identify_regex.ahk
+++ b/UX/inc/identify_regex.ahk
@@ -1,4 +1,4 @@
-get_identify_regex() => '
-(
-(?(DEFINE)(?(?(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z)))(?(?=[ `t]*+(?&line_comment)?(?m:$)))(?(?:(?&eol).*\R|(?&block_comment))++)(?(?:[^ `t`r`n]++|[ `t]*+(?!(?&eol)))*+)(?[ `t]*+\((?i:Join[^ `t`r`n]*+|(?&line_comment)|[^ `t`r`n()]++|[ `t]++)*+\R(?:[ `t]*+(?!\)).*\R)*+[ `t]*+\))(?[ `t]*+(?:,(?!::| +& )|[<>=/|^,?:\.+\-*&!~](?![^"'`r`n]*?(?:".*?::(?!.*?")|'.*?::(?!.*?')|::))|(?i:AND|OR)(?=[ `t])))(?(?&eol)(?:(?(?<=:=)|(?<=[:,]))|(?<=[<>=/|^,?:\.+\-*&!~](?(?&tosol)(?:(?&solcont)(?&subexp)|[ `t]*+,[ `t]*+(?=%)(?&pct)|(?&contsec)(?&ambig)))(?(?:.*+(?&v1_cont))*.*+)(?(?:(?&exp)|(?&v1_cont)|.*+)++(*:~))(?(?=%[ `t])(?:(?&subexp)(?&exp)|(?&v1_fin)(*:v1-pct)))(?(*:exp)(?&exp))(?(?&toeol)(?:(?&tosol)(?:(?&solcont)|(?&contsec))(?&v1_lines))?)(?(?=/|^,?:\.*&!~])(?\R(?:(?&contsec)|(?!(?&solcont))(*:v2-cbe)|))(?(?:[, `t]++|(?&enclf)|(?&subexp)|(?&line_comment))*+)(?%(?:[^,`r`n;\[\]{}()"%']*+|,(?![ `t]*+%)|(?&subexp))*+%(*:v2-pct)|=>(*:v2-fat))(?(?:(?!(?&otb))(?&eolcont)?[ `t]*+(?:[^ `t;,`r`n=\[\]{}()"%']++|\((?&encex)\)|\[(?&encex)\]|\{(?&encex)\}|(?>"(?>[^"``\r\n]|``.)*"|'(?>[^'``\r\n]|``.)*'(*:v2-sq))|'(?&tosol)(?&contsec)'(*:v2-sq)|(?(?:(?&subexp)|[ `t]*+,|(?&eol))++(?&otb)?))(?:[ `t]*+(?&line_comment)(*SKIP)(?!)|(?m:^)[ `t{}]*(?:(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z))(*SKIP)(?!)|(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])|~?(?>\w+|[^ `t`r`n]) & ~?(?>\w+|[^ `t`r`n]))(?i:[ `t]+up)?::(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])(?&eol)(*:remap?)|(?&eol)(?!(?&tosol)[ `t]*+(?:[\{#]|.*?::|[\w[:^ascii:]]++\())(*:v1-hk)|(*:hotkey))|(?(?=:[^\:`r`n]*[xX]):[[:alnum:]\?\*\- ]*:.*(?=/|^,?:\.+\-*&!~ `t()\[\]{}%]++|(?>"(?>[^"``\r\n]|``.)*"|'(?>[^'``\r\n]|``.)*'(*:v2-sq))|['"].*)*+(?=[ `t]*+(?:(?[\w[:^ascii:]#@$]++|%[\w[:^ascii:]#@$]++%)++(?:[ `t]++(?i:not[ `t]++)?(?i:in|contains|between)[ `t]++(?&v1_fin)(*:v1-if)|[ `t]*+(?:[<>]=?|!?=)(?&ambig))|(?&expm))|[\w[:^ascii:]]++(?:[ `t]*+=(?:>(?&ambig)|.*?\?.+?:.*(?&ambig)|(?&v1_fin)(*:v1-ass))|[\(\[](?=.*[\)\]][ `t`r`n]*\{)(?:[ `t]*+[\w[:^ascii:]]++[ `t]*+(?::=(?&subexp))?[ `t]*+,)*+[ `t]*+(?:(?i:ByRef)[ `t]++[\w[:^ascii:]](*:v1-ref)|&(*:v2-ref)|[\w[:^ascii:]]++[ `t]*+=(*:v1-def)|\*[ `t]*+[\)\]](*:v2-vfn)).*|(?=[\(\[\.\?]|[ `t]*+(?>[\:\+\-\*/\.\|&\^]|<<|>>|//)=)(?&expm)|,(?&v1_fin)(*:v1-cmd)|(?&eol)(?&ambig)|[ `t]++(?:[ `t]*+(?:\^|(?:(?!\{)[\w[:^ascii:]<>=/|^,?:\.+\-*&!~ `t()\[\]{}%]|(?>"(?>[^"``\r\n]|``.)*"|'(?>[^'``\r\n]|``.)*'(*:v2-sq)))*+\{[ `t]*+(?:\w+|.)(?:[ `t]++\w+)?[ `t]*+\})(?&v1_fin)(*:v1-send)|(?:[^`r`n,\[\]{}()"%']*+,[ `t]*+)*+(?&pct)|(?&ambig)(*:cmd?)))|(?:\+\+|--)(?&expm)|.(?&ambig)(*:!!)))
-)'
+get_identify_regex() => '
+(
+(?(DEFINE)(?(?(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z)))(?(?=[ `t]*+(?&line_comment)?(?m:$)))(?(?:(?&eol).*\R|(?&block_comment))++)(?(?:[^ `t`r`n]++|[ `t]*+(?!(?&eol)))*+)(?[ `t]*+\((?i:Join[^ `t`r`n]*+|(?&line_comment)|[^ `t`r`n()]++|[ `t]++)*+\R(?:[ `t]*+(?!\)).*\R)*+[ `t]*+\))(?[ `t]*+(?:,(?!::| +& )|[<>=/|^,?:\.+\-*&!~](?![^"'`r`n]*?(?:".*?::(?!.*?")|'.*?::(?!.*?')|::))|(?i:AND|OR)(?=[ `t])))(?(?&eol)(?:(?(?<=:=)|(?<=[:,]))|(?<=[<>=/|^,?:\.+\-*&!~](?(?&tosol)(?:(?&solcont)(?&subexp)|[ `t]*+,[ `t]*+(?=%)(?&pct)|(?&contsec)(?&ambig)))(?(?:.*+(?&v1_cont))*.*+)(?(?:(?&exp)|(?&v1_cont)|.*+)++(*:~))(?(?=%[ `t])(?:(?&subexp)(?&exp)|(?&v1_fin)(*:v1-pct)))(?(*:exp)(?&exp))(?(?&toeol)(?:(?&tosol)(?:(?&solcont)|(?&contsec))(?&v1_lines))?)(?(?=/|^,?:\.*&!~])(?\R(?:(?&contsec)|(?!(?&solcont))(*:v2-cbe)|))(?(?:[, `t]++|(?&enclf)|(?&subexp)|(?&line_comment))*+)(?%(?:[^,`r`n;\[\]{}()"%']*+|,(?![ `t]*+%)|(?&subexp))*+%(*:v2-pct)|=>(*:v2-fat))(?(?:(?!(?&otb))(?&eolcont)?[ `t]*+(?:[^ `t;,`r`n=\[\]{}()"%']++|\((?&encex)\)|\[(?&encex)\]|\{(?&encex)\}|(?>"(?>[^"``\r\n]|``["'``])*+"|'(?>[^'``\r\n]|``["'``])*+'(*:v2-sq))|'(?&tosol)(?&contsec)'(*:v2-sq)|(?(?:(?&subexp)|[ `t]*+,|(?&eol))++(?&otb)?))(?:[ `t]*+(?&line_comment)(*SKIP)(?!)|(?m:^)[ `t{}]*(?:(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z))(*SKIP)(?!)|(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])|~?(?>\w+|[^ `t`r`n]) & ~?(?>\w+|[^ `t`r`n]))(?i:[ `t]+up)?::(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])(?&eol)(*:remap?)|(?&eol)(?!(?&tosol)[ `t]*+(?:[\{#]|.*?::|[\w[:^ascii:]]++\())(*:v1-hk)|(*:hotkey))|(?(?=:[^\:`r`n]*[xX]):[[:alnum:]\?\*\- ]*:.*(?=/|^,?:\.+\-*&!~ `t()\[\]{}%]++|(?>"(?>[^"``\r\n]|``["'``])*+"|'(?>[^'``\r\n]|``["'``])*+'(*:v2-sq))|['"].*)*+(?=[ `t]*+(?:(?[\w[:^ascii:]#@$]++|%[\w[:^ascii:]#@$]++%)++(?:[ `t]++(?i:not[ `t]++)?(?i:in|contains|between)[ `t]++(?&v1_fin)(*:v1-if)|[ `t]*+(?:[<>]=?|!?=)(?&ambig))|(?&expm))|[\w[:^ascii:]]++(?:[ `t]*+=(?:>(?&ambig)|.*?\?.+?:.*(?&ambig)|(?&v1_fin)(*:v1-ass))|[\(\[](?=.*[\)\]][ `t`r`n]*\{)(?:[ `t]*+[\w[:^ascii:]]++[ `t]*+(?::=(?&subexp))?[ `t]*+,)*+[ `t]*+(?:(?i:ByRef)[ `t]++[\w[:^ascii:]](*:v1-ref)|&(*:v2-ref)|[\w[:^ascii:]]++[ `t]*+=(*:v1-def)|\*[ `t]*+[\)\]](*:v2-vfn)).*|(?=[\(\[\.\?]|[ `t]*+(?>[\:\+\-\*/\.\|&\^]|<<|>>|//)=)(?&expm)|,(?&v1_fin)(*:v1-cmd)|(?&eol)(?&ambig)|[ `t]++(?:[ `t]*+(?:\^|(?:(?!\{)[\w[:^ascii:]<>=/|^,?:\.+\-*&!~ `t()\[\]{}%]|(?>"(?>[^"``\r\n]|``["'``])*+"|'(?>[^'``\r\n]|``["'``])*+'(*:v2-sq)))*+\{[ `t]*+(?:\w+|.)(?:[ `t]++\w+)?[ `t]*+\})(?&v1_fin)(*:v1-send)|(?:[^`r`n,\[\]{}()"%']*+,[ `t]*+)*+(?&pct)|(?&ambig)(*:cmd?)))|(?:\+\+|--)(?&expm)|.(?&ambig)(*:!!)))
+)'
diff --git a/UX/inc/launcher-common.ahk b/UX/inc/launcher-common.ahk
index 039a500..2e76083 100644
--- a/UX/inc/launcher-common.ahk
+++ b/UX/inc/launcher-common.ahk
@@ -1,78 +1,78 @@
-
-#include common.ahk
-
-GetExeInfo(exe) {
- if !(verSize := DllCall("version\GetFileVersionInfoSize", "str", exe, "uint*", 0, "uint"))
- || !DllCall("version\GetFileVersionInfo", "str", exe, "uint", 0, "uint", verSize, "ptr", verInfo := Buffer(verSize))
- throw OSError()
- prop := {Path: exe}
- static Properties := {
- Version: 'FileVersion',
- Description: 'FileDescription',
- ProductName: 'ProductName'
- }
- for propName, infoName in Properties.OwnProps()
- if DllCall("version\VerQueryValue", "ptr", verInfo, "str", "\StringFileInfo\040904b0\" infoName, "ptr*", &p:=0, "uint*", &len:=0)
- prop.%propName% := StrGet(p, len)
- else throw OSError()
- if InStr(exe, '_UIA')
- prop.Description .= ' UIA'
- prop.Version := RegExReplace(prop.Version, 'i)[a-z]{2,}\K(?=\d)|, ', '.') ; Hack-fix for erroneous version numbers (AutoHotkey_H v2.0-beta3-H...)
- return prop
-}
-
-IsUsableAutoHotkey(exeinfo) {
- return exeinfo.HasProp('Description')
- && RegExMatch(exeinfo.Description, '^AutoHotkey.* (32|64)-bit', &m)
- && (m.1 != '64' || A_Is64bitOS)
- && !InStr(exeinfo.Path, '\AutoHotkeyUX.exe')
-}
-
-GetMajor(v) {
- Loop Parse, v, '.-+'
- return Integer(A_LoopField)
- throw ValueError('Invalid version number', -1, v)
-}
-
-ReadHashes(path, filter?) {
- filemap := Map(), filemap.CaseSense := 0
- if !FileExist(path)
- return filemap
- csvfile := FileOpen(path, 'r')
- props := StrSplit(csvfile.ReadLine(), ',')
- while !csvfile.AtEOF {
- item := {}
- Loop Parse csvfile.ReadLine(), 'CSV'
- item.%props[A_Index]% := A_LoopField
- if IsSet(filter) && !filter(item)
- continue
- filemap[item.Path] := item
- }
- return filemap
-}
-
-GetUsableAutoHotkeyExes() {
- static files
- if IsSet(files) {
- trace '![Launcher] returning hashes again'
- return files
- }
- files := ReadHashes(ROOT_DIR '\UX\installed-files.csv',
- item => IsUsableAutoHotkey(item) && (
- item.Path ~= '^(?!\w:|\\\\)' && item.Path := ROOT_DIR '\' item.Path,
- true
- ))
- if files.Count {
- trace '![Launcher] returning hashes from cache'
- return files
- }
- Loop Files ROOT_DIR '\AutoHotkey*.exe', 'R' {
- try {
- item := GetExeInfo(A_LoopFilePath)
- if IsUsableAutoHotkey(item)
- files[item.Path] := item
- }
- }
- trace '![Launcher] returning hashes from filesystem'
- return files
-}
+
+#include common.ahk
+
+GetExeInfo(exe) {
+ if !(verSize := DllCall("version\GetFileVersionInfoSize", "str", exe, "uint*", 0, "uint"))
+ || !DllCall("version\GetFileVersionInfo", "str", exe, "uint", 0, "uint", verSize, "ptr", verInfo := Buffer(verSize))
+ throw OSError()
+ prop := {Path: exe}
+ static Properties := {
+ Version: 'FileVersion',
+ Description: 'FileDescription',
+ ProductName: 'ProductName'
+ }
+ for propName, infoName in Properties.OwnProps()
+ if DllCall("version\VerQueryValue", "ptr", verInfo, "str", "\StringFileInfo\040904b0\" infoName, "ptr*", &p:=0, "uint*", &len:=0)
+ prop.%propName% := StrGet(p, len)
+ else throw OSError()
+ if InStr(exe, '_UIA')
+ prop.Description .= ' UIA'
+ prop.Version := RegExReplace(prop.Version, 'i)[a-z]{2,}\K(?=\d)|, ', '.') ; Hack-fix for erroneous version numbers (AutoHotkey_H v2.0-beta3-H...)
+ return prop
+}
+
+IsUsableAutoHotkey(exeinfo) {
+ return exeinfo.HasProp('Description')
+ && RegExMatch(exeinfo.Description, '^AutoHotkey.* (32|64)-bit', &m)
+ && (m.1 != '64' || A_Is64bitOS)
+ && !InStr(exeinfo.Path, '\AutoHotkeyUX.exe')
+}
+
+GetMajor(v) {
+ Loop Parse, v, '.-+'
+ return Integer(A_LoopField)
+ throw ValueError('Invalid version number', -1, v)
+}
+
+ReadHashes(path, filter?) {
+ filemap := Map(), filemap.CaseSense := 0
+ if !FileExist(path)
+ return filemap
+ csvfile := FileOpen(path, 'r')
+ props := StrSplit(csvfile.ReadLine(), ',')
+ while !csvfile.AtEOF {
+ item := {}
+ Loop Parse csvfile.ReadLine(), 'CSV'
+ item.%props[A_Index]% := A_LoopField
+ if IsSet(filter) && !filter(item)
+ continue
+ filemap[item.Path] := item
+ }
+ return filemap
+}
+
+GetUsableAutoHotkeyExes() {
+ static files
+ if IsSet(files) {
+ trace '![Launcher] returning hashes again'
+ return files
+ }
+ files := ReadHashes(ROOT_DIR '\UX\installed-files.csv',
+ item => IsUsableAutoHotkey(item) && (
+ item.Path ~= '^(?!\w:|\\\\)' && item.Path := ROOT_DIR '\' item.Path,
+ true
+ ))
+ if files.Count {
+ trace '![Launcher] returning hashes from cache'
+ return files
+ }
+ Loop Files ROOT_DIR '\AutoHotkey*.exe', 'R' {
+ try {
+ item := GetExeInfo(A_LoopFilePath)
+ if IsUsableAutoHotkey(item)
+ files[item.Path] := item
+ }
+ }
+ trace '![Launcher] returning hashes from filesystem'
+ return files
+}
diff --git a/UX/install-ahk2exe.ahk b/UX/install-ahk2exe.ahk
index 2037257..4329556 100644
--- a/UX/install-ahk2exe.ahk
+++ b/UX/install-ahk2exe.ahk
@@ -1,53 +1,53 @@
-; Run this script to launch or download and install Ahk2Exe into A_ScriptDir '\..\Compiler'.
-#requires AutoHotkey v2.0
-
-#include install.ahk
-#include inc\GetGitHubReleaseAssetURL.ahk
-
-#SingleInstance Force
-InstallAhk2Exe
-
-InstallAhk2Exe() {
- inst := Installation()
- inst.ResolveInstallDir() ; This sets inst.InstallDir and inst.UserInstall
-
- finalPath := inst.InstallDir '\Compiler\Ahk2Exe.exe'
- if FileExist(finalPath) {
- ShellRun finalPath
- ExitApp
- }
-
- if !A_Args.Length {
- (inst.UserInstall) || SetTimer(() => (
- WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) &&
- SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C
- ), -25)
- if MsgBox("Ahk2Exe is not installed, but we can download and install it for you.", "AutoHotkey", 'OkCancel') = 'Cancel'
- ExitApp
- if !A_IsAdmin && !inst.UserInstall {
- Run Format('*RunAs "{1}" /restart /script "{2}" /Y', A_AhkPath, A_ScriptFullPath)
- ExitApp
- }
- }
-
- tempDir := A_ScriptDir '\.staging' ; Avoid A_Temp for security reasons
- DirCreate tempDir
- SetWorkingDir tempDir
-
- TrayTip "Downloading Ahk2Exe", "AutoHotkey"
- url := GetGitHubReleaseAssetURL('AutoHotkey/Ahk2Exe')
- Download url, 'Ahk2Exe.zip'
-
- TrayTip "Installing Ahk2Exe", "AutoHotkey"
- DirCopy 'Ahk2Exe.zip', 'Compiler', true
- FileDelete 'Ahk2Exe.zip'
-
- inst.AddCompiler(tempDir '\Compiler')
- inst.Apply()
-
- ; Working dir may have been changed
- DirDelete tempDir '\Compiler', true
- DirDelete tempDir
-
- ShellRun finalPath
-}
+; Run this script to launch or download and install Ahk2Exe into A_ScriptDir '\..\Compiler'.
+#requires AutoHotkey v2.0
+
+#include install.ahk
+#include inc\GetGitHubReleaseAssetURL.ahk
+
+#SingleInstance Force
+InstallAhk2Exe
+
+InstallAhk2Exe() {
+ inst := Installation()
+ inst.ResolveInstallDir() ; This sets inst.InstallDir and inst.UserInstall
+
+ finalPath := inst.InstallDir '\Compiler\Ahk2Exe.exe'
+ if FileExist(finalPath) {
+ ShellRun finalPath
+ ExitApp
+ }
+
+ if !A_Args.Length {
+ (inst.UserInstall) || SetTimer(() => (
+ WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) &&
+ SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C
+ ), -25)
+ if MsgBox("Ahk2Exe is not installed, but we can download and install it for you.", "AutoHotkey", 'OkCancel') = 'Cancel'
+ ExitApp
+ if !A_IsAdmin && !inst.UserInstall {
+ Run Format('*RunAs "{1}" /restart /script "{2}" /Y', A_AhkPath, A_ScriptFullPath)
+ ExitApp
+ }
+ }
+
+ tempDir := A_ScriptDir '\.staging' ; Avoid A_Temp for security reasons
+ DirCreate tempDir
+ SetWorkingDir tempDir
+
+ TrayTip "Downloading Ahk2Exe", "AutoHotkey"
+ url := GetGitHubReleaseAssetURL('AutoHotkey/Ahk2Exe')
+ Download url, 'Ahk2Exe.zip'
+
+ TrayTip "Installing Ahk2Exe", "AutoHotkey"
+ DirCopy 'Ahk2Exe.zip', 'Compiler', true
+ FileDelete 'Ahk2Exe.zip'
+
+ inst.AddCompiler(tempDir '\Compiler')
+ inst.Apply()
+
+ ; Working dir may have been changed
+ DirDelete tempDir '\Compiler', true
+ DirDelete tempDir
+
+ ShellRun finalPath
+}
diff --git a/UX/install-version.ahk b/UX/install-version.ahk
index 7707c4e..b470a5e 100644
--- a/UX/install-version.ahk
+++ b/UX/install-version.ahk
@@ -1,109 +1,109 @@
-; Run this script to download and install an additional AutoHotkey version.
-; Specify the version as a single command line parameter. If omitted or
-; incomplete like "1.1" or "2.0", the latest version will be downloaded.
-#requires AutoHotkey v2.0
-
-#include install.ahk
-
-A_ScriptName := "AutoHotkey"
-
-InstallAutoHotkey A_Args.Length ? A_Args[1] : '1.1'
-
-InstallAutoHotkey(version) {
- abort(message, extra?) {
- if IsSet(extra)
- message .= "`n`nSpecifically: " SubStr(extra, 1, 100)
- MsgBox message,, "Iconx"
- ExitApp
- }
-
- ; Determine base version, for download directory
- baseVersion := RegExReplace(version, '^\d+(?:\.\d+)?\b\K.*')
- if IsInteger(baseVersion)
- baseVersion .= baseVersion = '1' ? '.1' : '.0'
- else if !IsNumber(baseVersion)
- abort "Invalid version.", version
-
- ; If version number is not exact, try to determine the latest compatible version
- if IsNumber(version) {
- url := Format('https://www.autohotkey.com/download/{}/version.txt', baseVersion)
- req := ComObject('Msxml2.XMLHTTP')
- req.open('GET', url, false)
- req.send()
- if req.status != 200
- throw Error(req.status ' - ' req.statusText, -1)
- currentVersion := req.responseText
- if VerCompare(currentVersion, baseVersion) < 0 || VerCompare(currentVersion, Round(baseVersion + 1)) >= 0
- abort "An error occurred while trying to identify the latest available version. The downloaded version.txt was invalid.", currentVersion
- version := currentVersion
- }
-
- ; Only versions for which a zip is available are supported by this script.
- ; 1.1.00.00 - 1.1.22.06 have no downloads available at the time of writing.
- ; 1.1.22.07 - 1.1.24.01 have one zip per exe.
- if VerCompare(version, '1.1') >= 0 && VerCompare(version, '1.1.24.02') < 0
- abort "This version cannot be installed automatically.", version
-
- inst := Installation()
- inst.ResolveInstallDir()
-
- if A_Args.Length < 2 && !A_IsAdmin && !inst.UserInstall {
- ExitApp RunWait(Format('*RunAs "{1}" /force /script "{2}" {3} /Y', A_AhkPath, A_ScriptFullPath, version))
- }
-
- zipName := VerCompare(version, '1.1.24.02') < 0
- ? 'AutoHotkey' StrReplace(version, '.') '.zip'
- : 'AutoHotkey_' version '.zip'
- url := Format('https://www.autohotkey.com/download/{}/{}', baseVersion, zipName)
-
- try {
- tempDir := inst.InstallDir '\.staging' ; Avoid A_Temp for security reasons
- try {
- DirCreate tempDir
- SetWorkingDir tempDir
- }
- catch OSError as e
- abort "An error occured while preparing the temporary directory.", e.Message
-
- TrayTip "Downloading AutoHotkey v" version, "AutoHotkey"
- try
- Download url, tempDir '\' zipName
- catch
- abort "Download failed.`n`nURL: " url
-
- TrayTip "Installing AutoHotkey v" version, "AutoHotkey"
- inst.SourceDir := tempDir '\v' version
- try
- DirCopy tempDir '\' zipName, inst.SourceDir, true
- catch
- abort "Extraction failed."
- finally
- try
- FileDelete zipName
- catch
- MsgBox 'Unable to delete temporary file "' tempDir '\' zipName '".',, "Icon!"
-
- try localUX := inst.Hashes['UX\install.ahk']
- catch
- localUX := {Version: ''}
- try {
- if VerCompare(localUX.Version, version) < 0
- && FileExist(inst.SourceDir '\UX\install.ahk')
- && FileExist(inst.SourceDir '\AutoHotkey32.exe') {
- Run Format('"{1}\AutoHotkey32.exe" UX\install.ahk /to "{2}"', inst.SourceDir, inst.InstallDir), inst.SourceDir
- ExitApp
- }
- else
- inst.InstallExtraVersion
- }
- catch as e
- abort "An error occurred during installation.", e.Message
- }
- finally {
- try DirDelete tempDir '\v' version, true
- try DirDelete tempDir
- }
-
- TrayTip
- MsgBox "AutoHotkey v" version " is now installed."
-}
+; Run this script to download and install an additional AutoHotkey version.
+; Specify the version as a single command line parameter. If omitted or
+; incomplete like "1.1" or "2.0", the latest version will be downloaded.
+#requires AutoHotkey v2.0
+
+#include install.ahk
+
+A_ScriptName := "AutoHotkey"
+
+InstallAutoHotkey A_Args.Length ? A_Args[1] : '1.1'
+
+InstallAutoHotkey(version) {
+ abort(message, extra?) {
+ if IsSet(extra)
+ message .= "`n`nSpecifically: " SubStr(extra, 1, 100)
+ MsgBox message,, "Iconx"
+ ExitApp
+ }
+
+ ; Determine base version, for download directory
+ baseVersion := RegExReplace(version, '^\d+(?:\.\d+)?\b\K.*')
+ if IsInteger(baseVersion)
+ baseVersion .= baseVersion = '1' ? '.1' : '.0'
+ else if !IsNumber(baseVersion)
+ abort "Invalid version.", version
+
+ ; If version number is not exact, try to determine the latest compatible version
+ if IsNumber(version) {
+ url := Format('https://www.autohotkey.com/download/{}/version.txt', baseVersion)
+ req := ComObject('Msxml2.XMLHTTP')
+ req.open('GET', url, false)
+ req.send()
+ if req.status != 200
+ throw Error(req.status ' - ' req.statusText, -1)
+ currentVersion := req.responseText
+ if VerCompare(currentVersion, baseVersion) < 0 || VerCompare(currentVersion, Round(baseVersion + 1)) >= 0
+ abort "An error occurred while trying to identify the latest available version. The downloaded version.txt was invalid.", currentVersion
+ version := currentVersion
+ }
+
+ ; Only versions for which a zip is available are supported by this script.
+ ; 1.1.00.00 - 1.1.22.06 have no downloads available at the time of writing.
+ ; 1.1.22.07 - 1.1.24.01 have one zip per exe.
+ if VerCompare(version, '1.1') >= 0 && VerCompare(version, '1.1.24.02') < 0
+ abort "This version cannot be installed automatically.", version
+
+ inst := Installation()
+ inst.ResolveInstallDir()
+
+ if A_Args.Length < 2 && !A_IsAdmin && !inst.UserInstall {
+ ExitApp RunWait(Format('*RunAs "{1}" /force /script "{2}" {3} /Y', A_AhkPath, A_ScriptFullPath, version))
+ }
+
+ zipName := VerCompare(version, '1.1.24.02') < 0
+ ? 'AutoHotkey' StrReplace(version, '.') '.zip'
+ : 'AutoHotkey_' version '.zip'
+ url := Format('https://www.autohotkey.com/download/{}/{}', baseVersion, zipName)
+
+ try {
+ tempDir := inst.InstallDir '\.staging' ; Avoid A_Temp for security reasons
+ try {
+ DirCreate tempDir
+ SetWorkingDir tempDir
+ }
+ catch OSError as e
+ abort "An error occured while preparing the temporary directory.", e.Message
+
+ TrayTip "Downloading AutoHotkey v" version, "AutoHotkey"
+ try
+ Download url, tempDir '\' zipName
+ catch
+ abort "Download failed.`n`nURL: " url
+
+ TrayTip "Installing AutoHotkey v" version, "AutoHotkey"
+ inst.SourceDir := tempDir '\v' version
+ try
+ DirCopy tempDir '\' zipName, inst.SourceDir, true
+ catch
+ abort "Extraction failed."
+ finally
+ try
+ FileDelete zipName
+ catch
+ MsgBox 'Unable to delete temporary file "' tempDir '\' zipName '".',, "Icon!"
+
+ try localUX := inst.Hashes['UX\install.ahk']
+ catch
+ localUX := {Version: ''}
+ try {
+ if VerCompare(localUX.Version, version) < 0
+ && FileExist(inst.SourceDir '\UX\install.ahk')
+ && FileExist(inst.SourceDir '\AutoHotkey32.exe') {
+ Run Format('"{1}\AutoHotkey32.exe" UX\install.ahk /to "{2}"', inst.SourceDir, inst.InstallDir), inst.SourceDir
+ ExitApp
+ }
+ else
+ inst.InstallExtraVersion
+ }
+ catch as e
+ abort "An error occurred during installation.", e.Message
+ }
+ finally {
+ try DirDelete tempDir '\v' version, true
+ try DirDelete tempDir
+ }
+
+ TrayTip
+ MsgBox "AutoHotkey v" version " is now installed."
+}
diff --git a/UX/install.ahk b/UX/install.ahk
index 0b1a2f4..7be51a9 100644
--- a/UX/install.ahk
+++ b/UX/install.ahk
@@ -1,1013 +1,1024 @@
-; This script contains AutoHotkey (un)installation routines.
-; See the AutoHotkey v2 documentation for usage.
-#include inc\bounce-v1.ahk
-/* v1 stops here */
-#requires AutoHotkey v2.0
-
-#SingleInstance Off ; Needed for elevation with *runas.
-
-#include inc\launcher-common.ahk
-#include inc\HashFile.ahk
-#include inc\CreateAppShortcut.ahk
-#include inc\EnableUIAccess.ahk
-#include inc\ShellRun.ahk
-
-if A_LineFile = A_ScriptFullPath
- Install_Main
-
-Install_Main() {
- try {
- inst := Installation()
- method := 'InstallFull'
- params := []
- while A_Index <= A_Args.Length {
- switch A_Args[A_Index], 'off' {
- case '/install':
- method := 'InstallExtraVersion'
- inst.SourceDir := A_Args[++A_Index]
- case '/uninstall':
- method := 'Uninstall'
- if A_Index < A_Args.Length && SubStr(A_Args[A_Index+1],1,1) != '/'
- params.Push(A_Args[++A_Index])
- case '/to', '/installto':
- inst.InstallDir := A_Args[++A_Index]
- case '/elevate':
- inst.RequireAdmin := true
- case '/user':
- inst.UserInstall := true
- case '/silent':
- inst.Silent := true
- default:
- MsgBox 'Invalid arg "' A_Args[A_Index] '"', inst.DialogTitle, "Iconx"
- ExitApp 1
- }
- }
- inst.%method%(params*)
- }
- catch as e {
- DllCall(CallbackCreate(errBox.Bind(e)))
- ExitApp 1
- }
- errBox(e) {
- throw e
- }
-}
-
-class Installation {
- ProductName := "AutoHotkey"
- ProductURL := "https://autohotkey.com"
- Publisher := "AutoHotkey Foundation LLC"
- Version := A_AhkVersion
- AppUserModelID := 'AutoHotkey.AutoHotkey'
-
- UserInstall := !A_IsAdmin
- Interpreter := A_AhkPath
- Silent := false
-
- ScriptProgId := 'AutoHotkeyScript'
- SoftwareSubKey := 'Software\AutoHotkey'
- RootKey => this.UserInstall ? 'HKCU' : 'HKLM'
- SoftwareKey => this.RootKey '\' this.SoftwareSubKey
- ClassesKey => this.RootKey '\Software\Classes'
- FileTypeKey => this.ClassesKey '\' this.ScriptProgId
- UninstallKey => this.RootKey '\Software\Microsoft\Windows\CurrentVersion\Uninstall\AutoHotkey'
- StartFolder => (this.UserInstall ? A_Programs : A_ProgramsCommon)
- UninstallCmd => this.CmdStr('UX\ui-uninstall.ahk', ((A_IsAdmin && this.UserInstall) ? '/elevate' : ''))
- QUninstallCmd => this.CmdStr('UX\install.ahk', '/uninstall /silent')
-
- DialogTitle => this.ProductName " Setup"
-
- FileItems := [] ; [{Source, Dest}]
- RegItems := [] ; [{Key, ValueName, Value}]
- PreCheck := [] ; [Callback(this)]
- PreAction := [] ; [Callback(this)]
- PostAction := [] ; [Callback(this)]
-
- ResolveInstallDir() {
- if hadInstallDir := this.HasProp('InstallDir')
- DirCreate installDir := this.InstallDir
- else
- installDir := A_ScriptDir '\..'
- Loop Files installDir, 'D'
- this.InstallDir := installDir := A_LoopFileFullPath
- else
- throw ValueError("Invalid target directory",, installDir)
- SetRegView 64
- installDirs := []
- for rootKey in ['HKLM', 'HKCU']
- installDirs.Push RegRead(rootKey '\' this.SoftwareSubKey, 'InstallDir', '')
- ; Override installation mode if already installed here
- if installDirs[1] = installDir
- this.UserInstall := false
- else if installDirs[2] = installDir
- this.UserInstall := true
- ; If this.InstallDir wasn't set upon entry to this method...
- else if !hadInstallDir {
- ; Default to the location of an existing installation matching this.UserInstall
- if installDirs[this.UserInstall?2:1]
- this.InstallDir := installDirs[this.UserInstall?2:1]
- ; Default to the location and mode of any other existing installation
- else if installDirs[this.UserInstall?1:2]
- this.InstallDir := installDirs[this.UserInstall?1:2], this.UserInstall := !this.UserInstall
- }
- }
-
- ResolveSourceDir() {
- if !this.HasProp('SourceDir') {
- if A_IsCompiled && IsSet(UnpackFiles)
- this.SourceDir := UnpackFiles(this.InstallDir)
- else
- this.SourceDir := A_ScriptDir '\..'
- }
- Loop Files this.SourceDir, 'D'
- this.SourceDir := A_LoopFileFullPath
- else
- throw ValueError("Invalid source directory",, this.SourceDir)
- }
-
- HashesPath => this.InstallDir '\UX\installed-files.csv'
- Hashes => (
- this.DefineProp('Hashes', {value: hashes := this.ReadHashes()}),
- hashes
- )
-
- Apply() {
- if !DirExist(this.InstallDir)
- DirCreate this.InstallDir
- SetWorkingDir this.InstallDir
-
- ; Execute pre-check actions
- for item in this.PreCheck
- item(this)
-
- ; Detect possible conflicts before taking action
- this.PreApplyChecks()
-
- ; Execute pre-install actions
- for item in this.PreAction
- item(this)
-
- ; Install files
- for item in this.FileItems {
- SplitPath item.Dest,, &destDir
- if destDir != ''
- DirCreate destDir
- try
- FileCopy item.Source, item.Dest, true
- catch
- this.WarnBox 'Copy failed`nsource: ' item.Source '`ndest: ' item.Dest
- else {
- ; If source files were extracted from a zip, they may have a Zone.Identifier
- ; stream identifying them as coming from the Internet. These must be deleted
- ; to prevent warnings when the user runs the installed files.
- try FileDelete item.Dest ":Zone.Identifier"
- this.AddFileHash(item.Dest, this.Version)
- }
- }
-
- ; Install registry settings
- for item in this.RegItems {
- if item.HasProp('Value') {
- RegWrite(item.Value, item.Value is Integer ? 'REG_DWORD' : 'REG_SZ'
- , item.Key, item.ValueName)
- } else {
- try RegDelete(item.Key, item.ValueName)
- }
- }
-
- ; Execute post-install actions
- for item in this.PostAction
- item(this)
-
- ; Write file list to disk
- if this.Hashes.Count
- this.WriteHashes
- }
-
- ElevationNeeded => !A_IsAdmin && (!this.UserInstall || this.HasProp('RequireAdmin'))
-
- ElevateIfNeeded() {
- if this.ElevationNeeded {
- try RunWait '*runas ' DllCall('GetCommandLine', 'str')
- ExitApp
- }
- }
-
- ;{ Installation entry points
-
- InstallFull() {
- SetRegView 64
-
- this.ResolveInstallDir
- this.ResolveSourceDir
-
- doFiles := this.InstallDir != this.SourceDir
-
- ; If a newer version is already installed, integrate with it
- ux := doFiles && this.GetTargetUX()
- if ux && VerCompare(ux.Version, this.Version) > 0 {
- cmd := StrReplace(ux.InstallCommand, '%1', this.SourceDir,, &replaced)
- if !replaced
- cmd .= ' "' this.SourceDir '"'
- if this.ElevationNeeded
- cmd := '*runas ' cmd
- try
- exitCode := RunWait(cmd, this.InstallDir)
- catch as e
- if InStr(e.Message e.Extra, 'cancel')
- exitCode := 1
- else
- throw e
- ExitApp exitCode
- }
-
- this.ElevateIfNeeded
-
- ; If a legacy version is installed, upgrade it
- wowKey(k) => StrReplace(k, '\Software\', '\Software\Wow6432Node\')
- installedVersion := RegRead(key := wowKey(this.SoftwareKey), 'Version', '')
- || RegRead(key := this.SoftwareKey, 'Version', '')
- if SubStr(installedVersion, 1, 2) = '1.' {
- this.SoftwareKeyV1 := key
- this.UninstallKeyV1 := InStr(key, 'Wow64') ? wowKey(this.UninstallKey) : this.UninstallKey
- this.AddPreCheck this.PrepareUpgradeV1.Bind(, installedVersion)
- this.AddPreAction this.UpgradeV1.Bind(, installedVersion)
- }
-
- if doFiles || FileExist(this.InstallDir '\UX\AutoHotkeyUX.exe')
- this.Interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe'
-
- if doFiles {
- if this.GetLinkAttrib(this.InstallDir '\v2')
- this.AddPreAction this.DeleteLink.Bind(, 'v2')
- this.AddCoreFiles 'v2'
- ; Give UX its own AutoHotkey.exe for a few reasons:
- ; 1. The Start menu shortcut needs a stable path, since pinning to taskbar creates
- ; a copy that won't get updated (obsolete since using 'v2' and DisplaceFile now).
- ; 2. LauncherConfigGui may store the path under HKCU, which mightn't get updated.
- ; 3. It helps differentiate the launcher from other scripts in Task Manager.
- ; 4. It makes the UX scripts independent from the installed interpreters.
- srcExe := this.SourceDir '\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe'
- dstExe := this.InstallDir '\UX\AutoHotkeyUX.exe'
- if FileExist(srcExe) || (!FileExist(dstExe) && (srcExe := A_AhkPath))
- this.AddFileCopy srcExe, 'UX\AutoHotkeyUX.exe' ; Must be relative
- this.AddPostAction this.UpdateV2Link
-
- this.AddUXFiles
- this.AddMiscFiles
- }
-
- this.AddPostAction this.CreateWindowSpyRedirect
-
- this.AddUninstallReg
- this.AddSoftwareReg
- this.AddFileTypeReg
-
- this.Apply
-
- if FileExist(this.InstallDir '\UX\reset-assoc.ahk')
- RunWait this.CmdStr('UX\reset-assoc.ahk', '/check')
-
- if !this.Silent
- ShellRun this.Interpreter, 'UX\ui-dash.ahk', this.InstallDir
- }
-
- InstallExtraVersion() {
- SetRegView 64
-
- this.ResolveInstallDir
- this.ResolveSourceDir
-
- Loop Files this.SourceDir '\AutoHotkey*.exe' {
- exe := GetExeInfo(A_LoopFilePath)
- break
- } else
- throw Error("AutoHotkey*.exe not found in source directory",, this.SourceDir)
-
- this.ElevateIfNeeded
-
- coreDir := 'v' (this.Version := exe.Version)
-
- if VerCompare(this.Version, '2.0-') < 0 {
- try
- exe := GetExeInfo(this.InstallDir '\AutoHotkeyU32.exe')
- catch
- {}
- else if VerCompare(this.Version, exe.Version) > 0
- && !FileExist(this.InstallDir '\v' exe.Version) {
- this.AddPreAction this.DisplaceV1.Bind(, exe.Version)
- coreDir := '.'
- }
- }
-
- this.AddCoreFiles(coreDir)
-
- if FileExist(this.SourceDir '\Compiler\Ahk2Exe.exe') {
- compilerVersion := GetExeInfo(this.SourceDir '\Compiler\Ahk2Exe.exe').Version
- installedCompiler := this.Hashes.Get('Compiler\Ahk2Exe.exe', '')
- if !installedCompiler || VerCompare(compilerVersion, installedCompiler.Version) > 0
- this.AddCompiler(this.SourceDir '\Compiler')
- }
-
- this.Apply
- }
-
- ;}
-
- ;{ Uninstallation
-
- UninstallRegistry() {
- SetRegView 64
- delKey this.FileTypeKey
- delKey this.ClassesKey '\.ahk'
- delKey this.SoftwareKey
- delKey this.UninstallKey
- if this.RootKey = 'HKLM' {
- delKey 'HKCU\' this.SoftwareSubKey
- delKey 'HKCU\Software\Classes\' this.ScriptProgId
- for k in ['AutoHotkey.exe', 'Ahk2Exe.exe'] ; made by v1 installer
- delKey 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\' k
- }
-
- delKey(key) {
- try
- RegDeleteKey key
- catch OSError as e
- if e.number != 2 ; ERROR_FILE_NOT_FOUND
- throw
- }
-
- this.NotifyAssocChanged
- }
-
- GetHashesForVersions(versions) {
- versions := ',' versions ','
- files := Map(), files.CaseSense := "off"
- for , cfiles in this.GetComponents(v => InStr(versions, ',' v ','))
- for fh in cfiles
- files[fh.Path] := fh
- return files
- }
-
- Uninstall(versions:='') {
- this.ResolveInstallDir
-
- this.ElevateIfNeeded
-
- files := versions = '' ? this.Hashes.Clone() : this.GetHashesForVersions(versions)
- if !files.Count && versions = ''
- this.GetConfirmation("Installation data missing. Files will not be deleted.", 'x')
-
- ; Close scripts and help files
- this.PreUninstallChecks files
-
- ; Remove from registry only if being fully uninstalled
- if versions = ''
- this.UninstallRegistry
-
- ; Remove files
- SetWorkingDir this.InstallDir
- modified := ""
- dirs := ""
- for path, f in files {
- if !FileExist(path)
- continue
- if HashFile(path) != f.Hash {
- modified .= "`n" path
- continue
- }
- if this.InstallDir '\' path = A_AhkPath {
- postponed := A_AhkPath
- this.Hashes.Delete(path)
- continue
- }
- FileDelete path
- this.Hashes.Delete(path)
- SplitPath path,, &dir
- if dir != ""
- dirs .= dir "`n"
- }
- if modified != "" {
- this.InfoBox("The following files were not deleted as they appear to have been modified:"
- . modified)
- }
-
- ; Update or remove hashes file
- if this.Hashes.Count
- this.WriteHashes
- else
- try FileDelete this.HashesPath
-
- ; Remove empty directories
- for dir in StrSplit(Sort(RTrim(dirs, "`n"), 'UR'), "`n") {
- this.DeleteLink dir '\AutoHotkey.exe'
- try DirDelete dir, false
- }
-
- if versions = '' ; Full uninstall
- this.DeleteLink this.InstallDir '\v2'
-
- if IsSet(postponed) {
- ; Try delete via cmd.exe after we exit
- Run(A_ComSpec ' /c "'
- 'ping -n 2 127.1 > NUL & '
- 'del "' postponed '" & '
- 'cd %TEMP% & '
- 'rmdir "' postponed '\.." & '
- 'rmdir "' A_WorkingDir '"'
- '"',, 'Hide')
- }
- ExitApp
- }
-
- ;}
-
- ;{ Conflict prevention
-
- PreApplyChecks() {
- ; Check for files that might be overwritten
- writeFiles := Map(), writeFiles.CaseSense := 'off'
- hasChm := false
- unknownFiles := ''
- modifiedFiles := ''
- hashes := this.Hashes
- if (item := hashes.Get(this.InstallDir '\UX\AutoHotkeyUX.exe', false)) ; Erroneous entry by v2.0-beta.4
- hashes.Delete(item.Path), hashes[item.Path := 'UX\AutoHotkeyUX.exe'] := item ; Make it relative
- for item in this.FileItems {
- if !(attrib := FileExist(item.Dest))
- continue
- if InStr(attrib, 'D')
- this.FatalError("The following file cannot be installed because a directory by this name already exists:`n"
- item.Dest "`n`nNo changes have been made.")
- SplitPath item.Dest,, &destDir, &ext, &destName
- if destDir = 'v2' && this.GetLinkAttrib(destDir)
- continue ; Symlink should be deleted, so item.Dest won't exist.
- if ext = 'exe'
- writeFiles[this.InstallDir '\' item.Dest] := true
- else if ext = 'chm'
- hasChm := true
- if !(curFile := hashes.Get(item.Dest, ''))
- unknownFiles .= item.Dest "`n"
- else if destDir = 'v2' && curFile.Version && curFile.Version != this.Version {
- this.AddPreAction this.DisplaceFile.Bind(, item.Dest
- , 'v' curFile.Version '\' destName '.' ext, curFile.Version)
- if ext = 'exe' && FileExist('v2\' destName '_UIA.exe')
- this.AddPreAction this.DisplaceFile.Bind(, 'v2\' destName '_UIA.exe'
- , 'v' curFile.Version '\' destName '_UIA.exe', '')
- }
- else if curFile.Hash != HashFile(item.Dest)
- modifiedFiles .= item.Dest "`n"
- }
-
- ; Find any scripts being executed by files that will be overwritten
- if writeFiles.Has(this.InstallDir '\AutoHotkeyU32.exe') ; Rough check for v1 upgrade
- writeFiles[this.InstallDir '\AutoHotkey.exe'] := true
- ours(exe) => writeFiles.Has(exe) || writeFiles.Has(StrReplace(exe, '_UIA'))
- scripts := this.ScriptsUsingOurFiles(ours)
-
- ; Show confirmation prompt
- message := ""
- if scripts.Length {
- message .= "The following scripts will be closed automatically:`n"
- for w in scripts
- message .= this.ScriptTitle(w) "`n"
- message .= "`n"
- }
- if unknownFiles != '' {
- message .= "The following files not created by setup will be overwritten:`n"
- . unknownFiles
- message .= "`n"
- }
- if modifiedFiles != '' {
- message .= "The following files appear to contain modifications that will be lost:`n"
- . modifiedFiles
- message .= "`n"
- }
- if message != ''
- this.GetConfirmation(message)
-
- this.CloseScriptsUsingOurFiles(scripts, ours)
- }
-
- PreUninstallChecks(files) {
- ours(exe) => files.Has(this.RelativePath(exe))
- scripts := this.ScriptsUsingOurFiles(ours)
- this.CloseScriptsUsingOurFiles(scripts, ours)
- }
-
- CloseScriptsUsingOurFiles(scripts, ours) {
- ; Close scripts and help files
- static WM_CLOSE := 0x10
- for w in WinGetList("AutoHotkey ahk_class HH Parent")
- try PostMessage WM_CLOSE,,, w
- for w in scripts
- try PostMessage WM_CLOSE,,, w
- ; Wait for windows/scripts to close
- WinWaitClose "AutoHotkey ahk_class HH Parent"
- loop {
- Sleep 100
- ; Refresh the list in case scripts have started/stopped
- scripts := this.ScriptsUsingOurFiles(ours)
- ; Prompt again after around 3 seconds of waiting
- if scripts.Length && Mod(A_Index, 30) = 0 {
- message := "The following scripts must be closed manually before setup can continue:`n"
- for w in scripts
- message .= this.ScriptTitle(w) "`n"
- this.GetConfirmation(message)
- }
- } until scripts.Length = 0
- }
-
- ScriptsUsingOurFiles(ours) {
- scripts := [], dhw := A_DetectHiddenWindows
- DetectHiddenWindows true
- for w in WinGetList('ahk_class AutoHotkey') {
- if w = A_ScriptHwnd
- continue
- if ours(WinGetProcessPath(w))
- scripts.Push(w)
- }
- DetectHiddenWindows dhw
- return scripts
- }
-
- ScriptTitle(wnd) {
- try
- return RegExReplace(WinGetTitle(wnd), ' - AutoHotkey v.*')
- catch
- return "(unable to retrieve title - already exited?)"
- }
-
- ;}
-
- ;{ Components to install
-
- AddCoreFiles(destSubDir) {
- this.AddFiles(this.SourceDir, destSubDir
- , 'AutoHotkey*.exe', 'AutoHotkey.chm'
- )
- this.AddFiles(this.SourceDir, destSubDir = '.' ? 'Compiler' : destSubDir
- , 'Compiler\*.bin' ; Legacy base files for compiler - even if Ahk2Exe is not installed yet.
- )
-
- ; Queue creation of UIA executable files
- if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) && VerCompare(this.Version, '1.1.19') >= 0
- Loop Files this.SourceDir '\AutoHotkey*.exe'
- this.AddPostAction this.MakeUIA.Bind(, destSubDir '\' A_LoopFileName)
- }
-
- AddMiscFiles() {
- this.AddFiles(this.SourceDir, '.', 'license.txt')
- }
-
- AddCompiler(compilerSourceDir) {
- this.AddFiles(compilerSourceDir, 'Compiler', 'Ahk2Exe.exe')
- this.AddVerb('Compile', 'Compiler\Ahk2Exe.exe', '/in "%l" %*', "Compile script")
- this.AddVerb('Compile-Gui', 'Compiler\Ahk2Exe.exe', '/gui /in "%l" %*', "Compile script (GUI)...")
- this.AddPostAction this.CreateCompilerShortcut
- }
-
- AddUXFiles() {
- this.AddFiles(this.SourceDir '\UX', 'UX', '*.ahk')
- this.AddFiles(this.SourceDir '\UX', 'UX\inc', 'inc\*')
- this.AddFiles(this.SourceDir '\UX\Templates', 'UX\Templates', '*.ahk')
- this.AddPostAction this.CreateStartShortcut
- }
-
- AddSoftwareReg() {
- this.AddRegValues(this.SoftwareKey, [
- {ValueName: 'InstallDir', Value: this.InstallDir},
- {ValueName: 'InstallCommand', Value: this.CmdStr('UX\install.ahk', '/install "%1"')},
- {ValueName: 'Version', Value: this.Version},
- ])
- }
-
- AddUninstallReg() {
- this.AddRegValues(this.UninstallKey, [
- {ValueName: 'DisplayName', Value: this.ProductName (this.RootKey = 'HKCU' ? " (user)" : "")},
- {ValueName: 'UninstallString', Value: this.UninstallCmd},
- {ValueName: 'QuietUninstallString', Value: this.QUninstallCmd},
- {ValueName: 'NoModify', Value: 1},
- {ValueName: 'DisplayIcon', Value: this.Interpreter},
- {ValueName: 'DisplayVersion', Value: this.Version},
- {ValueName: 'URLInfoAbout', Value: this.ProductURL},
- {ValueName: 'Publisher', Value: this.Publisher},
- {ValueName: 'InstallLocation', Value: this.InstallDir},
- ])
-
- }
-
- AddFileTypeReg() {
- this.AddRegValues(this.ClassesKey, [
- {Key: '.ahk', Value: this.ScriptProgId},
- {Key: '.ahk\ShellNew', ValueName: 'Command', Value: this.CmdStr('UX\ui-newscript.ahk', '"%1"')},
- {Key: '.ahk\ShellNew', ValueName: 'FileName'},
- {Key: '.ahk\PersistentHandler', Value: '{5e941d80-bf96-11cd-b579-08002b30bfeb}'}
- ])
- this.AddRegValues(this.FileTypeKey, [
- {Value: "AutoHotkey Script"},
- {ValueName: 'AppUserModelID', Value: this.AppUserModelID}, ; Testing produced inconsistent results, but it seems sometimes this must be here, sometimes under the verb.
- {Key: 'DefaultIcon', Value: this.Interpreter ",1"},
- {Key: 'Shell', Value: 'Open runas UIAccess Edit'}, ; Including 'runas' in lower-case fixes the shield icon not appearing on Windows 11.
- {Key: 'Shell\Open', ValueName: 'FriendlyAppName', Value: 'AutoHotkey Launcher'},
- ])
- this.AddRunVerbs()
- this.AddEditVerbIfUnset()
- this.AddPostAction this.NotifyAssocChanged
- }
-
- AddRunVerbs() {
- aumid := {ValueName: 'AppUserModelID', Value: this.AppUserModelID}
- this.AddVerb('Open', 'UX\launcher.ahk', '"%1" %*', "Run script",
- aumid
- )
- this.AddVerb('RunAs', 'UX\launcher.ahk', '"%1" %*',
- aumid, {ValueName: 'HasLUAShield', Value: ""}
- )
- if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) {
- this.AddVerb('UIAccess', 'UX\launcher.ahk', '/runwith UIA "%1" %*',
- "Run with UI access", aumid)
- }
- ; Add *Launch as a hidden verb, accessible via Run('*Launch ' file).
- this.AddVerb('Launch', 'UX\launcher.ahk', '/Launch "%1" %*', "Launch",
- aumid, {ValueName: 'ProgrammaticAccessOnly', Value: ""}
- )
- }
-
- AddEditVerbIfUnset() {
- static v1_edit_cmd := 'notepad.exe %1'
- ; Add edit verb only if it is undefined or has its default v1 value.
- if RegRead(this.FileTypeKey '\Shell\Edit\Command',, v1_edit_cmd) = v1_edit_cmd
- this.AddVerb('Edit', 'UX\ui-editor.ahk', '"%1"', "Edit script")
- }
-
- ;}
-
- ;{ Utility functions
-
- RelativePath(p) => (
- i := this.InstallDir '\',
- SubStr(p, 1, StrLen(i)) = i ? SubStr(p, StrLen(i) + 1) : p
- )
-
- CmdStr(script, args:='')
- => RTrim(Format((InStr(script, '.ahk') ? '"{1}" ' : '') '"{2}\{3}" {4}'
- , this.Interpreter, this.InstallDir, script, args))
-
- AddRegValues(key, values) {
- for v in values {
- i := {}
- i.Key := key (v.HasProp('Key') ? '\' v.Key : '')
- i.ValueName := v.HasProp('ValueName') ? v.ValueName : ''
- (v is Primitive) ? i.Value := v :
- (v.HasProp('Value')) ? i.Value := v.Value : 0
- this.RegItems.Push(i)
- }
- }
-
- AddVerb(name, script, args, values*) {
- this.AddRegValues(this.FileTypeKey '\Shell\' name, [
- {Key: 'Command', Value: this.CmdStr(script, args)},
- values*
- ])
- }
-
- AddFileCopy(sourcePath, destPath) {
- this.FileItems.Push {Source: sourcePath, Dest: destPath}
- }
-
- AddFiles(sourceDir, destSubDir, patterns*) {
- destSubDir := (destSubDir != '.' ? destSubDir '\' : '')
- for p in patterns {
- Loop Files sourceDir '\' p {
- this.AddFileCopy A_LoopFileFullPath, destSubDir . A_LoopFileName
- }
- }
- }
-
- AddPreCheck(f) => this.PreCheck.Push(f)
- AddPreAction(f) => this.PreAction.Push(f)
- AddPostAction(f) => this.PostAction.Push(f)
-
- ReadHashes() {
- ; For maintainability, don't assume the caller has set the working dir.
- wd := A_WorkingDir, A_WorkingDir := this.InstallDir
- hashes := ReadHashes(this.HashesPath, item => FileExist(item.Path))
- A_WorkingDir := wd
- return hashes
- }
-
- AddFileHash(f, v) {
- this.Hashes[f] := {Path: f, Hash: HashFile(f), Version: v}
- }
-
- WriteHashes() {
- s := "Hash,Version,Path,Description`r`n"
- for ,item in this.Hashes {
- if !item.HasProp('Description') {
- try ; Cache the file description for the launcher
- exe := GetExeInfo(item.Path)
- catch
- item.Description := ""
- else {
- item.Description := exe.Description
- if InStr(item.Description, 'AutoHotkey')
- item.Version := exe.Version ; Ensure accuracy for the launcher
- }
- }
- s .= Format('{},{},"{}","{}"`r`n', item.Hash, item.Version, item.Path, item.Description)
- }
- FileOpen(this.HashesPath, 'w').Write(s)
- }
-
- GetComponents(versionFilter?) {
- callerwd := A_WorkingDir
- SetWorkingDir this.InstallDir
- versions := Map()
- maxes := Map()
- for , fh in this.Hashes {
- if fh.Path ~= 'i)^UX\\|^[A-Z]:|^\\\\|^(WindowSpy\.ahk|license\.txt)$'
- . '|^Compiler\\(?!.*\.bin$)'
- continue
- try fh.Version := GetExeInfo(fh.Path = 'AutoHotkey.chm' ? 'AutoHotkeyU32.exe' : fh.Path).Version ; Auto-fix inaccurate versions in Hashes
- if !files := versions.Get(fh.Version, 0) {
- if IsSet(versionFilter) && !versionFilter(fh.Version)
- continue
- versions[fh.Version] := files := []
- v := RegExReplace(fh.Version, '^\d+\.\d+\b\K.*')
- if VerCompare(prevMax := maxes.Get(v, ''), fh.Version) < 0 {
- maxes[v] := fh.Version
- if prevMax != ''
- versions[prevMax].superseded := true
- }
- else
- files.superseded := true
- }
- files.InsertAt(1, fh)
- }
- SetWorkingDir callerwd
- return versions
- }
-
- NotifyAssocChanged() {
- DllCall("shell32\SHChangeNotify", "uint", 0x08000000 ; SHCNE_ASSOCCHANGED
- , "uint", 0, "ptr", 0, "ptr", 0)
- }
-
- GetConfirmation(message, icon:='!') {
- if !this.Silent && MsgBox(message, this.DialogTitle, 'Icon' icon ' OkCancel') = 'Cancel'
- ExitApp 1
- }
-
- WarnBox(message) {
- if !this.Silent
- MsgBox message, this.DialogTitle, "Icon!"
- }
-
- InfoBox(message) {
- if !this.Silent
- MsgBox message, this.DialogTitle, "Iconi"
- }
-
- FatalError(message) {
- if !this.Silent
- MsgBox message, this.DialogTitle, 'Iconx'
- ExitApp 1
- }
-
- GetTargetUX() {
- try {
- ; For registered installations, InstallCommand allows for future changes.
- return {
- Version: RegRead(this.SoftwareKey, 'Version'),
- InstallCommand: RegRead(this.SoftwareKey, 'InstallCommand')
- }
- }
- try {
- ; Target installation not in registry, or has no InstallCommand (e.g. too old).
- ; Allow non-registry installations that follow protocol as commented below.
- ux := {}
- ; Version information must be provided by the file at this.HashesPath:
- ux.Version := this.Hashes['UX\install.ahk'].Version
- ; Interpreter must be located at the path calculated below:
- interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe'
- if FileExist(interpreter) {
- ; Additional interpreters must be installable with this command line:
- ux.InstallCommand := Format('"{1}" "{2}\UX\install.ahk" /install "%1"'
- , interpreter, this.InstallDir)
- return ux
- }
- }
- ; Otherwise, UX script or appropriate interpreter not found.
- }
-
- ; Delete a symbolic link, or do nothing if path does not refer to a symbolic link.
- DeleteLink(path) {
- switch this.GetLinkAttrib(path) {
- case 'D': DirDelete path
- case 'F': FileDelete path
- }
- }
-
- GetLinkAttrib(path) {
- attrib := DllCall('GetFileAttributes', 'str', path)
- ; FILE_ATTRIBUTE_REPARSE_POINT = 0x400
- ; FILE_ATTRIBUTE_DIRECTORY = 0x10
- return (attrib != -1 && (attrib & 0x400)) ? ((attrib & 0x10) ? 'D' : 'F') : ''
- }
-
- UpdateV2Link() {
- ; Create a symlink for AutoHotkey.exe to simplify use by tools.
- DllCall('CreateSymbolicLink', 'str', this.InstallDir '\v2\AutoHotkey.exe'
- , 'str', 'AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe', 'uint', 0)
- }
-
- CreateWindowSpyRedirect() {
- ; Permit overwrite only when upgrading a legacy v1 installation,
- ; or if it is known to have been created by us.
- if !FileExist('WindowSpy.ahk') || this.HasOwnProp('SoftwareKeyV1')
- || this.Hashes.Get('WindowSpy.ahk', {Hash: ''}).Hash = HashFile('WindowSpy.ahk') {
- FileOpen('WindowSpy.ahk', 'w').Write('
- (
- #include UX
- #include inc\bounce-v1.ahk
- /**/
- #requires AutoHotkey v2.0
- try Run('"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"'), ExitApp()
- #include WindowSpy.ahk
- )')
- this.AddFileHash('WindowSpy.ahk', this.Version)
- }
- }
-
- CreateStartShortcut() {
- CreateAppShortcut(
- lnk := this.StartFolder '\AutoHotkey.lnk', {
- target: this.Interpreter,
- args: Format('"{1}\UX\ui-dash.ahk"', this.InstallDir),
- desc: "AutoHotkey Dash",
- aumid: this.AppUserModelID,
- uninst: this.UninstallCmd
- }
- )
- this.AddFileHash lnk, this.Version
-
- CreateAppShortcut(
- lnk := this.StartFolder '\AutoHotkey Window Spy.lnk', {
- target: this.Interpreter,
- args: Format('"{1}\UX\WindowSpy.ahk"', this.InstallDir),
- desc: "AutoHotkey Window Spy",
- aumid: 'AutoHotkey.WindowSpy',
- icon: Format('{1}\UX\inc\spy.ico', this.InstallDir), iconIndex: 0,
- uninst: this.UninstallCmd
- }
- )
- this.AddFileHash lnk, this.Version
- }
-
- CreateCompilerShortcut() {
- CreateAppShortcut(
- lnk := this.StartFolder '\Ahk2Exe.lnk', {
- target: this.InstallDir '\Compiler\Ahk2Exe.exe',
- desc: "Convert .ahk to .exe",
- aumid: 'AutoHotkey.Ahk2Exe',
- uninst: this.UninstallCmd
- }
- )
- this.AddFileHash lnk, this.Version
- }
-
- MakeUIA(baseFile) {
- SplitPath baseFile,, &baseDir,, &baseName
- baseDir := baseDir = '.' ? '' : baseDir '\'
- FileCopy baseFile, newPath := baseDir baseName '_UIA.exe', true
- static abort := false ; Let "Abort" disable MakeUIA calls, but let other PostActions complete.
- while !abort {
- try {
- EnableUIAccess newPath
- break
- }
- catch as e {
- try FileDelete newPath
- if e.What != "EndUpdateResource"
- throw
- if this.Silent {
- if A_Index > 4
- break
- Sleep 500
- }
- switch MsgBox("Unable to create " baseName ". Try adding an exclusion in your antivirus software. If that doesn't work, please report the issue.`n`nError: " e.Message
- ,, "a/r/i") {
- case "Abort": abort := true
- case "Ignore": break
- }
- }
- }
- this.AddFileHash newPath, '' ; For uninstall
- }
-
- IsTrustedLocation(path) { ; http://msdn.com/library/bb756929
- other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432")
- return InStr(path, A_ProgramFiles "\") = 1
- || other && InStr(path, other "\") = 1
- }
-
- ;}
-
- ;{ Upgrade from v1
-
- PrepareUpgradeV1(installedVersion) {
- ; This needs to be done before conflict-checking
- if FileExist('license.txt')
- this.AddFileHash('license.txt', installedVersion)
- }
-
- UpgradeV1(installedVersion) {
- try { ; Permit failure in case AutoHotkey.exe has been deleted.
- exe := GetExeInfo('AutoHotkey.exe')
- build := RegExReplace(exe.Description, '^AutoHotkey *')
- }
-
- ; Set default launcher settings
- if IsSet(build) && ConfigRead('Launcher\v1', 'Build', '!') = '!'
- ConfigWrite(build, 'Launcher\v1', 'Build')
- if ConfigRead('Launcher\v1', 'UTF8', '') = ''
- && InStr(RegRead('HKCR\' this.ScriptProgId '\Shell\Open\Command',, ''), '/cp65001 ')
- ConfigWrite(true, 'Launcher\v1', 'UTF8')
-
- ; Record these for Uninstall
- add 'AutoHotkey{1}.exe', '', 'A32', 'U32', 'U64', 'A32_UIA', 'U32_UIA', 'U64_UIA'
- add 'Compiler\{1}.bin', 'ANSI 32-bit', 'Unicode 32-bit', 'Unicode 64-bit', 'AutoHotkeySC'
- add '{1}', 'Compiler\Ahk2Exe.exe', 'AutoHotkey.chm', A_WinDir '\ShellNew\Template.ahk'
-
- add(fmt, patterns*) {
- for p in patterns
- if FileExist(f := Format(fmt, p))
- this.AddFileHash f, installedVersion
- }
-
- ; Remove obsolete files
- for item in ['Installer.ahk', 'AutoHotkey Website.url']
- try FileDelete item
-
- ; Remove the v1 shortcuts from the Start menu
- name := RegRead(this.SoftwareKeyV1, 'StartMenuFolder', '')
- if name != ''
- try DirDelete A_ProgramsCommon '\' name, true
-
- ; Remove the old sub-keys, which might be in the wrong reg view
- try RegDeleteKey this.SoftwareKeyV1
- try RegDeleteKey this.UninstallKeyV1
- }
-
- DisplaceFile(sourcePath, destPath, version) {
- SplitPath destPath,, &destDir
- if destDir != ""
- DirCreate destDir
- FileMove sourcePath, destPath
- try this.Hashes.Delete(sourcePath)
- this.AddFileHash destPath, version
- }
-
- DisplaceV1(v) {
- DirCreate dir := 'v' v
- displace(path) {
- if FileExist(path) {
- SplitPath path, &name
- this.DisplaceFile path, dir '\' name, v
- }
- }
- for build in ['U32', 'U64', 'A32'] {
- displace 'AutoHotkey' build '.exe'
- displace 'AutoHotkey' build '_UIA.exe'
- }
- try
- defaultBinSize := FileGetSize(defaultBinPath := 'Compiler\AutoHotkeySC.bin')
- for build in ['Unicode 32-bit', 'Unicode 64-bit', 'ANSI 32-bit'] {
- try
- if FileGetSize('Compiler\' build '.bin') = defaultBinSize
- this.AddFileCopy this.InstallDir '\Compiler\' build '.bin', defaultBinPath
- displace 'Compiler\' build '.bin'
- }
-
- displace 'AutoHotkey.chm'
- try {
- exe := GetExeInfo('AutoHotkey.exe')
- if exe.Version = v
- && RegExMatch(exe.Description, ' (A|U)\w+ (32|64)-bit$', &m) {
- ; Too early to add to FileItems, so use PostAction:
- this.AddPostAction this.CopyDefaultExe.Bind(, 'AutoHotkey' m.1 m.2 '.exe')
- }
- }
- }
-
- CopyDefaultExe(from) {
- try
- FileCopy from, 'AutoHotkey.exe', true
- catch
- return ; TODO: report to user?
- this.AddFileHash 'AutoHotkey.exe', this.Version
- }
-
- ;}
-}
+; This script contains AutoHotkey (un)installation routines.
+; See the AutoHotkey v2 documentation for usage.
+#include inc\bounce-v1.ahk
+/* v1 stops here */
+#requires AutoHotkey v2.0
+
+#SingleInstance Off ; Needed for elevation with *runas.
+
+#include inc\launcher-common.ahk
+#include inc\HashFile.ahk
+#include inc\CreateAppShortcut.ahk
+#include inc\EnableUIAccess.ahk
+#include inc\ShellRun.ahk
+
+if A_LineFile = A_ScriptFullPath
+ Install_Main
+
+Install_Main() {
+ try {
+ Installation.Instance := inst := Installation()
+ method := 'InstallFull'
+ params := []
+ while A_Index <= A_Args.Length {
+ switch A_Args[A_Index], 'off' {
+ case '/install':
+ method := 'InstallExtraVersion'
+ inst.SourceDir := A_Args[++A_Index]
+ case '/uninstall':
+ method := 'Uninstall'
+ if A_Index < A_Args.Length && SubStr(A_Args[A_Index+1],1,1) != '/'
+ params.Push(A_Args[++A_Index])
+ case '/to', '/installto':
+ inst.InstallDir := A_Args[++A_Index]
+ case '/elevate':
+ inst.RequireAdmin := true
+ case '/user':
+ inst.UserInstall := true
+ case '/silent':
+ inst.Silent := true
+ default:
+ MsgBox 'Invalid arg "' A_Args[A_Index] '"', inst.DialogTitle, "Iconx"
+ ExitApp 1
+ }
+ }
+ inst.%method%(params*)
+ }
+ catch as e {
+ DllCall(CallbackCreate(errBox.Bind(e)))
+ ExitApp 1
+ }
+ errBox(e) {
+ throw e
+ }
+}
+
+class Installation {
+ ProductName := "AutoHotkey"
+ ProductURL := "https://autohotkey.com"
+ Publisher := "AutoHotkey Foundation LLC"
+ Version := A_AhkVersion
+ AppUserModelID := 'AutoHotkey.AutoHotkey'
+
+ UserInstall := !A_IsAdmin
+ Interpreter := A_AhkPath
+ Silent := false
+
+ ScriptProgId := 'AutoHotkeyScript'
+ SoftwareSubKey := 'Software\AutoHotkey'
+ RootKey => this.UserInstall ? 'HKCU' : 'HKLM'
+ SoftwareKey => this.RootKey '\' this.SoftwareSubKey
+ ClassesKey => this.RootKey '\Software\Classes'
+ FileTypeKey => this.ClassesKey '\' this.ScriptProgId
+ UninstallKey => this.RootKey '\Software\Microsoft\Windows\CurrentVersion\Uninstall\AutoHotkey'
+ StartFolder => (this.UserInstall ? A_Programs : A_ProgramsCommon)
+ UninstallCmd => this.CmdStr('UX\ui-uninstall.ahk', ((A_IsAdmin && this.UserInstall) ? '/elevate' : ''))
+ QUninstallCmd => this.CmdStr('UX\install.ahk', '/uninstall /silent')
+
+ DialogTitle => this.ProductName " Setup"
+
+ FileItems := [] ; [{Source, Dest}]
+ RegItems := [] ; [{Key, ValueName, Value}]
+ PreCheck := [] ; [Callback(this)]
+ PreAction := [] ; [Callback(this)]
+ PostAction := [] ; [Callback(this)]
+
+ ResolveInstallDir() {
+ if hadInstallDir := this.HasProp('InstallDir')
+ DirCreate installDir := this.InstallDir
+ else
+ installDir := IsSet(InstallUtil) ? InstallUtil.DefaultDir : A_ScriptDir '\..'
+ Loop Files installDir, 'D'
+ installDir := A_LoopFileFullPath
+ this.InstallDir := installDir
+ SetRegView 64
+ installDirs := []
+ for rootKey in ['HKLM', 'HKCU']
+ installDirs.Push RegRead(rootKey '\' this.SoftwareSubKey, 'InstallDir', '')
+ ; Override installation mode if already installed here
+ if installDirs[1] = installDir
+ this.UserInstall := false
+ else if installDirs[2] = installDir
+ this.UserInstall := true
+ ; If this.InstallDir wasn't set upon entry to this method...
+ else if !hadInstallDir {
+ ; Default to the location of an existing installation matching this.UserInstall
+ if installDirs[this.UserInstall?2:1]
+ this.InstallDir := installDirs[this.UserInstall?2:1]
+ ; Default to the location and mode of any other existing installation
+ else if installDirs[this.UserInstall?1:2] {
+ if !A_IsAdmin && this.UserInstall {
+ ; Use the existing all-user installation only if elevation is successful
+ try
+ RunWait '*runas ' DllCall('GetCommandLine', 'str')
+ catch
+ return ; Presume user cancelled; continue as user
+ else
+ ExitApp
+ }
+ this.InstallDir := installDirs[this.UserInstall?1:2], this.UserInstall := !this.UserInstall
+ }
+ }
+ }
+
+ ResolveSourceDir() {
+ if !this.HasProp('SourceDir') {
+ if A_IsCompiled && IsSet(UnpackFiles)
+ this.SourceDir := UnpackFiles(this.InstallDir)
+ else
+ this.SourceDir := A_ScriptDir '\..'
+ }
+ Loop Files this.SourceDir, 'D'
+ this.SourceDir := A_LoopFileFullPath
+ else
+ throw ValueError("Invalid source directory",, this.SourceDir)
+ }
+
+ HashesPath => this.InstallDir '\UX\installed-files.csv'
+ Hashes => (
+ this.DefineProp('Hashes', {value: hashes := this.ReadHashes()}),
+ hashes
+ )
+
+ Apply() {
+ if !DirExist(this.InstallDir)
+ DirCreate this.InstallDir
+ SetWorkingDir this.InstallDir
+
+ ; Execute pre-check actions
+ for item in this.PreCheck
+ item(this)
+
+ ; Detect possible conflicts before taking action
+ this.PreApplyChecks()
+
+ ; Execute pre-install actions
+ for item in this.PreAction
+ item(this)
+
+ ; Install files
+ for item in this.FileItems {
+ SplitPath item.Dest,, &destDir
+ if destDir != ''
+ DirCreate destDir
+ try
+ FileCopy item.Source, item.Dest, true
+ catch
+ this.WarnBox 'Copy failed`nsource: ' item.Source '`ndest: ' item.Dest
+ else {
+ ; If source files were extracted from a zip, they may have a Zone.Identifier
+ ; stream identifying them as coming from the Internet. These must be deleted
+ ; to prevent warnings when the user runs the installed files.
+ try FileDelete item.Dest ":Zone.Identifier"
+ this.AddFileHash(item.Dest, this.Version)
+ }
+ }
+
+ ; Install registry settings
+ for item in this.RegItems {
+ if item.HasProp('Value') {
+ RegWrite(item.Value, item.Value is Integer ? 'REG_DWORD' : 'REG_SZ'
+ , item.Key, item.ValueName)
+ } else {
+ try RegDelete(item.Key, item.ValueName)
+ }
+ }
+
+ ; Execute post-install actions
+ for item in this.PostAction
+ item(this)
+
+ ; Write file list to disk
+ if this.Hashes.Count
+ this.WriteHashes
+ }
+
+ ElevationNeeded => !A_IsAdmin && (!this.UserInstall || this.HasProp('RequireAdmin'))
+
+ ElevateIfNeeded() {
+ if this.ElevationNeeded {
+ try RunWait '*runas ' DllCall('GetCommandLine', 'str')
+ ExitApp
+ }
+ }
+
+ ;{ Installation entry points
+
+ InstallFull() {
+ SetRegView 64
+
+ this.ResolveInstallDir
+ this.ResolveSourceDir
+
+ doFiles := this.InstallDir != this.SourceDir
+
+ ; If a newer version is already installed, integrate with it
+ ux := doFiles && this.GetTargetUX()
+ if ux && VerCompare(ux.Version, this.Version) > 0 {
+ cmd := StrReplace(ux.InstallCommand, '%1', this.SourceDir,, &replaced)
+ if !replaced
+ cmd .= ' "' this.SourceDir '"'
+ if this.ElevationNeeded
+ cmd := '*runas ' cmd
+ try
+ exitCode := RunWait(cmd, this.InstallDir)
+ catch as e
+ if InStr(e.Message e.Extra, 'cancel')
+ exitCode := 1
+ else
+ throw e
+ ExitApp exitCode
+ }
+
+ this.ElevateIfNeeded
+
+ ; If a legacy version is installed, upgrade it
+ wowKey(k) => StrReplace(k, '\Software\', '\Software\Wow6432Node\')
+ installedVersion := RegRead(key := wowKey(this.SoftwareKey), 'Version', '')
+ || RegRead(key := this.SoftwareKey, 'Version', '')
+ if SubStr(installedVersion, 1, 2) = '1.' {
+ this.SoftwareKeyV1 := key
+ this.UninstallKeyV1 := InStr(key, 'Wow64') ? wowKey(this.UninstallKey) : this.UninstallKey
+ this.AddPreCheck this.PrepareUpgradeV1.Bind(, installedVersion)
+ this.AddPreAction this.UpgradeV1.Bind(, installedVersion)
+ }
+
+ if doFiles || FileExist(this.InstallDir '\UX\AutoHotkeyUX.exe')
+ this.Interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe'
+
+ if doFiles {
+ if this.GetLinkAttrib(this.InstallDir '\v2')
+ this.AddPreAction this.DeleteLink.Bind(, 'v2')
+ this.AddCoreFiles 'v2'
+ ; Give UX its own AutoHotkey.exe for a few reasons:
+ ; 1. The Start menu shortcut needs a stable path, since pinning to taskbar creates
+ ; a copy that won't get updated (obsolete since using 'v2' and DisplaceFile now).
+ ; 2. LauncherConfigGui may store the path under HKCU, which mightn't get updated.
+ ; 3. It helps differentiate the launcher from other scripts in Task Manager.
+ ; 4. It makes the UX scripts independent from the installed interpreters.
+ srcExe := this.SourceDir '\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe'
+ dstExe := this.InstallDir '\UX\AutoHotkeyUX.exe'
+ if FileExist(srcExe) || (!FileExist(dstExe) && (srcExe := A_AhkPath))
+ this.AddFileCopy srcExe, 'UX\AutoHotkeyUX.exe' ; Must be relative
+ this.AddPostAction this.UpdateV2Link
+
+ this.AddUXFiles
+ this.AddMiscFiles
+ }
+
+ this.AddPostAction this.CreateWindowSpyRedirect
+
+ this.AddUninstallReg
+ this.AddSoftwareReg
+ this.AddFileTypeReg
+
+ this.Apply
+
+ if FileExist(this.InstallDir '\UX\reset-assoc.ahk')
+ RunWait this.CmdStr('UX\reset-assoc.ahk', '/check')
+
+ if !this.Silent
+ ShellRun this.Interpreter, 'UX\ui-dash.ahk', this.InstallDir
+ }
+
+ InstallExtraVersion() {
+ SetRegView 64
+
+ this.ResolveInstallDir
+ this.ResolveSourceDir
+
+ Loop Files this.SourceDir '\AutoHotkey*.exe' {
+ exe := GetExeInfo(A_LoopFilePath)
+ break
+ } else
+ throw Error("AutoHotkey*.exe not found in source directory",, this.SourceDir)
+
+ this.ElevateIfNeeded
+
+ coreDir := 'v' (this.Version := exe.Version)
+
+ if VerCompare(this.Version, '2.0-') < 0 {
+ try
+ exe := GetExeInfo(this.InstallDir '\AutoHotkeyU32.exe')
+ catch
+ {}
+ else if VerCompare(this.Version, exe.Version) > 0
+ && !FileExist(this.InstallDir '\v' exe.Version) {
+ this.AddPreAction this.DisplaceV1.Bind(, exe.Version)
+ coreDir := '.'
+ }
+ }
+
+ this.AddCoreFiles(coreDir)
+
+ if FileExist(this.SourceDir '\Compiler\Ahk2Exe.exe') {
+ compilerVersion := GetExeInfo(this.SourceDir '\Compiler\Ahk2Exe.exe').Version
+ installedCompiler := this.Hashes.Get('Compiler\Ahk2Exe.exe', '')
+ if !installedCompiler || VerCompare(compilerVersion, installedCompiler.Version) > 0
+ this.AddCompiler(this.SourceDir '\Compiler')
+ }
+
+ this.Apply
+ }
+
+ ;}
+
+ ;{ Uninstallation
+
+ UninstallRegistry() {
+ SetRegView 64
+ delKey this.FileTypeKey
+ delKey this.ClassesKey '\.ahk'
+ delKey this.SoftwareKey
+ delKey this.UninstallKey
+ if this.RootKey = 'HKLM' {
+ delKey 'HKCU\' this.SoftwareSubKey
+ delKey 'HKCU\Software\Classes\' this.ScriptProgId
+ for k in ['AutoHotkey.exe', 'Ahk2Exe.exe'] ; made by v1 installer
+ delKey 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\' k
+ }
+
+ delKey(key) {
+ try
+ RegDeleteKey key
+ catch OSError as e
+ if e.number != 2 ; ERROR_FILE_NOT_FOUND
+ throw
+ }
+
+ this.NotifyAssocChanged
+ }
+
+ GetHashesForVersions(versions) {
+ versions := ',' versions ','
+ files := Map(), files.CaseSense := "off"
+ for , cfiles in this.GetComponents(v => InStr(versions, ',' v ','))
+ for fh in cfiles
+ files[fh.Path] := fh
+ return files
+ }
+
+ Uninstall(versions:='') {
+ this.ResolveInstallDir
+
+ this.ElevateIfNeeded
+
+ files := versions = '' ? this.Hashes.Clone() : this.GetHashesForVersions(versions)
+ if !files.Count && versions = ''
+ this.GetConfirmation("Installation data missing. Files will not be deleted.", 'x')
+
+ ; Close scripts and help files
+ this.PreUninstallChecks files
+
+ ; Remove from registry only if being fully uninstalled
+ if versions = ''
+ this.UninstallRegistry
+
+ ; Remove files
+ SetWorkingDir this.InstallDir
+ modified := ""
+ dirs := ""
+ for path, f in files {
+ if !FileExist(path)
+ continue
+ if HashFile(path) != f.Hash {
+ modified .= "`n" path
+ continue
+ }
+ if this.InstallDir '\' path = A_AhkPath {
+ postponed := A_AhkPath
+ this.Hashes.Delete(path)
+ continue
+ }
+ FileDelete path
+ this.Hashes.Delete(path)
+ SplitPath path,, &dir
+ if dir != ""
+ dirs .= dir "`n"
+ }
+ if modified != "" {
+ this.InfoBox("The following files were not deleted as they appear to have been modified:"
+ . modified)
+ }
+
+ ; Update or remove hashes file
+ if this.Hashes.Count
+ this.WriteHashes
+ else
+ try FileDelete this.HashesPath
+
+ ; Remove empty directories
+ for dir in StrSplit(Sort(RTrim(dirs, "`n"), 'UR'), "`n") {
+ this.DeleteLink dir '\AutoHotkey.exe'
+ try DirDelete dir, false
+ }
+
+ if versions = '' ; Full uninstall
+ this.DeleteLink this.InstallDir '\v2'
+
+ if IsSet(postponed) {
+ ; Try delete via cmd.exe after we exit
+ Run(A_ComSpec ' /c "'
+ 'ping -n 2 127.1 > NUL & '
+ 'del "' postponed '" & '
+ 'cd %TEMP% & '
+ 'rmdir "' postponed '\.." & '
+ 'rmdir "' A_WorkingDir '"'
+ '"',, 'Hide')
+ }
+ ExitApp
+ }
+
+ ;}
+
+ ;{ Conflict prevention
+
+ PreApplyChecks() {
+ ; Check for files that might be overwritten
+ writeFiles := Map(), writeFiles.CaseSense := 'off'
+ hasChm := false
+ unknownFiles := ''
+ modifiedFiles := ''
+ hashes := this.Hashes
+ if (item := hashes.Get(this.InstallDir '\UX\AutoHotkeyUX.exe', false)) ; Erroneous entry by v2.0-beta.4
+ hashes.Delete(item.Path), hashes[item.Path := 'UX\AutoHotkeyUX.exe'] := item ; Make it relative
+ for item in this.FileItems {
+ if !(attrib := FileExist(item.Dest))
+ continue
+ if InStr(attrib, 'D')
+ this.FatalError("The following file cannot be installed because a directory by this name already exists:`n"
+ item.Dest "`n`nNo changes have been made.")
+ SplitPath item.Dest,, &destDir, &ext, &destName
+ if destDir = 'v2' && this.GetLinkAttrib(destDir)
+ continue ; Symlink should be deleted, so item.Dest won't exist.
+ if ext = 'exe'
+ writeFiles[this.InstallDir '\' item.Dest] := true
+ else if ext = 'chm'
+ hasChm := true
+ if !(curFile := hashes.Get(item.Dest, ''))
+ unknownFiles .= item.Dest "`n"
+ else if destDir = 'v2' && curFile.Version && curFile.Version != this.Version {
+ this.AddPreAction this.DisplaceFile.Bind(, item.Dest
+ , 'v' curFile.Version '\' destName '.' ext, curFile.Version)
+ if ext = 'exe' && FileExist('v2\' destName '_UIA.exe')
+ this.AddPreAction this.DisplaceFile.Bind(, 'v2\' destName '_UIA.exe'
+ , 'v' curFile.Version '\' destName '_UIA.exe', '')
+ }
+ else if curFile.Hash != HashFile(item.Dest)
+ modifiedFiles .= item.Dest "`n"
+ }
+
+ ; Find any scripts being executed by files that will be overwritten
+ if writeFiles.Has(this.InstallDir '\AutoHotkeyU32.exe') ; Rough check for v1 upgrade
+ writeFiles[this.InstallDir '\AutoHotkey.exe'] := true
+ ours(exe) => writeFiles.Has(exe) || writeFiles.Has(StrReplace(exe, '_UIA'))
+ scripts := this.ScriptsUsingOurFiles(ours)
+
+ ; Show confirmation prompt
+ message := ""
+ if scripts.Length {
+ message .= "The following scripts will be closed automatically:`n"
+ for w in scripts
+ message .= this.ScriptTitle(w) "`n"
+ message .= "`n"
+ }
+ if unknownFiles != '' {
+ message .= "The following files not created by setup will be overwritten:`n"
+ . unknownFiles
+ message .= "`n"
+ }
+ if modifiedFiles != '' {
+ message .= "The following files appear to contain modifications that will be lost:`n"
+ . modifiedFiles
+ message .= "`n"
+ }
+ if message != ''
+ this.GetConfirmation(message)
+
+ this.CloseScriptsUsingOurFiles(scripts, ours)
+ }
+
+ PreUninstallChecks(files) {
+ ours(exe) => files.Has(this.RelativePath(exe))
+ scripts := this.ScriptsUsingOurFiles(ours)
+ this.CloseScriptsUsingOurFiles(scripts, ours)
+ }
+
+ CloseScriptsUsingOurFiles(scripts, ours) {
+ ; Close scripts and help files
+ static WM_CLOSE := 0x10
+ for w in WinGetList("AutoHotkey ahk_class HH Parent")
+ try PostMessage WM_CLOSE,,, w
+ for w in scripts
+ try PostMessage WM_CLOSE,,, w
+ ; Wait for windows/scripts to close
+ WinWaitClose "AutoHotkey ahk_class HH Parent"
+ loop {
+ Sleep 100
+ ; Refresh the list in case scripts have started/stopped
+ scripts := this.ScriptsUsingOurFiles(ours)
+ ; Prompt again after around 3 seconds of waiting
+ if scripts.Length && Mod(A_Index, 30) = 0 {
+ message := "The following scripts must be closed manually before setup can continue:`n"
+ for w in scripts
+ message .= this.ScriptTitle(w) "`n"
+ this.GetConfirmation(message)
+ }
+ } until scripts.Length = 0
+ }
+
+ ScriptsUsingOurFiles(ours) {
+ scripts := [], dhw := A_DetectHiddenWindows
+ DetectHiddenWindows true
+ for w in WinGetList('ahk_class AutoHotkey') {
+ if w = A_ScriptHwnd
+ continue
+ if ours(WinGetProcessPath(w))
+ scripts.Push(w)
+ }
+ DetectHiddenWindows dhw
+ return scripts
+ }
+
+ ScriptTitle(wnd) {
+ try
+ return RegExReplace(WinGetTitle(wnd), ' - AutoHotkey v.*')
+ catch
+ return "(unable to retrieve title - already exited?)"
+ }
+
+ ;}
+
+ ;{ Components to install
+
+ AddCoreFiles(destSubDir) {
+ this.AddFiles(this.SourceDir, destSubDir
+ , 'AutoHotkey*.exe', 'AutoHotkey.chm'
+ )
+ this.AddFiles(this.SourceDir, destSubDir = '.' ? 'Compiler' : destSubDir
+ , 'Compiler\*.bin' ; Legacy base files for compiler - even if Ahk2Exe is not installed yet.
+ )
+
+ ; Queue creation of UIA executable files
+ if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) && VerCompare(this.Version, '1.1.19') >= 0
+ Loop Files this.SourceDir '\AutoHotkey*.exe'
+ this.AddPostAction this.MakeUIA.Bind(, destSubDir '\' A_LoopFileName)
+ }
+
+ AddMiscFiles() {
+ this.AddFiles(this.SourceDir, '.', 'license.txt')
+ }
+
+ AddCompiler(compilerSourceDir) {
+ this.AddFiles(compilerSourceDir, 'Compiler', 'Ahk2Exe.exe')
+ this.AddVerb('Compile', 'Compiler\Ahk2Exe.exe', '/in "%l" %*', "Compile script")
+ this.AddVerb('Compile-Gui', 'Compiler\Ahk2Exe.exe', '/gui /in "%l" %*', "Compile script (GUI)...")
+ this.AddPostAction this.CreateCompilerShortcut
+ }
+
+ AddUXFiles() {
+ this.AddFiles(this.SourceDir '\UX', 'UX', '*.ahk')
+ this.AddFiles(this.SourceDir '\UX', 'UX\inc', 'inc\*')
+ this.AddFiles(this.SourceDir '\UX\Templates', 'UX\Templates', '*.ahk')
+ this.AddPostAction this.CreateStartShortcut
+ }
+
+ AddSoftwareReg() {
+ this.AddRegValues(this.SoftwareKey, [
+ {ValueName: 'InstallDir', Value: this.InstallDir},
+ {ValueName: 'InstallCommand', Value: this.CmdStr('UX\install.ahk', '/install "%1"')},
+ {ValueName: 'Version', Value: this.Version},
+ ])
+ }
+
+ AddUninstallReg() {
+ this.AddRegValues(this.UninstallKey, [
+ {ValueName: 'DisplayName', Value: this.ProductName (this.RootKey = 'HKCU' ? " (user)" : "")},
+ {ValueName: 'UninstallString', Value: this.UninstallCmd},
+ {ValueName: 'QuietUninstallString', Value: this.QUninstallCmd},
+ {ValueName: 'NoModify', Value: 1},
+ {ValueName: 'DisplayIcon', Value: this.Interpreter},
+ {ValueName: 'DisplayVersion', Value: this.Version},
+ {ValueName: 'URLInfoAbout', Value: this.ProductURL},
+ {ValueName: 'Publisher', Value: this.Publisher},
+ {ValueName: 'InstallLocation', Value: this.InstallDir},
+ ])
+
+ }
+
+ AddFileTypeReg() {
+ this.AddRegValues(this.ClassesKey, [
+ {Key: '.ahk', Value: this.ScriptProgId},
+ {Key: '.ahk\ShellNew', ValueName: 'Command', Value: this.CmdStr('UX\ui-newscript.ahk', '"%1"')},
+ {Key: '.ahk\ShellNew', ValueName: 'FileName'},
+ {Key: '.ahk\PersistentHandler', Value: '{5e941d80-bf96-11cd-b579-08002b30bfeb}'}
+ ])
+ this.AddRegValues(this.FileTypeKey, [
+ {Value: "AutoHotkey Script"},
+ {ValueName: 'AppUserModelID', Value: this.AppUserModelID}, ; Testing produced inconsistent results, but it seems sometimes this must be here, sometimes under the verb.
+ {Key: 'DefaultIcon', Value: this.Interpreter ",1"},
+ {Key: 'Shell', Value: 'Open runas UIAccess Edit'}, ; Including 'runas' in lower-case fixes the shield icon not appearing on Windows 11.
+ {Key: 'Shell\Open', ValueName: 'FriendlyAppName', Value: 'AutoHotkey Launcher'},
+ ])
+ this.AddRunVerbs()
+ this.AddEditVerbIfUnset()
+ this.AddPostAction this.NotifyAssocChanged
+ }
+
+ AddRunVerbs() {
+ aumid := {ValueName: 'AppUserModelID', Value: this.AppUserModelID}
+ this.AddVerb('Open', 'UX\launcher.ahk', '"%1" %*', "Run script",
+ aumid
+ )
+ this.AddVerb('RunAs', 'UX\launcher.ahk', '"%1" %*',
+ aumid, {ValueName: 'HasLUAShield', Value: ""}
+ )
+ if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) {
+ this.AddVerb('UIAccess', 'UX\launcher.ahk', '/runwith UIA "%1" %*',
+ "Run with UI access", aumid)
+ }
+ ; Add *Launch as a hidden verb, accessible via Run('*Launch ' file).
+ this.AddVerb('Launch', 'UX\launcher.ahk', '/Launch "%1" %*', "Launch",
+ aumid, {ValueName: 'ProgrammaticAccessOnly', Value: ""}
+ )
+ }
+
+ AddEditVerbIfUnset() {
+ static v1_edit_cmd := 'notepad.exe %1'
+ ; Add edit verb only if it is undefined or has its default v1 value.
+ if RegRead(this.FileTypeKey '\Shell\Edit\Command',, v1_edit_cmd) = v1_edit_cmd
+ this.AddVerb('Edit', 'UX\ui-editor.ahk', '"%1"', "Edit script")
+ }
+
+ ;}
+
+ ;{ Utility functions
+
+ RelativePath(p) => (
+ i := this.InstallDir '\',
+ SubStr(p, 1, StrLen(i)) = i ? SubStr(p, StrLen(i) + 1) : p
+ )
+
+ CmdStr(script, args:='')
+ => RTrim(Format((InStr(script, '.ahk') ? '"{1}" ' : '') '"{2}\{3}" {4}'
+ , this.Interpreter, this.InstallDir, script, args))
+
+ AddRegValues(key, values) {
+ for v in values {
+ i := {}
+ i.Key := key (v.HasProp('Key') ? '\' v.Key : '')
+ i.ValueName := v.HasProp('ValueName') ? v.ValueName : ''
+ (v is Primitive) ? i.Value := v :
+ (v.HasProp('Value')) ? i.Value := v.Value : 0
+ this.RegItems.Push(i)
+ }
+ }
+
+ AddVerb(name, script, args, values*) {
+ this.AddRegValues(this.FileTypeKey '\Shell\' name, [
+ {Key: 'Command', Value: this.CmdStr(script, args)},
+ values*
+ ])
+ }
+
+ AddFileCopy(sourcePath, destPath) {
+ this.FileItems.Push {Source: sourcePath, Dest: destPath}
+ }
+
+ AddFiles(sourceDir, destSubDir, patterns*) {
+ destSubDir := (destSubDir != '.' ? destSubDir '\' : '')
+ for p in patterns {
+ Loop Files sourceDir '\' p {
+ this.AddFileCopy A_LoopFileFullPath, destSubDir . A_LoopFileName
+ }
+ }
+ }
+
+ AddPreCheck(f) => this.PreCheck.Push(f)
+ AddPreAction(f) => this.PreAction.Push(f)
+ AddPostAction(f) => this.PostAction.Push(f)
+
+ ReadHashes() {
+ ; For maintainability, don't assume the caller has set the working dir.
+ wd := A_WorkingDir, A_WorkingDir := this.InstallDir
+ hashes := ReadHashes(this.HashesPath, item => FileExist(item.Path))
+ A_WorkingDir := wd
+ return hashes
+ }
+
+ AddFileHash(f, v) {
+ this.Hashes[f] := {Path: f, Hash: HashFile(f), Version: v}
+ }
+
+ WriteHashes() {
+ s := "Hash,Version,Path,Description`r`n"
+ for ,item in this.Hashes {
+ if !item.HasProp('Description') {
+ try ; Cache the file description for the launcher
+ exe := GetExeInfo(item.Path)
+ catch
+ item.Description := ""
+ else {
+ item.Description := exe.Description
+ if InStr(item.Description, 'AutoHotkey')
+ item.Version := exe.Version ; Ensure accuracy for the launcher
+ }
+ }
+ s .= Format('{},{},"{}","{}"`r`n', item.Hash, item.Version, item.Path, item.Description)
+ }
+ FileOpen(this.HashesPath, 'w').Write(s)
+ }
+
+ GetComponents(versionFilter?) {
+ callerwd := A_WorkingDir
+ SetWorkingDir this.InstallDir
+ versions := Map()
+ maxes := Map()
+ for , fh in this.Hashes {
+ if fh.Path ~= 'i)^UX\\|^[A-Z]:|^\\\\|^(WindowSpy\.ahk|license\.txt)$'
+ . '|^Compiler\\(?!.*\.bin$)'
+ continue
+ try fh.Version := GetExeInfo(fh.Path = 'AutoHotkey.chm' ? 'AutoHotkeyU32.exe' : fh.Path).Version ; Auto-fix inaccurate versions in Hashes
+ if !files := versions.Get(fh.Version, 0) {
+ if IsSet(versionFilter) && !versionFilter(fh.Version)
+ continue
+ versions[fh.Version] := files := []
+ v := RegExReplace(fh.Version, '^\d+\.\d+\b\K.*')
+ if VerCompare(prevMax := maxes.Get(v, ''), fh.Version) < 0 {
+ maxes[v] := fh.Version
+ if prevMax != ''
+ versions[prevMax].superseded := true
+ }
+ else
+ files.superseded := true
+ }
+ files.InsertAt(1, fh)
+ }
+ SetWorkingDir callerwd
+ return versions
+ }
+
+ NotifyAssocChanged() {
+ DllCall("shell32\SHChangeNotify", "uint", 0x08000000 ; SHCNE_ASSOCCHANGED
+ , "uint", 0, "ptr", 0, "ptr", 0)
+ }
+
+ GetConfirmation(message, icon:='!') {
+ if !this.Silent && MsgBox(message, this.DialogTitle, 'Icon' icon ' OkCancel') = 'Cancel'
+ ExitApp 1
+ }
+
+ WarnBox(message) {
+ if !this.Silent
+ MsgBox message, this.DialogTitle, "Icon!"
+ }
+
+ InfoBox(message) {
+ if !this.Silent
+ MsgBox message, this.DialogTitle, "Iconi"
+ }
+
+ FatalError(message) {
+ if !this.Silent
+ MsgBox message, this.DialogTitle, 'Iconx'
+ ExitApp 1
+ }
+
+ GetTargetUX() {
+ try {
+ ; For registered installations, InstallCommand allows for future changes.
+ return {
+ Version: RegRead(this.SoftwareKey, 'Version'),
+ InstallCommand: RegRead(this.SoftwareKey, 'InstallCommand')
+ }
+ }
+ try {
+ ; Target installation not in registry, or has no InstallCommand (e.g. too old).
+ ; Allow non-registry installations that follow protocol as commented below.
+ ux := {}
+ ; Version information must be provided by the file at this.HashesPath:
+ ux.Version := this.Hashes['UX\install.ahk'].Version
+ ; Interpreter must be located at the path calculated below:
+ interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe'
+ if FileExist(interpreter) {
+ ; Additional interpreters must be installable with this command line:
+ ux.InstallCommand := Format('"{1}" "{2}\UX\install.ahk" /install "%1"'
+ , interpreter, this.InstallDir)
+ return ux
+ }
+ }
+ ; Otherwise, UX script or appropriate interpreter not found.
+ }
+
+ ; Delete a symbolic link, or do nothing if path does not refer to a symbolic link.
+ DeleteLink(path) {
+ switch this.GetLinkAttrib(path) {
+ case 'D': DirDelete path
+ case 'F': FileDelete path
+ }
+ }
+
+ GetLinkAttrib(path) {
+ attrib := DllCall('GetFileAttributes', 'str', path)
+ ; FILE_ATTRIBUTE_REPARSE_POINT = 0x400
+ ; FILE_ATTRIBUTE_DIRECTORY = 0x10
+ return (attrib != -1 && (attrib & 0x400)) ? ((attrib & 0x10) ? 'D' : 'F') : ''
+ }
+
+ UpdateV2Link() {
+ ; Create a symlink for AutoHotkey.exe to simplify use by tools.
+ DllCall('CreateSymbolicLink', 'str', this.InstallDir '\v2\AutoHotkey.exe'
+ , 'str', 'AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe', 'uint', 0)
+ }
+
+ CreateWindowSpyRedirect() {
+ ; Permit overwrite only when upgrading a legacy v1 installation,
+ ; or if it is known to have been created by us.
+ if !FileExist('WindowSpy.ahk') || this.HasOwnProp('SoftwareKeyV1')
+ || this.Hashes.Get('WindowSpy.ahk', {Hash: ''}).Hash = HashFile('WindowSpy.ahk') {
+ FileOpen('WindowSpy.ahk', 'w').Write('
+ (
+ #include UX
+ #include inc\bounce-v1.ahk
+ /**/
+ #requires AutoHotkey v2.0
+ try Run('"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"'), ExitApp()
+ #include WindowSpy.ahk
+ )')
+ this.AddFileHash('WindowSpy.ahk', this.Version)
+ }
+ }
+
+ CreateStartShortcut() {
+ if this.Hashes.Has(this.StartFolder '\AutoHotkey.lnk')
+ try FileDelete this.StartFolder '\AutoHotkey.lnk'
+ CreateAppShortcut(
+ lnk := this.StartFolder '\AutoHotkey Dash.lnk', {
+ target: this.Interpreter,
+ args: Format('"{1}\UX\ui-dash.ahk"', this.InstallDir),
+ desc: "AutoHotkey Dash",
+ aumid: this.AppUserModelID,
+ uninst: this.UninstallCmd
+ }
+ )
+ this.AddFileHash lnk, this.Version
+
+ CreateAppShortcut(
+ lnk := this.StartFolder '\AutoHotkey Window Spy.lnk', {
+ target: this.Interpreter,
+ args: Format('"{1}\UX\WindowSpy.ahk"', this.InstallDir),
+ desc: "AutoHotkey Window Spy",
+ aumid: 'AutoHotkey.WindowSpy',
+ icon: Format('{1}\UX\inc\spy.ico', this.InstallDir), iconIndex: 0,
+ uninst: this.UninstallCmd
+ }
+ )
+ this.AddFileHash lnk, this.Version
+ }
+
+ CreateCompilerShortcut() {
+ CreateAppShortcut(
+ lnk := this.StartFolder '\Ahk2Exe.lnk', {
+ target: this.InstallDir '\Compiler\Ahk2Exe.exe',
+ desc: "Convert .ahk to .exe",
+ aumid: 'AutoHotkey.Ahk2Exe',
+ uninst: this.UninstallCmd
+ }
+ )
+ this.AddFileHash lnk, this.Version
+ }
+
+ MakeUIA(baseFile) {
+ SplitPath baseFile,, &baseDir,, &baseName
+ baseDir := baseDir = '.' ? '' : baseDir '\'
+ FileCopy baseFile, newPath := baseDir baseName '_UIA.exe', true
+ static abort := false ; Let "Abort" disable MakeUIA calls, but let other PostActions complete.
+ while !abort {
+ try {
+ EnableUIAccess newPath
+ break
+ }
+ catch as e {
+ try FileDelete newPath
+ if e.What != "EndUpdateResource"
+ throw
+ if this.Silent {
+ if A_Index > 4
+ break
+ Sleep 500
+ }
+ switch MsgBox("Unable to create " baseName ". Try adding an exclusion in your antivirus software. If that doesn't work, please report the issue.`n`nError: " e.Message
+ ,, "a/r/i") {
+ case "Abort": abort := true
+ case "Ignore": break
+ }
+ }
+ }
+ this.AddFileHash newPath, '' ; For uninstall
+ }
+
+ IsTrustedLocation(path) { ; http://msdn.com/library/bb756929
+ other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432")
+ return InStr(path, A_ProgramFiles "\") = 1
+ || other && InStr(path, other "\") = 1
+ }
+
+ ;}
+
+ ;{ Upgrade from v1
+
+ PrepareUpgradeV1(installedVersion) {
+ ; This needs to be done before conflict-checking
+ if FileExist('license.txt')
+ this.AddFileHash('license.txt', installedVersion)
+ }
+
+ UpgradeV1(installedVersion) {
+ try { ; Permit failure in case AutoHotkey.exe has been deleted.
+ exe := GetExeInfo('AutoHotkey.exe')
+ build := RegExReplace(exe.Description, '^AutoHotkey *')
+ }
+
+ ; Set default launcher settings
+ if IsSet(build) && ConfigRead('Launcher\v1', 'Build', '!') = '!'
+ ConfigWrite(build, 'Launcher\v1', 'Build')
+ if ConfigRead('Launcher\v1', 'UTF8', '') = ''
+ && InStr(RegRead('HKCR\' this.ScriptProgId '\Shell\Open\Command',, ''), '/cp65001 ')
+ ConfigWrite(true, 'Launcher\v1', 'UTF8')
+
+ ; Record these for Uninstall
+ add 'AutoHotkey{1}.exe', '', 'A32', 'U32', 'U64', 'A32_UIA', 'U32_UIA', 'U64_UIA'
+ add 'Compiler\{1}.bin', 'ANSI 32-bit', 'Unicode 32-bit', 'Unicode 64-bit', 'AutoHotkeySC'
+ add '{1}', 'Compiler\Ahk2Exe.exe', 'AutoHotkey.chm', A_WinDir '\ShellNew\Template.ahk'
+
+ add(fmt, patterns*) {
+ for p in patterns
+ if FileExist(f := Format(fmt, p))
+ this.AddFileHash f, installedVersion
+ }
+
+ ; Remove obsolete files
+ for item in ['Installer.ahk', 'AutoHotkey Website.url']
+ try FileDelete item
+
+ ; Remove the v1 shortcuts from the Start menu
+ name := RegRead(this.SoftwareKeyV1, 'StartMenuFolder', '')
+ if name != ''
+ try DirDelete A_ProgramsCommon '\' name, true
+
+ ; Remove the old sub-keys, which might be in the wrong reg view
+ try RegDeleteKey this.SoftwareKeyV1
+ try RegDeleteKey this.UninstallKeyV1
+ }
+
+ DisplaceFile(sourcePath, destPath, version) {
+ SplitPath destPath,, &destDir
+ if destDir != ""
+ DirCreate destDir
+ FileMove sourcePath, destPath
+ try this.Hashes.Delete(sourcePath)
+ this.AddFileHash destPath, version
+ }
+
+ DisplaceV1(v) {
+ DirCreate dir := 'v' v
+ displace(path) {
+ if FileExist(path) {
+ SplitPath path, &name
+ this.DisplaceFile path, dir '\' name, v
+ }
+ }
+ for build in ['U32', 'U64', 'A32'] {
+ displace 'AutoHotkey' build '.exe'
+ displace 'AutoHotkey' build '_UIA.exe'
+ }
+ try
+ defaultBinSize := FileGetSize(defaultBinPath := 'Compiler\AutoHotkeySC.bin')
+ for build in ['Unicode 32-bit', 'Unicode 64-bit', 'ANSI 32-bit'] {
+ try
+ if FileGetSize('Compiler\' build '.bin') = defaultBinSize
+ this.AddFileCopy this.InstallDir '\Compiler\' build '.bin', defaultBinPath
+ displace 'Compiler\' build '.bin'
+ }
+
+ displace 'AutoHotkey.chm'
+ try {
+ exe := GetExeInfo('AutoHotkey.exe')
+ if exe.Version = v
+ && RegExMatch(exe.Description, ' (A|U)\w+ (32|64)-bit$', &m) {
+ ; Too early to add to FileItems, so use PostAction:
+ this.AddPostAction this.CopyDefaultExe.Bind(, 'AutoHotkey' m.1 m.2 '.exe')
+ }
+ }
+ }
+
+ CopyDefaultExe(from) {
+ try
+ FileCopy from, 'AutoHotkey.exe', true
+ catch
+ return ; TODO: report to user?
+ this.AddFileHash 'AutoHotkey.exe', this.Version
+ }
+
+ ;}
+}
diff --git a/UX/launcher.ahk b/UX/launcher.ahk
index 59a34d2..d73adfa 100644
--- a/UX/launcher.ahk
+++ b/UX/launcher.ahk
@@ -1,363 +1,426 @@
-; This script is intended for indirect use via commands registered by install.ahk.
-; It can also be compiled as a replacement for AutoHotkey.exe, so tools which run
-; scripts by executing AutoHotkey.exe can benefit from automatic version selection.
-#requires AutoHotkey v2.0
-
-;@Ahk2Exe-SetDescription AutoHotkey Launcher
-#SingleInstance Off
-#NoTrayIcon
-
-#include inc\identify.ahk
-#include inc\launcher-common.ahk
-#include inc\ui-base.ahk
-
-if A_ScriptFullPath == A_LineFile {
- SetWorkingDir A_InitialWorkingDir
- Main
-}
-
-Main() {
- switches := []
- while A_Args.length {
- arg := A_Args.RemoveAt(1)
- if SubStr(arg,1,1) != '/' {
- ScriptPath := arg
- break
- }
- nextArgValue() {
- if !A_Args.Length {
- MsgBox "Invalid command line switches; missing value for " arg ".", "AutoHotkey Launcher", "icon!"
- ExitApp 1
- }
- return A_Args.RemoveAt(1)
- }
- switch arg, false {
- case '/RunWith': ; Launcher-specific
- A_Args.runwith := nextArgValue()
- case '/Launch': ; Launcher-specific
- A_Args.launch := true
- case '/Which':
- A_Args.which := true
- if trace.Enabled
- trace.DefineProp 'call', {call: (this, s) => OutputDebug(s)} ; Don't use stdout.
- case '/iLib', '/include':
- switches.push(arg)
- switches.push(nextArgValue())
- default:
- switches.push(arg)
- }
-
- }
- if !IsSet(ScriptPath)
- && !FileExist(ScriptPath := A_ScriptDir "\AutoHotkey.ahk")
- && !FileExist(ScriptPath := A_MyDocuments "\AutoHotkey.ahk") {
- ; TODO: something more useful?
- if FileExist(A_ScriptDir "\AutoHotkey.chm")
- Run 'hh.exe "ms-its:' A_ScriptDir '\AutoHotkey.chm::/docs/Welcome.htm"',, 'Max'
- else
- Run 'https://lexikos.github.io/v2/docs/Welcome.htm'
- ExitApp
- }
- if ScriptPath = '*'
- ExitApp 2 ; FIXME: code would need to be read in and then passed to the real AutoHotkey
- IdentifyAndLaunch ScriptPath, A_Args, switches
-}
-
-GetLaunchParameters(ScriptPath, interactive:=false) {
- code := FileRead(ScriptPath, 'UTF-8')
- if RegExMatch(code, 'im)^[ `t]*#Requires[ `t]+AutoHotkey[ `t]+(.*)', &m) {
- ; Replace "; prefer x." with "x" and remove other comments
- prefer := RegExReplace(m.1, 'i);\s*prefer([ `t]+[^;`r`n\.]+)|;.*', '$1')
- ; Extract version requirement
- if RegExMatch(prefer, '(?=)?v(\d\S+)', &m)
- v := m.1, prefer := SubStr(prefer, 1, m.Pos-1) . SubStr(prefer, m.Pos + m.Len)
- ; Insert commas as needed
- prefer := RegExReplace(prefer, '[^\s,]\K\s+(?!$)', ",")
- }
- if IsSet(v)
- i := {v: v, r: "#Requires"}
- else if ConfigRead('Launcher', 'Identify', true)
- i := IdentifyBySyntax(code)
- else
- i := {v: 0, r: "syntax-checking is disabled"}
- v := i.v || ConfigRead('Launcher', 'Fallback', "")
- trace "![Launcher] version " (v || "unknown") " -- " i.r
- if !v
- exe := interactive ? PromptMajorVersion(ScriptPath) : ""
- else
- if !exe := GetRequiredOrPreferredExe(v, prefer ?? '')
- if interactive
- exe := TryToInstallVersion(v, i.v ? i.r : '', ScriptPath)
- lp := {exe: exe, id: i, v: v, switches: []}
- if exe {
- if GetMajor(exe.Version) = 1 && ConfigRead('Launcher\v1', 'UTF8', false)
- lp.switches.Push('/CP65001')
- }
- return lp
-}
-
-IdentifyAndLaunch(ScriptPath, args, switches) {
- lp := GetLaunchParameters(ScriptPath, !(whichMode := args.HasProp('which')))
- if whichMode {
- try FileAppend(lp.v "`n"
- (lp.exe ? lp.exe.Path : "") "`n"
- (lp.switches.Length ? lp.switches[1] : "") "`n", '*', 'UTF-8-RAW')
- ExitApp lp.id.v ? GetMajor(lp.id.v) : 0
- }
- if !lp.exe
- ExitApp 2
- switches.Push(lp.switches*)
- ExitApp LaunchScript(lp.exe.Path, ScriptPath, args, switches)
-}
-
-TryToInstallVersion(v, r, ScriptPath) {
- SplitPath ScriptPath, &name
- m := ' script you are trying to run requires AutoHotkey v' v ', which is not installed.`n`nScript:`t' name
- m := !(r && r != '#Requires') ? 'The' m : 'It looks like the' m '`nRule:`t' r
- if downloadable := IsNumber(v) || VerCompare(v, '1.1.24.02') >= 0 {
- ; Get current version compatible with v.
- bv := v = 1 ? '1.1' : IsInteger(v) ? v '.0' : RegExReplace(v, '^\d+(?:\.\d+)?\b\K.*')
- req := ComObject('Msxml2.XMLHTTP')
- req.open('GET', Format('https://www.autohotkey.com/download/{}/version.txt', bv), false)
- req.send()
- if req.status = 200 && RegExMatch(cv := req.responseText, '^\d+\.[\w\+\-\.]+$') && VerCompare(cv, v) >= 0
- m .= '`n`nWe can try to download and install AutoHotkey v' cv ' for you, while retaining the ability to use the versions already installed.`n`nDownload and install AutoHotkey v' cv '?'
- else
- downloadable := false
- }
- if !A_IsAdmin && RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', "") = ROOT_DIR
- SetTimer(() => (
- WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) &&
- SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C
- ), -25)
- if MsgBox(m, 'AutoHotkey', downloadable ? 'Iconi y/n' : 'Icon!') != 'yes'
- return false
- if RunWait(Format('"{}" /script "{}\install-version.ahk" "{}"', A_AhkPath, A_ScriptDir, cv)) != 0
- return false
- return exe := GetRequiredOrPreferredExe(v)
-}
-
-GetRequiredOrPreferredExe(v, prefer:='') {
- section := 'Launcher\v' GetMajor(v)
- userv := ConfigRead(section, 'Version', "")
- prefer := (A_Args.HasProp('runwith') ? A_Args.runwith ',' : '') . prefer
- prefer .= ',' (ConfigRead(section, 'Build', (A_Is64bitOS ? "64," : "") "!ANSI"))
- prefer .= ',' (ConfigRead(section, 'UIA', false) ? 'UIA' : '!UIA')
- if vexact := (userv != "" && (IsInteger(v) || VerCompare(v, userv) < 0))
- v := userv
- return LocateExeByVersion(v, vexact, Trim(prefer, ','))
-}
-
-LocateExeByVersion(v, vexact:=false, prefer:='!UIA, 64, !ANSI') {
- trace '![Launcher] Attempting to locate v' v '; prefer ' prefer
- majorVer := GetMajor(v), best := '', bestscore := 0
- IsInteger(v) && v .= '-' ; Allow pre-release versions.
- for ,f in GetUsableAutoHotkeyExes() {
- try {
- relation := VerCompare(f.Version, v)
- if vexact ? relation != 0 : (relation < 0 || GetMajor(f.Version) > majorVer) {
- ; trace '![Launcher] Skipping v' f.Version ': ' f.Path
- continue
- }
- fscore := 0
- Loop Parse prefer, ",", " " {
- if A_LoopField = ""
- continue
- fscore <<= 1
- if !(A_LoopField ~= '^[<>=]' ? VerCompare(f.Version, A_LoopField)
- : matchPref(f.Description, A_LoopField))
- continue
- fscore |= 1
- }
- ; trace '![Launcher] ' fscore ' v' f.Version ' ' f.Path
- ; Prefer later version if all else matches. If version also matches, prefer later
- ; files enumeration order is generally AutoHotkey.exe, ..A32.exe, ..U32.exe, ..U64.exe.
- if bestscore < fscore
- || bestscore = fscore && (vexact || VerCompare(f.Version, best.Version) > 0)
- bestscore := fscore, best := f
- }
- catch as e {
- trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message
- trace "-[Launcher] " e.file ":" e.line
- }
- }
- return best
- matchPref(desc, pref) => SubStr(pref,1,1) != "!" ? InStr(desc, pref) : !InStr(desc, SubStr(pref,2))
-}
-
-PromptMajorVersion(ScriptPath:="") {
- majors := LocateMajorVersions()
- switch majors.Count {
- case 1:
- for , f in majors
- return f
- case 0:
- trace '-[Launcher] Failed to locate any interpreters; fallback to launcher'
- return {Path: A_AhkPath, Version: A_AhkVersion}
- }
- files := []
- for , f in majors
- files.Push(f)
- prompt := VersionSelectGui(ScriptPath, files)
- prompt.Show
- WinWaitClose prompt
- if !prompt.HasProp('selection') {
- trace '[Launcher] No version selected from menu'
- ExitApp
- }
- return prompt.selection
-}
-
-LocateMajorVersions(filePattern:='', fileLoopOpt:='R') {
- majors := Map()
- Loop 2
- if f := GetRequiredOrPreferredExe(A_Index)
- majors[A_Index] := f
- return majors
-}
-
-class Handle {
- __new(ptr:=0) => this.ptr := ptr
- __delete() => DllCall("CloseHandle", "ptr", this)
-}
-
-LaunchScript(exe, ahk, args:="", switches:="", encoding:="UTF-8") {
- ; Pass our own stdin/stdout handles (if any) to the child process.
- hStdIn := DllCall("GetStdHandle", "uint", -10, "ptr")
- hStdOut := DllCall("GetStdHandle", "uint", -11, "ptr")
- hStdErr := DllCall("GetStdHandle", "uint", -12, "ptr")
-
- ; Build command line to execute.
- makeArgs(args) {
- r := ''
- for arg in args is object ? args : [args]
- r .= ' ' (arg ~= '\s' ? '"' arg '"' : arg)
- return r
- }
- switches := makeArgs(switches)
- cmd := Format('"{1}"{2} "{3}"{4}', exe, switches, ahk, makeArgs(args))
- trace '>[Launcher] ' cmd
-
- ; For RunWait, stdout redirection, /validate, etc. to have the best chance of working,
- ; let the launcher exit early only if it can detect that it was executed from Explorer
- ; or the parent process appears to have exited already (or if the caller passed /launch).
- waitClose := !args.HasProp('launch')
- hParent := 0
- if IsSet(ProcessGetParent) {
- try {
- ; (PROCESS_QUERY_LIMITED_INFORMATION := 0x1000) | (SYNCHRONIZE := 0x100000)
- if hParent := DllCall("OpenProcess", "uint", 0x101000, "int", false
- , "uint", parentPid := ProcessGetParent(), "ptr")
- hParent := Handle(hParent)
- if !hParent || (parentName := ProcessGetName(parentPid)) = "explorer.exe"
- waitClose := false
- }
- catch as e
- trace '![Launcher] Failure checking parent process: ' e.Message
- }
-
- try {
- proc := RunWithHandles(cmd, {in: hStdIn, out: hStdOut, err: hStdErr})
- }
- catch OSError as e {
- if e.Number != 740 ; ERROR_ELEVATION_REQUIRED
- throw
- trace '![Launcher] elevation required; handles will not be redirected'
- cmd := RegExReplace(cmd, ' /ErrorStdOut(?:=\S*)?')
- Run cmd
- ExitApp
- }
-
- ; When the /launch switch is used, return the process ID as the launcher's exit code.
- if args.HasProp('launch')
- return proc.pid
-
- if waitClose {
- ; Wait for either the child process or our parent process (if determined) to terminate.
- NumPut 'ptr', proc.hProcess.ptr, 'ptr', hParent ? hParent.ptr : 0, waitHandles := Buffer(A_PtrSize*2)
- loop {
- Sleep -1
- waitResult := DllCall("MsgWaitForMultipleObjects", "uint", 1 + (hParent != 0), "ptr", waitHandles, "int", 0, "uint", -1, "uint", 0x04FF)
- } until waitResult = 0 || waitResult = 1
- }
-
- DllCall("GetExitCodeProcess", "ptr", proc.hProcess, "uint*", &exitCode:=0)
- if trace.Enabled {
- ; We have to return something numeric for ExitApp, so currently exitCode is left as 259
- ; if the process is still running.
- if exitCode = 259 && DllCall("WaitForSingleObject", "ptr", proc.hProcess, "uint", 0) = 258 { ; STILL_ACTIVE = 259, WAIT_TIMEOUT = 258
- if !(hParent ?? 1) || (waitResult ?? -1) = 1
- trace '>[Launcher] Process launched; now exiting because parent process has terminated.'
- else if (parentName ?? "") = "explorer.exe"
- trace '>[Launcher] Process launched; now exiting because parent is explorer.exe.'
- else
- trace '>[Launcher] Process launched; launcher exiting early.'
- }
- else
- trace '>[Launcher] Exit code: ' exitCode
- }
-
- return exitCode
-}
-
-RunWithHandles(cmd, handles, workingDir:="") {
- static STARTUPINFO_SIZE := A_PtrSize=8 ? 104 : 68
- , STARTUPINFO_dwFlags := A_PtrSize=8 ? 60 : 44
- , STARTUPINFO_hStdInput := A_PtrSize=8 ? 80 : 56
- , STARTF_USESTDHANDLES := 0x100
- , PROCESS_INFORMATION_SIZE := A_PtrSize=8 ? 24 : 16
- HandleValue(p) => HasProp(handles, p) && (IsInteger(h := handles.%p%) ? h : h.Ptr)
- si := Buffer(STARTUPINFO_SIZE, 0)
- NumPut("uint", STARTUPINFO_SIZE, si)
- NumPut("uint", STARTF_USESTDHANDLES, si, STARTUPINFO_dwFlags)
- NumPut("ptr", HandleValue("in")
- , "ptr", HandleValue("out")
- , "ptr", HandleValue("err")
- , si, STARTUPINFO_hStdInput)
- pi := Buffer(PROCESS_INFORMATION_SIZE)
- if !DllCall("CreateProcess", "ptr", 0, "str", cmd, "ptr", 0, "int", 0, "int", true
- , "int", 0x08000000, "int", 0, "ptr", workingDir ? StrPtr(workingDir) : 0
- , "ptr", si, "ptr", pi)
- throw OSError(, -1, cmd)
- return { hProcess: Handle(NumGet(pi, 0, "ptr"))
- , hThread: Handle(NumGet(pi, A_PtrSize, "ptr"))
- , pid: NumGet(pi, A_PtrSize*2, "uint") }
-}
-
-class VersionSelectGui extends AutoHotkeyUxGui {
- __new(script, files) {
- SplitPath script, &scriptName
- super.__new("Run " scriptName " with", '-MinimizeBox')
- DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT
- , 'int64*', 2 | (2<<32), 'int', 8) ; WTNCA_NODRAWICON=2
- lv := this.AddListMenu('vList LV0x40 w200', ["Version"])
- lv.OnEvent('Focus', 'Focused')
- lv.OnEvent('LoseFocus', 'Focused')
- lv.OnEvent('Click', 'Confirm')
- il := IL_Create(,, false)
- lv.SetImageList(il, 0)
- for f in this.files := files {
- lv.Add('Icon' IL_Add(il, f.Path), f.Version " " StrReplace(f.Description, "AutoHotkey "))
- }
- lv.AutoSize(8)
- lv.GetPos(&x, &y, &w, &h)
- this.Show('AutoSize Hide')
- this.AddButton('Default Hidden', "Confirm").OnEvent('Click', 'Confirm')
- }
-
- Confirm(*) {
- this.selection := this.files[this['List'].GetNext()]
- this.Hide()
- }
-
- Focused(ctrl, *) {
- OnMessage(0x101, keyup, ctrl.Focused)
- static keyup(wParam, lParam, nmsg, hwnd) {
- local this := GuiFromHwnd(hwnd, true)
- if IsDigit(GetKeyName(Format("vk{:x}", wParam))) && this['List'].GetNext() {
- this.Confirm()
- return true
- }
- }
- }
-}
+; This script is intended for indirect use via commands registered by install.ahk.
+; It can also be compiled as a replacement for AutoHotkey.exe, so tools which run
+; scripts by executing AutoHotkey.exe can benefit from automatic version selection.
+#requires AutoHotkey v2.0
+
+;@Ahk2Exe-SetDescription AutoHotkey Launcher
+#SingleInstance Off
+#NoTrayIcon
+
+#include inc\identify.ahk
+#include inc\launcher-common.ahk
+#include inc\ui-base.ahk
+
+if A_ScriptFullPath == A_LineFile || A_LineFile == '*#1' {
+ SetWorkingDir A_InitialWorkingDir
+ Main
+}
+
+Main() {
+ switches := []
+ while A_Args.length {
+ arg := A_Args.RemoveAt(1)
+ if SubStr(arg,1,1) != '/' {
+ ScriptPath := arg
+ break
+ }
+ nextArgValue() {
+ if !A_Args.Length {
+ MsgBox "Invalid command line switches; missing value for " arg ".", "AutoHotkey Launcher", "icon!"
+ ExitApp 1
+ }
+ return A_Args.RemoveAt(1)
+ }
+ switch arg, false {
+ case '/RunWith': ; Launcher-specific
+ A_Args.runwith := nextArgValue()
+ case '/Launch': ; Launcher-specific
+ A_Args.launch := true
+ case '/Which':
+ A_Args.which := true
+ if trace.Enabled
+ trace.DefineProp 'call', {call: (this, s) => OutputDebug(s "`n")} ; Don't use stdout.
+ case '/iLib', '/include':
+ switches.push(arg)
+ switches.push(nextArgValue())
+ default:
+ switches.push(arg)
+ }
+
+ }
+ if !IsSet(ScriptPath)
+ && !FileExist(ScriptPath := A_ScriptDir "\AutoHotkey.ahk")
+ && !FileExist(ScriptPath := A_MyDocuments "\AutoHotkey.ahk")
+ && !FileExist(ScriptPath := A_ScriptDir "\ui-dash.ahk") {
+ ExitApp
+ }
+ if ScriptPath = '*'
+ ExitApp 2 ; FIXME: code would need to be read in and then passed to the real AutoHotkey
+ IdentifyAndLaunch ScriptPath, A_Args, switches
+}
+
+GetLaunchParameters(ScriptPath, interactive:=false) {
+ code := FileRead(ScriptPath, 'UTF-8')
+ require := prefer := rule := exe := ""
+ if RegExMatch(code, 'im)^[ `t]*#Requires[ `t]+AutoHotkey[ `t]+([^;`r`n]*)(?:[ `t]*;[ `t]*prefer[ `t]+([^;`r`n\.]+))?', &m) {
+ trace "![Launcher] " m.0
+ require := RegExReplace(m.1, '[^\s,]\K\s+(?!$)', ",")
+ prefer := RegExReplace(m.2, '[^\s,]\K\s+(?!$)', ",")
+ rule := "#Requires"
+ }
+ else if ConfigRead('Launcher', 'Identify', true) {
+ i := IdentifyBySyntax(code)
+ trace "![Launcher] syntax says version " (i.v || "unknown") " -- " i.r
+ if i.v
+ require := String(i.v)
+ rule := i.r
+ }
+ else {
+ trace "![Launcher] version unknown - syntax-checking is disabled"
+ }
+ if !(hasv := RegExMatch(require, '(^|,)\s*(?!(32|64)-bit)[<>=]*v?\d')) {
+ ; No version specified or detected
+ if hasv := v := ConfigRead('Launcher', 'Fallback', "") {
+ require .= (require=''?'':',') v
+ trace "![Launcher] using fallback version " v
+ }
+ }
+ v := GetVersionToInstall(require) ; Currently used for multiple purposes
+ if !hasv
+ exe := interactive ? PromptMajorVersion(ScriptPath, require, prefer) : ""
+ else
+ if !exe := GetRequiredOrPreferredExe(require, prefer)
+ if interactive {
+ if v && !LocateExeByVersion(v, '')
+ exe := TryToInstallVersion(v, rule, ScriptPath, require, prefer)
+ else
+ RequirementNotMetMsgBox require, ScriptPath
+ }
+ lp := {exe: exe, id: exe ? GetMajor(exe.Version) : GetLikelyMajor(require), v: v, switches: []}
+ if exe {
+ if GetMajor(exe.Version) = 1 && ConfigRead('Launcher\v1', 'UTF8', false)
+ lp.switches.Push('/CP65001')
+ }
+ return lp
+}
+
+ParseRequiresVersion(s) {
+ return RegExMatch(s, 'i)^(?!(?:32|64)-bit$)(?[<>=]*)v?(?(?\d+)\b\S*)', &m) ? m : 0
+}
+
+GetLikelyMajor(r) {
+ if IsNumber(r)
+ return Integer(r)
+ ; Usually there would be either a version number with no operator
+ ; or a range where the lower and upper bound have the same major.
+ Loop Parse r, ",", " `t"
+ if (m := ParseRequiresVersion(A_LoopField)) && m.op != '<'
+ return m.major
+ return ''
+}
+
+GetVersionToInstall(r) {
+ ; TryToInstallVersion currently only supports the latest bug-fix release,
+ ; so don't try to install if there's a complex version requirement.
+ if IsNumber(r)
+ return r
+ v := ""
+ Loop Parse r, ",", " `t" {
+ if (m := ParseRequiresVersion(A_LoopField)) {
+ if m.op
+ return ''
+ v := m.version
+ }
+ }
+ return v
+}
+
+IdentifyAndLaunch(ScriptPath, args, switches) {
+ lp := GetLaunchParameters(ScriptPath, !(whichMode := args.HasProp('which')))
+ if whichMode {
+ try FileAppend(lp.v "`n"
+ (lp.exe ? lp.exe.Path : "") "`n"
+ (lp.switches.Length ? lp.switches[1] : "") "`n", '*', 'UTF-8-RAW')
+ ExitApp lp.id
+ }
+ if !lp.exe
+ ExitApp 2
+ switches.Push(lp.switches*)
+ ExitApp LaunchScript(lp.exe.Path, ScriptPath, args, switches)
+}
+
+TryToInstallVersion(v, r, ScriptPath, require, prefer) {
+ ; This is currently designed only for downloading the latest bug-fix of a given minor version.
+ SplitPath ScriptPath, &name
+ m := ' script you are trying to run requires AutoHotkey v' v ', which is not installed.`n`nScript:`t' name
+ m := !(r && r != '#Requires') ? 'The' m : 'It looks like the' m '`nRule:`t' r
+ if downloadable := IsNumber(v) || VerCompare(v, '1.1.24.02') >= 0 {
+ ; Get current version compatible with v.
+ bv := v = 1 ? '1.1' : IsInteger(v) ? v '.0' : RegExReplace(v, '^\d+(?:\.\d+)?\b\K.*')
+ req := ComObject('Msxml2.XMLHTTP')
+ req.open('GET', Format('https://www.autohotkey.com/download/{}/version.txt', bv), false)
+ try req.send()
+ if req.status = 200 && RegExMatch(cv := req.responseText, '^\d+\.[\w\+\-\.]+$') && VerCompare(cv, v) >= 0
+ m .= '`n`nWe can try to download and install AutoHotkey v' cv ' for you, while retaining the ability to use the versions already installed.`n`nDownload and install AutoHotkey v' cv '?'
+ else
+ downloadable := false
+ }
+ if downloadable && !A_IsAdmin && RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', "") = ROOT_DIR
+ SetTimer(() => (
+ WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) &&
+ SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C
+ ), -25)
+ if MsgBox(m, 'AutoHotkey', downloadable ? 'Iconi y/n' : 'Icon!') != 'yes'
+ return false
+ if RunWait(Format('"{}" /script "{}\install-version.ahk" "{}"', A_AhkPath, A_ScriptDir, cv)) != 0
+ return false
+ return exe := GetRequiredOrPreferredExe(require, prefer)
+}
+
+RequirementNotMetMsgBox(require, ScriptPath) {
+ SplitPath ScriptPath, &name
+ MsgBox 'Unable to locate the appropriate interpreter to run this script.`n`nScript:`t' name '`nRequires: ' StrReplace(require, ',', ' '), 'AutoHotkey', 'Icon!'
+}
+
+GetRequiredOrPreferredExe(require, prefer:='') {
+ if A_Args.HasProp('runwith')
+ prefer := A_Args.runwith ',' prefer
+ return LocateExeByVersion(require, Trim(prefer, ','))
+}
+
+LocateExeByVersion(require, prefer:='!UIA, 64, !ANSI') {
+ trace '![Launcher] locating exe: require ' require (prefer='' ? '' : '; prefer ' prefer)
+ best := '', bestscore := 0, cPrefMap := Map()
+ for ,f in GetUsableAutoHotkeyExes() {
+ try {
+ ; Check requirements first
+ fMajor := GetMajor(f.Version)
+ Loop Parse require, ",", " " {
+ if A_LoopField = ""
+ continue
+ if m := ParseRequiresVersion(A_LoopField) {
+ if !VerCompare(f.Version, (m.op ? '' : '>=') A_LoopField) {
+ ; trace '![Launcher] ' f.Version ' ' (m.op ? '' : '>=') A_LoopField ' = false'
+ continue 2
+ }
+ if !m.op && fMajor > m.major { ; No operator implies it must be same major version
+ ; trace '![Launcher] major ' f.Version ' > ' m.version
+ continue 2
+ }
+ }
+ else if !matchPref(f.Description, A_LoopField) {
+ ; trace '![Launcher] no match for "' A_LoopField '" in ' f.Description
+ continue 2
+ }
+ }
+ ; Determine additional user preferences based on major version
+ if !(cPref := cPrefMap.Get(fMajor, 0)) {
+ section := 'Launcher\v' fMajor
+ cPref := ConfigRead(section, 'Version', "")
+ cPref := {
+ V: cPref ? '=' cPref ',' : '<0,',
+ D: ',' (ConfigRead(section, 'Build', (A_Is64bitOS ? "64," : "") "!ANSI"))
+ . ',' (ConfigRead(section, 'UIA', false) ? 'UIA' : '!UIA')
+ }
+ cPrefMap.Set(fMajor, cPref)
+ }
+ ; Calculate preference score
+ fscore := 0
+ Loop Parse cPref.V prefer cPref.D, ",", " " {
+ if A_LoopField = ""
+ continue
+ fscore <<= 1
+ if !(A_LoopField ~= '^[<>=]' ? VerCompare(f.Version, A_LoopField)
+ : matchPref(f.Description, A_LoopField))
+ continue
+ fscore |= 1
+ }
+ ; trace '![Launcher] ' fscore ' v' f.Version ' ' f.Path
+ ; Prefer later version if all else matches. If version also matches, prefer later files,
+ ; as enumeration order is generally AutoHotkey.exe, ..A32.exe, ..U32.exe, ..U64.exe.
+ if bestscore < fscore
+ || bestscore = fscore && VerCompare(f.Version, best.Version) >= 0
+ bestscore := fscore, best := f
+ }
+ catch as e {
+ trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message
+ trace "-[Launcher] " e.file ":" e.line
+ }
+ }
+ return best
+ matchPref(desc, pref) => SubStr(pref,1,1) != "!" ? InStr(desc, pref) : !InStr(desc, SubStr(pref,2))
+}
+
+PromptMajorVersion(ScriptPath, require:='', prefer:='') {
+ majors := Map()
+ Loop 2
+ if f := GetRequiredOrPreferredExe(A_Index ',' require, prefer)
+ majors[A_Index] := f
+ switch majors.Count {
+ case 1:
+ for , f in majors
+ return f
+ case 0:
+ trace '-[Launcher] Failed to locate any interpreters; fallback to launcher'
+ return {Path: A_AhkPath, Version: A_AhkVersion}
+ }
+ files := []
+ for , f in majors
+ files.Push(f)
+ prompt := VersionSelectGui(ScriptPath, files)
+ prompt.Show
+ WinWaitClose prompt
+ if !prompt.HasProp('selection') {
+ trace '[Launcher] No version selected from menu'
+ ExitApp
+ }
+ return prompt.selection
+}
+
+class Handle {
+ __new(ptr:=0) => this.ptr := ptr
+ __delete() => DllCall("CloseHandle", "ptr", this)
+}
+
+LaunchScript(exe, ahk, args:="", switches:="", encoding:="UTF-8") {
+ ; Pass our own stdin/stdout handles (if any) to the child process.
+ hStdIn := DllCall("GetStdHandle", "uint", -10, "ptr")
+ hStdOut := DllCall("GetStdHandle", "uint", -11, "ptr")
+ hStdErr := DllCall("GetStdHandle", "uint", -12, "ptr")
+
+ ; Build command line to execute.
+ makeArgs(args) {
+ r := ''
+ for arg in args is object ? args : [args]
+ r .= ' ' (arg ~= '\s' ? '"' arg '"' : arg)
+ return r
+ }
+ switches := makeArgs(switches)
+ cmd := Format('"{1}"{2} "{3}"{4}', exe, switches, ahk, makeArgs(args))
+ trace '>[Launcher] ' cmd
+
+ ; For RunWait, stdout redirection, /validate, etc. to have the best chance of working,
+ ; let the launcher exit early only if it can detect that it was executed from Explorer
+ ; or the parent process appears to have exited already (or if the caller passed /launch).
+ waitClose := !args.HasProp('launch')
+ hParent := 0
+ if IsSet(ProcessGetParent) {
+ try {
+ ; (PROCESS_QUERY_LIMITED_INFORMATION := 0x1000) | (SYNCHRONIZE := 0x100000)
+ if hParent := DllCall("OpenProcess", "uint", 0x101000, "int", false
+ , "uint", parentPid := ProcessGetParent(), "ptr")
+ hParent := Handle(hParent)
+ if !hParent || (parentName := ProcessGetName(parentPid)) = "explorer.exe"
+ waitClose := false
+ }
+ catch as e
+ trace '![Launcher] Failure checking parent process: ' e.Message
+ }
+
+ try {
+ proc := RunWithHandles(cmd, {in: hStdIn, out: hStdOut, err: hStdErr})
+ }
+ catch OSError as e {
+ if e.Number != 740 ; ERROR_ELEVATION_REQUIRED
+ throw
+ trace '![Launcher] elevation required; handles will not be redirected'
+ cmd := RegExReplace(cmd, ' /ErrorStdOut(?:=\S*)?')
+ Run cmd
+ ExitApp
+ }
+
+ ; When the /launch switch is used, return the process ID as the launcher's exit code.
+ if args.HasProp('launch')
+ return proc.pid
+
+ if waitClose {
+ ; Wait for either the child process or our parent process (if determined) to terminate.
+ NumPut 'ptr', proc.hProcess.ptr, 'ptr', hParent ? hParent.ptr : 0, waitHandles := Buffer(A_PtrSize*2)
+ loop {
+ Sleep -1
+ waitResult := DllCall("MsgWaitForMultipleObjects", "uint", 1 + (hParent != 0), "ptr", waitHandles, "int", 0, "uint", -1, "uint", 0x04FF)
+ } until waitResult = 0 || waitResult = 1
+ }
+
+ DllCall("GetExitCodeProcess", "ptr", proc.hProcess, "uint*", &exitCode:=0)
+ if trace.Enabled {
+ ; We have to return something numeric for ExitApp, so currently exitCode is left as 259
+ ; if the process is still running.
+ if exitCode = 259 && DllCall("WaitForSingleObject", "ptr", proc.hProcess, "uint", 0) = 258 { ; STILL_ACTIVE = 259, WAIT_TIMEOUT = 258
+ if !(hParent ?? 1) || (waitResult ?? -1) = 1
+ trace '>[Launcher] Process launched; now exiting because parent process has terminated.'
+ else if (parentName ?? "") = "explorer.exe"
+ trace '>[Launcher] Process launched; now exiting because parent is explorer.exe.'
+ else
+ trace '>[Launcher] Process launched; launcher exiting early.'
+ }
+ else
+ trace '>[Launcher] Exit code: ' exitCode
+ }
+
+ return exitCode
+}
+
+RunWithHandles(cmd, handles, workingDir:="") {
+ static STARTUPINFO_SIZE := A_PtrSize=8 ? 104 : 68
+ , STARTUPINFO_dwFlags := A_PtrSize=8 ? 60 : 44
+ , STARTUPINFO_hStdInput := A_PtrSize=8 ? 80 : 56
+ , STARTF_USESTDHANDLES := 0x100
+ , PROCESS_INFORMATION_SIZE := A_PtrSize=8 ? 24 : 16
+ HandleValue(p) => HasProp(handles, p) && (IsInteger(h := handles.%p%) ? h : h.Ptr)
+ si := Buffer(STARTUPINFO_SIZE, 0)
+ NumPut("uint", STARTUPINFO_SIZE, si)
+ NumPut("uint", STARTF_USESTDHANDLES, si, STARTUPINFO_dwFlags)
+ NumPut("ptr", HandleValue("in")
+ , "ptr", HandleValue("out")
+ , "ptr", HandleValue("err")
+ , si, STARTUPINFO_hStdInput)
+ pi := Buffer(PROCESS_INFORMATION_SIZE)
+ if !DllCall("CreateProcess", "ptr", 0, "str", cmd, "ptr", 0, "int", 0, "int", true
+ , "int", 0x08000000, "int", 0, "ptr", workingDir ? StrPtr(workingDir) : 0
+ , "ptr", si, "ptr", pi)
+ throw OSError(, -1, cmd)
+ return { hProcess: Handle(NumGet(pi, 0, "ptr"))
+ , hThread: Handle(NumGet(pi, A_PtrSize, "ptr"))
+ , pid: NumGet(pi, A_PtrSize*2, "uint") }
+}
+
+class VersionSelectGui extends AutoHotkeyUxGui {
+ __new(script, files) {
+ SplitPath script, &scriptName
+ super.__new("Run " scriptName " with", '-MinimizeBox')
+ DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT
+ , 'int64*', 2 | (2<<32), 'int', 8) ; WTNCA_NODRAWICON=2
+ lv := this.AddListMenu('vList LV0x40 w200', ["Version"])
+ lv.OnEvent('Focus', 'Focused')
+ lv.OnEvent('LoseFocus', 'Focused')
+ lv.OnEvent('Click', 'Confirm')
+ il := IL_Create(,, false)
+ lv.SetImageList(il, 0)
+ for f in this.files := files {
+ lv.Add('Icon' IL_Add(il, f.Path), f.Version " " StrReplace(f.Description, "AutoHotkey "))
+ }
+ lv.AutoSize(8)
+ lv.GetPos(&x, &y, &w, &h)
+ this.Show('AutoSize Hide')
+ this.AddButton('Default Hidden', "Confirm").OnEvent('Click', 'Confirm')
+ }
+
+ Confirm(*) {
+ if !(i := this['List'].GetNext())
+ return
+ this.selection := this.files[i]
+ this.Hide()
+ }
+
+ Focused(ctrl, *) {
+ OnMessage(0x101, keyup, ctrl.Focused)
+ static keyup(wParam, lParam, nmsg, hwnd) {
+ local this := GuiFromHwnd(hwnd, true)
+ if IsDigit(GetKeyName(Format("vk{:x}", wParam))) && this['List'].GetNext() {
+ this.Confirm()
+ return true
+ }
+ }
+ }
+}
diff --git a/UX/reload-v1.ahk b/UX/reload-v1.ahk
index cadf63e..08ba895 100644
--- a/UX/reload-v1.ahk
+++ b/UX/reload-v1.ahk
@@ -1,22 +1,22 @@
-; This file is part of a trick for allowing a v2 script to relaunch itself with
-; v2 when the user attempts to execute it with v1. See inc\bounce-v1.ahk.
-
-#NoTrayIcon
-
-if (A_ScriptFullPath = A_LineFile)
-{
- MsgBox 16,, This script is not meant to be executed.
- ExitApp 2
-}
-
-if (!A_Args.Length())
-{
- Loop Files, %A_ScriptDir%\..\AutoHotkey32.exe, FR
- {
- Run "%A_LoopFileLongPath%" /force "%A_ScriptFullPath%"
- ExitApp
- }
-}
-
-MsgBox 16,, This script requires AutoHotkey v2, but was launched with v1.
+; This file is part of a trick for allowing a v2 script to relaunch itself with
+; v2 when the user attempts to execute it with v1. See inc\bounce-v1.ahk.
+
+#NoTrayIcon
+
+if (A_ScriptFullPath = A_LineFile)
+{
+ MsgBox 16,, This script is not meant to be executed.
+ ExitApp 2
+}
+
+if (!A_Args.Length())
+{
+ Loop Files, %A_ScriptDir%\..\AutoHotkey32.exe, FR
+ {
+ Run "%A_LoopFileLongPath%" /force "%A_ScriptFullPath%"
+ ExitApp
+ }
+}
+
+MsgBox 16,, This script requires AutoHotkey v2, but was launched with v1.
ExitApp 2
\ No newline at end of file
diff --git a/UX/reset-assoc.ahk b/UX/reset-assoc.ahk
index e6536ff..c0c2507 100644
--- a/UX/reset-assoc.ahk
+++ b/UX/reset-assoc.ahk
@@ -1,34 +1,38 @@
-; This script clears any file type assocation made via the "open with" dialog,
-; so that the standard registration under HKCR\.ahk can take effect.
-#include inc\bounce-v1.ahk
-/* v1 stops here */
-#requires AutoHotkey v2.0
-
-keyname := "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ahk\UserChoice"
-initial_progid := RegRead(keyname, "ProgId", "")
-if A_Args.Length && A_Args[1] = '/check' {
- if initial_progid = "" || initial_progid = "AutoHotkeyScript"
- || MsgBox("It looks like you've used an unsupported method to set the default program for .ahk files. "
- . "This will prevent the standard context menu and launcher (version auto-detect) functionality "
- . "from working. Would you like this setting to be reset for you?", "AutoHotkey", "Icon! y/n") != "yes"
- ExitApp
-}
-reg_file_path := A_Temp "\reset-ahk-file-association.reg"
-FileOpen(reg_file_path, "w").Write("Windows Registry Editor Version 5.00`n"
- . "[-" keyname "]`n")
-EnvSet "__COMPAT_LAYER", "RunAsInvoker"
-RunWait 'regedit.exe /S "' reg_file_path '"'
-EnvSet "__COMPAT_LAYER", ""
-DllCall("shell32\SHChangeNotify", "uint", 0x08000000, "uint", 0, "int", 0, "int", 0) ; SHCNE_ASSOCCHANGED
-FileDelete reg_file_path
-new_progid := RegRead(keyname, "ProgId", "")
-if (new_progid != "" || A_LastError != 2)
- MsgBox "Something went wrong and the reset probably "
- . "didn't work.`n`nCurrent association: "
- . (new_progid = "" ? "(unknown)" : new_progid), "AutoHotkey", "Icon!"
-else if (initial_progid != "")
- MsgBox "Association of .ahk files for the current user has been reset.", "AutoHotkey", "Iconi"
-else
- MsgBox "It looks as though the current user's settings "
- . "weren't overriding the default .ahk file options. A reset was "
+; This script clears any file type assocation made via the "open with" dialog,
+; so that the standard registration under HKCR\.ahk can take effect.
+#include inc\bounce-v1.ahk
+/* v1 stops here */
+#requires AutoHotkey v2.0
+
+keyname := "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ahk\UserChoice"
+initial_progid := RegRead(keyname, "ProgId", "")
+legacy_key := "HKCU\Software\Classes\.ahk"
+legacy_assoc := RegRead(legacy_key,, "AutoHotkeyScript")
+if A_Args.Length && A_Args[1] = '/check' {
+ if (initial_progid = "" || initial_progid = "AutoHotkeyScript") && legacy_assoc = "AutoHotkeyScript"
+ || MsgBox("It looks like you've used an unsupported method to set the default program for .ahk files. "
+ . "This will prevent the standard context menu and launcher (version auto-detect) functionality "
+ . "from working. Would you like this setting to be reset for you?", "AutoHotkey", "Icon! y/n") != "yes"
+ ExitApp
+}
+reg_file_path := A_Temp "\reset-ahk-file-association.reg"
+FileOpen(reg_file_path, "w").Write("Windows Registry Editor Version 5.00`n"
+ . "[-" keyname "]`n")
+EnvSet "__COMPAT_LAYER", "RunAsInvoker"
+RunWait 'regedit.exe /S "' reg_file_path '"'
+EnvSet "__COMPAT_LAYER", ""
+if legacy_assoc != "AutoHotkeyScript"
+ RegWrite "AutoHotkeyScript", "REG_SZ", legacy_key
+DllCall("shell32\SHChangeNotify", "uint", 0x08000000, "uint", 0, "int", 0, "int", 0) ; SHCNE_ASSOCCHANGED
+FileDelete reg_file_path
+new_progid := RegRead(keyname, "ProgId", "")
+if (new_progid != "" || A_LastError != 2)
+ MsgBox "Something went wrong and the reset probably "
+ . "didn't work.`n`nCurrent association: "
+ . (new_progid = "" ? "(unknown)" : new_progid), "AutoHotkey", "Icon!"
+else if (initial_progid != "" || legacy_assoc != "AutoHotkeyScript")
+ MsgBox "Association of .ahk files for the current user has been reset.", "AutoHotkey", "Iconi"
+else
+ MsgBox "It looks as though the current user's settings "
+ . "weren't overriding the default .ahk file options. A reset was "
. "attempted anyway, but it probably had no effect.", "AutoHotkey", "Icon!"
\ No newline at end of file
diff --git a/UX/ui-dash.ahk b/UX/ui-dash.ahk
index b7a2109..65bbf42 100644
--- a/UX/ui-dash.ahk
+++ b/UX/ui-dash.ahk
@@ -1,200 +1,200 @@
-; Dash: AutoHotkey's "main menu".
-; Run the script to show the GUI.
-#include inc\bounce-v1.ahk
-/* v1 stops here */
-#requires AutoHotkey v2.0
-
-#NoTrayIcon
-#SingleInstance Force
-
-#include inc\ui-base.ahk
-#include ui-launcherconfig.ahk
-#include ui-editor.ahk
-#include ui-newscript.ahk
-
-DashRegKey := 'HKCU\Software\AutoHotkey\Dash'
-
-class AutoHotkeyDashGui extends AutoHotkeyUxGui {
- __new() {
- super.__new("AutoHotkey Dash")
-
- lv := this.AddListMenu('vLV LV0x40 w250', ["Name", "Desc"])
- lv.OnEvent("Click", "ItemClicked")
- lv.OnEvent("ItemFocus", "ItemFocused")
- lv.OnNotify(-155, "KeyPressed")
-
- this.AddButton("xp yp wp yp Hidden Default").OnEvent("Click", "EnterPressed")
-
- il := IL_Create(,, true)
- lv.SetImageList(il, 0)
- il2 := IL_Create(,, false)
- lv.SetImageList(il2, 1)
- addIcon(p*) =>(IL_Add(il, p*), IL_Add(il2, p*))
-
- lv.Add("Icon" addIcon(A_AhkPath, 2)
- , "New script", "Create a script or manage templates")
- lv.Add("Icon" addIcon("imageres.dll", -111)
- , "Compile", "Open Ahk2Exe - convert .ahk to .exe")
- lv.Add("Icon" addIcon("imageres.dll", -99)
- , "Help files (F1)")
- lv.Add("Icon" addIcon(A_ScriptDir '\inc\spy.ico', 1)
- , "Window spy")
- lv.Add("Icon" addIcon("imageres.dll", -116)
- , "Launch settings", "Configure how .ahk files are opened")
- lv.Add("Icon" addIcon("notepad.exe", 1)
- , "Editor settings", "Set your default script editor")
- ; lv.Add("Icon" addIcon("mmc.exe")
- ; , "Maintenance", "Repair settings or add/remove versions")
- ; lv.Add(, "Auto-start", "Run scripts automatically at logon")
- ; lv.Add(, "Downloads", "Get related tools")
-
- lv.AutoSize()
- lv.GetPos(,,, &h)
-
- if !RegRead(DashRegKey, 'SuppressIntro', false) {
- this.SetFont('s12')
- this.AddText('yp x+m', "Welcome!")
- this.SetFont('s9')
- this.AddText('xp', "This is the Dash. It provides access to tools, settings and help files.")
- this.AddText('xp', "To learn how to use AutoHotkey, refer to:")
- this.AddLink('xp', "
- (
- `s • Using the Program
- • How to Write Hotkeys
- • How to Send Keystrokes
- • How to Run Programs
- • How to Manage Windows
- • Quick Reference
- )").OnEvent('Click', 'LinkClicked')
-
- checkBox := this.AddCheckbox('Checked', "Show this info next time")
- checkBox.GetPos(,,, &hc)
- checkBox.Move(, h - hc)
- checkBox.OnEvent('Click', 'SetIntroPref')
- }
-
- this.Show("Hide h" (h + this.MarginY*2))
- }
-
- LinkClicked(ctrl, id, href) {
- if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm')
- Run 'hh.exe "ms-its:' chm '::docs/' href '"'
- else
- Run 'https://www.autohotkey.com/docs/v2/' href
- }
-
- SetIntroPref(checkBox, *) {
- if checkBox.Value { ; Show intro
- try RegDelete(DashRegKey, 'SuppressIntro')
- } else
- RegWrite(true, 'REG_DWORD', DashRegKey, 'SuppressIntro')
- }
-
- KeyPressed(lv, lParam) {
- switch NumGet(lParam, A_PtrSize * 3, "Short") {
- case 0x70: ; F1
- ShowHelpFile()
- }
- }
-
- EnterPressed(*) {
- lv := this["LV"]
- this.ItemClicked(lv, lv.GetNext(,'F'))
- }
-
- ItemClicked(lv, item) {
- switch item && RegExReplace(lv.GetText(item), ' .*') {
- case "New":
- NewScriptGui.Show()
- case "Compile":
- if WinExist("Ahk2Exe ahk_class AutoHotkeyGUI")
- WinActivate
- else if FileExist(ROOT_DIR '\Compiler\Ahk2Exe.exe')
- Run '"' ROOT_DIR '\Compiler\Ahk2Exe.exe"'
- else
- Run Format('"{1}" /script "{2}\install-ahk2exe.ahk"', A_AhkPath, A_ScriptDir)
- case "Help":
- ShowHelpFile()
- case "Window":
- try {
- Run '"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"'
- return
- }
- static AHK_FILE_WINDOWSPY := 0xFF7A ; 65402
- static WM_COMMAND := 0x111 ; 273
- SendMessage WM_COMMAND, AHK_FILE_WINDOWSPY, 0, A_ScriptHwnd
- if WinWait("Window Spy ahk_class AutoHotkeyGUI",, 1)
- WinActivate
- case "Launch":
- LauncherConfigGui.Show()
- case "Editor":
- DefaultEditorGui.Show()
- }
- }
-
- ItemFocused(lv, item) {
- static WM_CHANGEUISTATE := 0x127 ; 295
- SendMessage WM_CHANGEUISTATE, 0x10001, 0, lv
- }
-}
-
-ShowHelpFile() {
- SetWorkingDir ROOT_DIR
- main := Map(), sub := Map()
- other := Map(), other.CaseSense := "off"
- hashes := ReadHashes('UX\installed-files.csv', item => item.Path ~= 'i)\.chm$')
- Loop Files "*.chm", "FR" {
- SplitPath A_LoopFilePath,, &dir,, &name
- if name = "AutoHotkey" {
- if !(f := hashes.Get(A_LoopFilePath, false))
- continue
- v := GetMajor(f.Version)
- if !(cur := main.Get(v, false)) || VerCompare(cur.Version, f.Version) < 0
- main[v] := f
- sub[f.Version] := f
- }
- else
- other[A_LoopFilePath] := name (dir != "" && name != dir ? " (" dir ")" : "")
- }
-
- if sub.Count = 1 && other.Count = 0 {
- for , f in main { ; Don't bother showing online options in this case.
- Run f.Path
- return
- }
- }
-
- m := Menu()
- if main.Count {
- m.Add "Offline help", (*) => 0
- m.Disable "1&"
- }
- for , f in main {
- m.Add "v&" f.Version, openIt.Bind(f.Path)
- sub.Delete(f.Version)
- }
- if sub.Count {
- subm := Menu()
- for , f in sub
- subm.Insert "1&", "v" f.Version, openIt.Bind(f.Path)
- m.Add "More", subm
- }
-
- m.Add "Online help", (*) => 0
- m.Disable "Online help"
- prefix := main.Count ? "v" : "v&"
- m.Add prefix "1.1", (*) => Run("https://www.autohotkey.com/docs/v1/")
- m.Add prefix "2.0", (*) => Run("https://www.autohotkey.com/docs/v2/")
-
- if other.Count {
- m.Add "Other files", (*) => 0
- m.Disable "Other files"
- }
- for f, t in other
- m.Add t, openIt.Bind(f)
-
- m.Show
- openIt(f, *) => Run(f)
-}
-
-AutoHotkeyDashGui.Show()
+; Dash: AutoHotkey's "main menu".
+; Run the script to show the GUI.
+#include inc\bounce-v1.ahk
+/* v1 stops here */
+#requires AutoHotkey v2.0
+
+#NoTrayIcon
+#SingleInstance Force
+
+#include inc\ui-base.ahk
+#include ui-launcherconfig.ahk
+#include ui-editor.ahk
+#include ui-newscript.ahk
+
+DashRegKey := 'HKCU\Software\AutoHotkey\Dash'
+
+class AutoHotkeyDashGui extends AutoHotkeyUxGui {
+ __new() {
+ super.__new("AutoHotkey Dash")
+
+ lv := this.AddListMenu('vLV LV0x40 w250', ["Name", "Desc"])
+ lv.OnEvent("Click", "ItemClicked")
+ lv.OnEvent("ItemFocus", "ItemFocused")
+ lv.OnNotify(-155, "KeyPressed")
+
+ this.AddButton("xp yp wp yp Hidden Default").OnEvent("Click", "EnterPressed")
+
+ il := IL_Create(,, true)
+ lv.SetImageList(il, 0)
+ il2 := IL_Create(,, false)
+ lv.SetImageList(il2, 1)
+ addIcon(p*) =>(IL_Add(il, p*), IL_Add(il2, p*))
+
+ lv.Add("Icon" addIcon(A_AhkPath, 2)
+ , "New script", "Create a script or manage templates")
+ lv.Add("Icon" addIcon("imageres.dll", -111)
+ , "Compile", "Open Ahk2Exe - convert .ahk to .exe")
+ lv.Add("Icon" addIcon("imageres.dll", -99)
+ , "Help files (F1)")
+ lv.Add("Icon" addIcon(A_ScriptDir '\inc\spy.ico', 1)
+ , "Window spy")
+ lv.Add("Icon" addIcon("imageres.dll", -116)
+ , "Launch settings", "Configure how .ahk files are opened")
+ lv.Add("Icon" addIcon("notepad.exe", 1)
+ , "Editor settings", "Set your default script editor")
+ ; lv.Add("Icon" addIcon("mmc.exe")
+ ; , "Maintenance", "Repair settings or add/remove versions")
+ ; lv.Add(, "Auto-start", "Run scripts automatically at logon")
+ ; lv.Add(, "Downloads", "Get related tools")
+
+ lv.AutoSize()
+ lv.GetPos(,,, &h)
+
+ if !RegRead(DashRegKey, 'SuppressIntro', false) {
+ this.SetFont('s12')
+ this.AddText('yp x+m', "Welcome!")
+ this.SetFont('s9')
+ this.AddText('xp', "This is the Dash. It provides access to tools, settings and help files.")
+ this.AddText('xp', "To learn how to use AutoHotkey, refer to:")
+ this.AddLink('xp', "
+ (
+ `s • Using the Program
+ • How to Write Hotkeys
+ • How to Send Keystrokes
+ • How to Run Programs
+ • How to Manage Windows
+ • Quick Reference
+ )").OnEvent('Click', 'LinkClicked')
+
+ checkBox := this.AddCheckbox('Checked', "Show this info next time")
+ checkBox.GetPos(,,, &hc)
+ checkBox.Move(, h - hc)
+ checkBox.OnEvent('Click', 'SetIntroPref')
+ }
+
+ this.Show("Hide h" (h + this.MarginY*2))
+ }
+
+ LinkClicked(ctrl, id, href) {
+ if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm')
+ Run 'hh.exe "ms-its:' chm '::docs/' href '"'
+ else
+ Run 'https://www.autohotkey.com/docs/v2/' href
+ }
+
+ SetIntroPref(checkBox, *) {
+ if checkBox.Value { ; Show intro
+ try RegDelete(DashRegKey, 'SuppressIntro')
+ } else
+ RegWrite(true, 'REG_DWORD', DashRegKey, 'SuppressIntro')
+ }
+
+ KeyPressed(lv, lParam) {
+ switch NumGet(lParam, A_PtrSize * 3, "Short") {
+ case 0x70: ; F1
+ ShowHelpFile()
+ }
+ }
+
+ EnterPressed(*) {
+ lv := this["LV"]
+ this.ItemClicked(lv, lv.GetNext(,'F'))
+ }
+
+ ItemClicked(lv, item) {
+ switch item && RegExReplace(lv.GetText(item), ' .*') {
+ case "New":
+ NewScriptGui.Show()
+ case "Compile":
+ if WinExist("Ahk2Exe ahk_class AutoHotkeyGUI")
+ WinActivate
+ else if FileExist(ROOT_DIR '\Compiler\Ahk2Exe.exe')
+ Run '"' ROOT_DIR '\Compiler\Ahk2Exe.exe"'
+ else
+ Run Format('"{1}" /script "{2}\install-ahk2exe.ahk"', A_AhkPath, A_ScriptDir)
+ case "Help":
+ ShowHelpFile()
+ case "Window":
+ try {
+ Run '"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"'
+ return
+ }
+ static AHK_FILE_WINDOWSPY := 0xFF7A ; 65402
+ static WM_COMMAND := 0x111 ; 273
+ SendMessage WM_COMMAND, AHK_FILE_WINDOWSPY, 0, A_ScriptHwnd
+ if WinWait("Window Spy ahk_class AutoHotkeyGUI",, 1)
+ WinActivate
+ case "Launch":
+ LauncherConfigGui.Show()
+ case "Editor":
+ DefaultEditorGui.Show()
+ }
+ }
+
+ ItemFocused(lv, item) {
+ static WM_CHANGEUISTATE := 0x127 ; 295
+ SendMessage WM_CHANGEUISTATE, 0x10001, 0, lv
+ }
+}
+
+ShowHelpFile() {
+ SetWorkingDir ROOT_DIR
+ main := Map(), sub := Map()
+ other := Map(), other.CaseSense := "off"
+ hashes := ReadHashes('UX\installed-files.csv', item => item.Path ~= 'i)\.chm$')
+ Loop Files "*.chm", "FR" {
+ SplitPath A_LoopFilePath,, &dir,, &name
+ if name = "AutoHotkey" {
+ if !(f := hashes.Get(A_LoopFilePath, false))
+ continue
+ v := GetMajor(f.Version)
+ if !(cur := main.Get(v, false)) || VerCompare(cur.Version, f.Version) < 0
+ main[v] := f
+ sub[f.Version] := f
+ }
+ else
+ other[A_LoopFilePath] := name (dir != "" && name != dir ? " (" dir ")" : "")
+ }
+
+ if sub.Count = 1 && other.Count = 0 {
+ for , f in main { ; Don't bother showing online options in this case.
+ Run f.Path
+ return
+ }
+ }
+
+ m := Menu()
+ if main.Count {
+ m.Add "Offline help", (*) => 0
+ m.Disable "1&"
+ }
+ for , f in main {
+ m.Add "v&" f.Version, openIt.Bind(f.Path)
+ sub.Delete(f.Version)
+ }
+ if sub.Count {
+ subm := Menu()
+ for , f in sub
+ subm.Insert "1&", "v" f.Version, openIt.Bind(f.Path)
+ m.Add "More", subm
+ }
+
+ m.Add "Online help", (*) => 0
+ m.Disable "Online help"
+ prefix := main.Count ? "v" : "v&"
+ m.Add prefix "1.1", (*) => Run("https://www.autohotkey.com/docs/v1/")
+ m.Add prefix "2.0", (*) => Run("https://www.autohotkey.com/docs/v2/")
+
+ if other.Count {
+ m.Add "Other files", (*) => 0
+ m.Disable "Other files"
+ }
+ for f, t in other
+ m.Add t, openIt.Bind(f)
+
+ m.Show
+ openIt(f, *) => Run(f)
+}
+
+AutoHotkeyDashGui.Show()
diff --git a/UX/ui-editor.ahk b/UX/ui-editor.ahk
index 02409e1..e15acf4 100644
--- a/UX/ui-editor.ahk
+++ b/UX/ui-editor.ahk
@@ -1,238 +1,238 @@
-; This script shows a GUI for setting the default .ahk editor.
-#requires AutoHotkey v2.0
-
-#NoTrayIcon
-#SingleInstance Off
-
-#include launcher.ahk
-#include inc\CommandLineToArgs.ahk
-
-class EditorSelectionGui extends AutoHotkeyUxGui {
- __new(cmdLine) {
- super.__new("Select an editor")
-
- lv := this.AddListMenu('vEds LV0x40 w300', ["Editor"])
- this.IconList := il := IL_Create(,, true)
- lv.SetImageList(il, 0)
- for app in this.Apps := GetEditorApps() {
- try
- icon := IL_Add(il, app.exe)
- catch
- icon := -1
- lv.Add('Icon' icon, app.name)
- }
- this.SelectEditorByCmd(cmdLine)
- lv.AutoSize(8)
- lv.GetPos(&x, &y, &w, &h)
- x += w
- y += h
-
- this.AddText('xm w' w ' y' y, "Command line")
- this.AddEdit('xm wp r2 -WantReturn vCmd', cmdLine).OnEvent('Change', 'CmdChanged')
-
- this.AddText('xm h25 18 w' w)
- this.AddPicture('x56 Icon-81 w16 yp+4', "imageres.dll")
- this.AddLink('x76 yp-1', "Editors with AutoHotkey support")
- .OnEvent('Click', 'ShowHelpEditors')
-
- this.AddButton('xm w80', "&Browse").OnEvent('Click', 'Browse')
- this.AddButton('Default yp w80 x' x - 160 - this.MarginY, "&OK").OnEvent('Click', 'Confirm')
- this.AddButton('yp w80 x' x - 80, "&Cancel").OnEvent('Click', (c, *) => c.Gui.Hide())
-
- this["Eds"].OnEvent("ItemFocus", "EditorSelected")
- this.CmdChanged()
- }
-
- ShowHelpEditors(*) {
- if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm')
- Run 'hh.exe "ms-its:' chm '::docs/lib/Edit.htm#Editors"'
- else
- Run 'https://www.autohotkey.com/docs/v2/lib/Edit.htm#Editors'
- }
-
- Browse(*) {
- app := this.FileSelect(3,,, "Apps (*.exe; *.ahk)")
- if app = ""
- return
- this['Cmd'].Value := this.GetAppCmd(app)
- this.CmdChanged()
- }
-
- GetAppCmd(app) {
- SplitPath app,,, &ext
- if ext != "ahk"
- return Format('"{1}" "%l"', app)
- lp := GetLaunchParameters(app, true)
- if !lp.exe
- return ""
- ; Try to use a path that will work if the user installs a new version and removes this one
- ; (rather than invoking the launcher every time the edit verb is executed).
- adaptivePath := RegExReplace(lp.exe.Path, lp.v = 1 ? '\\v1[^\\]*(?=\\[^\\]*$)' : '\\v2\K[^\\]+(?=\\[^\\]*$)')
- trace '![Launcher] ' adaptivePath
- try
- adaptiveExe := GetExeInfo(adaptivePath)
- if !IsSet(adaptiveExe) || VerCompare(adaptiveExe.Version, lp.v) < 0
- adaptivePath := ""
- cmd := Format('"{}"', FileExist(adaptivePath) ? adaptivePath : lp.exe.Path)
- for sw in lp.switches
- cmd .= ' ' sw
- return cmd .= Format(' "{}" "%l"', app)
- }
-
- EditorSelected(lv, index) {
- this['Cmd'].Value := this.Apps[index].cmd
- this.CmdChanged()
- }
-
- CmdChanged(ctrl:=unset, *) {
- if IsSet(ctrl) && n := this['Eds'].GetNext()
- this['Eds'].Modify(n, '-Select')
- cmd := this['Cmd'].Value
- this['OK'].Enabled := cmd != "" && FindExecutable(CommandLineToArgs(cmd)[1]) != ""
- }
-
- SelectEditorByCmd(cmd) {
- if cmd = ""
- return
- for app in this.Apps {
- if app.cmd = cmd {
- this['Eds'].Modify(A_Index, 'Focus Select')
- return
- }
- }
- ; App not in the list, so add it.
- args := CommandLineToArgs(cmd)
- try {
- exe := GetExeInfo(FindExecutable(args.RemoveAt(1)))
- app := {cmd: cmd, exe: exe.Path, name: exe.Description}
- if SubStr(app.name, 1, 10) = "AutoHotkey" && (ahk := ahkArg(args)) {
- ; The app itself appears to be a script, so show the script name.
- app.name := ahk
- }
- this.Apps.Push(app)
- try
- icon := IL_Add(this.IconList, app.exe, IsSet(ahk) && ahk ? 2 : 1)
- catch
- icon := -1
- this['Eds'].Add('Focus Select Icon' icon, app.name)
- }
-
- ahkArg(args) {
- for arg in args {
- if SubStr(arg, 1, 1) = '/'
- continue
- return SubStr(arg, -4) = '.ahk' ? arg : ''
- }
- return ''
- }
- }
-
- Confirm(*) {
- this.Hide()
- this.OnConfirm(this['Cmd'].Value)
- }
-}
-
-class DefaultEditorGui extends EditorSelectionGui {
- __new(scriptToEdit:=unset) {
- cmd := RegRead('HKCR\AutoHotkeyScript\shell\edit\command',, '')
- if InStr(cmd, A_LineFile)
- cmd := ''
- super.__new(cmd)
- if IsSet(scriptToEdit)
- this.ScriptToEdit := scriptToEdit
- }
-
- OnConfirm(cmd) {
- RegWrite(cmd, 'REG_SZ', 'HKCU\Software\Classes\AutoHotkeyScript\shell\edit\command')
- if this.HasProp('ScriptToEdit')
- Run('edit "' this.ScriptToEdit '"')
- }
-}
-
-GetEditorApps() {
- apps := []
- apps.byName := Map(), apps.byName.CaseSense := 'off'
- addAssoc(assoc, flag, src) {
- static ASSOCSTR_COMMAND := 1
- static ASSOCSTR_EXECUTABLE := 2
- static ASSOCSTR_FRIENDLYAPPNAME := 4
- try {
- name := AssocQueryString(flag, ASSOCSTR_FRIENDLYAPPNAME, assoc)
- exe := AssocQueryString(flag, ASSOCSTR_EXECUTABLE, assoc)
- }
- if !IsSet(exe) || name ~= 'i)^AutoHotkey|^Ahk2Exe'
- return
- try
- cmd := AssocQueryString(flag, ASSOCSTR_COMMAND, assoc)
- catch
- cmd := '"' exe '" "%l"'
- else if cmd = ""
- return
- if !(InStr(cmd, "%1") || InStr(cmd, "%l"))
- cmd .= ' "%l"'
- if name = "NOTEPAD.EXE"
- name := "Notepad"
- if apps.byName.Has(name)
- return
- app := {cmd: cmd, exe: exe, name: name}
- apps.byName[name] := app
- apps.Push(app)
- }
- addAppExe(app, src) {
- static ASSOCF_INIT_BYEXENAME := 2 ; 0x2
- addAssoc app, ASSOCF_INIT_BYEXENAME, src
- }
- addProgId(app, src) {
- addAssoc app, 0, src
- }
-
- for ext in ["ahk", "txt"] {
- owl := "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\." ext "\OpenWithList"
- mru := RegRead(owl, "MRUList", "")
- Loop Parse mru {
- if app := RegRead(owl, A_LoopField, "")
- addAppExe app, "Explorer\." ext "\OWL"
- }
- Loop Reg "HKCR\." ext "\OpenWithProgIds", "V" {
- addProgId A_LoopRegName, "HKCR\." ext "\OWPI"
- }
- Loop Reg "HKCR\." ext "\OpenWithList", "K" {
- if !InStr(A_LoopRegName, "Ahk2Exe")
- addAppExe A_LoopRegName, "HKCR\." ext "\OWL"
- }
- }
- addAppExe "notepad.exe", "explicit"
- return apps
-}
-
-; Return the path of an executable file, if given something that could be executed
-; in a shell verb (excluding args). Although shell verb commands require an exe,
-; they do also look in \App Paths\, like ShellExecute, unlike CreateProcess.
-FindExecutable(name) {
- if SubStr(name, -4) != ".exe" ; For odd cases like 'notepad "%1"'
- name .= ".exe"
- if FileExist(name)
- return name
- if InStr(name, "\") || InStr(name, ":")
- return ""
- buf := Buffer(260*2)
- if DllCall("shell32\FindExecutable", "str", name, "ptr", 0, "ptr", buf, "uint") > 32 ; "Returns a value greater than 32 if successful"
- return StrGet(buf)
- static AppPaths := '\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\'
- path := RegRead('HKCU' AppPaths name,, '') || RegRead('HKLM' AppPaths name,, '')
- ; The system permits quotes, and some applications are registered that way.
- return StrReplace(path, '"')
-}
-
-AssocQueryString(flags, strtype, assoc) {
- DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc
- , "ptr", 0, "ptr", 0, "uint*", &bufsize := 0, "hresult")
- buf := Buffer(bufsize * 2)
- DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc
- , "ptr", 0, "ptr", buf, "uint*", &bufsize, "hresult")
- return StrGet(buf, "UTF-16")
-}
-
-if A_LineFile = A_ScriptFullPath
- DefaultEditorGui.Show(A_Args*)
+; This script shows a GUI for setting the default .ahk editor.
+#requires AutoHotkey v2.0
+
+#NoTrayIcon
+#SingleInstance Off
+
+#include launcher.ahk
+#include inc\CommandLineToArgs.ahk
+
+class EditorSelectionGui extends AutoHotkeyUxGui {
+ __new(cmdLine) {
+ super.__new("Select an editor")
+
+ lv := this.AddListMenu('vEds LV0x40 w300', ["Editor"])
+ this.IconList := il := IL_Create(,, true)
+ lv.SetImageList(il, 0)
+ for app in this.Apps := GetEditorApps() {
+ try
+ icon := IL_Add(il, app.exe)
+ catch
+ icon := -1
+ lv.Add('Icon' icon, app.name)
+ }
+ this.SelectEditorByCmd(cmdLine)
+ lv.AutoSize(8)
+ lv.GetPos(&x, &y, &w, &h)
+ x += w
+ y += h
+
+ this.AddText('xm w' w ' y' y, "Command line")
+ this.AddEdit('xm wp r2 -WantReturn vCmd', cmdLine).OnEvent('Change', 'CmdChanged')
+
+ this.AddText('xm h25 18 w' w)
+ this.AddPicture('x56 Icon-81 w16 yp+4', "imageres.dll")
+ this.AddLink('x76 yp-1', "Editors with AutoHotkey support")
+ .OnEvent('Click', 'ShowHelpEditors')
+
+ this.AddButton('xm w80', "&Browse").OnEvent('Click', 'Browse')
+ this.AddButton('Default yp w80 x' x - 160 - this.MarginY, "&OK").OnEvent('Click', 'Confirm')
+ this.AddButton('yp w80 x' x - 80, "&Cancel").OnEvent('Click', (c, *) => c.Gui.Hide())
+
+ this["Eds"].OnEvent("ItemFocus", "EditorSelected")
+ this.CmdChanged()
+ }
+
+ ShowHelpEditors(*) {
+ if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm')
+ Run 'hh.exe "ms-its:' chm '::docs/misc/Editors.htm"'
+ else
+ Run 'https://www.autohotkey.com/docs/v2/misc/Editors.htm'
+ }
+
+ Browse(*) {
+ app := this.FileSelect(3,,, "Apps (*.exe; *.ahk)")
+ if app = ""
+ return
+ this['Cmd'].Value := this.GetAppCmd(app)
+ this.CmdChanged()
+ }
+
+ GetAppCmd(app) {
+ SplitPath app,,, &ext
+ if ext != "ahk"
+ return Format('"{1}" "%l"', app)
+ lp := GetLaunchParameters(app, true)
+ if !lp.exe
+ return ""
+ ; Try to use a path that will work if the user installs a new version and removes this one
+ ; (rather than invoking the launcher every time the edit verb is executed).
+ adaptivePath := RegExReplace(lp.exe.Path, lp.v = 1 ? '\\v1[^\\]*(?=\\[^\\]*$)' : '\\v2\K[^\\]+(?=\\[^\\]*$)')
+ trace '![Launcher] ' adaptivePath
+ try
+ adaptiveExe := GetExeInfo(adaptivePath)
+ if !IsSet(adaptiveExe) || VerCompare(adaptiveExe.Version, lp.v) < 0
+ adaptivePath := ""
+ cmd := Format('"{}"', FileExist(adaptivePath) ? adaptivePath : lp.exe.Path)
+ for sw in lp.switches
+ cmd .= ' ' sw
+ return cmd .= Format(' "{}" "%l"', app)
+ }
+
+ EditorSelected(lv, index) {
+ this['Cmd'].Value := this.Apps[index].cmd
+ this.CmdChanged()
+ }
+
+ CmdChanged(ctrl:=unset, *) {
+ if IsSet(ctrl) && n := this['Eds'].GetNext()
+ this['Eds'].Modify(n, '-Select')
+ cmd := this['Cmd'].Value
+ this['OK'].Enabled := cmd != "" && FindExecutable(CommandLineToArgs(cmd)[1]) != ""
+ }
+
+ SelectEditorByCmd(cmd) {
+ if cmd = ""
+ return
+ for app in this.Apps {
+ if app.cmd = cmd {
+ this['Eds'].Modify(A_Index, 'Focus Select')
+ return
+ }
+ }
+ ; App not in the list, so add it.
+ args := CommandLineToArgs(cmd)
+ try {
+ exe := GetExeInfo(FindExecutable(args.RemoveAt(1)))
+ app := {cmd: cmd, exe: exe.Path, name: exe.Description}
+ if SubStr(app.name, 1, 10) = "AutoHotkey" && (ahk := ahkArg(args)) {
+ ; The app itself appears to be a script, so show the script name.
+ app.name := ahk
+ }
+ this.Apps.Push(app)
+ try
+ icon := IL_Add(this.IconList, app.exe, IsSet(ahk) && ahk ? 2 : 1)
+ catch
+ icon := -1
+ this['Eds'].Add('Focus Select Icon' icon, app.name)
+ }
+
+ ahkArg(args) {
+ for arg in args {
+ if SubStr(arg, 1, 1) = '/'
+ continue
+ return SubStr(arg, -4) = '.ahk' ? arg : ''
+ }
+ return ''
+ }
+ }
+
+ Confirm(*) {
+ this.Hide()
+ this.OnConfirm(this['Cmd'].Value)
+ }
+}
+
+class DefaultEditorGui extends EditorSelectionGui {
+ __new(scriptToEdit:=unset) {
+ cmd := RegRead('HKCR\AutoHotkeyScript\shell\edit\command',, '')
+ if InStr(cmd, A_LineFile)
+ cmd := ''
+ super.__new(cmd)
+ if IsSet(scriptToEdit)
+ this.ScriptToEdit := scriptToEdit
+ }
+
+ OnConfirm(cmd) {
+ RegWrite(cmd, 'REG_SZ', 'HKCU\Software\Classes\AutoHotkeyScript\shell\edit\command')
+ if this.HasProp('ScriptToEdit')
+ Run('edit "' this.ScriptToEdit '"')
+ }
+}
+
+GetEditorApps() {
+ apps := []
+ apps.byName := Map(), apps.byName.CaseSense := 'off'
+ addAssoc(assoc, flag, src) {
+ static ASSOCSTR_COMMAND := 1
+ static ASSOCSTR_EXECUTABLE := 2
+ static ASSOCSTR_FRIENDLYAPPNAME := 4
+ try {
+ name := AssocQueryString(flag, ASSOCSTR_FRIENDLYAPPNAME, assoc)
+ exe := AssocQueryString(flag, ASSOCSTR_EXECUTABLE, assoc)
+ }
+ if !IsSet(exe) || name ~= 'i)^AutoHotkey|^Ahk2Exe'
+ return
+ try
+ cmd := AssocQueryString(flag, ASSOCSTR_COMMAND, assoc)
+ catch
+ cmd := '"' exe '" "%l"'
+ else if cmd = ""
+ return
+ if !(InStr(cmd, "%1") || InStr(cmd, "%l"))
+ cmd .= ' "%l"'
+ if name = "NOTEPAD.EXE"
+ name := "Notepad"
+ if apps.byName.Has(name)
+ return
+ app := {cmd: cmd, exe: exe, name: name}
+ apps.byName[name] := app
+ apps.Push(app)
+ }
+ addAppExe(app, src) {
+ static ASSOCF_INIT_BYEXENAME := 2 ; 0x2
+ addAssoc app, ASSOCF_INIT_BYEXENAME, src
+ }
+ addProgId(app, src) {
+ addAssoc app, 0, src
+ }
+
+ for ext in ["ahk", "txt"] {
+ owl := "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\." ext "\OpenWithList"
+ mru := RegRead(owl, "MRUList", "")
+ Loop Parse mru {
+ if app := RegRead(owl, A_LoopField, "")
+ addAppExe app, "Explorer\." ext "\OWL"
+ }
+ Loop Reg "HKCR\." ext "\OpenWithProgIds", "V" {
+ addProgId A_LoopRegName, "HKCR\." ext "\OWPI"
+ }
+ Loop Reg "HKCR\." ext "\OpenWithList", "K" {
+ if !InStr(A_LoopRegName, "Ahk2Exe")
+ addAppExe A_LoopRegName, "HKCR\." ext "\OWL"
+ }
+ }
+ addAppExe "notepad.exe", "explicit"
+ return apps
+}
+
+; Return the path of an executable file, if given something that could be executed
+; in a shell verb (excluding args). Although shell verb commands require an exe,
+; they do also look in \App Paths\, like ShellExecute, unlike CreateProcess.
+FindExecutable(name) {
+ if SubStr(name, -4) != ".exe" ; For odd cases like 'notepad "%1"'
+ name .= ".exe"
+ if FileExist(name)
+ return name
+ if InStr(name, "\") || InStr(name, ":")
+ return ""
+ buf := Buffer(260*2)
+ if DllCall("shell32\FindExecutable", "str", name, "ptr", 0, "ptr", buf, "uint") > 32 ; "Returns a value greater than 32 if successful"
+ return StrGet(buf)
+ static AppPaths := '\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\'
+ path := RegRead('HKCU' AppPaths name,, '') || RegRead('HKLM' AppPaths name,, '')
+ ; The system permits quotes, and some applications are registered that way.
+ return StrReplace(path, '"')
+}
+
+AssocQueryString(flags, strtype, assoc) {
+ DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc
+ , "ptr", 0, "ptr", 0, "uint*", &bufsize := 0, "hresult")
+ buf := Buffer(bufsize * 2)
+ DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc
+ , "ptr", 0, "ptr", buf, "uint*", &bufsize, "hresult")
+ return StrGet(buf, "UTF-16")
+}
+
+if A_LineFile = A_ScriptFullPath
+ DefaultEditorGui.Show(A_Args*)
diff --git a/UX/ui-launcherconfig.ahk b/UX/ui-launcherconfig.ahk
index c9449ab..644f155 100644
--- a/UX/ui-launcherconfig.ahk
+++ b/UX/ui-launcherconfig.ahk
@@ -1,183 +1,191 @@
-; This script shows a GUI for configuring the launcher.
-#requires AutoHotkey v2.0
-
-#NoTrayIcon
-
-#include inc\launcher-common.ahk
-#include inc\ui-base.ahk
-
-GetVersions() {
- vmap := Map(1, Map(), 2, Map())
- for ,f in GetUsableAutoHotkeyExes() {
- try
- vmap[GetMajor(f.Version)][f.Version] := true
- catch as e
- trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message
- }
- vmap[1] := [vmap[1]*]
- vmap[2] := [vmap[2]*]
- return vmap
-}
-
-class LauncherConfigGui extends AutoHotkeyUxGui {
- __new() {
- super.__new("AutoHotkey Launch Config")
-
- cmd := RegRead('HKCR\AutoHotkeyScript\shell\open\command',, '')
- usingLauncher := InStr(cmd, 'UX\launcher.ahk') != 0
- currentExe := !usingLauncher && RegExMatch(cmd, '^"(.*?)"(?= )', &m) ? m.1 : ""
- try
- if currentExe && GetExeInfo(currentExe).Description = "AutoHotkey Launcher" ; Support compiled launcher
- usingLauncher := true, currentExe := ""
- if InStr(currentExe, '\AutoHotkeyUX.exe')
- currentExe := "" ; Don't default to AutoHotkeyUX.exe when disabling launcher
-
- versions := GetVersions()
-
- ; this.AddCheckbox("Checked", "Enable drag && drop on .ahk files")
-
- this.AddRadio('vUseLauncher Checked' usingLauncher, "Auto-detect version when launching script")
- .OnEvent('Click', 'ChangedMode')
- this.AddRadio('vUseSpecific Checked' (!usingLauncher), "Run all scripts with a specific interpreter")
- .OnEvent('Click', 'ChangedMode')
-
- tab := this.AddTab('w0 h0 y+0 vTab -TabStop', ["Launcher", "Specific"])
-
- tab.UseTab(1)
- this.AddText('xm yp+12 Section', "Preferred interpreter by major version")
- this.AddDDL('vVersion1 y+3 w110 Choose1', ["Latest 1.x", versions[1]*])
- .OnEvent('Change', "ChangedVersion")
- this.AddComboBox('vBuild1 yp w150', ["Unicode 64-bit", "Unicode 32-bit", "ANSI 32-bit"])
- .OnEvent('Change', 'ChangedBuild')
- this.AddCheckBox('vUIA1 x+m yp+2', "UI Access")
- .OnEvent('Click', 'ChangedUIA')
- this.AddDDL('vVersion2 xs w110 Choose1', ["Latest 2.x", versions[2]*])
- .OnEvent('Change', 'ChangedVersion')
- this.AddComboBox('vBuild2 yp w150', ["64-bit", "32-bit"])
- .OnEvent('Change', 'ChangedBuild')
- this.AddCheckBox('vUIA2 x+m yp+2', "UI Access")
- .OnEvent('Click', 'ChangedUIA')
- this.AddText('xs y+m+8', "When detection fails")
- this.AddDDL('vFallback y+3 w110 Choose3', ["Use v1.x", "Use v2.x", "Ask the user"])
- .OnEvent('Change', "ChangedFallback")
- this.AddCheckbox('vIdentify xs y+m+8 Checked', "Try to identify version based on syntax")
- .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher', 'Identify'))
- this.AddCheckbox('vLauncherUTF8 xs Checked', "Default to UTF-8 even for v1 scripts")
- .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher\v1', 'UTF8'))
-
- tab.UseTab(2)
- exeBox := this.AddEdit('vExePath xs ys w326 ReadOnly')
- if currentExe
- exeBox.Text := currentExe
- else if FileExist(f := ROOT_DIR '\v2\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe')
- exeBox.Text := f
-
- static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' SysGet(49), &imgtype)
- this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse")
- .OnEvent('Click', 'BrowseForExe')
-
- this.AddCheckBox('vCustomUTF8 xm y+m+4 Hidden', "Default to UTF-8")
- .OnEvent('Click', 'UpdateVerbs')
-
- tab.UseTab()
- this.AddButton('vClose x292 w70 Default', "&Close")
- .OnEvent('Click', (ctrl, *) => ctrl.Gui.Hide())
-
- ; size := 32 * A_ScreenDPI // 96
- ; DllCall("comctl32\LoadIconWithScaleDown", "ptr", 0, "int", 32514, "int", size, "int", size, "ptr*", &icon:=0, "hresult")
- ; help := this.AddButton('0x40 x322 ym w' 40 ' h' 40)
- ; SendMessage(0xF7, 1, icon, help)
-
- tab.Choose(usingLauncher ? 1 : 2)
-
- Loop 2 {
- section := 'Launcher\v' A_Index
- v := ConfigRead(section, 'Version', '')
- try this['Version' A_Index].Text := v || 'Latest ' A_Index '.x'
- try this['Build' A_Index].Text := ConfigRead(section, 'Build', '')
- try this['UIA' A_Index].Value := ConfigRead(section, 'UIA', false)
- if this['Build' A_Index].Text = ""
- ControlChooseIndex(1, this['Build' A_Index])
- }
- v := ConfigRead('Launcher', 'Fallback', '')
- IsInteger(v) && this['Fallback'].Value := v
- this['Identify'].Value := ConfigRead('Launcher', 'Identify', true)
- this['LauncherUTF8'].Value := ConfigRead('Launcher\v1', 'UTF8', false)
-
- this.ChangedMode()
- this.ChangedExe()
- }
-
- ChangedMode(sourceCtrl:=false, *) {
- usingLauncher := this['UseLauncher'].Value
- this['Tab'].Choose(usingLauncher ? 1 : 2)
- this[usingLauncher ? 'LauncherUTF8' : 'CustomUTF8'].GetPos(, &y1, , &h1)
- this['Close'].GetPos(,,, &h2)
- this['Close'].Move(, y1)
- this.Show('h' (y1 + h2 + 10))
- if sourceCtrl
- this.UpdateVerbs()
- }
-
- ChangedExe() {
- exe := this['ExePath'].Text
- if exe = ""
- return
- try
- exeVersion := FileGetVersion(exe)
- catch {
- MsgBox "The selected EXE appears to be invalid.`n`nSpecifically: " exe,, 'Icon!'
- return
- }
- if !this['CustomUTF8'].Visible && this['LauncherUTF8'].Value
- this['CustomUTF8'].Value := true
- this['CustomUTF8'].Visible := VerCompare(exeVersion, '2') < 0
- }
-
- ChangedVersion(ctrl, *) =>
- ConfigWrite(ctrl.Value = 1 ? '' : ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Version')
-
- ChangedBuild(ctrl, *) =>
- ConfigWrite(ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Build')
-
- ChangedUIA(ctrl, *) =>
- ConfigWrite(ctrl.Value, 'Launcher\v' SubStr(ctrl.Name, -1), 'UIA')
-
- ChangedFallback(ctrl, *) =>
- ConfigWrite(ctrl.Value = 3 ? '' : ctrl.Value, 'Launcher', 'Fallback')
-
- BrowseForExe(*) {
- exe := this.FileSelect('3', this['ExePath'].Text, "Select an AutoHotkey.exe", "EXE Files (*.exe)")
- if exe = ""
- return
- this['ExePath'].Text := exe
- this.ChangedExe()
- this.UpdateVerbs()
- }
-
- UpdateVerbs(*) {
- ; FIXME: reg key and FriendlyAppName should be defined in only one place
- static key := 'HKCU\Software\Classes\AutoHotkeyScript\Shell\'
- if this['UseLauncher'].Value {
- cmd := Format('"{1}" "{2}\launcher.ahk" "%1" %*', A_AhkPath, A_ScriptDir)
- appname := "AutoHotkey Launcher"
- } else {
- exe := this['ExePath'].Text
- if exe = ""
- return
- if !FileExist(exe)
- throw
- cmd := Format('"{1}" {2}"%1" %*', exe, this['CustomUTF8'].Value ? '/cp65001 ' : '')
- ; Deleting FriendlyAppName from HKCU won't work if the installer uses HKLM,
- ; so explicitly set it to the file's description
- appname := GetExeInfo(exe).Description
- }
- RegWrite appname, 'REG_SZ', key 'Open', 'FriendlyAppName'
- RegWrite cmd, 'REG_SZ', key 'Open\Command'
- if RegRead('HKCR\AutoHotkeyScript\Shell\RunAs',, '')
- RegWrite cmd, 'REG_SZ', key 'RunAs\Command'
- }
-}
-
-if A_ScriptFullPath = A_LineFile
+; This script shows a GUI for configuring the launcher.
+#requires AutoHotkey v2.0
+
+#NoTrayIcon
+
+#include inc\launcher-common.ahk
+#include inc\ui-base.ahk
+
+GetVersions() {
+ vmap := Map(1, Map(), 2, Map())
+ for ,f in GetUsableAutoHotkeyExes() {
+ try
+ vmap[GetMajor(f.Version)][f.Version] := true
+ catch as e
+ trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message
+ }
+ vmap[1] := [vmap[1]*]
+ vmap[2] := [vmap[2]*]
+ return vmap
+}
+
+class LauncherConfigGui extends AutoHotkeyUxGui {
+ __new() {
+ super.__new("AutoHotkey Launch Config")
+
+ cmd := RegRead('HKCR\AutoHotkeyScript\shell\open\command',, '')
+ usingLauncher := InStr(cmd, 'UX\launcher.ahk') != 0
+ currentExe := !usingLauncher && RegExMatch(cmd, '^"(.*?)"(?= )', &m) ? m.1 : ""
+ try
+ if currentExe && GetExeInfo(currentExe).Description = "AutoHotkey Launcher" ; Support compiled launcher
+ usingLauncher := true, currentExe := ""
+ if InStr(currentExe, '\AutoHotkeyUX.exe')
+ currentExe := "" ; Don't default to AutoHotkeyUX.exe when disabling launcher
+
+ versions := GetVersions()
+
+ ; this.AddCheckbox("Checked", "Enable drag && drop on .ahk files")
+
+ this.AddRadio('vUseLauncher Checked' usingLauncher, "Auto-detect version when launching script")
+ .OnEvent('Click', 'ChangedMode')
+ this.AddRadio('vUseSpecific Checked' (!usingLauncher), "Run all scripts with a specific interpreter")
+ .OnEvent('Click', 'ChangedMode')
+
+ tab := this.AddTab('w0 h0 y+0 vTab -TabStop', ["Launcher", "Specific"])
+
+ tab.UseTab(1)
+ this.AddText('xm yp+12 Section', "Preferred interpreter by major version")
+ this.AddDDL('vVersion1 y+3 w110 Choose1', ["Latest 1.x", versions[1]*])
+ .OnEvent('Change', "ChangedVersion")
+ this.AddComboBox('vBuild1 yp w150', ["Unicode 64-bit", "Unicode 32-bit", "ANSI 32-bit"])
+ .OnEvent('Change', 'ChangedBuild')
+ this.AddCheckBox('vUIA1 x+m yp+2', "UI Access")
+ .OnEvent('Click', 'ChangedUIA')
+ this.AddDDL('vVersion2 xs w110 Choose1', ["Latest 2.x", versions[2]*])
+ .OnEvent('Change', 'ChangedVersion')
+ this.AddComboBox('vBuild2 yp w150', ["64-bit", "32-bit"])
+ .OnEvent('Change', 'ChangedBuild')
+ this.AddCheckBox('vUIA2 x+m yp+2', "UI Access")
+ .OnEvent('Click', 'ChangedUIA')
+ this.AddText('xs y+m+8', "When detection fails")
+ this.AddDDL('vFallback y+3 w110 Choose3', ["Use v1.x", "Use v2.x", "Ask the user"])
+ .OnEvent('Change', "ChangedFallback")
+ this.AddCheckbox('vIdentify xs y+m+8 Checked', "Try to identify version based on syntax")
+ .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher', 'Identify'))
+ this.AddCheckbox('vLauncherUTF8 xs Checked', "Default to UTF-8 even for v1 scripts")
+ .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher\v1', 'UTF8'))
+
+ tab.UseTab(2)
+ exeBox := this.AddEdit('vExePath xs ys w326 ReadOnly')
+ if currentExe
+ exeBox.Text := currentExe
+ else if FileExist(f := ROOT_DIR '\v2\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe')
+ exeBox.Text := f
+
+ static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' SysGet(49), &imgtype)
+ this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse")
+ .OnEvent('Click', 'BrowseForExe')
+
+ this.AddCheckBox('vCustomUTF8 xm y+m+4 Hidden', "Default to UTF-8")
+ .OnEvent('Click', 'UpdateVerbs')
+
+ tab.UseTab()
+ this.AddButton('vClose x292 w70 Default', "&Close")
+ .OnEvent('Click', (ctrl, *) => ctrl.Gui.Hide())
+
+ ; size := 32 * A_ScreenDPI // 96
+ ; DllCall("comctl32\LoadIconWithScaleDown", "ptr", 0, "int", 32514, "int", size, "int", size, "ptr*", &icon:=0, "hresult")
+ ; help := this.AddButton('0x40 x322 ym w' 40 ' h' 40)
+ ; SendMessage(0xF7, 1, icon, help)
+
+ tab.Choose(usingLauncher ? 1 : 2)
+
+ Loop 2 {
+ section := 'Launcher\v' A_Index
+ v := ConfigRead(section, 'Version', '')
+ try this['Version' A_Index].Text := v || 'Latest ' A_Index '.x'
+ try this['Build' A_Index].Text := ConfigRead(section, 'Build', '')
+ try this['UIA' A_Index].Value := ConfigRead(section, 'UIA', false)
+ if this['Build' A_Index].Text = ""
+ ControlChooseIndex(1, this['Build' A_Index])
+ }
+ v := ConfigRead('Launcher', 'Fallback', '')
+ IsInteger(v) && this['Fallback'].Value := v
+ this['Identify'].Value := ConfigRead('Launcher', 'Identify', true)
+ this['LauncherUTF8'].Value := ConfigRead('Launcher\v1', 'UTF8', false)
+
+ this.ChangedMode()
+ this.ChangedExe()
+ }
+
+ ChangedMode(sourceCtrl:=false, *) {
+ usingLauncher := this['UseLauncher'].Value
+ this['Tab'].Choose(usingLauncher ? 1 : 2)
+ this[usingLauncher ? 'LauncherUTF8' : 'CustomUTF8'].GetPos(, &y1, , &h1)
+ this['Close'].GetPos(,,, &h2)
+ this['Close'].Move(, y1)
+ this.Show('h' (y1 + h2 + 10))
+ if sourceCtrl
+ this.UpdateVerbs()
+ }
+
+ ChangedExe() {
+ exe := this['ExePath'].Text
+ if exe = ""
+ return
+ try
+ exeVersion := FileGetVersion(exe)
+ catch {
+ MsgBox "The selected EXE appears to be invalid.`n`nSpecifically: " exe,, 'Icon!'
+ return
+ }
+ if !this['CustomUTF8'].Visible && this['LauncherUTF8'].Value
+ this['CustomUTF8'].Value := true
+ this['CustomUTF8'].Visible := VerCompare(exeVersion, '2') < 0
+ }
+
+ ChangedVersion(ctrl, *) =>
+ ConfigWrite(ctrl.Value = 1 ? '' : ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Version')
+
+ ChangedBuild(ctrl, *) =>
+ ConfigWrite(ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Build')
+
+ ChangedUIA(ctrl, *) =>
+ ConfigWrite(ctrl.Value, 'Launcher\v' SubStr(ctrl.Name, -1), 'UIA')
+
+ ChangedFallback(ctrl, *) =>
+ ConfigWrite(ctrl.Value = 3 ? '' : ctrl.Value, 'Launcher', 'Fallback')
+
+ BrowseForExe(*) {
+ exe := this.FileSelect('3', this['ExePath'].Text, "Select an AutoHotkey.exe", "EXE Files (*.exe)")
+ if exe = ""
+ return
+ this['ExePath'].Text := exe
+ this.ChangedExe()
+ this.UpdateVerbs()
+ }
+
+ UpdateVerbs(*) {
+ ; FIXME: reg key and FriendlyAppName should be defined in only one place
+ static key := 'HKCU\Software\Classes\AutoHotkeyScript\Shell\'
+ if this['UseLauncher'].Value {
+ cmd := Format('"{1}" "{2}\launcher.ahk" "%1" %*', A_AhkPath, A_ScriptDir)
+ appname := "AutoHotkey Launcher"
+ } else {
+ exe := this['ExePath'].Text
+ if exe = ""
+ return
+ if !FileExist(exe)
+ throw
+ exe_uia := SubStr(exe, 1, -4) "_UIA.exe"
+ switches := this['CustomUTF8'].Visible && this['CustomUTF8'].Value ? '/cp65001 ' : ''
+ cmd := Format('"{1}" {2}"%1" %*', exe, switches)
+ ; Deleting FriendlyAppName from HKCU won't work if the installer uses HKLM,
+ ; so explicitly set it to the file's description
+ appname := GetExeInfo(exe).Description
+ }
+ RegWrite appname, 'REG_SZ', key 'Open', 'FriendlyAppName'
+ RegWrite cmd, 'REG_SZ', key 'Open\Command'
+ RegWrite cmd, 'REG_SZ', key 'RunAs\Command'
+ if RegRead('HKCR\AutoHotkeyScript\Shell\UIAccess\Command',, '') {
+ if IsSet(exe_uia) && FileExist(exe_uia)
+ cmd := StrReplace(cmd, exe, exe_uia)
+ else
+ cmd := Format('"{1}" "{2}\launcher.ahk" /runwith UIA "%1" %*', A_AhkPath, A_ScriptDir)
+ RegWrite cmd, 'REG_SZ', key 'UIAccess\Command'
+ }
+ }
+}
+
+if A_ScriptFullPath = A_LineFile
LauncherConfigGui.Show()
\ No newline at end of file
diff --git a/UX/ui-newscript.ahk b/UX/ui-newscript.ahk
index e5881f9..7f5b205 100644
--- a/UX/ui-newscript.ahk
+++ b/UX/ui-newscript.ahk
@@ -1,296 +1,296 @@
-#requires AutoHotkey v2.0
-
-#NoTrayIcon
-#SingleInstance Off
-
-#include inc\common.ahk
-#include inc\ui-base.ahk
-
-class NewScriptGui extends AutoHotkeyUxGui {
- __new(path:="") {
- super.__new("New Script")
-
- SplitPath path,, &dir,, &name
- if this.ExplorerHwnd := WinActive("ahk_class CabinetWClass") {
- this.Opt '+Owner' this.ExplorerHwnd
- if dir = ""
- dir := GetPathForExplorerWindow(this.ExplorerHwnd)
- }
- if dir = ""
- dir := ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey")
-
- name := this.AddEdit('vName w272', name != "New AutoHotkey Script" ? name : "")
- static EM_SETCUEBANNER := 0x1501
- SendMessage(EM_SETCUEBANNER, true, StrPtr("Untitled"), name)
-
- static IconSize := SysGet(49) ; SM_CXSMICON
-
- static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' IconSize, &imgtype)
- this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse")
- .OnEvent('Click', 'Browse')
-
- this.AddEdit('vDir xm w300 r1 ReadOnly -TabStop', dir)
-
- static LVS_SHOWSELALWAYS := 8 ; Seems to have the opposite effect with Explorer theme, at least on Windows 11.
- lv := this.AddListMenu("vLV xm w300 -" LVS_SHOWSELALWAYS, ["Name", "Desc", "Path", "Exec"])
- lv.OnEvent('DoubleClick', 'DoubleClicked')
- lv.OnEvent('ContextMenu', 'RightClicked')
-
- deft := GetDefaultTemplate()
- lv.Add(deft = "" ? 'Select' : '', "Empty", "Clean slate")
- for ,t in this.Templates := GetScriptTemplates() {
- if ConfigRead('New\HideTemplate', t.name, false)
- continue
- lv.Add(deft = t.name ? 'Select Focus' : '', t.name, t.desc)
- }
-
- lv.AutoSize(8)
- lv.GetPos(&x, &y, &w, &h)
-
- static DefaultsIcon := LoadPicture("imageres.dll", 'Icon-114 w' IconSize, &imgtype)
- this.AddIconButton('vDefaults r1 w28 xm y' y + h + this.MarginY, DefaultsIcon, "&Defaults")
- .OnEvent('Click', 'ChangeDefaults')
-
- this.AddButton('vCreate yp w75 x150', "&Create").OnEvent('Click', 'Confirm')
- this.AddButton('vEdit Default yp wp xm+225', "&Edit").OnEvent('Click', 'Confirm')
- if ConfigRead('New', 'DefaultButton', 'Edit') = 'Create'
- this['Create'].Opt('Default')
-
- if this.ExplorerHwnd {
- this.Show('AutoSize Hide')
- WinGetPos(,, &gw, &gh, this)
- WinGetPos(&x, &y, &ew, &eh)
- WinMove(x + (ew - gw) // 2, y + (eh - gh) // 2,,, this)
- }
- }
-
- static __new() {
- OnMessage(WM_KEYDOWN := 0x100, KeyDown)
- KeyDown(wParam, lParam, nmsg, hwnd) {
- static VK_UP := 0x26
- static VK_DOWN := 0x28
- if !(wParam = VK_UP || wParam = VK_DOWN)
- return
- g := GuiFromHwnd(hwnd, true)
- if g is NewScriptGui && g.FocusedCtrl is Gui.Edit {
- PostMessage nmsg, wParam, lParam, g['LV']
- return true
- }
- }
- }
-
- Browse(*) {
- path := this.FileSelect('S', this['Dir'].Value "\" this['Name'].Value, this.Title, "Script Files (*.ahk)")
- if path = ""
- return
- SplitPath path, &name, &dir
- this['Name'].Value := name
- this['Dir'].Value := dir
- this['LV'].Focus()
- }
-
- ChangeDefaults(btn, *) {
- lv := this["LV"]
-
- m := Menu()
- m.Add "Default to Create", setDefBtn, "Radio"
- m.Add "Default to Edit", setDefBtn, "Radio"
- m.Add "Stay open", toggleStayOpen
- if stayOpen := ConfigRead('New', 'StayOpen', false)
- m.Check "Stay open"
- m.Add
- m.Add "Set folder as default", (*) => SetDefaultDir(this['Dir'].Value)
- if this['Dir'].Value = GetDefaultDir()
- m.Check "Set folder as default"
- m.Add "Open templates folder", (*) => OpenTemplatesFolder()
-
- static DM_GETDEFID := 0x400
- m.Check (1 + (SendMessage(DM_GETDEFID,,, this) & 0xFFFF = DllCall('GetDlgCtrlID', 'ptr', this['Edit'].Hwnd))) "&"
-
- this.Opt "-DPIScale"
- btn.GetPos &x, &y, &w, &h
- ;btn.Focus ; This would also apply the "default" style, which would not revert correctly.
- ControlFocus btn
- m.Show x, y + h
-
- setDefBtn(itemname, itempos, *) {
- itemname := SubStr(itemname, 12)
- this[itemname].Opt('Default')
- ConfigWrite(itemname, 'New', 'DefaultButton')
- }
- toggleStayOpen(*) {
- ConfigWrite(stayOpen := !stayOpen, 'New', 'StayOpen')
- }
- }
-
- Confirm(btn, *) {
- lv := this['LV']
- if !index := lv.GetNext()
- return MsgBox("You need to select a template first.",, 'icon!')
- t := index = 1 ? '' : this.Templates[lv.GetText(index)]
-
- stayOpen := GetKeyState('Ctrl') || ConfigRead('New', 'StayOpen', false)
-
- DirCreate dir := this['Dir'].Value
- basename := this['Name'].Value
- (basename != '') || basename := "Untitled"
- SubStr(basename, -4) = ".ahk" && basename := SubStr(basename, 1, -4)
- newPath := dir "\" basename ".ahk"
- while FileExist(newPath)
- newPath := dir "\" basename "-" A_Index ".ahk"
-
- if t && t.execute
- RunWait Format('"{1}" "{2}" {3}', t.path, newPath, btn.Name), dir
- else {
- code := t = '' ? '' : FileRead(t.path)
- code := RegExReplace(code, 'si)/\*\s+\[NewScriptTemplate\].*\*/\R?')
- FileOpen(newPath, 'w', 'UTF-8').Write(code)
- }
-
- if this.ExplorerHwnd && (xp := GetExplorerByHwnd(this.ExplorerHwnd))
- || dir = A_Desktop && (xp := GetExplorerForDesktop()) {
- SplitPath newPath, &basename
- SelectExplorerItem xp, basename
- }
- else if btn.Name != 'Edit'
- Run 'explorer /select,"' newPath '"'
-
- if btn.Name = 'Edit'
- Run 'edit "' newPath '"'
-
- if !stayOpen
- this.Hide
- }
-
- RightClicked(lv, item, isRClick, x, y) {
- if !item
- return
- t := item > 1 ? this.Templates[lv.GetText(item)] : {name: ''}
-
- m := Menu()
- if item > 1 {
- m.Add "&Edit template", (*) => EditTemplate(t)
- m.Add "&Hide template", hideTemplate
- }
- m.Add "Set as &default", (*) => SetDefaultTemplate(t.name)
- if t.name = GetDefaultTemplate()
- m.Check "Set as &default"
- m.Show x, y
-
- hideTemplate(*) {
- ConfigWrite(true, 'New\HideTemplate', t.name)
- lv.Delete item
- static LVM_ARRANGE := 0x1016
- SendMessage LVM_ARRANGE,,, lv ; Fill in any gap left by item (in Tile view).
- }
- }
-
- DoubleClicked(lv, row) {
- if row
- this.Confirm(this.GetDefaultButton())
- }
-
- GetDefaultButton() {
- static DM_GETDEFID := 0x400
- hwnd := DllCall("GetDlgItem", "ptr", this.hwnd, "int", SendMessage(DM_GETDEFID,,, this) & 0xFFFF, "uint")
- return hwnd && this[hwnd]
- }
-}
-
-class NewScriptTemplate {
- __new(path, name:="", description:="") {
- this.path := path
- if name = ""
- SplitPath path,,,, &name
- this.name := name
- this.desc := IniRead(path, 'NewScriptTemplate', 'Description', description)
- this.execute := false
- switch IniRead(path, 'NewScriptTemplate', 'Execute', false) {
- case true, "true": this.execute := true
- }
- }
-}
-
-OpenTemplatesFolder() {
- dir := GetUserTemplateFolder(true)
- if dir != ""
- Run dir "\"
-}
-
-GetUserTemplateFolder(checkAndPrompt:=false) {
- static dir := A_MyDocuments "\AutoHotkey\Templates"
- if checkAndPrompt && !FileExist(dir) {
- if MsgBox("User-created templates should be placed in the following folder, "
- "which does not yet exist:`n`n" dir "`n`nCreate it now?",, "YesNo") = "No"
- return
- DirCreate dir
- }
- return dir
-}
-
-EditTemplate(t) {
- dir := GetUserTemplateFolder(true)
- if dir = ""
- return
- userPath := dir "\" t.name ".ahk"
- if !FileExist(userPath) {
- FileCopy t.path, userPath
- t.path := userPath
- }
- Run 'edit "' userPath '"'
-}
-
-GetScriptTemplates() {
- tmap := Map(), tmap.CaseSense := "off"
- sources := [
- A_ScriptDir "\Templates",
- GetUserTemplateFolder()
- ]
- if FileExist(t := A_WinDir '\ShellNew\Template.ahk')
- tmap["Legacy"] := NewScriptTemplate(t, "Legacy", "From " A_WinDir "\ShellNew")
- for source in sources {
- loop files source "\*.ahk" {
- t := NewScriptTemplate(A_LoopFilePath)
- tmap[t.name] := t
- }
- }
- return tmap
-}
-
-SetDefaultTemplate(t) => ConfigWrite(t = "Empty" ? "" : t, 'New', 'DefaultTemplate')
-GetDefaultTemplate() => ConfigRead('New', 'DefaultTemplate', "")
-
-SetDefaultDir(dir) => ConfigWrite(dir, 'New', 'DefaultDir')
-GetDefaultDir() => ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey")
-
-SelectExplorerItem(e, name) {
- sfv := e.Document
- if fi := sfv.Folder.ParseName(name)
- sfv.SelectItem(fi, 1|4|8|16)
- return e
-}
-
-GetExplorerByHwnd(hwnd) {
- for window in ComObject("Shell.Application").Windows
- if window.hwnd = hwnd
- return window
-}
-
-GetExplorerForDesktop() {
- hwndBuf := Buffer(4, 0), hwndRef := ComValue(0x4003, hwndBuf.Ptr)
- return ComObject("Shell.Application").Windows.FindWindowSW(0, "", 8, hwndRef, 1)
-}
-
-GetPathForExplorerWindow(hwnd) {
- try return GetExplorerByHwnd(hwnd).Document.Folder.Self.Path
-}
-
-GetExplorerByPath(path) {
- for window in ComObject("Shell.Application").Windows
- try if window.Document.Folder.Self.Path = path
- return window
- return ""
-}
-
-if A_ScriptFullPath = A_LineFile
- NewScriptGui.Show(A_Args.Length ? A_Args[1] : "")
+#requires AutoHotkey v2.0
+
+#NoTrayIcon
+#SingleInstance Off
+
+#include inc\common.ahk
+#include inc\ui-base.ahk
+
+class NewScriptGui extends AutoHotkeyUxGui {
+ __new(path:="") {
+ super.__new("New Script")
+
+ SplitPath path,, &dir,, &name
+ if this.ExplorerHwnd := WinActive("ahk_class CabinetWClass") {
+ this.Opt '+Owner' this.ExplorerHwnd
+ if dir = ""
+ dir := GetPathForExplorerWindow(this.ExplorerHwnd)
+ }
+ if dir = ""
+ dir := ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey")
+
+ name := this.AddEdit('vName w272', name != "New AutoHotkey Script" ? name : "")
+ static EM_SETCUEBANNER := 0x1501
+ SendMessage(EM_SETCUEBANNER, true, StrPtr("Untitled"), name)
+
+ static IconSize := SysGet(49) ; SM_CXSMICON
+
+ static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' IconSize, &imgtype)
+ this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse")
+ .OnEvent('Click', 'Browse')
+
+ this.AddEdit('vDir xm w300 r1 ReadOnly -TabStop', dir)
+
+ static LVS_SHOWSELALWAYS := 8 ; Seems to have the opposite effect with Explorer theme, at least on Windows 11.
+ lv := this.AddListMenu("vLV xm w300 -" LVS_SHOWSELALWAYS, ["Name", "Desc", "Path", "Exec"])
+ lv.OnEvent('DoubleClick', 'DoubleClicked')
+ lv.OnEvent('ContextMenu', 'RightClicked')
+
+ deft := GetDefaultTemplate()
+ lv.Add(deft = "" ? 'Select Focus' : '', "Empty", "Clean slate")
+ for ,t in this.Templates := GetScriptTemplates() {
+ if ConfigRead('New\HideTemplate', t.name, false)
+ continue
+ lv.Add(deft = t.name ? 'Select Focus' : '', t.name, t.desc)
+ }
+
+ lv.AutoSize(8)
+ lv.GetPos(&x, &y, &w, &h)
+
+ static DefaultsIcon := LoadPicture("imageres.dll", 'Icon-114 w' IconSize, &imgtype)
+ this.AddIconButton('vDefaults r1 w28 xm y' y + h + this.MarginY, DefaultsIcon, "&Defaults")
+ .OnEvent('Click', 'ChangeDefaults')
+
+ this.AddButton('vCreate yp w75 x150', "&Create").OnEvent('Click', 'Confirm')
+ this.AddButton('vEdit Default yp wp xm+225', "&Edit").OnEvent('Click', 'Confirm')
+ if ConfigRead('New', 'DefaultButton', 'Edit') = 'Create'
+ this['Create'].Opt('Default')
+
+ if this.ExplorerHwnd {
+ this.Show('AutoSize Hide')
+ WinGetPos(,, &gw, &gh, this)
+ WinGetPos(&x, &y, &ew, &eh)
+ WinMove(x + (ew - gw) // 2, y + (eh - gh) // 2,,, this)
+ }
+ }
+
+ static __new() {
+ OnMessage(WM_KEYDOWN := 0x100, KeyDown)
+ KeyDown(wParam, lParam, nmsg, hwnd) {
+ static VK_UP := 0x26
+ static VK_DOWN := 0x28
+ if !(wParam = VK_UP || wParam = VK_DOWN)
+ return
+ gc := GuiCtrlFromHwnd(hwnd)
+ if gc.Gui is NewScriptGui && gc is Gui.Edit {
+ PostMessage nmsg, wParam, lParam, gc.Gui['LV']
+ return true
+ }
+ }
+ }
+
+ Browse(*) {
+ path := this.FileSelect('S', this['Dir'].Value "\" this['Name'].Value, this.Title, "Script Files (*.ahk)")
+ if path = ""
+ return
+ SplitPath path, &name, &dir
+ this['Name'].Value := name
+ this['Dir'].Value := dir
+ this['LV'].Focus()
+ }
+
+ ChangeDefaults(btn, *) {
+ lv := this["LV"]
+
+ m := Menu()
+ m.Add "Default to Create", setDefBtn, "Radio"
+ m.Add "Default to Edit", setDefBtn, "Radio"
+ m.Add "Stay open", toggleStayOpen
+ if stayOpen := ConfigRead('New', 'StayOpen', false)
+ m.Check "Stay open"
+ m.Add
+ m.Add "Set folder as default", (*) => SetDefaultDir(this['Dir'].Value)
+ if this['Dir'].Value = GetDefaultDir()
+ m.Check "Set folder as default"
+ m.Add "Open templates folder", (*) => OpenTemplatesFolder()
+
+ static DM_GETDEFID := 0x400
+ m.Check (1 + (SendMessage(DM_GETDEFID,,, this) & 0xFFFF = DllCall('GetDlgCtrlID', 'ptr', this['Edit'].Hwnd))) "&"
+
+ this.Opt "-DPIScale"
+ btn.GetPos &x, &y, &w, &h
+ ;btn.Focus ; This would also apply the "default" style, which would not revert correctly.
+ ControlFocus btn
+ m.Show x, y + h
+
+ setDefBtn(itemname, itempos, *) {
+ itemname := SubStr(itemname, 12)
+ this[itemname].Opt('Default')
+ ConfigWrite(itemname, 'New', 'DefaultButton')
+ }
+ toggleStayOpen(*) {
+ ConfigWrite(stayOpen := !stayOpen, 'New', 'StayOpen')
+ }
+ }
+
+ Confirm(btn, *) {
+ lv := this['LV']
+ if !index := lv.GetNext()
+ return MsgBox("You need to select a template first.",, 'icon!')
+ t := index = 1 ? '' : this.Templates[lv.GetText(index)]
+
+ stayOpen := GetKeyState('Ctrl') || ConfigRead('New', 'StayOpen', false)
+
+ DirCreate dir := this['Dir'].Value
+ basename := this['Name'].Value
+ (basename != '') || basename := "Untitled"
+ SubStr(basename, -4) = ".ahk" && basename := SubStr(basename, 1, -4)
+ newPath := dir "\" basename ".ahk"
+ while FileExist(newPath)
+ newPath := dir "\" basename "-" A_Index ".ahk"
+
+ if t && t.execute
+ RunWait Format('"{1}" "{2}" {3}', t.path, newPath, btn.Name), dir
+ else {
+ code := t = '' ? '' : FileRead(t.path)
+ code := RegExReplace(code, 'si)/\*\s+\[NewScriptTemplate\].*\*/\R?')
+ FileOpen(newPath, 'w', 'UTF-8').Write(code)
+ }
+
+ if this.ExplorerHwnd && (xp := GetExplorerByHwnd(this.ExplorerHwnd))
+ || dir = A_Desktop && (xp := GetExplorerForDesktop()) {
+ SplitPath newPath, &basename
+ SelectExplorerItem xp, basename
+ }
+ else if btn.Name != 'Edit'
+ Run 'explorer /select,"' newPath '"'
+
+ if btn.Name = 'Edit'
+ Run 'edit "' newPath '"'
+
+ if !stayOpen
+ this.Hide
+ }
+
+ RightClicked(lv, item, isRClick, x, y) {
+ if !item
+ return
+ t := item > 1 ? this.Templates[lv.GetText(item)] : {name: ''}
+
+ m := Menu()
+ if item > 1 {
+ m.Add "&Edit template", (*) => EditTemplate(t)
+ m.Add "&Hide template", hideTemplate
+ }
+ m.Add "Set as &default", (*) => SetDefaultTemplate(t.name)
+ if t.name = GetDefaultTemplate()
+ m.Check "Set as &default"
+ m.Show x, y
+
+ hideTemplate(*) {
+ ConfigWrite(true, 'New\HideTemplate', t.name)
+ lv.Delete item
+ static LVM_ARRANGE := 0x1016
+ SendMessage LVM_ARRANGE,,, lv ; Fill in any gap left by item (in Tile view).
+ }
+ }
+
+ DoubleClicked(lv, row) {
+ if row
+ this.Confirm(this.GetDefaultButton())
+ }
+
+ GetDefaultButton() {
+ static DM_GETDEFID := 0x400
+ hwnd := DllCall("GetDlgItem", "ptr", this.hwnd, "int", SendMessage(DM_GETDEFID,,, this) & 0xFFFF, "uint")
+ return hwnd && this[hwnd]
+ }
+}
+
+class NewScriptTemplate {
+ __new(path, name:="", description:="") {
+ this.path := path
+ if name = ""
+ SplitPath path,,,, &name
+ this.name := name
+ this.desc := IniRead(path, 'NewScriptTemplate', 'Description', description)
+ this.execute := false
+ switch IniRead(path, 'NewScriptTemplate', 'Execute', false) {
+ case true, "true": this.execute := true
+ }
+ }
+}
+
+OpenTemplatesFolder() {
+ dir := GetUserTemplateFolder(true)
+ if dir != ""
+ Run dir "\"
+}
+
+GetUserTemplateFolder(checkAndPrompt:=false) {
+ static dir := A_MyDocuments "\AutoHotkey\Templates"
+ if checkAndPrompt && !FileExist(dir) {
+ if MsgBox("User-created templates should be placed in the following folder, "
+ "which does not yet exist:`n`n" dir "`n`nCreate it now?",, "YesNo") = "No"
+ return
+ DirCreate dir
+ }
+ return dir
+}
+
+EditTemplate(t) {
+ dir := GetUserTemplateFolder(true)
+ if dir = ""
+ return
+ userPath := dir "\" t.name ".ahk"
+ if !FileExist(userPath) {
+ FileCopy t.path, userPath
+ t.path := userPath
+ }
+ Run 'edit "' userPath '"'
+}
+
+GetScriptTemplates() {
+ tmap := Map(), tmap.CaseSense := "off"
+ sources := [
+ A_ScriptDir "\Templates",
+ GetUserTemplateFolder()
+ ]
+ if FileExist(t := A_WinDir '\ShellNew\Template.ahk')
+ tmap["Legacy"] := NewScriptTemplate(t, "Legacy", "From " A_WinDir "\ShellNew")
+ for source in sources {
+ loop files source "\*.ahk" {
+ t := NewScriptTemplate(A_LoopFilePath)
+ tmap[t.name] := t
+ }
+ }
+ return tmap
+}
+
+SetDefaultTemplate(t) => ConfigWrite(t = "Empty" ? "" : t, 'New', 'DefaultTemplate')
+GetDefaultTemplate() => ConfigRead('New', 'DefaultTemplate', "")
+
+SetDefaultDir(dir) => ConfigWrite(dir, 'New', 'DefaultDir')
+GetDefaultDir() => ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey")
+
+SelectExplorerItem(e, name) {
+ sfv := e.Document
+ if fi := sfv.Folder.ParseName(name)
+ sfv.SelectItem(fi, 1|4|8|16)
+ return e
+}
+
+GetExplorerByHwnd(hwnd) {
+ for window in ComObject("Shell.Application").Windows
+ if window.hwnd = hwnd
+ return window
+}
+
+GetExplorerForDesktop() {
+ hwndBuf := Buffer(4, 0), hwndRef := ComValue(0x4003, hwndBuf.Ptr)
+ return ComObject("Shell.Application").Windows.FindWindowSW(0, "", 8, hwndRef, 1)
+}
+
+GetPathForExplorerWindow(hwnd) {
+ try return GetExplorerByHwnd(hwnd).Document.Folder.Self.Path
+}
+
+GetExplorerByPath(path) {
+ for window in ComObject("Shell.Application").Windows
+ try if window.Document.Folder.Self.Path = path
+ return window
+ return ""
+}
+
+if A_ScriptFullPath = A_LineFile
+ NewScriptGui.Show(A_Args.Length ? A_Args[1] : "")
diff --git a/UX/ui-setup.ahk b/UX/ui-setup.ahk
index 396f914..65673ad 100644
--- a/UX/ui-setup.ahk
+++ b/UX/ui-setup.ahk
@@ -1,171 +1,175 @@
-; This script shows the initial setup GUI.
-; It is not intended for use after installation.
-#requires AutoHotkey v2.0
-
-#NoTrayIcon
-#SingleInstance Force
-
-#include inc\ui-base.ahk
-
-A_ScriptName := "AutoHotkey Setup"
-SetRegView 64
-InstallGui.Show()
-
-class InstallGui extends AutoHotkeyUxGui {
- __new() {
- super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox')
-
- DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT
- , 'int64*', 3 | (3<<32), 'int', 8) ; WTNCA_NODRAWCAPTION=1, WTNCA_NODRAWICON=2
-
- static TitleBack := 'BackgroundWhite'
- static TitleFore := 'c3F627F'
- static TotalWidth := 350
- this.AddText('x0 y0 w' TotalWidth ' h84 ' TitleBack)
- this.AddPicture('x32 y16 w32 h32 ' TitleBack, A_AhkPath)
- this.SetFont('s12', 'Segoe UI')
- this.AddText('x+20 yp+4 ' TitleFore ' ' TitleBack, "AutoHotkey v" A_AhkVersion)
- this.SetFont('s9')
-
- ; SS_SUNKEN := 0x1000 ; 4096
- this.AddText('x-4 y84 w' TotalWidth+4 ' h188 0x1000 -Background')
-
- this.AddText('xm yp+16', 'Install &to:')
- dirEdit := this.AddEdit('vInstallDir w' TotalWidth - (2 * this.MarginX) - 88)
- this.AddButton('vBrowseButton w80 x+8 yp-1', '&Browse')
- .OnEvent('Click', 'Browse')
-
- rect := Buffer(16, 0)
- DllCall('GetClientRect', 'ptr', dirEdit.hwnd, 'ptr', rect)
- NumPut('int', 4, 'int', 2, rect)
- ; DllCall('InflateRect', 'ptr', rect, 'int', 0, 'int', )
- EM_SETRECT := 0xB3 ; 179
- SendMessage 0xB3, 0, rect.ptr, dirEdit
-
- this.AddGroupBox('xm y+0 w' TotalWidth - (2 * this.MarginX) ' h44')
- this.AddText('xp+8 yp+16', "Install mode:")
- this.AddRadio('vModeAll x+m yp Checked', "&All users")
- .OnEvent('Click', 'ModeChange')
- this.AddRadio('vModeUser x+4', "&Current user")
- .OnEvent('Click', 'ModeChange')
- this.AddRadio('vModePortable x+4 Disabled', "Portable")
- .OnEvent('Click', 'ModeChange')
-
- this.AddButton('vInstallButton x' (TotalWidth - 80) // 2 ' w80 y+36 Default', "&Install")
- .OnEvent('Click', 'Install')
-
- this.ModeChange()
-
- this.MarginY := -1
- this.Show('Hide w' TotalWidth)
- this['InstallButton'].Focus()
- }
-
- Browse(*) {
- if ControlGetStyle(this['InstallDir']) & 0x800 { ; ES_READONLY
- rootKey := this['ModeAll'].Value ? 'HKLM' : 'HKCU'
- message := "Changing the installation directory is not recommended."
- if InStr(RegRead(rootKey '\Software\AutoHotkey', 'Version', ''), '1.') = 1
- message .= "`n`nThis installation package is designed to allow both v1 and v2 to be associated with .ahk files, but only if they are installed in the same directory."
- message .= "`n`nExisting files will not be moved."
- if MsgBox(message,, "OKCancel Icon!") != "OK"
- return
- this['InstallDir'].Opt('-ReadOnly')
- }
- dir := this.FileSelect('D', this['InstallDir'].Value '\', "Select installation directory")
- if dir != '' {
- this['InstallDir'].Value := dir
- this.InstallDirChange()
- }
- }
-
- ModeChange(p*) {
- if !this.CheckAlreadyInstalled(p.Length = 0, true) {
- ; Ensure InstallDir makes sense for the new mode
- static DefaultAllDir := (EnvGet('ProgramW6432') || A_ProgramFiles) '\AutoHotkey'
- static DefaultUserDir := EnvGet('LocalAppData') '\Programs\AutoHotkey'
- installDir := this['InstallDir'].Value
- if this['ModeAll'].Value {
- if installDir = '' || installDir = DefaultUserDir
- this['InstallDir'].Value := DefaultAllDir, this['InstallDir'].Opt('-ReadOnly')
- } else
- if installDir = '' || IsInProgramFiles(installDir)
- this['InstallDir'].Value := DefaultUserDir, this['InstallDir'].Opt('-ReadOnly')
- }
- this.UpdateShield()
- }
-
- InstallDirChange(*) {
- this.UpdateShield()
- }
-
- UpdateShield() {
- requireAdmin := this['ModeAll'].Value && !A_IsAdmin
- SendMessage 0x160C, 0, requireAdmin, this['InstallButton'] ; BCM_SETSHIELD
- }
-
- CheckAlreadyInstalled(setMode:=false, setDir:=false) {
- for rootKey in setMode ? ['HKCU', 'HKLM'] : [this['ModeAll'].Value ? 'HKLM' : 'HKCU'] {
- dir := RegRead(rootKey '\Software\AutoHotkey', 'InstallDir', '')
- if dir != '' {
- if setDir {
- this['InstallDir'].Value := dir
- this['InstallDir'].Opt('+ReadOnly')
- }
- if setMode
- this[A_Index = 1 ? 'ModeUser' : 'ModeAll'].Value := true
- return dir
- }
- }
- return ''
- }
-
- Install(*) {
- problem := ''
- requireAdmin := this['ModeAll'].Value
- installDir := this['InstallDir'].Value
- buf := Buffer(260*2)
- n := DllCall('GetFullPathName', 'str', installDir, 'uint', 260, 'ptr', buf, 'ptr', 0)
- if !n || n > 259 {
- MsgBox "Please enter a valid path.",, 'Icon!'
- return
- }
- fullPath := StrGet(buf)
- if installDir != fullPath {
- problem .= '"' installDir '" resolves to "' fullPath '".`n`n'
- installDir := fullPath
- }
- dir := this.CheckAlreadyInstalled()
- if dir && dir != installDir
- problem .= 'The existing installation in "' dir '" will not be moved or integrated with the new installation.`n`n'
- if requireAdmin && !IsInProgramFiles(installDir) && dir != installDir
- problem .= 'Enabling UI Access will not be possible because the installation directory is not a sub-directory of Program Files. Without UI Access, non-elevated scripts cannot interact with windows of elevated programs.`n`n'
- if problem && MsgBox(problem,, 'OKCancel Default2 Icon!') = 'Cancel'
- return
- if A_IsCompiled && IsSet(Installation)
- cmd := Format('"{1}" /to "{2}"', A_ScriptFullPath, installDir)
- else
- cmd := Format('"{1}" /script "{2}\install.ahk" /to "{3}"', A_AhkPath, A_ScriptDir, installDir)
- if !requireAdmin
- cmd .= ' /user'
- else if !A_IsAdmin
- cmd := '*RunAs ' cmd
- try
- Run cmd,,, &pid
- catch as e {
- if A_LastError != 1223 ; ERROR_CANCELLED
- MsgBox e.Message "`n`n" e.Extra,, 'IconX'
- } else {
- this['InstallButton'].Enabled := false
- this['InstallButton'].Text := "Installing..."
- ProcessWaitClose pid
- ExitApp
- }
- }
-}
-
-IsInProgramFiles(path) {
- other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432")
- return InStr(path, A_ProgramFiles "\") = 1
- || other && InStr(path, other "\") = 1
+; This script shows the initial setup GUI.
+; It is not intended for use after installation.
+#requires AutoHotkey v2.0
+
+#NoTrayIcon
+#SingleInstance Force
+
+#include inc\ui-base.ahk
+
+A_ScriptName := "AutoHotkey Setup"
+SetRegView 64
+InstallGui.Show()
+
+class InstallGui extends AutoHotkeyUxGui {
+ __new() {
+ super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox')
+
+ DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT
+ , 'int64*', 3 | (3<<32), 'int', 8) ; WTNCA_NODRAWCAPTION=1, WTNCA_NODRAWICON=2
+
+ static TitleBack := 'BackgroundWhite'
+ static TitleFore := 'c3F627F'
+ static TotalWidth := 350
+ this.AddText('x0 y0 w' TotalWidth ' h84 ' TitleBack)
+ this.AddPicture('x32 y16 w32 h32 ' TitleBack, A_AhkPath)
+ this.SetFont('s12', 'Segoe UI')
+ this.AddText('x+20 yp+4 ' TitleFore ' ' TitleBack, "AutoHotkey v" A_AhkVersion)
+ this.SetFont('s9')
+
+ ; SS_SUNKEN := 0x1000 ; 4096
+ this.AddText('x-4 y84 w' TotalWidth+4 ' h188 0x1000 -Background')
+
+ this.AddText('xm yp+16', 'Install &to:')
+ dirEdit := this.AddEdit('vInstallDir w' TotalWidth - (2 * this.MarginX) - 88)
+ this.AddButton('vBrowseButton w80 x+8 yp-1', '&Browse')
+ .OnEvent('Click', 'Browse')
+
+ rect := Buffer(16, 0)
+ DllCall('GetClientRect', 'ptr', dirEdit.hwnd, 'ptr', rect)
+ NumPut('int', 4, 'int', 2, rect)
+ ; DllCall('InflateRect', 'ptr', rect, 'int', 0, 'int', )
+ EM_SETRECT := 0xB3 ; 179
+ SendMessage 0xB3, 0, rect.ptr, dirEdit
+
+ this.AddGroupBox('xm y+0 w' TotalWidth - (2 * this.MarginX) ' h44')
+ this.AddText('xp+8 yp+16', "Install mode:")
+ this.AddRadio('vModeAll x+m yp Checked', "&All users")
+ .OnEvent('Click', 'ModeChange')
+ this.AddRadio('vModeUser x+4', "&Current user")
+ .OnEvent('Click', 'ModeChange')
+ this.AddRadio('vModePortable x+4 Disabled', "Portable")
+ .OnEvent('Click', 'ModeChange')
+
+ this.AddButton('vInstallButton x' (TotalWidth - 80) // 2 ' w80 y+36 Default', "&Install")
+ .OnEvent('Click', 'Install')
+
+ this.ModeChange()
+
+ this.MarginY := -1
+ this.Show('Hide w' TotalWidth)
+ this['InstallButton'].Focus()
+ }
+
+ Browse(*) {
+ if ControlGetStyle(this['InstallDir']) & 0x800 { ; ES_READONLY
+ rootKey := this['ModeAll'].Value ? 'HKLM' : 'HKCU'
+ message := "Changing the installation directory is not recommended."
+ if InStr(RegRead(rootKey '\Software\AutoHotkey', 'Version', ''), '1.') = 1
+ message .= "`n`nThis installation package is designed to allow both v1 and v2 to be associated with .ahk files, but only if they are installed in the same directory."
+ message .= "`n`nExisting files will not be moved."
+ if MsgBox(message,, "OKCancel Icon!") != "OK"
+ return
+ this['InstallDir'].Opt('-ReadOnly')
+ }
+ dir := this.FileSelect('D', this['InstallDir'].Value '\', "Select installation directory")
+ if dir != '' {
+ this['InstallDir'].Value := dir
+ this.InstallDirChange()
+ }
+ }
+
+ ModeChange(p*) {
+ if !this.CheckAlreadyInstalled(p.Length = 0, true) {
+ ; Ensure InstallDir makes sense for the new mode
+ installDir := this['InstallDir'].Value
+ if this['ModeAll'].Value {
+ if installDir = '' || installDir = InstallUtil.DefaultUserDir
+ this['InstallDir'].Value := InstallUtil.DefaultAllDir, this['InstallDir'].Opt('-ReadOnly')
+ } else
+ if installDir = '' || IsInProgramFiles(installDir)
+ this['InstallDir'].Value := InstallUtil.DefaultUserDir, this['InstallDir'].Opt('-ReadOnly')
+ }
+ this.UpdateShield()
+ }
+
+ InstallDirChange(*) {
+ this.UpdateShield()
+ }
+
+ UpdateShield() {
+ requireAdmin := this['ModeAll'].Value && !A_IsAdmin
+ SendMessage 0x160C, 0, requireAdmin, this['InstallButton'] ; BCM_SETSHIELD
+ }
+
+ CheckAlreadyInstalled(setMode:=false, setDir:=false) {
+ for rootKey in setMode ? ['HKCU', 'HKLM'] : [this['ModeAll'].Value ? 'HKLM' : 'HKCU'] {
+ dir := RegRead(rootKey '\Software\AutoHotkey', 'InstallDir', '')
+ if dir != '' {
+ if setDir {
+ this['InstallDir'].Value := dir
+ this['InstallDir'].Opt('+ReadOnly')
+ }
+ if setMode
+ this[A_Index = 1 ? 'ModeUser' : 'ModeAll'].Value := true
+ return dir
+ }
+ }
+ return ''
+ }
+
+ Install(*) {
+ problem := ''
+ requireAdmin := this['ModeAll'].Value
+ installDir := this['InstallDir'].Value
+ buf := Buffer(260*2)
+ n := DllCall('GetFullPathName', 'str', installDir, 'uint', 260, 'ptr', buf, 'ptr', 0)
+ if !n || n > 259 {
+ MsgBox "Please enter a valid path.",, 'Icon!'
+ return
+ }
+ fullPath := StrGet(buf)
+ if installDir != fullPath {
+ problem .= '"' installDir '" resolves to "' fullPath '".`n`n'
+ installDir := fullPath
+ }
+ dir := this.CheckAlreadyInstalled()
+ if dir && dir != installDir
+ problem .= 'The existing installation in "' dir '" will not be moved or integrated with the new installation.`n`n'
+ if requireAdmin && !IsInProgramFiles(installDir) && dir != installDir
+ problem .= 'Enabling UI Access will not be possible because the installation directory is not a sub-directory of Program Files. Without UI Access, non-elevated scripts cannot interact with windows of elevated programs.`n`n'
+ if problem && MsgBox(problem,, 'OKCancel Default2 Icon!') = 'Cancel'
+ return
+ if A_IsCompiled && IsSet(Installation)
+ cmd := Format('"{1}" /to "{2}"', A_ScriptFullPath, installDir)
+ else
+ cmd := Format('"{1}" /script "{2}\install.ahk" /to "{3}"', A_AhkPath, A_ScriptDir, installDir)
+ if !requireAdmin
+ cmd .= ' /user'
+ else if !A_IsAdmin
+ cmd := '*RunAs ' cmd
+ try
+ Run cmd,,, &pid
+ catch as e {
+ if A_LastError != 1223 ; ERROR_CANCELLED
+ MsgBox e.Message "`n`n" e.Extra,, 'IconX'
+ } else {
+ this['InstallButton'].Enabled := false
+ this['InstallButton'].Text := "Installing..."
+ ProcessWaitClose pid
+ ExitApp
+ }
+ }
+}
+
+class InstallUtil {
+ static DefaultAllDir := (EnvGet('ProgramW6432') || A_ProgramFiles) '\AutoHotkey'
+ static DefaultUserDir := EnvGet('LocalAppData') '\Programs\AutoHotkey'
+ static DefaultDir := A_IsAdmin ? this.DefaultAllDir : this.DefaultUserDir
+}
+
+IsInProgramFiles(path) {
+ other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432")
+ return InStr(path, A_ProgramFiles "\") = 1
+ || other && InStr(path, other "\") = 1
}
\ No newline at end of file
diff --git a/UX/ui-uninstall.ahk b/UX/ui-uninstall.ahk
index c24618f..eab3cf5 100644
--- a/UX/ui-uninstall.ahk
+++ b/UX/ui-uninstall.ahk
@@ -1,68 +1,68 @@
-; This script shows a GUI for uninstalling AutoHotkey or specific versions.
-#include inc\bounce-v1.ahk
-/* v1 stops here */
-#requires AutoHotkey v2.0
-
-#include inc\ui-base.ahk
-#include install.ahk
-
-#NoTrayIcon
-#SingleInstance Force
-
-A_ScriptName := "AutoHotkey Setup"
-SetRegView 64
-ModifySetupGui.Show()
-
-class ModifySetupGui extends AutoHotkeyUxGui {
- __new() {
- super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox')
-
- this.inst := Installation()
- this.inst.ResolveInstallDir()
- versions := this.inst.GetComponents()
-
- this.AddText(, "Remove which versions?")
- iv := this.AddListView('vComponents Checked -Hdr R10 w248', ["Version"])
- iv.OnEvent('ItemCheck', 'Checked')
- for v, files in versions
- iv.Add(files.HasProp('superseded') ? 'Check' : '', v)
-
- anyChecked := iv.GetNext(0, 'C')
- this.AddButton('vRemoveAll w120 ' (anyChecked ? '' : 'Default'), "Remove &all")
- .OnEvent('Click', 'ClickedRemove')
- this.AddButton('vRemove w120 yp ' (anyChecked ? 'Default' : 'Disabled'), "Remove &checked")
- .OnEvent('Click', 'ClickedRemove')
-
- if !this.inst.UserInstall && !A_IsAdmin {
- SendMessage 0x160C,, true, 'Button1', this ; BCM_SETSHIELD := 0x160C
- SendMessage 0x160C,, true, 'Button2', this
- }
- }
-
- ClickedRemove(btn, *) {
- cmd := ''
- if btn.Name = 'Remove' {
- n := 0, iv := this['Components'], count := 0
- while n := iv.GetNext(n, 'C')
- cmd .= ',' iv.GetText(n), ++count
- if count = iv.GetCount()
- cmd := '' ; All are selected - do a full uninstall
- else
- cmd := ' "' SubStr(cmd, 2) '"'
- }
- cmd := Format('"{1}" "{2}\install.ahk" /uninstall{3}', A_AhkPath, A_ScriptDir, cmd)
- if (!this.inst.UserInstall || A_Args.Length && A_Args[1] = '/elevate') && !A_IsAdmin
- cmd := '*RunAs ' cmd
- try {
- Run cmd
- ExitApp
- }
- catch as e
- if !InStr(e.Message e.Extra, 'cancel')
- throw e
- }
-
- Checked(iv, *) {
- this['Remove'].Enabled := iv.GetNext(0, 'C')
- }
+; This script shows a GUI for uninstalling AutoHotkey or specific versions.
+#include inc\bounce-v1.ahk
+/* v1 stops here */
+#requires AutoHotkey v2.0
+
+#include inc\ui-base.ahk
+#include install.ahk
+
+#NoTrayIcon
+#SingleInstance Force
+
+A_ScriptName := "AutoHotkey Setup"
+SetRegView 64
+ModifySetupGui.Show()
+
+class ModifySetupGui extends AutoHotkeyUxGui {
+ __new() {
+ super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox')
+
+ this.inst := Installation()
+ this.inst.ResolveInstallDir()
+ versions := this.inst.GetComponents()
+
+ this.AddText(, "Remove which versions?")
+ iv := this.AddListView('vComponents Checked -Hdr R10 w248', ["Version"])
+ iv.OnEvent('ItemCheck', 'Checked')
+ for v, files in versions
+ iv.Add(files.HasProp('superseded') ? 'Check' : '', v)
+
+ anyChecked := iv.GetNext(0, 'C')
+ this.AddButton('vRemoveAll w120 ' (anyChecked ? '' : 'Default'), "Remove &all")
+ .OnEvent('Click', 'ClickedRemove')
+ this.AddButton('vRemove w120 yp ' (anyChecked ? 'Default' : 'Disabled'), "Remove &checked")
+ .OnEvent('Click', 'ClickedRemove')
+
+ if !this.inst.UserInstall && !A_IsAdmin {
+ SendMessage 0x160C,, true, 'Button1', this ; BCM_SETSHIELD := 0x160C
+ SendMessage 0x160C,, true, 'Button2', this
+ }
+ }
+
+ ClickedRemove(btn, *) {
+ cmd := ''
+ if btn.Name = 'Remove' {
+ n := 0, iv := this['Components'], count := 0
+ while n := iv.GetNext(n, 'C')
+ cmd .= ',' iv.GetText(n), ++count
+ if count = iv.GetCount()
+ cmd := '' ; All are selected - do a full uninstall
+ else
+ cmd := ' "' SubStr(cmd, 2) '"'
+ }
+ cmd := Format('"{1}" "{2}\install.ahk" /uninstall{3}', A_AhkPath, A_ScriptDir, cmd)
+ if (!this.inst.UserInstall || A_Args.Length && A_Args[1] = '/elevate') && !A_IsAdmin
+ cmd := '*RunAs ' cmd
+ try {
+ Run cmd
+ ExitApp
+ }
+ catch as e
+ if !InStr(e.Message e.Extra, 'cancel')
+ throw e
+ }
+
+ Checked(iv, *) {
+ this['Remove'].Enabled := iv.GetNext(0, 'C')
+ }
}
\ No newline at end of file
diff --git a/WindowSpy.ahk b/WindowSpy.ahk
index 2a9a294..e1050f8 100644
--- a/WindowSpy.ahk
+++ b/WindowSpy.ahk
@@ -1,2 +1,2 @@
-#Requires AutoHotkey v2.0-beta
+#Requires AutoHotkey v2.0-beta
#Include UX\WindowSpy.ahk
\ No newline at end of file