diff --git a/Lib/v2/AHK_Common.ahk b/Lib/v2/AHK_Common.ahk index f5d5923..3b9eb35 100644 --- a/Lib/v2/AHK_Common.ahk +++ b/Lib/v2/AHK_Common.ahk @@ -57,13 +57,16 @@ InitScript(requireUIA := true, requireAdmin := false, optimize := true) { } FindExe(name, fallbacks := []) { + if (name == "") + return "" if FileExist(name) return name Loop Parse, EnvGet("PATH"), ";" { - p := Trim(A_LoopField) + p := Trim(A_LoopField, " `t`"") if !p continue + p := RTrim(p, "\/") cand := p . "\" . name if FileExist(cand) return cand diff --git a/Lib/v2/WindowManager.ahk b/Lib/v2/WindowManager.ahk index f35d566..c1fcc0e 100644 --- a/Lib/v2/WindowManager.ahk +++ b/Lib/v2/WindowManager.ahk @@ -33,13 +33,11 @@ MakeFullscreen(winTitle) { MaximizeWindow(winTitle) } -WaitForWindow(winTitle, timeout := 30) { - try { - WinWait(winTitle, , timeout) - return true - } catch TimeoutError { - return false - } +WaitForWindow(winTitle, timeout := 30, api := "") { + if !api + api := SystemWindowAPI() + + return api.WinWait(winTitle, , timeout) != 0 } WaitForProcess(processName, timeout := 30) { @@ -47,12 +45,13 @@ WaitForProcess(processName, timeout := 30) { } GetMonitorAtPos(x, y, api := "") { - if !api - api := SystemWindowAPI() - - count := api.MonitorGetCount() + count := IsObject(api) ? api.MonitorGetCount() : MonitorGetCount() Loop count { - api.MonitorGet(A_Index, &l, &t, &r, &b) + if IsObject(api) + api.MonitorGet(A_Index, &l, &t, &r, &b) + else + MonitorGet(A_Index, &l, &t, &r, &b) + if (l <= x && x <= r && t <= y && y <= b) return A_Index } @@ -93,6 +92,7 @@ RestoreWindowBorders(winTitle) { } class SystemWindowAPI { + WinWait(winTitle, winText?, timeout?) => WinWait(winTitle, winText?, timeout?) WinGetStyle(winTitle) => WinGetStyle(winTitle) WinSetStyle(style, winTitle) => WinSetStyle(style, winTitle) WinMove(x, y, w, h, winTitle) => WinMove(x, y, w, h, winTitle) diff --git a/Other/7zEmuPrepper/Final.ahk b/Other/7zEmuPrepper/Final.ahk index 4ebe633..1f23d86 100644 --- a/Other/7zEmuPrepper/Final.ahk +++ b/Other/7zEmuPrepper/Final.ahk @@ -4,7 +4,6 @@ ; 7zEmuPrepper command builder (for 7zEmuPrepper.ps1) ; Fields -> emits a ready-to-run PowerShell command. Includes a Copy button. -sevenZEmuPrepperPath := "" sevenZipPath := "" emulatorPath := "" arguments := "" @@ -46,15 +45,6 @@ gui.Add("Button", "x340 y430 w80 h23", "Exit").OnEvent("Click", (*) => ExitApp() gui.Show("w480 h470") return -SelectFileOrDir(btn) { - global - text := btn.Text - if (text = "…") { - ; infer based on label order - idx := btn.Hwnd - } -} - ; Simplified per-control selection based on Y position SelectFileOrDir(btn, *) { y := btn.Pos.Y diff --git a/Other/Citra_mods/Citra_3DS_Manager.ahk b/Other/Citra_mods/Citra_3DS_Manager.ahk index ec79d4a..0acac95 100644 --- a/Other/Citra_mods/Citra_3DS_Manager.ahk +++ b/Other/Citra_mods/Citra_3DS_Manager.ahk @@ -27,9 +27,10 @@ LoadDestinations(){ DestMap := {} if !FileExist(DestCsv) return - Loop, Read, %DestCsv% + FileRead, csvData, %DestCsv% + Loop, Parse, csvData, `n, `r { - line := Trim(A_LoopReadLine) + line := Trim(A_LoopField) if (line = "" || !InStr(line, ",")) continue StringSplit, p, line, `, @@ -56,29 +57,35 @@ ScanMods(){ ;----------------- Config Helpers ----------------- UpdateConfig(content, updates){ remaining := updates.Clone() + remCount := remaining.Count() ; Pre-allocate buffer for performance. - ; content size + estimated size for updates + null terminator - VarSetCapacity(newContent, (StrLen(content) + updates.Count() * 100 + 1) * (A_IsUnicode ? 2 : 1)) newContent := "" + VarSetCapacity(newContent, (StrLen(content) + remCount * 100 + 1) * (A_IsUnicode ? 2 : 1)) Loop, Parse, content, `n, `r { line := A_LoopField - pos := InStr(line, "=") - if (pos > 1){ - keyCandidate := RTrim(SubStr(line, 1, pos-1)) - if (remaining.HasKey(keyCandidate)){ - val := remaining[keyCandidate] - line := keyCandidate "=" val - remaining.Delete(keyCandidate) + if (remCount > 0){ + pos := InStr(line, "=") + if (pos > 1){ + keyCandidate := RTrim(SubStr(line, 1, pos-1)) + if (remaining.HasKey(keyCandidate)){ + line := keyCandidate "=" remaining[keyCandidate] + remaining.Delete(keyCandidate) + remCount-- + } } } - newContent .= line "`n" + newContent .= line + newContent .= "`n" } for k, v in remaining { - newContent .= k "=" v "`n" + newContent .= k + newContent .= "=" + newContent .= v + newContent .= "`n" } return SubStr(newContent, 1, -1) diff --git a/Other/Citra_mods/Citra_Mod_Manager.ahk b/Other/Citra_mods/Citra_Mod_Manager.ahk index e789f5b..b69b2b1 100644 --- a/Other/Citra_mods/Citra_Mod_Manager.ahk +++ b/Other/Citra_mods/Citra_Mod_Manager.ahk @@ -16,11 +16,12 @@ root := OneDrive "\Backup\Game\Emul\Citra\nightly-mingw\Mods" ; Read CSV once and cache destinations in an associative array Destinations := {} -loop Read, % A_ScriptDir "\Destination.csv" +FileRead, csvContent, % A_ScriptDir "\Destination.csv" +loop Parse, csvContent, `n, `r { - if (InStr(A_LoopReadLine, ",")) + if (InStr(A_LoopField, ",")) { - parts := StrSplit(A_LoopReadLine, ",") + parts := StrSplit(A_LoopField, ",") if (parts.Length() >= 2) Destinations[parts[1]] := parts[2] } @@ -61,12 +62,7 @@ FileActions(Root, Button) Fullpath := Zielpfad "\" button.Ziel "\" button.Name Checkdir := Zielpfad "\" button.Ziel Quellpfad := button.Path - dirHasItems := false - Loop, Files, %Checkdir%\*, DF ;Loop through items in dir - { - dirHasItems := true ;Mark that we found at least one item - Break ;Optimization: Break after finding the first item - } + dirHasItems := FileExist(Checkdir "\*") != "" If !dirHasItems ;Is directory empty? { FileCopyDir, %Quellpfad%, %Fullpath% diff --git a/Other/Citra_per_game_config/tf.ahk b/Other/Citra_per_game_config/tf.ahk index 520fd60..5e4f498 100644 --- a/Other/Citra_per_game_config/tf.ahk +++ b/Other/Citra_per_game_config/tf.ahk @@ -889,12 +889,11 @@ TF_Merge(FileList, Separator = "`n", FileName = "merged.txt") OW=0 Loop, Parse, FileList, `n, `r { - Append2File= ; Just make sure it is empty - IfExist, %A_LoopField% + FileRead, Append2File, %A_LoopField% + If not ErrorLevel ; Successfully loaded { - FileRead, Append2File, %A_LoopField% - If not ErrorLevel ; Successfully loaded - Output .= Append2File Separator + Output .= Append2File + Output .= Separator } } diff --git a/Other/Citra_per_game_config/v2/CitraConfigHelpers.ahk b/Other/Citra_per_game_config/v2/CitraConfigHelpers.ahk index 48a40fc..713f623 100644 --- a/Other/Citra_per_game_config/v2/CitraConfigHelpers.ahk +++ b/Other/Citra_per_game_config/v2/CitraConfigHelpers.ahk @@ -18,7 +18,7 @@ ; Escaped string safe for use in regex patterns ; ============================================================================ RegExEscape(str) { - return RegExReplace(str, "([\\()\[\]{}?*+|^$.])", "\$1") + return RegExReplace(str, "([\\()\[\]{}?*+|^$.})", "\\$1") } ; ============================================================================ @@ -36,9 +36,9 @@ RegExEscape(str) { ; Modified configuration content ; ============================================================================ SetKey(content, key, value) { - pat := "m)^(" . RegExEscape(key) . ")\s*=.*$" + pat := "m)^(" . RegExEscape(key) . ")\\s*=.*$" if RegExMatch(content, pat) - return RegExReplace(content, pat, "$1=" value, , 1) + return RegExReplace(content, pat, "$1=" StrReplace(value, "$", "$$"), , 1) else return content "`n" key "=" value } diff --git a/Other/Citra_per_game_config/v2/CitraConfigHelpers_Test.ahk b/Other/Citra_per_game_config/v2/CitraConfigHelpers_Test.ahk index d1c56b0..b4a367b 100644 --- a/Other/Citra_per_game_config/v2/CitraConfigHelpers_Test.ahk +++ b/Other/Citra_per_game_config/v2/CitraConfigHelpers_Test.ahk @@ -73,6 +73,22 @@ TestSetKey() { content2 := "path\to\file=exists`n" result := SetKey(content2, "path\to\file", "updated") AssertEqual(InStr(result, "path\to\file=updated") > 0, true, "SetKey updates key with backslashes") + + ; Test 6: Empty value + result := SetKey(content, "key1", "") + AssertEqual(InStr(result, "key1=") > 0 && InStr(result, "key1=value1") == 0, true, "SetKey handles empty value") + + ; Test 7: Value with literal $ sign + result := SetKey(content, "key1", "val$ue") + AssertEqual(InStr(result, "key1=val$ue") > 0, true, "SetKey handles value with literal $ sign") + + ; Test 8: Value with literal $1 + result := SetKey(content, "key2", "val$1ue") + AssertEqual(InStr(result, "key2=val$1ue") > 0, true, "SetKey handles value with literal $1") + + ; Test 9: Add new key with $ sign + result := SetKey(content, "key4", "new$val") + AssertEqual(InStr(result, "key4=new$val") > 0, true, "SetKey adds new key with $ sign") } TestReplaceInFile() { @@ -116,7 +132,6 @@ TestReplaceInFile() { AssertEqual(content, "goodbye world`ngoodbye citra", "ReplaceInFile leaves content unchanged when text not found") ; Test 5: ReplaceInFile handles try/catch error (SaveConfig fails or LoadConfig fails) - ; In this case, we can pass a directory to trigger an error testDir := A_ScriptDir . "\test_replace_dir" if !DirExist(testDir) DirCreate(testDir) diff --git a/Other/Lossless_Scaling_Manager.ahk b/Other/Lossless_Scaling_Manager.ahk index b82e254..4ed51be 100644 --- a/Other/Lossless_Scaling_Manager.ahk +++ b/Other/Lossless_Scaling_Manager.ahk @@ -13,7 +13,9 @@ EnsureAdmin() { try Run('*RunAs "' A_AhkPath '" "' A_ScriptFullPath '" ' A_Args.Join(" ")) ExitApp } -EnsureAdmin() +if (A_LineFile == A_ScriptFullPath) { + EnsureAdmin() +} MinimizeLS() { try { @@ -35,23 +37,29 @@ StopLS() { try ProcessClose("LosslessScaling.exe") } -ToggleLS() { - if ProcessExist("LosslessScaling.exe") - StopLS() +ToggleLS(mockProcessExist := "", mockStopLS := "", mockStartLS := "") { + fnProcessExist := (mockProcessExist != "") ? mockProcessExist : ProcessExist + fnStopLS := (mockStopLS != "") ? mockStopLS : StopLS + fnStartLS := (mockStartLS != "") ? mockStartLS : StartLS + + if fnProcessExist("LosslessScaling.exe") + fnStopLS() else - StartLS() + fnStartLS() } ; ---------- Dispatch ---------- -cmd := (A_Args.Length ? StrLower(A_Args[1]) : "toggle") -try { - switch cmd { - case "start": StartLS() - case "stop", "close": StopLS() - case "toggle": ToggleLS() - default: - MsgBox("Usage: " A_ScriptName " [start|stop|toggle]") +if (A_LineFile == A_ScriptFullPath) { + cmd := (A_Args.Length ? StrLower(A_Args[1]) : "toggle") + try { + switch cmd { + case "start": StartLS() + case "stop", "close": StopLS() + case "toggle": ToggleLS() + default: + MsgBox("Usage: " A_ScriptName " [start|stop|toggle]") + } + } catch Error as err { + MsgBox("Error: " err.Message) } -} catch Error as err { - MsgBox("Error: " err.Message) } diff --git a/Other/playnite-all.ahk b/Other/playnite-all.ahk index 54eee9c..7e44945 100644 --- a/Other/playnite-all.ahk +++ b/Other/playnite-all.ahk @@ -19,8 +19,7 @@ PlayBootVideo() { vlcPath := MustGetExe("vlc.exe", ["C:\Program Files\VideoLAN\VLC\vlc.exe"]) bootVideo := A_ScriptDir . "\BootVideo.mp4" vlcArgs := '--fullscreen --video-on-top --play-and-exit --no-video-title -Idummy "' . bootVideo . '"' - RunWait('cmd.exe /c START "" "' . vlcPath . '" ' . vlcArgs, , "hide") - DllCall("kernel32.dll\Sleep", "UInt", 3000) + RunWait('"' . vlcPath . '" ' . vlcArgs, , "hide") } LaunchPlaynite() { playniteExe := MustGetExe( diff --git a/ahk/Black_ops_6/bo6-afk.ahk b/ahk/Black_ops_6/bo6-afk.ahk index b791d9d..05d1084 100644 --- a/ahk/Black_ops_6/bo6-afk.ahk +++ b/ahk/Black_ops_6/bo6-afk.ahk @@ -71,99 +71,171 @@ RegisterDefaultHotkeys() { Hotkey("F9", (*) => ExitApp()) } + + ; ---------------- Loops ---------------- BalconyLoop(id) { global runningMode, runId - while (runId = id && runningMode = "balcony") { - rand := Random(250, 2001) - Sleep(rand) - Send("{p}") - Sleep(1001) - Send("{2}") - Sleep(1001) - Send("{RButton down}") - Sleep(1001) - Send("{RButton up}") - Sleep(3001) - Send("{c}") - Sleep(1001) - Send("{p}") - Sleep(75001) + step := 1 + NextStep() { + if (runId != id || runningMode != "balcony") { + ; Cleanup if aborted mid-sequence + if (step > 4 && step <= 5) { + Send("{RButton up}") + } + return + } + + if (step == 1) { + rand := Random(250, 2001) + SetTimer(NextStep, -rand) + step := 2 + } else if (step == 2) { + Send("{p}") + SetTimer(NextStep, -1001) + step := 3 + } else if (step == 3) { + Send("{2}") + SetTimer(NextStep, -1001) + step := 4 + } else if (step == 4) { + Send("{RButton down}") + SetTimer(NextStep, -1001) + step := 5 + } else if (step == 5) { + Send("{RButton up}") + SetTimer(NextStep, -3001) + step := 6 + } else if (step == 6) { + Send("{c}") + SetTimer(NextStep, -1001) + step := 7 + } else if (step == 7) { + Send("{p}") + SetTimer(NextStep, -75001) + step := 1 + } } + NextStep() } BankRoofCleanLoop(id) { global runningMode, runId - while (runId = id && runningMode = "bank_basic") { - start := A_TickCount - while (runId = id && runningMode = "bank_basic" && A_TickCount - start < 40000) { - rand := Random(0, 20) - DllCall("Sleep", "UInt", rand) - Send("{LButton}") - Sleep(10) - Send("{g}") - Sleep(100) - } + start := A_TickCount + phase := "shoot" + + Tick() { if (runId != id || runningMode != "bank_basic") - break - CleanUpZombies() - Sleep(2000) - CleanUpZombies() - Sleep(12000) + return + + if (phase == "shoot") { + if (A_TickCount - start < 40000) { + rand := Random(0, 20) + if (rand > 0) + DllCall("Sleep", "UInt", rand) + Send("{LButton}") + Sleep(10) + Send("{g}") + SetTimer(Tick, -100) + } else { + phase := "cleanup1" + CleanUpZombies() + SetTimer(Tick, -2000) + } + } else if (phase == "cleanup1") { + CleanUpZombies() + phase := "cleanup2" + SetTimer(Tick, -12000) + } else if (phase == "cleanup2") { + start := A_TickCount + phase := "shoot" + SetTimer(Tick, -10) + } } + Tick() } BankRoofLootLoop(id) { global runningMode, runId - while (runId = id && runningMode = "bank_loot") { - start := A_TickCount - walked20 := false, walked35 := false - while (runId = id && runningMode = "bank_loot" && A_TickCount - start < 40000) { - rand := Random(0, 20) - DllCall("Sleep", "UInt", rand) - Send("{LButton}") - Sleep(10) - Send("{g}") - Sleep(100) + start := A_TickCount + walked20 := false + walked35 := false + phase := "shoot" + + Tick() { + if (runId != id || runningMode != "bank_loot") { + return + } + + if (phase == "shoot") { elapsed := A_TickCount - start - if (!walked20 && elapsed >= 20000 && elapsed < 21000) { - WalkForwardAndBack() - walked20 := true - } - if (!walked35 && elapsed >= 35000 && elapsed < 36000) { - WalkForwardAndBack() - walked35 := true + if (elapsed < 40000) { + if (!walked20 && elapsed >= 20000 && elapsed < 21000) { + WalkForwardAndBack() + walked20 := true + } + if (!walked35 && elapsed >= 35000 && elapsed < 36000) { + WalkForwardAndBack() + walked35 := true + } + + rand := Random(0, 20) + if (rand > 0) + DllCall("Sleep", "UInt", rand) + Send("{LButton}") + Sleep(10) + Send("{g}") + SetTimer(Tick, -100) + } else { + phase := "cleanup1" + CleanUpZombies() + SetTimer(Tick, -2000) } + } else if (phase == "cleanup1") { + CleanUpZombies() + phase := "cleanup2" + SetTimer(Tick, -12000) + } else if (phase == "cleanup2") { + start := A_TickCount + walked20 := false + walked35 := false + phase := "shoot" + SetTimer(Tick, -10) } - if (runId != id || runningMode != "bank_loot") - break - CleanUpZombies() - Sleep(2000) - CleanUpZombies() - Sleep(12000) } + Tick() } BankRoofAlwaysLoop(id) { global runningMode, runId - while (runId = id && runningMode = "bank_always") { + + Tick() { + if (runId != id || runningMode != "bank_always") + return + rand := Random(0, 20) - DllCall("Sleep", "UInt", rand) + if (rand > 0) + DllCall("Sleep", "UInt", rand) Send("{LButton}") Sleep(10) Send("{g}") - Sleep(90) ; ~120ms per cycle + SetTimer(Tick, -90) } + Tick() } HoldClickLoop(id) { global runningMode, runId Send("{LButton down}") - while (runId = id && runningMode = "hold_click") { - Sleep(100) ; light yield + + Tick() { + if (runId != id || runningMode != "hold_click") { + return + } + SetTimer(Tick, -100) } - Send("{LButton up}") + Tick() } ; ---------------- Helpers ---------------- diff --git a/ahk/Fullscreen.ahk b/ahk/Fullscreen.ahk index 0b89c80..37e3857 100644 --- a/ahk/Fullscreen.ahk +++ b/ahk/Fullscreen.ahk @@ -1,7 +1,7 @@ #Requires AutoHotkey v2.0 ; ============================================================================ -; Fullscreen.ahk - Borderless fullscreen toggle with multi-monitor support +; Fullscreen.ahk - Borderless fullscreen toggle ; Version: 2.0.0 (Migrated to AHK v2 and consolidated from 3 variants) ; ; Consolidates: @@ -10,7 +10,7 @@ ; - Fullscreen_double.ahk (separate enter/exit hotkeys) ; ; Hotkeys: -; End - Toggle borderless fullscreen (multi-monitor aware) +; End - Toggle borderless fullscreen ; Ctrl+Alt+K - Enter borderless fullscreen ; Ctrl+Alt+L - Exit and restore window ; Ctrl+Alt+End - Toggle with always-on-top @@ -23,10 +23,10 @@ InitScript(true, true, true) ; UIA + Admin + Performance optimizations Persistent ; ============================================================================ -; Primary hotkey - End key toggles fullscreen with multi-monitor support +; Primary hotkey - End key toggles fullscreen ; ============================================================================ End:: { - ToggleFakeFullscreenMultiMonitor("A") + ToggleFakeFullscreen("A") } ; ============================================================================ diff --git a/ahk/GUI/QuotePathIfNeeded_Test.ahk b/ahk/GUI/QuotePathIfNeeded_Test.ahk new file mode 100644 index 0000000..4992ecb --- /dev/null +++ b/ahk/GUI/QuotePathIfNeeded_Test.ahk @@ -0,0 +1,61 @@ +#Requires AutoHotkey v2.0 +#SingleInstance Force + +; We only include the target file to avoid full framework initialization +#Include %A_ScriptDir%\GUI_Shared.ahk + +; Setup testing output +stdout := FileOpen("*", "w `n") +testsPassed := 0 +testsFailed := 0 + +AssertEqual(expected, actual, context) { + global testsPassed, testsFailed, stdout + if (expected == actual) { + testsPassed++ + stdout.WriteLine("PASS: " . context) + } else { + testsFailed++ + stdout.WriteLine("FAIL: " . context . " - Expected '" . expected . "', but got '" . actual . "'") + } +} + +try { + stdout.WriteLine("Starting tests for QuotePathIfNeeded...") + + ; Test 1: Empty string + AssertEqual("", QuotePathIfNeeded(""), "Should return empty string for empty input") + + ; Test 2: Normal path without spaces + AssertEqual("C:\path\to\file.exe", QuotePathIfNeeded("C:\path\to\file.exe"), "Should not quote path without spaces") + + ; Test 3: Path with spaces + AssertEqual('"C:\Program Files\App\app.exe"', QuotePathIfNeeded("C:\Program Files\App\app.exe"), "Should quote path with spaces") + + ; Test 4: Path with spaces, already fully quoted + AssertEqual('"C:\Program Files\App\app.exe"', QuotePathIfNeeded('"C:\Program Files\App\app.exe"'), "Should not double-quote an already quoted path") + + ; Test 5: Path with spaces, quoted only at the start (edge case based on current logic) + AssertEqual('"C:\Program Files\App\app.exe', QuotePathIfNeeded('"C:\Program Files\App\app.exe'), "Should not double-quote if starting quote is present (current logic)") + + ; Test 6: Path with spaces, quoted only at the end (edge case based on current logic) + AssertEqual('C:\Program Files\App\app.exe"', QuotePathIfNeeded('C:\Program Files\App\app.exe"'), "Should not double-quote if ending quote is present (current logic)") + + ; Test 7: String with only spaces + AssertEqual('" "', QuotePathIfNeeded(" "), "Should quote a string containing only spaces") + + ; Final Results + stdout.WriteLine("---") + stdout.WriteLine("Tests Passed: " . testsPassed) + stdout.WriteLine("Tests Failed: " . testsFailed) + +} catch as e { + stdout.WriteLine("Test execution failed with error: " . e.Message) + testsFailed++ +} + +if (testsFailed > 0) { + ExitApp(1) +} + +ExitApp(0) diff --git a/ahk/Keys.ahk b/ahk/Keys.ahk index 3db7e9f..118485a 100644 --- a/ahk/Keys.ahk +++ b/ahk/Keys.ahk @@ -1,9 +1,22 @@ #Requires AutoHotkey v2.0 -#SingleInstance Force -#Include ..\Lib\v2\AHK_Common.ahk -InitScript(true, false, true) +#SingleInstance Force +SendMode "Input" +SetWorkingDir A_ScriptDir + +#Include %A_ScriptDir%\..\Lib\v2\AHK_Common.ahk +InitScript(true, false, false) ; UIA required, no admin, manual tuning + +if (A_LineFile == A_ScriptFullPath) { +KeyHistory(0) +ListLines False +SetKeyDelay(-1, -1) +SetMouseDelay(-1) +SetDefaultMouseSpeed(0) +SetWinDelay(-1) +SetControlDelay(-1) SetTitleMatchMode(3) +SetTitleMatchMode("Fast") SetNumLockState("AlwaysOn") SetCapsLockState("AlwaysOff") SetScrollLockState("AlwaysOff") @@ -12,7 +25,6 @@ SetScrollLockState("AlwaysOff") mw := GetMonitorWidth() mh := GetMonitorHeight() pw := mw / 3 -ph := mh / 3 lw := (2 * pw > 1024) ? 1024 : 2 * pw rw := mw - lw positions := Map() @@ -23,6 +35,7 @@ SetTimer(SetAlwaysOnTop, 1000) ; Tray documentation entry A_TrayMenu.Add("Documentation", ShowDocumentation) +} #HotIf !WinActive("ahk_exe Explorer.EXE") +F1::WinMinimize("A") #HotIf @@ -73,40 +86,48 @@ AddDateToSelection() { } } -MoveWindowLeft() { +MoveWindowLeft(api := "") { global lw, mh - SaveWindowPosition() - if IsResizable() { - WinMove(0, 0, lw, mh, "A") + if !api + api := KeysWindowAPI() + SaveWindowPosition(api) + if IsResizable(api) { + api.WinMove(0, 0, lw, mh, "A") } else { - WinMove(0, 0, , , "A") + api.WinMoveNoSize(0, 0, "A") } } -MoveWindowRight() { +MoveWindowRight(api := "") { global lw, rw, mh - SaveWindowPosition() - if IsResizable() { - WinMove(lw, 0, rw, mh, "A") + if !api + api := KeysWindowAPI() + SaveWindowPosition(api) + if IsResizable(api) { + api.WinMove(lw, 0, rw, mh, "A") } else { - WinMove(lw, 0, , , "A") + api.WinMoveNoSize(lw, 0, "A") } } -RestoreWindowPosition() { +RestoreWindowPosition(api := "") { global positions - hwnd := WinExist("A") + if !api + api := KeysWindowAPI() + hwnd := api.WinExist("A") if !positions.Has(hwnd) return pos := positions[hwnd] - WinMove(pos[1], pos[2], pos[3], pos[4], "A") + api.WinMove(pos[1], pos[2], pos[3], pos[4], "A") } -SaveWindowPosition() { +SaveWindowPosition(api := "") { global positions - hwnd := WinExist("A") - WinGetPos(&x, &y, &w, &h, "A") + if !api + api := KeysWindowAPI() + hwnd := api.WinExist("A") + api.WinGetPos(&x, &y, &w, &h, "A") positions[hwnd] := [x, y, w, h] } @@ -120,8 +141,10 @@ GetMonitorHeight() { return bottom - top } -IsResizable() { - style := WinGetStyle("A") +IsResizable(api := "") { + if !api + api := KeysWindowAPI() + style := api.WinGetStyle("A") return (style & 0x00040000) != 0 ; WS_SIZEBOX } @@ -191,3 +214,14 @@ ShowDocumentation(*) { tabs.UseTab() docGui.Show("Center") } + + + + +class KeysWindowAPI { + WinExist(winTitle) => WinExist(winTitle) + WinGetPos(&x, &y, &w, &h, winTitle) => WinGetPos(&x, &y, &w, &h, winTitle) + WinGetStyle(winTitle) => WinGetStyle(winTitle) + WinMove(x, y, w, h, winTitle) => WinMove(x, y, w, h, winTitle) + WinMoveNoSize(x, y, winTitle) => WinMove(x, y, , , winTitle) +} diff --git a/ahk/test_Keys.ahk b/ahk/test_Keys.ahk new file mode 100644 index 0000000..1ce16b2 --- /dev/null +++ b/ahk/test_Keys.ahk @@ -0,0 +1,195 @@ +#Requires AutoHotkey v2.0 +#SingleInstance Force + +#Include %A_ScriptDir%\..\Lib\v2\AHK_Common.ahk + +InitScript(false, false, false) + +; Define the test mock variables used by Keys.ahk logic +global positions := Map() +global lw := 800 +global rw := 900 +global mh := 1000 +global mw := 1920 + +; Override API for test +class MockKeysAPI { + __New() { + this.calls := [] + this.mockStyle := 0 + this.mockHwnd := 12345 + this.mockPos := {x: 10, y: 20, w: 800, h: 600} + } + + WinExist(winTitle) { + this.calls.Push({method: "WinExist", args: [winTitle]}) + return this.mockHwnd + } + + WinGetPos(&x, &y, &w, &h, winTitle) { + this.calls.Push({method: "WinGetPos", args: [winTitle]}) + x := this.mockPos.x + y := this.mockPos.y + w := this.mockPos.w + h := this.mockPos.h + } + + WinGetStyle(winTitle) { + this.calls.Push({method: "WinGetStyle", args: [winTitle]}) + return this.mockStyle + } + + WinMove(x, y, w, h, winTitle) { + this.calls.Push({method: "WinMove", args: [x, y, w, h, winTitle]}) + } + + WinMoveNoSize(x, y, winTitle) { + this.calls.Push({method: "WinMoveNoSize", args: [x, y, winTitle]}) + } +} + +stdout := FileOpen("*", "w `n") +testsPassed := 0 +testsFailed := 0 + +AssertEqual(expected, actual, context) { + global testsPassed, testsFailed, stdout + + expectedStr := IsObject(expected) ? ObjToString(expected) : expected + actualStr := IsObject(actual) ? ObjToString(actual) : actual + + if (expectedStr == actualStr) { + testsPassed++ + stdout.WriteLine("PASS: " . context) + } else { + testsFailed++ + stdout.WriteLine("FAIL: " . context . " - Expected '" . expectedStr . "', but got '" . actualStr . "'") + } +} + +ObjToString(obj) { + if !IsObject(obj) + return obj + if (Type(obj) == "Array") { + str := "[" + for v in obj + str .= ObjToString(v) . ", " + return RTrim(str, ", ") . "]" + } + return Type(obj) +} + +; Include the actual Keys.ahk so we can call the functions +#Include %A_ScriptDir%\Keys.ahk + +try { + stdout.WriteLine("Starting tests for Keys.ahk window functions...") + + ; Test SaveWindowPosition + global positions := Map() + api := MockKeysAPI() + api.mockHwnd := 9999 + api.mockPos := {x: 50, y: 60, w: 1024, h: 768} + + SaveWindowPosition(api) + + AssertEqual(true, positions.Has(9999), "SaveWindowPosition should store entry for hwnd") + if (positions.Has(9999)) { + AssertEqual([50, 60, 1024, 768], positions[9999], "SaveWindowPosition should save correct coordinates") + } + + ; Test IsResizable + api := MockKeysAPI() + api.mockStyle := 0x00000000 + AssertEqual(false, IsResizable(api), "IsResizable should return false without WS_SIZEBOX") + + api.mockStyle := 0x00040000 + AssertEqual(true, IsResizable(api), "IsResizable should return true with WS_SIZEBOX") + + ; Test RestoreWindowPosition + global positions := Map() + positions[7777] := [100, 200, 800, 600] + api := MockKeysAPI() + api.mockHwnd := 7777 + + RestoreWindowPosition(api) + + AssertEqual("WinMove", api.calls[2].method, "RestoreWindowPosition should call WinMove") + if (api.calls.Length >= 2) { + AssertEqual([100, 200, 800, 600, "A"], api.calls[2].args, "RestoreWindowPosition should move to saved pos") + } + + ; Test RestoreWindowPosition - No saved position + api := MockKeysAPI() + api.mockHwnd := 8888 + + RestoreWindowPosition(api) + AssertEqual(1, api.calls.Length, "RestoreWindowPosition should not call WinMove if not saved") + + ; Test MoveWindowLeft (Resizable) + global positions := Map() + api := MockKeysAPI() + api.mockHwnd := 1111 + api.mockStyle := 0x00040000 ; Resizable + + global lw := 800 + global mh := 1000 + + MoveWindowLeft(api) + + AssertEqual(true, positions.Has(1111), "MoveWindowLeft should save position first") + AssertEqual("WinMove", api.calls[4].method, "MoveWindowLeft should call WinMove") + if (api.calls.Length >= 4) { + AssertEqual([0, 0, 800, 1000, "A"], api.calls[4].args, "MoveWindowLeft should move to left half") + } + + + ; Test MoveWindowLeft (Not Resizable) + global positions := Map() + api := MockKeysAPI() + api.mockHwnd := 3333 + api.mockStyle := 0 ; Not resizable + + global lw := 800 + global mh := 1000 + + MoveWindowLeft(api) + + AssertEqual(true, positions.Has(3333), "MoveWindowLeft should save position first") + AssertEqual("WinMoveNoSize", api.calls[4].method, "MoveWindowLeft should call WinMoveNoSize") + if (api.calls.Length >= 4) { + AssertEqual([0, 0, "A"], api.calls[4].args, "MoveWindowLeft non-resizable should only move x,y") + } + + ; Test MoveWindowRight (Not Resizable) + global positions := Map() + api := MockKeysAPI() + api.mockHwnd := 2222 + api.mockStyle := 0 ; Not resizable + + global lw := 800 + global rw := 900 + global mh := 1000 + + MoveWindowRight(api) + + AssertEqual(true, positions.Has(2222), "MoveWindowRight should save position first") + AssertEqual("WinMoveNoSize", api.calls[4].method, "MoveWindowRight should call WinMoveNoSize") + if (api.calls.Length >= 4) { + AssertEqual([800, 0, "A"], api.calls[4].args, "MoveWindowRight non-resizable should only move x,y") + } + + stdout.WriteLine("---") + stdout.WriteLine("Tests Passed: " . testsPassed) + stdout.WriteLine("Tests Failed: " . testsFailed) + +} catch as e { + stdout.WriteLine("Error during tests: " e.Message) + testsFailed++ +} + +if (testsFailed > 0) { + ExitApp(1) +} + +ExitApp(0) diff --git a/ahk/test_WindowManager.ahk b/ahk/test_WindowManager.ahk index 7d599b2..15191b4 100644 --- a/ahk/test_WindowManager.ahk +++ b/ahk/test_WindowManager.ahk @@ -1,5 +1,5 @@ #Requires AutoHotkey v2.0 -#Include A_ScriptDir "\..\Lib\v2\WindowManager.ahk" +#Include A_ScriptDir "\.\..\Lib\v2\WindowManager.ahk" class MockWindowAPI { __New() { @@ -10,6 +10,11 @@ class MockWindowAPI { this.monitors := [] } + WinWait(winTitle, winText:="", timeout:="") { + this.calls.Push({method: "WinWait", args: [winTitle, winText, timeout]}) + return 0 ; Simulate timeout + } + WinGetStyle(winTitle) { this.calls.Push({method: "WinGetStyle", args: [winTitle]}) return this.mockStyle @@ -149,12 +154,50 @@ TestGetMonitorAtPos_MultipleMonitors() { return true } + +TestWaitForWindow_Timeout() { + mockApi := MockWindowAPI() + + res := WaitForWindow("FakeWindow", 0.1, mockApi) + + ; Verify result + if (res != false) { + FileOpen("*", "w `n").Write("Fail: Expected false, got " res "`n") + return false + } + + ; Verify calls + if (mockApi.calls.Length != 1) { + FileOpen("*", "w `n").Write("Fail: Expected 1 call, got " mockApi.calls.Length "`n") + return false + } + + if (mockApi.calls[1].method != "WinWait") { + FileOpen("*", "w `n").Write("Fail: First call was not WinWait`n") + return false + } + + if (mockApi.calls[1].args[1] != "FakeWindow") { + FileOpen("*", "w `n").Write("Fail: Expected winTitle 'FakeWindow', got '" mockApi.calls[1].args[1] "'`n") + return false + } + + if (mockApi.calls[1].args[3] != 0.1) { + FileOpen("*", "w `n").Write("Fail: Expected timeout 0.1, got '" mockApi.calls[1].args[3] "'`n") + return false + } + + FileOpen("*", "w `n").Write("Pass: WaitForWindow_Timeout`n") + return true +} + TestToggleFakeFullscreen_MakeFullscreen() TestToggleFakeFullscreen_RestoreWindow() TestGetMonitorAtPos_InsideMonitor() TestGetMonitorAtPos_OutsideMonitors() TestGetMonitorAtPos_MultipleMonitors() +TestWaitForWindow_Timeout() FileOpen("*", "w `n").Write("All tests complete.`n") ExitApp(0) diff --git a/tests/FindExe_Test.ahk b/tests/FindExe_Test.ahk index 938847e..af431d4 100644 --- a/tests/FindExe_Test.ahk +++ b/tests/FindExe_Test.ahk @@ -29,6 +29,11 @@ DirCreate(testBaseDir) DirCreate(testBaseDir . "\PathDir1") DirCreate(testBaseDir . "\PathDir2") DirCreate(testBaseDir . "\FallbackDir") +DirCreate(testBaseDir . "\PathDir3") +DirCreate(testBaseDir . "\PathDir4") +FileAppend("", testBaseDir . "\PathDir3\tool_trailing.exe") +DirCreate(testBaseDir . "\PathDir4\subdir") +FileAppend("", testBaseDir . "\PathDir4\subdir\subtool.exe") ; Create dummy files FileAppend("", testBaseDir . "\PathDir2\tool.exe") @@ -59,12 +64,35 @@ try { EnvSet("PATH", ";;" . mockPath . ";;") AssertEqual(testBaseDir . "\PathDir2\tool.exe", FindExe("tool.exe"), "Should handle empty entries in PATH") + ; Test 6: Whitespace around PATH entries + EnvSet("PATH", " " . testBaseDir . "\PathDir2 ") + AssertEqual(testBaseDir . "\PathDir2\tool.exe", FindExe("tool.exe"), "Should handle whitespace in PATH entries") + + ; Test 7: Quoted PATH entries + EnvSet("PATH", "`"" . testBaseDir . "\PathDir2`"") + AssertEqual(testBaseDir . "\PathDir2\tool.exe", FindExe("tool.exe"), "Should handle quoted PATH entries") + + ; Test 8: Trailing backslash in PATH entry + EnvSet("PATH", testBaseDir . "\PathDir3\") + AssertEqual(testBaseDir . "\PathDir3\tool_trailing.exe", FindExe("tool_trailing.exe"), "Should handle PATH entries with trailing backslashes") + + ; Test 9: Subdirectory path in name + EnvSet("PATH", mockPath . ";" . testBaseDir . "\PathDir4") + AssertEqual(testBaseDir . "\PathDir4\subdir\subtool.exe", FindExe("subdir\subtool.exe"), "Should resolve relative subdirectory paths in name") + + ; Test 10: Empty string name + AssertEqual("", FindExe(""), "Should return empty string for empty name") + + ; Test 11: Empty fallback array + EnvSet("PATH", mockPath) + AssertEqual("", FindExe("nonexistent.exe", []), "Should handle empty fallbacks array gracefully") + ; MustGetExe Tests - ; Test 6: MustGetExe success path + ; Test 12: MustGetExe success path AssertEqual(directPath, MustGetExe(directPath), "MustGetExe should return path if found") - ; Test 7: MustGetExe failure path + ; Test 13: MustGetExe failure path mockState := Map("msgBoxCalled", false, "exitAppCalled", false, "exitCode", "") mockMsgBox := (msg) => (mockState["msgBoxCalled"] := true) diff --git a/tests/Other/Lossless_Scaling_Manager_Test.ahk b/tests/Other/Lossless_Scaling_Manager_Test.ahk new file mode 100644 index 0000000..f67ab87 --- /dev/null +++ b/tests/Other/Lossless_Scaling_Manager_Test.ahk @@ -0,0 +1,88 @@ +#Requires AutoHotkey v2.0 +#SingleInstance Force + +; Setup testing output +stdout := FileOpen("*", "w `n") +testsPassed := 0 +testsFailed := 0 + +AssertEqual(expected, actual, context) { + global testsPassed, testsFailed, stdout + if (expected == actual) { + testsPassed++ + stdout.WriteLine("PASS: " . context) + } else { + testsFailed++ + stdout.WriteLine("FAIL: " . context . " - Expected '" . expected . "', but got '" . actual . "'") + } +} + +#Include %A_ScriptDir%\..\..\Other\Lossless_Scaling_Manager.ahk + +global mockProcessExistCalled := 0 +global mockProcessExistArg := "" +global mockProcessExistResult := 0 + +global mockStartLSCalled := 0 +global mockStopLSCalled := 0 + +MockProcessExist(exeName) { + global mockProcessExistCalled, mockProcessExistArg, mockProcessExistResult + mockProcessExistCalled++ + mockProcessExistArg := exeName + return mockProcessExistResult +} + +MockStartLS() { + global mockStartLSCalled + mockStartLSCalled++ +} + +MockStopLS() { + global mockStopLSCalled + mockStopLSCalled++ +} + +ResetMocks() { + global mockProcessExistCalled := 0 + global mockProcessExistArg := "" + global mockProcessExistResult := 0 + global mockStartLSCalled := 0 + global mockStopLSCalled := 0 +} + +try { + stdout.WriteLine("Starting ToggleLS tests...") + + ; Test 1: Process exists, should call StopLS + ResetMocks() + mockProcessExistResult := 1234 ; some PID + ToggleLS(MockProcessExist, MockStopLS, MockStartLS) + AssertEqual(1, mockProcessExistCalled, "ToggleLS checks if process exists") + AssertEqual("LosslessScaling.exe", mockProcessExistArg, "ToggleLS checks for LosslessScaling.exe") + AssertEqual(1, mockStopLSCalled, "StopLS called when process exists") + AssertEqual(0, mockStartLSCalled, "StartLS NOT called when process exists") + + ; Test 2: Process does NOT exist, should call StartLS + ResetMocks() + mockProcessExistResult := 0 + ToggleLS(MockProcessExist, MockStopLS, MockStartLS) + AssertEqual(1, mockProcessExistCalled, "ToggleLS checks if process exists") + AssertEqual("LosslessScaling.exe", mockProcessExistArg, "ToggleLS checks for LosslessScaling.exe") + AssertEqual(0, mockStopLSCalled, "StopLS NOT called when process does not exist") + AssertEqual(1, mockStartLSCalled, "StartLS called when process does not exist") + + ; Final Results + stdout.WriteLine("---") + stdout.WriteLine("Tests Passed: " . testsPassed) + stdout.WriteLine("Tests Failed: " . testsFailed) +} catch Error as err { + stdout.WriteLine("Test script threw an error: " . err.Message) + testsFailed++ +} + +if (testsFailed > 0) { + ExitApp(1) +} + +ExitApp(0) diff --git a/tests/WindowManager_Test.ahk b/tests/WindowManager_Test.ahk new file mode 100644 index 0000000..c891203 --- /dev/null +++ b/tests/WindowManager_Test.ahk @@ -0,0 +1,94 @@ +#Requires AutoHotkey v2.0 +#SingleInstance Force + +#Include %A_ScriptDir%\..\Lib\v2\AHK_Common.ahk +#Include %A_ScriptDir%\..\Lib\v2\WindowManager.ahk + +InitScript(false, false, false) ; disable UIA, Admin, and optimization for the test + +; Setup testing output +stdout := FileOpen("*", "w `n") +testsPassed := 0 +testsFailed := 0 + +AssertEqual(expected, actual, context) { + global testsPassed, testsFailed, stdout + if (expected == actual) { + testsPassed++ + stdout.WriteLine("PASS: " . context) + } else { + testsFailed++ + stdout.WriteLine("FAIL: " . context . " - Expected '" . expected . "', but got '" . actual . "'") + } +} + +class MockSystemWindowAPI { + __New(monitors) { + this.monitors := monitors + } + + MonitorGetCount() { + return this.monitors.Length + } + + MonitorGet(N, &Left, &Top, &Right, &Bottom) { + if (N < 1 || N > this.monitors.Length) + return false + mon := this.monitors[N] + Left := mon.l + Top := mon.t + Right := mon.r + Bottom := mon.b + return true + } +} + +try { + stdout.WriteLine("Starting GetMonitorAtPos tests...") + + ; Test Set 1: Single Monitor (0,0 to 1920,1080) + mon1 := {l: 0, t: 0, r: 1920, b: 1080} + apiSingle := MockSystemWindowAPI([mon1]) + + AssertEqual(1, GetMonitorAtPos(960, 540, apiSingle), "Single monitor - Center") + AssertEqual(1, GetMonitorAtPos(0, 0, apiSingle), "Single monitor - Top Left edge") + AssertEqual(1, GetMonitorAtPos(1920, 1080, apiSingle), "Single monitor - Bottom Right edge") + AssertEqual(-1, GetMonitorAtPos(-1, 540, apiSingle), "Single monitor - Out of bounds Left") + AssertEqual(-1, GetMonitorAtPos(1921, 540, apiSingle), "Single monitor - Out of bounds Right") + AssertEqual(-1, GetMonitorAtPos(960, -1, apiSingle), "Single monitor - Out of bounds Top") + AssertEqual(-1, GetMonitorAtPos(960, 1081, apiSingle), "Single monitor - Out of bounds Bottom") + + ; Test Set 2: Multiple Monitors + mon_multi_1 := {l: 0, t: 0, r: 1920, b: 1080} + mon_multi_2 := {l: -1080, t: 0, r: 0, b: 1920} + mon_multi_3 := {l: 1920, t: 0, r: 3840, b: 1080} + mon_multi_4 := {l: 0, t: -1080, r: 1920, b: 0} + apiMulti := MockSystemWindowAPI([mon_multi_1, mon_multi_2, mon_multi_3, mon_multi_4]) + + AssertEqual(1, GetMonitorAtPos(960, 540, apiMulti), "Multi monitor - Primary Center") + AssertEqual(2, GetMonitorAtPos(-500, 500, apiMulti), "Multi monitor - Left Monitor") + AssertEqual(3, GetMonitorAtPos(2500, 500, apiMulti), "Multi monitor - Right Monitor") + AssertEqual(4, GetMonitorAtPos(960, -500, apiMulti), "Multi monitor - Top Monitor") + AssertEqual(1, GetMonitorAtPos(0, 0, apiMulti), "Multi monitor - Origin overlapping") + AssertEqual(1, GetMonitorAtPos(1920, 500, apiMulti), "Multi monitor - Border overlapping Primary/Right") + AssertEqual(-1, GetMonitorAtPos(960, 2000, apiMulti), "Multi monitor - Out of bounds Bottom") + AssertEqual(-1, GetMonitorAtPos(-2000, 500, apiMulti), "Multi monitor - Out of bounds Far Left") + + ; Test Set 3: Zero Monitors + apiZero := MockSystemWindowAPI([]) + AssertEqual(-1, GetMonitorAtPos(0, 0, apiZero), "Zero monitors - Should return -1") + + ; Final Results + stdout.WriteLine("---") + stdout.WriteLine("Tests Passed: " . testsPassed) + stdout.WriteLine("Tests Failed: " . testsFailed) + +} finally { + ; No teardown needed +} + +if (testsFailed > 0) { + ExitApp(1) +} + +ExitApp(0)