#Requires AutoHotkey v2.0 #SingleInstance Force ;================================================ ; MULTISHORTCUTS v7.6 - 2026 ; STARTER & FULL MODES - ; / ` / = SHORTCUT FAMILIES ; MEGABAR - F-KEY MANAGER ;================================================ ; © GnuKey LLC ; Implementation assisted by Claude (Anthropic) ; Code review by Gemini (Google) ; Part of the SpeedKee Ecosystem ;================================================ ; HOTKEYS: ; Alt+T Add Text Expansion ; Alt+A Add App Launcher ; Alt+C Capture Selected Text as Expansion ; Alt+D Add Document Launcher ; Alt+E Edit Shortcuts GUI ; Alt+W Add Web/Email Launcher ; Alt+B Backup Now ; Alt+Q Quick Expansion Picker ; Alt+Z Undo Last Expansion ; Alt+P Pause/Resume ; Alt+H Help ; Alt+R Reload ;================================================ ;------------------------------------------------ ; CONFIGURATION ;------------------------------------------------ global config := Map( "Hotkeys", Map( "AddExpansion", "!t", "AddLauncher", "!a", "CaptureExpansion", "!c", "AddDocumentLauncher", "!d", "ShowEditGUI", "!e", "AddWebLauncher", "!w", "ImmediateBackup", "!b", "QuickTextExpansion", "!q", "ManageLaunchers", "!l", "ManageExpansions", "!m", "ShowAllShortcuts", "!s", "ReloadScript", "!r", "ShowHelp", "!h", "PauseResume", "!p", "UndoExpansion", "!z", "SnipTool", "!+s" ), "Security", Map( "ConfirmDangerousTargets", true, "MaxShortcutLength", 50, "MinShortcutLength", 2, "BlockedProtocols", ["cmd", "powershell", "runas", "regedit", "ms-settings", "vbscript", "javascript", "wscript", "cscript", "ms-msdt", "search-ms", "shell", "calculator", "ms-help"] ), "Performance", Map( "MaxBackupFiles", 10, "FileRetryAttempts", 3, "FileRetryDelay", 100, "LargeTextThreshold", 6000, "FileWatchInterval", 5000, "SequenceDelay", 150, ; ms between sequence tokens (key/wait/focus) - increase for slow apps "ClipWaitTimeout", 3 ; seconds to wait for clipboard after Ctrl+C ), "TextExpander", Map( "SmartTriggering", true, "AutoSpace", true, "BackupOnSave", true, "MaxHistorySize", 20 ), "Triggers", Map( ; Trigger characters for the built-in shortcut families. ; User-editable via tray menu → "Trigger Keys Setup…" ; Persisted in prefs.ini [Triggers]. "WinCmd", ";", ; Windows commands family (e.g. ;c = Ctrl+C) "AppLaunch", "``" ; Quick app launch family (e.g. `S = Snip Tool) ), "Feedback", Map( "PlaySounds", false, "ExpandSound", "*48", "LaunchSound", "*48", "ErrorSound", "*64", "BackupSound", "*48" ) ) ;------------------------------------------------ ; MODE: STARTER (default) vs FULL ; Starter = only ; ` = families active. Lightweight intro. ; Full = everything: text expansion engine, launchers, GUI editor, ; F-Key Hub, Megabar, file watcher, CSV import/export, etc. ; ; Stored in prefs.ini [Mode] Level=starter | full ; One-way unlock via tray menu - once Full, stays Full. ;------------------------------------------------ global appMode := "starter" ; flipped to "full" during init if prefs say so ;------------------------------------------------ ; GLOBAL STATE ;------------------------------------------------ global launcherPath := A_ScriptDir . "\launchers.ahk" global textExpansionPath := A_ScriptDir . "\text_expansion.ahk" global backupDir := A_ScriptDir . "\backups" global statsPath := A_ScriptDir . "\usage_stats.ini" global historyPath := A_ScriptDir . "\expansion_history.ini" global exportPath := A_ScriptDir . "\shortcuts_export.csv" global prefsPath := A_ScriptDir . "\prefs.ini" global loadedLaunchers := Map() global loadedExpansions := Map() global registeredLaunchers := [] global registeredExpansions := Map() ; shortcut -> opts used for registration global expansionHistory := [] global usageStats := Map() global lvSortCol := Map("launchers", 0, "expansions", 0) global lvSortAsc := Map("launchers", true, "expansions", true) ;------------------------------------------------ ; MEGABAR + FKEY MANAGER GLOBALS ;------------------------------------------------ global megaBarPath := A_ScriptDir . "\megabar.ini" global megaBarPaused := false global megaBarSlots := Map() global fkeyDownTime := Map() global fkeyLastTap := Map() global activeInputHook := "" ; tracks active combo InputHook for clean pause global fkeyComboWaiting := Map() global LONGPRESS_MS := 500 global DOUBLETAP_MS := 350 global COMBO_WAIT_MS := 1000 ; F-Key exemptions and remaps (saved to prefs.ini [FKeys]) global fkeyExempt := Map() ; e.g. fkeyExempt["F5"] := true global fkeyRemap := Map() ; e.g. fkeyRemap["F11"] := "F1" ;------------------------------------------------ ; AUTO-RELOAD ON EXTERNAL FILE CHANGE ;------------------------------------------------ global lastLauncherMod := 0 global lastExpMod := 0 global lastReload := 0 CheckFileTimes() { global launcherPath, textExpansionPath, lastLauncherMod, lastExpMod, lastReload if (A_TickCount - lastReload < 5000) return ; Don't reload if a write is in progress (lock file present) if (FileExist(launcherPath . ".lock") || FileExist(textExpansionPath . ".lock")) return tLauncher := 0 tExp := 0 if (FileExist(launcherPath)) tLauncher := FileGetTime(launcherPath, "M") if (FileExist(textExpansionPath)) tExp := FileGetTime(textExpansionPath, "M") if (tLauncher > lastLauncherMod || tExp > lastExpMod) { ; Verify files aren't mid-write by an external editor (no .lock file present) ; FileOpen read attempt will fail if file is exclusively locked by another process try { if (tLauncher > lastLauncherMod) FileOpen(launcherPath, "r").Close() if (tExp > lastExpMod) FileOpen(textExpansionPath, "r").Close() } catch { return ; File still busy - skip this cycle, will retry on next poll } lastLauncherMod := tLauncher lastExpMod := tExp lastReload := A_TickCount ; Don't reload if user has a script GUI open - would wipe unsaved input if (!WinExist("ahk_class AutoHotkeyGUI")) { TrayTip("File Change Detected", "Shortcuts file modified externally.`nReloading in 2 seconds...", "Iconi") SetTimer(ReloadScriptSilent, -2000) } } } ReloadScriptSilent() { Reload() } ;------------------------------------------------ ; INITIALIZATION ;------------------------------------------------ SafeInitialize() SafeInitialize() { global backupDir, loadedLaunchers, loadedExpansions, appMode global launcherPath, textExpansionPath, lastLauncherMod, lastExpMod try { ; Mode + sound + triggers always load (cheap, needed in both modes) LoadSoundPrefs() LoadTriggerPrefs() LoadModePref() ; Always set up the data files - seeding the built-in ; ` = families ; happens here. Starter mode users get them too. if (!DirExist(backupDir)) DirCreate(backupDir) InitializeFiles() MigrateDateSyntax() MigratePipeSeparators() LoadLaunchers() LoadTextExpansions() ; The built-in families are registered as hotstrings via LoadTextExpansions ; and LoadLaunchers above. Those work in BOTH modes. ; What gets gated by Full mode is everything *else* - the editor, the ; user-customizable workflows, the heavy background machinery. if (appMode = "full") { LoadUsageStats() LoadExpansionHistory() LoadFKeySettings() LoadMegaBar() SetupHotkeys() SetupMegaBarHotkeys() SetTimer(SaveUsageStats, 60000) if (FileExist(launcherPath)) lastLauncherMod := FileGetTime(launcherPath, "M") else lastLauncherMod := 0 if (FileExist(textExpansionPath)) lastExpMod := FileGetTime(textExpansionPath, "M") else lastExpMod := 0 SetTimer(CheckFileTimes, config["Performance"]["FileWatchInterval"]) } SetupTrayMenu() CheckFirstRun() if (appMode = "full") { TrayTip("MultiShortcuts v7.6 - Full", "Online - " . loadedLaunchers.Count . " launchers, " . loadedExpansions.Count . " expansions`nAuto-reload active", "Iconi") } else { starterMsg := "Ready! Built-in shortcuts work right away." starterMsg .= "`nRight-click the tray icon to learn more." TrayTip("MultiShortcuts v7.6 - Starter", starterMsg, "Iconi") } } catch Error as err { MsgBox("Initialization failed: " . err.Message . "`n`nScript will continue in limited mode.", "Error", "Icon!") } } ;------------------------------------------------ ; FIRST RUN SETUP ;------------------------------------------------ CheckFirstRun() { global prefsPath, config, appMode if (FileExist(prefsPath)) return result := MsgBox( "Welcome to MultiShortcuts!`n`n" . "Would you like sound feedback when shortcuts fire?`n`n" . "You can change this anytime from the tray menu.", "First Run Setup", "YesNo Iconi") if (result = "Yes") { config["Feedback"]["PlaySounds"] := true try A_TrayMenu.Check("Sound Feedback") } try FileAppend("FirstRun=done`n", prefsPath) wc := config["Triggers"]["WinCmd"] al := config["Triggers"]["AppLaunch"] if (appMode = "starter") { MsgBox( "You're running MultiShortcuts in Starter mode.`n`n" . "Try these right now - they work anywhere you can type:`n`n" . " " . wc . "c - Copy " . wc . "v - Paste`n" . " " . wc . "s - Save " . wc . "p - Print`n" . " " . wc . "t - Top of page " . wc . "b - Bottom`n" . " " . al . "S - Snip Tool " . al . "C - Calculator`n" . " -= - Today's date 1m= - One month from now`n`n" . "When you want more - custom shortcuts, an editor GUI, F-key powers - `n" . "right-click the tray icon and choose * Unlock Full Features.", "MultiShortcuts - Starter Mode", "Iconi") } else { MsgBox( "Quick Start (Full mode):`n`n" . " Alt+T - Add a text expansion`n" . " Alt+A - Add an app launcher`n" . " Alt+C - Capture selected text`n" . " Alt+E - Edit all shortcuts`n" . " Alt+H - Help`n`n" . "Right-click the tray icon for all options.", "MultiShortcuts - Getting Started", "Iconi") } } SetupHotkeys() { global config hk := config["Hotkeys"] try { Hotkey(hk["AddExpansion"], (*) => AddNewExpansion()) Hotkey(hk["AddLauncher"], (*) => AddNewLauncher()) Hotkey(hk["CaptureExpansion"], (*) => CaptureSelectedText()) Hotkey(hk["AddDocumentLauncher"], (*) => AddDocumentLauncher()) Hotkey(hk["ShowEditGUI"], (*) => ShowEditGUI("launchers")) Hotkey(hk["AddWebLauncher"], (*) => AddWebLauncher()) Hotkey(hk["ImmediateBackup"], (*) => ImmediateBackup()) Hotkey(hk["QuickTextExpansion"], (*) => QuickTextExpansion()) Hotkey(hk["ManageLaunchers"], (*) => ShowEditGUI("launchers")) Hotkey(hk["ManageExpansions"], (*) => ShowEditGUI("expansions")) Hotkey(hk["ShowAllShortcuts"], (*) => ShowEditGUI("launchers")) Hotkey(hk["ReloadScript"], (*) => ReloadScript()) Hotkey(hk["ShowHelp"], (*) => ShowHelp()) Hotkey(hk["PauseResume"], (*) => ToggleMacrosPause(), "S") Hotkey(hk["UndoExpansion"], (*) => UndoLastExpansion()) Hotkey(hk["SnipTool"], (*) => Send("#+s")) } catch Error as err { MsgBox("Hotkey setup error: " . err.Message, "Warning", "Icon!") } } HotkeyLabel(raw) { label := StrReplace(raw, "+", "Shift+") label := StrReplace(label, "!", "Alt+") label := StrReplace(label, "^", "Ctrl+") label := StrReplace(label, "#", "Win+") return StrUpper(label) } SetupTrayMenu() { global appMode A_TrayMenu.Delete() if (appMode = "starter") { SetupStarterTrayMenu() } else { SetupFullTrayMenu() } UpdateTrayTooltip() } SetupStarterTrayMenu() { global config A_TrayMenu.Add("MultiShortcuts (Starter)", (*) => 0) A_TrayMenu.Disable("MultiShortcuts (Starter)") A_TrayMenu.Add() A_TrayMenu.Add("Show Built-in Shortcuts", (*) => ShowHelp()) A_TrayMenu.Add("Trigger Keys Setup...", (*) => ShowTriggerSetup()) A_TrayMenu.Add() A_TrayMenu.Add("* Unlock Full Features", (*) => UnlockFullMode()) A_TrayMenu.Add() A_TrayMenu.Add("Run on Windows Startup", (*) => ToggleStartup()) A_TrayMenu.Add("Sound Feedback", (*) => ToggleSounds()) A_TrayMenu.Add("Reload Script", (*) => Reload()) A_TrayMenu.Add("Exit", (*) => ExitApp()) A_TrayMenu.Default := "Show Built-in Shortcuts" if (FileExist(GetStartupLinkPath())) A_TrayMenu.Check("Run on Windows Startup") if (config["Feedback"]["PlaySounds"]) A_TrayMenu.Check("Sound Feedback") } SetupFullTrayMenu() { A_TrayMenu.Add("Show Help", (*) => ShowHelp()) A_TrayMenu.Add("Edit Shortcuts (Alt+E)", (*) => ShowEditGUI("launchers")) A_TrayMenu.Add() A_TrayMenu.Add("Add Text Expansion (Alt+T)", (*) => AddNewExpansion()) A_TrayMenu.Add("Add App Launcher (Alt+A)", (*) => AddNewLauncher()) A_TrayMenu.Add("Add Document (Alt+D)", (*) => AddDocumentLauncher()) A_TrayMenu.Add("Add Web/Email (Alt+W)", (*) => AddWebLauncher()) A_TrayMenu.Add("Capture Selection (Alt+C)", (*) => CaptureSelectedText()) A_TrayMenu.Add("Snip Tool (Alt+Shift+S)", (*) => Send("#+s")) A_TrayMenu.Add() A_TrayMenu.Add("F-Key Hub Setup", (*) => ShowMegaBarSetup()) A_TrayMenu.Add("F-Key Manager", (*) => ShowFKeyManager()) A_TrayMenu.Add("Pause F-Key Hub", (*) => ToggleMegaBar()) A_TrayMenu.Add() A_TrayMenu.Add("Quick Expansion Picker (Alt+Q)", (*) => QuickTextExpansion()) A_TrayMenu.Add("Undo Last Expansion (Alt+Z)", (*) => UndoLastExpansion()) A_TrayMenu.Add() A_TrayMenu.Add("Usage Statistics", (*) => ShowUsageStats()) A_TrayMenu.Add("Backup Now (Alt+B)", (*) => ImmediateBackup()) A_TrayMenu.Add("Export to CSV", (*) => ExportToCSV()) A_TrayMenu.Add("Import from CSV", (*) => ImportFromCSV()) A_TrayMenu.Add() A_TrayMenu.Add("Run on Windows Startup", (*) => ToggleStartup()) A_TrayMenu.Add("Sound Feedback", (*) => ToggleSounds()) A_TrayMenu.Add("Trigger Keys Setup...", (*) => ShowTriggerSetup()) A_TrayMenu.Add("Pause/Resume (Alt+P)", (*) => ToggleMacrosPause()) A_TrayMenu.Add("Reload Script", (*) => Reload()) A_TrayMenu.Add("Exit", (*) => ExitApp()) A_TrayMenu.Default := "Edit Shortcuts (Alt+E)" if (FileExist(GetStartupLinkPath())) A_TrayMenu.Check("Run on Windows Startup") if (config["Feedback"]["PlaySounds"]) A_TrayMenu.Check("Sound Feedback") } UpdateTrayTooltip() { global loadedLaunchers, loadedExpansions, megaBarPaused, appMode if (appMode = "starter") { A_IconTip := "MultiShortcuts (Starter) v7.6" . "`nBuilt-in trigger families active" . "`nRight-click for menu - Unlock Full when ready" return } status := A_IsSuspended ? "PAUSED" : "Active" mbStatus := megaBarPaused ? "Paused" : "Active" A_IconTip := "MultiShortcuts (Full) v7.6 (" . status . ")" . "`nLaunchers: " . loadedLaunchers.Count . " Expansions: " . loadedExpansions.Count . "`nF-Key Hub: " . mbStatus } ;------------------------------------------------ ; SHARED HELPERS ;------------------------------------------------ CheckShortcutConflict(shortcut, existingMap) { if (!existingMap.Has(shortcut)) return true result := MsgBox("Shortcut '" . shortcut . "' already exists.`n`nOverwrite it?", "Shortcut Exists", "YesNo Icon?") return result = "Yes" } CheckGlobalConflict(shortcut) { global loadedLaunchers, loadedExpansions hits := [] if (loadedLaunchers.Has(shortcut)) hits.Push("Launcher (launchers.ahk)") if (loadedExpansions.Has(shortcut)) hits.Push("Text Expansion (text_expansion.ahk)") if (hits.Length = 0) return true whereList := "" for , h in hits whereList .= " * " . h . "`n" answer := MsgBox( "Shortcut '" . shortcut . "' already exists in:`n`n" . whereList . "`nOverwrite it everywhere?", "Shortcut Conflict", "YesNo Icon?") return answer = "Yes" } SaveEntry(filePath, shortcut, target, reloadFn, successTitle, successMsg) { global config, loadedLaunchers, loadedExpansions, launcherPath, textExpansionPath try { if (config["TextExpander"]["BackupOnSave"]) BackupFile(filePath) ; True overwrite: if shortcut exists, rewrite file instead of appending ownerMap := "" ownerHeader := "" if (filePath = launcherPath) { ownerMap := loadedLaunchers ownerHeader := "; Launcher file - Format: shortcut target" } else if (filePath = textExpansionPath) { ownerMap := loadedExpansions ownerHeader := "; Text Expansion file - Format: shortcut expansion" } if (ownerMap != "" && ownerMap.Has(shortcut)) { ownerMap[shortcut] := target RewriteFile(filePath, ownerMap, ownerHeader) } else { LockedFileAppend(filePath, shortcut . " " . StrReplace(target, "`n", "[NEWLINE]") . "`n") } reloadFn.Call() TrayTip(successTitle, successMsg, "Iconi") return true } catch Error as err { MsgBox("Failed to save: " . err.Message, "Error", "Icon!") return false } } RewriteFile(filePath, dataMap, headerComment) { global config lockPath := filePath . ".lock" tempPath := filePath . ".tmp" backPath := filePath . ".bak" try { if (config["TextExpander"]["BackupOnSave"]) BackupFile(filePath) content := headerComment . "`n" for shortcut, target in dataMap content .= shortcut . " " . StrReplace(target, "`n", "[NEWLINE]") . "`n" retries := 10 while (FileExist(lockPath) && retries > 0) { Sleep(100) retries-- } FileAppend("", lockPath, "UTF-8") try { ; Write to temp first if (FileExist(tempPath)) FileDelete(tempPath) FileAppend(content, tempPath, "UTF-8") ; Keep backup of original in case copy fails if (FileExist(filePath)) FileCopy(filePath, backPath, 1) ; Overwrite original with temp (FileCopy is safer than delete+move) ; Atomic OS-level file replacement - safe against power loss/crash if (!DllCall("ReplaceFile", "Str", filePath, "Str", tempPath, "Ptr", 0, "UInt", 1, "Ptr", 0, "Ptr", 0)) { ; Fallback to FileCopy if ReplaceFile fails (e.g. file doesn't exist yet) FileCopy(tempPath, filePath, 1) } ; Clean up try FileDelete(tempPath) try FileDelete(backPath) } finally { try FileDelete(lockPath) } return true } catch Error as err { try FileDelete(lockPath) try FileDelete(tempPath) ; Restore from backup if available if (FileExist(backPath) && !FileExist(filePath)) try FileCopy(backPath, filePath, 1) try FileDelete(backPath) MsgBox("Failed to rewrite file: " . err.Message, "Error", "Icon!") return false } } LockedFileAppend(filePath, content) { lockPath := filePath . ".lock" retries := 10 while (FileExist(lockPath) && retries > 0) { Sleep(100) retries-- } ; Fix 6: if lock file still exists after retries, alert user instead of silent fail if (FileExist(lockPath)) { MsgBox("Could not save - the file is locked:`n" . filePath . "`n`nIf this keeps happening, delete the file:`n" . lockPath, "Save Failed", "Icon!") return } try FileAppend("", lockPath, "UTF-8") try { FileAppend(content, filePath, "UTF-8") } finally { try FileDelete(lockPath) } } PromptShortcut(prompt, title) { IB := InputBox(prompt, title, "w370 h150") if (IB.Result = "Cancel" || IB.Value = "") return "" shortcut := Trim(IB.Value) if (!IsValidShortcut(shortcut)) { MsgBox("Invalid shortcut. Cannot be empty or contain the | character.", "Invalid Input", "Icon!") return "" } return shortcut } ;------------------------------------------------ ; AUTO-BACKUP ;------------------------------------------------ BackupFile(filePath) { global backupDir if (!FileExist(filePath)) return try { SplitPath(filePath, &fName) timestamp := FormatTime(, "yyyyMMdd_HHmmss") destPath := backupDir . "\" . fName . "." . timestamp . ".bak" FileCopy(filePath, destPath) PruneBackups(fName) } catch { ; Silent } } PruneBackups(baseName) { global backupDir, config maxFiles := config["Performance"]["MaxBackupFiles"] backups := [] loop files, backupDir . "\" . baseName . ".*.bak" { backups.Push([A_LoopFileFullPath, A_LoopFileTimeCreated]) } loop backups.Length - 1 { i := A_Index + 1 while (i > 1 && backups[i][2] < backups[i-1][2]) { tmp := backups[i] backups[i] := backups[i-1] backups[i-1] := tmp i-- } } while (backups.Length > maxFiles) { try FileDelete(backups[1][1]) backups.RemoveAt(1) } } SortArray(arr) { if (arr.Length = 0) return [] joined := "" for , val in arr joined .= val "`n" sorted_str := Sort(joined, "D`n C") result := StrSplit(sorted_str, "`n") while (result.Length > 0 && result[result.Length] = "") result.Pop() return result } ;------------------------------------------------ ; ADD NEW ENTRY FUNCTIONS ;------------------------------------------------ AddNewLauncher() { global launcherPath, loadedLaunchers shortcut := PromptShortcut( "Enter shortcut trigger name:`n`nExample: 'word' to open Microsoft Word", "Add App Launcher") if (shortcut = "") return if (!CheckGlobalConflict(shortcut)) return selectedFile := FileSelect(1, A_ProgramFiles, "Select Application Executable", "Applications (*.exe; *.lnk)") if (selectedFile = "") return ; Resolve .lnk shortcut files to their real target before validation ; A .lnk could point to cmd.exe or powershell.exe with arguments targetToValidate := selectedFile if (StrLower(SubStr(selectedFile, -3)) = ".lnk") { try { FileGetShortcut(selectedFile, &lnkTarget) if (lnkTarget != "") targetToValidate := lnkTarget } } if (!IsAllowedTarget(targetToValidate)) return SaveEntry(launcherPath, shortcut, selectedFile, LoadLaunchers, "App Launcher Added", shortcut . " -> " . selectedFile) } AddDocumentLauncher() { global launcherPath, loadedLaunchers shortcut := PromptShortcut( "Enter shortcut name for the document:`n`nExample: 'resume'", "Add Document Launcher") if (shortcut = "") return if (!CheckGlobalConflict(shortcut)) return selectedFile := FileSelect(1, A_MyDocuments, "Select Document", "Documents (*.doc;*.docx;*.pdf;*.txt;*.xlsx;*.pptx;*.csv)") if (selectedFile = "") return if (!IsAllowedTarget(selectedFile)) return SaveEntry(launcherPath, shortcut, selectedFile, LoadLaunchers, "Document Launcher Added", shortcut . " -> " . selectedFile) } ;---------------------------------------------- ; Is Allowed ;--------------------------------------------- IsAllowedTarget(target) { global config ; Normalise: strip leading/trailing whitespace and quotes, lowercase for comparison cleaned := StrLower(Trim(target, " `t`"'")) ; Block environment variable injection patterns like %comspec% if (RegExMatch(cleaned, "%\w+%")) { MsgBox("Environment variable references (e.g. %comspec%) are not permitted.`n" . "Use a full path instead.", "Security Block", "Icon!") return false } ; Block shell metacharacters that could enable command injection ; BUT only for file/executable targets - URLs legitimately contain & ? # ( ) etc. isUrl := (SubStr(cleaned, 1, 7) = "http://" || SubStr(cleaned, 1, 8) = "https://" || SubStr(cleaned, 1, 7) = "mailto:" || SubStr(cleaned, 1, 6) = "ftp://") if (!isUrl && (RegExMatch(cleaned, "[&;``><|$()#?*]"))) { MsgBox("Shell metacharacters are not permitted in launcher targets.`nUse a direct file path without command chaining.", "Security Block", "Icon!") return false } ; Block dangerous protocols for , proto in config["Security"]["BlockedProtocols"] { if (RegExMatch(cleaned, "^" . proto . ":")) { MsgBox("Blocked protocol: '" . proto . "'.`n" . "Only http/https/mailto and normal file paths are permitted.", "Security Block", "Icon!") return false } } ; Block dangerous script file extensions including AHK itself blockedExts := [".bat", ".cmd", ".ps1", ".vbs", ".wsf", ".hta", ".ahk", ".ah2"] for , ext in blockedExts { if (RegExMatch(cleaned, "\Q" . ext . "\E(\s|$)")) { MsgBox("File type '" . ext . "' is not permitted as a launcher target.`n" . "Use a .exe or a folder path instead.", "Security Block", "Icon!") return false } } ; Warn on dangerous system executables if ConfirmDangerousTargets is enabled if (config["Security"]["ConfirmDangerousTargets"]) { ; Use SplitPath to extract just the executable filename ; This catches "C:\Windows\System32\cmd.exe /c del *.*" style bypasses exeTarget := cleaned if (SubStr(cleaned, 1, 1) = '"') { ; Quoted path - extract content between first pair of quotes if (RegExMatch(cleaned, '^"([^"]+)"', &m)) exeTarget := m[1] } else { ; Unquoted - take everything before first space (the exe path) exeTarget := StrSplit(cleaned, " ")[1] } SplitPath(exeTarget, &outFileName) cleanedExe := StrLower(outFileName) dangerousExes := ["cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe", "cscript.exe", "mshta.exe", "regsvr32.exe", "rundll32.exe", "msiexec.exe", "wmic.exe", "forfiles.exe", "regedit.exe"] dangerousDirs := ["system32", "syswow64", "sysnative"] isDangerous := false ; Check by exact executable filename for , exe in dangerousExes { if (cleanedExe = exe) { isDangerous := true break } } ; Also check by directory path if (!isDangerous) { for , dir in dangerousDirs { if (InStr(StrLower(exeTarget), "\" . dir . "\")) { isDangerous := true break } } } if (isDangerous) { result := MsgBox("Warning: This target appears to be a sensitive system executable.`n`n" . target . "`n`nAre you sure you want to add this launcher?", "Security Warning", "YesNo Icon!") if (result != "Yes") return false } } return true } ;--------------------------------------------- AddWebLauncher() { global launcherPath, loadedLaunchers shortcut := PromptShortcut( "Enter trigger name:`n`nExample: 'gmail' or 'news'", "Web/Email Launcher") if (shortcut = "") return if (!CheckGlobalConflict(shortcut)) return I2 := InputBox( "Enter URL or Email address:`n`nExamples:`nhttps://gmail.com`njohn@example.com", "Target Address", "w400 h160", "https://") if (I2.Result = "Cancel" || I2.Value = "") return target := Trim(I2.Value) if (InStr(target, "@") && !InStr(target, "://") && !InStr(target, "mailto:")) target := "mailto:" . target ; Warn if user adds plain HTTP instead of HTTPS if (RegExMatch(target, "^http://")) { result := MsgBox("This URL uses HTTP which is not secure.`n`n" . "Would you like to use HTTPS instead?", "Security Warning", "YesNo Icon!") if (result = "Yes") target := "https://" . SubStr(target, 8) } if (!IsAllowedTarget(target)) return SaveEntry(launcherPath, shortcut, target, LoadLaunchers, "Web Launcher Added", shortcut . " -> " . target) } AddNewExpansion() { global textExpansionPath, loadedExpansions shortcut := PromptShortcut( "Enter abbreviation to expand:`n`nExample: 'addr' for your address", "Add Text Expansion") if (shortcut = "") return if (!CheckGlobalConflict(shortcut)) return gExp := Gui("+ToolWindow +AlwaysOnTop", "Add Text Expansion - " . shortcut) gExp.SetFont("s10", "Segoe UI") gExp.Add("Text", "x10 y10 w460 cGray", "Enter expansion text. Multi-line paste supported.") editVal := gExp.Add("Edit", "x10 y32 w460 h140 +Multi +WantReturn", "") gExp.Add("Text", "x10 y180 w460 h100 cGray", "Placeholders: {clip} {clip:100} {clip:trim} {clip:lower} {clip:upper}" . "`n {date} {date:locale} {date:dd/MM/yyyy} {time} {time:h:mm tt}" . "`n {day} {month} {year} {user} = login name {comp} = computer name" . "`n {cursor} = place caret here after expansion {cursor:5} = move left 5" . "`n {prompt:Question?} = ask for input {prompt:Question?|default}" . "`n -- Sequence tokens --" . "`n {key:F6} = press a key {key:^s} = Ctrl+S {key:{Tab}} = Tab" . "`n {wait:500} = pause 500 ms {focus:Notepad} = activate window by title") gExp.Add("Text", "x10 y286 w480 cGray", "Quick insert:") gExp.Add("Button", "x10 y304 w106 h24", "Date yyyy-MM-dd") .OnEvent("Click", (*) => InsertIntoEdit(editVal, "{date}")) gExp.Add("Button", "x122 y304 w106 h24", "Date dd/MM/yyyy") .OnEvent("Click", (*) => InsertIntoEdit(editVal, "{date:dd/MM/yyyy}")) gExp.Add("Button", "x234 y304 w100 h24", "Time HH:mm") .OnEvent("Click", (*) => InsertIntoEdit(editVal, "{time}")) gExp.Add("Button", "x340 y304 w130 h24", "Time h:mm AM/PM") .OnEvent("Click", (*) => InsertIntoEdit(editVal, "{time:h:mm tt}")) gExp.Add("Button", "x10 y338 w110 h28", "&Save") .OnEvent("Click", DoSave) gExp.Add("Button", "x128 y338 w110 h28", "&Preview") .OnEvent("Click", (*) => MsgBox(ResolvePlaceholders(editVal.Value), "Preview", "Iconi")) gExp.Add("Button", "x246 y338 w110 h28", "Cancel") .OnEvent("Click", (*) => gExp.Destroy()) gExp.Show("w480 h380") editVal.Focus() DoSave(*) { expansion := editVal.Value if (expansion = "") { MsgBox("Expansion text cannot be empty.", "Validation", "Icon!") return } gExp.Destroy() SaveEntry(textExpansionPath, shortcut, expansion, LoadTextExpansions, "Expansion Added", shortcut . " -> " . SubStr(expansion, 1, 40)) } } ;------------------------------------------------ ; CAPTURE SELECTED TEXT AS EXPANSION ;------------------------------------------------ CaptureSelectedText() { global textExpansionPath, loadedExpansions clipSaved := ClipboardAll() A_Clipboard := "" Send("^c") if (!ClipWait(config["Performance"]["ClipWaitTimeout"])) { A_Clipboard := clipSaved MsgBox("No text was selected.`n`nHighlight some text first, then press Alt+C.", "Nothing Selected", "Iconi") return } selectedText := A_Clipboard A_Clipboard := clipSaved if (Trim(selectedText) = "") { MsgBox("No text was selected.`n`nHighlight some text first, then press Alt+C.", "Nothing Selected", "Iconi") return } preview := StrLen(selectedText) > 150 ? SubStr(selectedText, 1, 147) . "..." : selectedText gCap := Gui("+ToolWindow +AlwaysOnTop", "Capture Text as Expansion") gCap.SetFont("s10", "Segoe UI") gCap.Add("Text", "x10 y10 w440 cGray", "Captured text:") previewCtrl := gCap.Add("Edit", "x10 y30 w440 h80 +ReadOnly +Multi", preview) previewCtrl.SetFont("s9", "Consolas") gCap.Add("Text", "x10 y120 w440", "Enter a shortcut trigger for this text:") scEdit := gCap.Add("Edit", "x10 y142 w440 h24") gCap.Add("Button", "x10 y178 w120 h28", "Save") .OnEvent("Click", DoCaptureSave) gCap.Add("Button", "x138 y178 w120 h28", "Cancel") .OnEvent("Click", (*) => gCap.Destroy()) gCap.Show("w460 h218") scEdit.Focus() DoCaptureSave(*) { shortcut := Trim(scEdit.Value) if (shortcut = "") { MsgBox("Please enter a shortcut name.", "Required", "Icon!") return } if (!IsValidShortcut(shortcut)) { MsgBox("Invalid shortcut. Cannot be empty or contain the | character.", "Invalid", "Icon!") return } if (!CheckGlobalConflict(shortcut)) return gCap.Destroy() ; close window immediately sc := shortcut txt := selectedText SetTimer(() => SaveEntry(textExpansionPath, sc, txt, LoadTextExpansions, "Expansion Captured", sc . " -> " . SubStr(txt, 1, 40)), -1) } } ;------------------------------------------------ ; IMMEDIATE BACKUP ;------------------------------------------------ ImmediateBackup() { global launcherPath, textExpansionPath try { BackupFile(launcherPath) BackupFile(textExpansionPath) PlayFeedback("BackupSound") TrayTip("Backup Complete", "Both files backed up to \backups\ folder", "Iconi") } catch Error as err { PlayFeedback("ErrorSound") MsgBox("Backup failed: " . err.Message, "Error", "Icon!") } } ;------------------------------------------------ ; QUICK TEXT EXPANSION ;------------------------------------------------ QuickTextExpansion() { global loadedExpansions if (loadedExpansions.Count = 0) { MsgBox("No text expansions defined yet.`n`nUse Alt+T to add one.", "No Expansions", "Iconi") return } items := [] for shortcut, expansion in loadedExpansions { previewText := StrReplace(StrReplace(expansion, "`r`n", " <- "), "`n", " <- ") preview := StrLen(previewText) > 60 ? SubStr(previewText, 1, 57) . "..." : previewText items.Push(shortcut . " -> " . preview) } items := SortArray(items) gPick := Gui("+ToolWindow +AlwaysOnTop", "Quick Text Expansion") gPick.SetFont("s10", "Segoe UI") gPick.BackColor := "FFFFFF" gPick.Add("Text", "x10 y10 w380 cGray", "Search and double-click or press Enter to paste:") searchBox := gPick.Add("Edit", "x10 y32 w380 h24 vSearch") lv := gPick.Add("ListView", "x10 y62 w380 h240 -Hdr Grid", [""]) lv.ModifyCol(1, 370) PopulateQuickList(lv, items, "") searchBox.OnEvent("Change", (*) => PopulateQuickList(lv, items, searchBox.Value)) PasteSelected(*) { row := lv.GetNext(0, "Focused") if (!row) row := lv.GetNext() if (!row) return rowText := lv.GetText(row, 1) sc := Trim(StrSplit(rowText, " -> ")[1]) if (loadedExpansions.Has(sc)) { gPick.Destroy() Sleep(80) expansion := loadedExpansions[sc] RecordExpansionHistory(sc, sc, expansion) SendText(expansion) TrackUsage(sc, "expansion") } } lv.OnEvent("DoubleClick", PasteSelected) gPick.Add("Button", "x10 y312 w180 h28", "Paste Selected") .OnEvent("Click", PasteSelected) gPick.Add("Button", "x200 y312 w190 h28", "Cancel") .OnEvent("Click", (*) => gPick.Destroy()) gPick.Show("w400 h352") searchBox.Focus() } PopulateQuickList(lv, items, filter := "") { lv.Delete() ; Fix 8: use default parameter instead of ?? operator for v2 compatibility for , item in items { if (filter = "" || InStr(item, filter)) lv.Add("", item) } if (lv.GetCount() > 0) lv.Modify(1, "Select Focus") } ;------------------------------------------------ ; UNDO LAST EXPANSION ;------------------------------------------------ UndoLastExpansion() { global expansionHistory if (expansionHistory.Length = 0) { TrayTip("Nothing to Undo", "No recent expansions in history", "Iconi") return } last := expansionHistory[expansionHistory.Length] original := last["shortcut"] ; Use stored length + 1 for trailing space added by SendExpandText delCount := last.Has("length") ? last["length"] + 1 : StrLen(original) + 1 Send("{BS " . delCount . "}") Sleep(30) SendText(original) expansionHistory.Pop() SaveExpansionHistory() TrayTip("Undone", "Restored '" . original . "'", "Iconi") } RecordExpansionHistory(shortcut, original, expanded) { global expansionHistory, config maxSize := config["TextExpander"]["MaxHistorySize"] ; Store only shortcut + timestamp + length - NOT the expanded text ; This protects patient data, private messages, and sensitive content expansionHistory.Push(Map( "shortcut", shortcut, "original", original, "length", StrLen(expanded), "time", A_Now )) while (expansionHistory.Length > maxSize) expansionHistory.RemoveAt(1) SaveExpansionHistory() } ;------------------------------------------------ ; EXPANSION HISTORY PERSISTENCE ;------------------------------------------------ SaveExpansionHistory() { global expansionHistory, historyPath try { content := "" for entry in expansionHistory { sc := StrReplace(entry["shortcut"], "|", "[PIPE]") len := entry.Has("length") ? entry["length"] : 0 content .= sc . "|" . len . "|" . entry["time"] . "`n" } if (FileExist(historyPath)) FileDelete(historyPath) if (content != "") FileAppend(content, historyPath) } catch Error as err { if (InStr(err.Message, "disk") || InStr(err.Message, "space") || InStr(err.Message, "denied") || InStr(err.Message, "permission")) TrayTip("Save Warning", "Could not save history: " . err.Message, "Icon!") } } LoadExpansionHistory() { global expansionHistory, historyPath, config expansionHistory := [] if (!FileExist(historyPath)) return try { maxSize := config["TextExpander"]["MaxHistorySize"] content := FileRead(historyPath, "UTF-8") for line in StrSplit(content, "`n", "`r") { line := Trim(line) if (line = "") continue parts := StrSplit(line, "|", , 3) if (parts.Length >= 3) { expansionHistory.Push(Map( "shortcut", StrReplace(parts[1], "[PIPE]", "|"), "length", Integer(parts[2]), "time", parts[3] )) } } while (expansionHistory.Length > maxSize) expansionHistory.RemoveAt(1) } catch Error as err { ; Silent } } ;------------------------------------------------ ; USAGE STATS PERSISTENCE ;------------------------------------------------ SaveUsageStats() { global usageStats, statsPath try { content := "" for key, data in usageStats content .= key . "|" . data["count"] . "|" . data["lastUsed"] . "`n" if (FileExist(statsPath)) FileDelete(statsPath) if (content != "") FileAppend(content, statsPath) } catch Error as err { ; Fix 9: surface disk-full or permission errors if (InStr(err.Message, "disk") || InStr(err.Message, "space") || InStr(err.Message, "denied") || InStr(err.Message, "permission")) TrayTip("Save Warning", "Could not save stats: " . err.Message, "Icon!") } } LoadUsageStats() { global usageStats, statsPath usageStats := Map() if (!FileExist(statsPath)) return try { content := FileRead(statsPath, "UTF-8") for line in StrSplit(content, "`n", "`r") { line := Trim(line) if (line = "") continue parts := StrSplit(line, "|", , 3) if (parts.Length >= 3) usageStats[parts[1]] := Map( "count", Integer(parts[2]), "lastUsed", parts[3]) } } catch Error as err { ; Silent } } TrackUsage(shortcut, type) { global usageStats key := type . "|" . shortcut if (!usageStats.Has(key)) usageStats[key] := Map("count", 0, "lastUsed", "") usageStats[key]["count"]++ usageStats[key]["lastUsed"] := A_Now ; Stats are saved to disk by a background timer every 60 seconds ; rather than on every keystroke to prevent disk thrashing } ;------------------------------------------------ ; USAGE STATS GUI ;------------------------------------------------ ShowUsageStats() { global usageStats if (usageStats.Count = 0) { MsgBox("No usage data yet.`n`nShortcuts are tracked as you use them.", "Usage Statistics", "Iconi") return } rows := [] for key, data in usageStats { parts := StrSplit(key, "|", , 2) rows.Push([data["count"], parts[2], parts[1], data["lastUsed"]]) } loop rows.Length - 1 { i := A_Index + 1 while (i > 1 && rows[i][1] > rows[i-1][1]) { tmp := rows[i] rows[i] := rows[i-1] rows[i-1] := tmp i-- } } gStats := Gui("+ToolWindow +Resize", "Usage Statistics") gStats.SetFont("s10", "Segoe UI") gStats.Add("Text", "x10 y10 w560 cGray", "Most-used shortcuts (all time):") lv := gStats.Add("ListView", "x10 y32 w560 h320 Grid", ["Shortcut", "Type", "Uses", "Last Used"]) loop lv.GetCount("Col") lv.ModifyCol(A_Index, "AutoHdr") for row in rows { lastUsed := row[4] != "" ? FormatTime(row[4], "yyyy-MM-dd HH:mm") : "-" lv.Add("", row[2], row[3], row[1], lastUsed) } gStats.Add("Button", "x10 y362 w120 h28", "Clear Stats") .OnEvent("Click", (*) => ClearStats(gStats)) gStats.Add("Button", "x138 y362 w120 h28", "Export Stats") .OnEvent("Click", (*) => ExportStats(rows)) gStats.Add("Button", "x448 y362 w120 h28", "Close") .OnEvent("Click", (*) => gStats.Destroy()) gStats.OnEvent("Close", (*) => gStats.Destroy()) gStats.OnEvent("Escape", (*) => gStats.Destroy()) gStats.Show("w580 h402") } ClearStats(gStats) { global usageStats if (MsgBox("Clear all usage statistics?", "Confirm", "YesNo Icon?") = "Yes") { usageStats := Map() SaveUsageStats() gStats.Destroy() TrayTip("Cleared", "Usage stats reset", "Iconi") } } ExportStats(rows) { savePath := FileSelect("S", A_ScriptDir . "\usage_stats_export.csv", "Export Usage Stats", "CSV Files (*.csv)") if (savePath = "") return try { content := "Shortcut,Type,Uses,Last Used`n" for row in rows { lastUsed := row[4] != "" ? FormatTime(row[4], "yyyy-MM-dd HH:mm") : "" content .= EscapeCSV(row[2]) . "," . EscapeCSV(row[3]) . "," . row[1] . "," . EscapeCSV(lastUsed) . "`n" } if (FileExist(savePath)) FileDelete(savePath) FileAppend(content, savePath) TrayTip("Stats Exported", "Saved to " . savePath, "Iconi") } catch Error as err { MsgBox("Export failed: " . err.Message, "Error", "Icon!") } } ;------------------------------------------------ ; EXPORT TO CSV ;------------------------------------------------ ExportToCSV() { global loadedLaunchers, loadedExpansions, exportPath savePath := FileSelect("S", exportPath, "Export Shortcuts to CSV", "CSV Files (*.csv)") if (savePath = "") return try { content := "Type,Shortcut,Target/Expansion`n" for sc, target in loadedLaunchers content .= "Launcher," . EscapeCSV(sc) . "," . EscapeCSV(target) . "`n" for sc, exp in loadedExpansions content .= "Expansion," . EscapeCSV(sc) . "," . EscapeCSV(exp) . "`n" if (FileExist(savePath)) FileDelete(savePath) FileAppend(content, savePath) MsgBox("Exported " . (loadedLaunchers.Count + loadedExpansions.Count) . " shortcuts to:`n" . savePath, "Export Complete", "Iconi") } catch Error as err { MsgBox("Export failed: " . err.Message, "Error", "Icon!") } } EscapeCSV(val) { ; Substitute newlines with token before CSV encoding val := StrReplace(val, "`r`n", "[NEWLINE]") val := StrReplace(val, "`n", "[NEWLINE]") val := StrReplace(val, "`r", "[NEWLINE]") ; Prefix cells starting with formula characters to prevent Excel injection ; e.g. =cmd|, @SUM, +calc, -1+1+cmd| if (RegExMatch(val, "^[=@+\-]")) val := "'" . val if (InStr(val, ",") || InStr(val, '"')) { val := StrReplace(val, '"', '""') return '"' . val . '"' } return val } ;------------------------------------------------ ; IMPORT FROM CSV ;------------------------------------------------ ImportFromCSV() { global loadedLaunchers, loadedExpansions, launcherPath, textExpansionPath ; Warn user about trusted sources before importing result := MsgBox("Only import shortcut files from sources you trust.`n`n" . "Imported expansions may send keystrokes, use clipboard content,`n" . "or activate windows on your computer.`n`nContinue with import?", "Import Warning", "YesNo Icon!") if (result != "Yes") return filePath := FileSelect(1, A_ScriptDir, "Import Shortcuts from CSV", "CSV Files (*.csv)") if (filePath = "") return ; ── Pre-scan pass: count expansions that contain risky sequence tokens ; before importing anything. {key:...} sends raw keystrokes - an ; imported expansion with {key:#r} would open Run, {key:^w} would close ; the active window, etc. We don't block, but we make the user aware. riskyCount := 0 riskySamples := [] try { for line in StrSplit(FileRead(filePath, "UTF-8"), "`n", "`r") { line := Trim(line) if (line = "" || SubStr(line, 1, 4) = "Type") continue parts := StrSplit(line, ",", , 3) if (parts.Length < 3) continue if (Trim(parts[1]) = "Expansion") { val := StrReplace(parts[3], '"', "") if (InStr(val, "{key:") || InStr(val, "{focus:")) { riskyCount++ if (riskySamples.Length < 3) riskySamples.Push(Trim(StrReplace(parts[2], '"', ""))) } } } } if (riskyCount > 0) { sampleList := "" for , s in riskySamples sampleList .= " * " . s . "`n" if (riskyCount > riskySamples.Length) sampleList .= " * ...and " . (riskyCount - riskySamples.Length) . " more`n" warn := riskyCount . " expansion(s) in this CSV send keystrokes or activate " warn .= "windows via {key:} or {focus:} tokens.`n`n" warn .= sampleList . "`n" warn .= "These can perform actions like closing windows, opening dialogs, " warn .= "or running Win+R. Only import if you trust the source.`n`n" warn .= "Proceed with the import?" if (MsgBox(warn, "Risky Tokens Detected", "YesNo Icon!") != "Yes") return } try { content := FileRead(filePath, "UTF-8") lines := StrSplit(content, "`n", "`r") imported := 0 skipped := 0 for i, line in lines { if (i = 1 && InStr(line, "Type,Shortcut")) continue line := Trim(line) if (line = "") continue parts := StrSplit(line, ",", , 3) if (parts.Length < 3) { skipped++ continue } type := Trim(parts[1]) sc := Trim(StrReplace(parts[2], '"', "")) val := Trim(StrReplace(parts[3], '"', "")) ; Restore [NEWLINE] tokens to actual newlines (written by EscapeCSV) val := StrReplace(val, "[NEWLINE]", "`n") if (!IsValidShortcut(sc, false)) { skipped++ continue } if (type = "Launcher" && !loadedLaunchers.Has(sc)) { if (!IsAllowedTarget(val)) { skipped++ } else { FileAppend(sc . " " . StrReplace(val, "`n", "[NEWLINE]") . "`n", launcherPath) imported++ } } else if (type = "Expansion" && !loadedExpansions.Has(sc)) { FileAppend(sc . " " . StrReplace(val, "`n", "[NEWLINE]") . "`n", textExpansionPath) imported++ } else { skipped++ } } LoadLaunchers() LoadTextExpansions() MsgBox("Import complete.`n`nImported: " . imported . "`nSkipped: " . skipped, "Import Results", "Iconi") } catch Error as err { MsgBox("Import failed: " . err.Message, "Error", "Icon!") } } ;------------------------------------------------ ; RUN ON STARTUP ;------------------------------------------------ GetStartupLinkPath() { return A_Startup . "\MultiShortcuts.lnk" } ToggleStartup() { linkPath := GetStartupLinkPath() if (FileExist(linkPath)) { FileDelete(linkPath) A_TrayMenu.Uncheck("Run on Windows Startup") TrayTip("Startup Disabled", "Script will not run on login", "Iconi") } else { FileCreateShortcut(A_ScriptFullPath, linkPath, A_ScriptDir) A_TrayMenu.Check("Run on Windows Startup") TrayTip("Startup Enabled", "Script will run automatically on login", "Iconi") } } LoadSoundPrefs() { global prefsPath, config if (!FileExist(prefsPath)) return try { saved := IniRead(prefsPath, "Settings", "PlaySounds", "unset") if (saved != "unset") { config["Feedback"]["PlaySounds"] := (saved = "true") } } } ToggleSounds() { global config, prefsPath ; Toggle the boolean value in memory currentStatus := config["Feedback"]["PlaySounds"] newStatus := !currentStatus config["Feedback"]["PlaySounds"] := newStatus ; Properly save only the PlaySounds key to the [Settings] section try { IniWrite(newStatus ? "true" : "false", prefsPath, "Settings", "PlaySounds") } if (newStatus) { A_TrayMenu.Check("Sound Feedback") try SoundPlay("*48") TrayTip("Sounds On", "Sound feedback enabled", "Iconi") } else { A_TrayMenu.Uncheck("Sound Feedback") TrayTip("Sounds Off", "Sound feedback disabled", "Iconi") } } ;------------------------------------------------ ; TRIGGER KEYS (; family for Windows commands, ` family for app launch) ;------------------------------------------------ LoadTriggerPrefs() { global prefsPath, config if (!FileExist(prefsPath)) return try { winCmd := IniRead(prefsPath, "Triggers", "WinCmd", "unset") if (winCmd != "unset" && winCmd != "") config["Triggers"]["WinCmd"] := winCmd appLaunch := IniRead(prefsPath, "Triggers", "AppLaunch", "unset") if (appLaunch != "unset" && appLaunch != "") config["Triggers"]["AppLaunch"] := appLaunch } } SaveTriggerPrefs() { global prefsPath, config try { IniWrite(config["Triggers"]["WinCmd"], prefsPath, "Triggers", "WinCmd") IniWrite(config["Triggers"]["AppLaunch"], prefsPath, "Triggers", "AppLaunch") } } ;------------------------------------------------ ; MODE: Starter / Full ;------------------------------------------------ LoadModePref() { global prefsPath, appMode if (!FileExist(prefsPath)) return try { saved := IniRead(prefsPath, "Mode", "Level", "starter") if (saved = "full") appMode := "full" } } SaveModePref(level) { global prefsPath try IniWrite(level, prefsPath, "Mode", "Level") } UnlockFullMode() { global appMode if (appMode = "full") { TrayTip("Already Unlocked", "Full features are already active.", "Iconi") return } msg := "Unlock MultiShortcuts Full?`n`n" msg .= "Full mode adds:`n" msg .= " * Custom text expansions and app launchers`n" msg .= " * Edit Shortcuts GUI`n" msg .= " * F-Key Hub (assign actions to F1-F12)`n" msg .= " * Capture selected text as expansion`n" msg .= " * Quick Expansion Picker - Undo - Backups`n" msg .= " * CSV import/export - Usage stats`n`n" msg .= "All free. The built-in shortcuts you already use will keep working.`n`n" msg .= "This is a one-way unlock - Starter mode won't be available after.`n`n" msg .= "Continue?" if (MsgBox(msg, "Unlock Full Features", "YesNo Icon?") != "Yes") return SaveModePref("full") TrayTip("Unlocking Full Mode", "Reloading MultiShortcuts...", "Iconi") SetTimer(ReloadScriptSilent, -600) } ; Validate a candidate trigger character. ; Must be a single character, not whitespace, not used by other families, ; and not a character that breaks hotstring registration. IsValidTriggerChar(ch, otherTrigger) { if (StrLen(ch) != 1) return "Trigger must be exactly one character." if (ch = " " || ch = "`t") return "Trigger cannot be whitespace." ; Block characters that would conflict with other families or break parsing. ; '=' is reserved by the date/utility family. ; '|' is the file separator in the data files. ; '"' would break the quoted string in the file. reserved := ["=", "|", '"', "/", "\"] for , r in reserved { if (ch = r) return "The character '" . ch . "' is reserved and cannot be used as a trigger." } if (ch = otherTrigger) return "That character is already in use by the other trigger family." ; Block plain letters/digits - they'd fire mid-word constantly if (RegExMatch(ch, "[A-Za-z0-9]")) return "Letters and digits make poor triggers (they fire mid-word).`nUse a punctuation character like comma, period, or tilde." return "" ; empty string = valid } ShowTriggerSetup() { global config curWinCmd := config["Triggers"]["WinCmd"] curAppLaunch := config["Triggers"]["AppLaunch"] g := Gui("+ToolWindow", "Trigger Keys Setup") g.SetFont("s10", "Segoe UI") g.BackColor := "FFFFFF" g.Add("Text", "x15 y15 w470", "Customize the prefix character for the two built-in shortcut families.`n" . "Changes apply on save. Existing shortcuts using the old prefix will be migrated.") g.Add("Text", "x15 y65 w200", "Windows Commands family (default semicolon)") edWinCmd := g.Add("Edit", "x230 y62 w40 Limit1 Center", curWinCmd) g.Add("Text", "x290 y65 w200 cGray", "e.g. semicolon-c = Copy, semicolon-v = Paste") g.Add("Text", "x15 y100 w200", "App Launch family (default backtick)") edAppLaunch := g.Add("Edit", "x230 y97 w40 Limit1 Center", curAppLaunch) g.Add("Text", "x290 y100 w200 cGray", "e.g. backtick-S = Snip, backtick-C = Calc") g.Add("Text", "x15 y140 w470 cGray", "Tip: avoid letters and digits - they fire in the middle of words.`n" . "Good choices: comma, period, tilde, or slash (each character must be unique)") g.Add("Button", "x230 y195 w90 Default", "Save").OnEvent("Click", SaveTriggers) g.Add("Button", "x330 y195 w90", "Cancel").OnEvent("Click", (*) => g.Destroy()) g.OnEvent("Escape", (*) => g.Destroy()) g.Show("w500 h240") SaveTriggers(*) { newWin := edWinCmd.Value newApp := edAppLaunch.Value ; Validate each character (each must be valid against the *other* new value) err := IsValidTriggerChar(newWin, newApp) if (err != "") { MsgBox("Windows Commands trigger:`n`n" . err, "Invalid Trigger", "Icon!") return } err := IsValidTriggerChar(newApp, newWin) if (err != "") { MsgBox("App Launch trigger:`n`n" . err, "Invalid Trigger", "Icon!") return } ; No-op if nothing changed if (newWin = curWinCmd && newApp = curAppLaunch) { g.Destroy() return } ; Migrate existing shortcuts that use the old prefixes migrated := MigrateTriggerPrefix(curWinCmd, newWin) + MigrateTriggerPrefix(curAppLaunch, newApp) config["Triggers"]["WinCmd"] := newWin config["Triggers"]["AppLaunch"] := newApp SaveTriggerPrefs() ; Re-seed defaults under the new prefixes (won't overwrite anything that exists) SeedDefaultExpansions() g.Destroy() msg := "Triggers updated:`n Windows Commands: " . newWin msg .= "`n App Launch: " . newApp if (migrated > 0) msg .= "`n`n" . migrated . " existing shortcut(s) migrated to the new prefix." msg .= "`n`nReloading..." TrayTip("Trigger Keys Updated", msg, "Iconi") SetTimer(ReloadScriptSilent, -800) } } ; Rewrites the appropriate data file to replace oldPrefix with newPrefix on ; built-in family shortcuts. Returns count of shortcuts migrated. ; ; A shortcut is considered a "family member" if it starts with the old prefix ; AND the rest is a single letter, digit, or symbol (everything the seeder uses). ; We migrate from text_expansion.ahk for ; family and launchers.ahk for ` family. MigrateTriggerPrefix(oldPrefix, newPrefix) { global loadedExpansions, loadedLaunchers global textExpansionPath, launcherPath if (oldPrefix = newPrefix || oldPrefix = "" || newPrefix = "") return 0 total := 0 total += MigratePrefixIn(loadedExpansions, textExpansionPath, oldPrefix, newPrefix, "; Text Expansion file - Format: shortcut expansion") total += MigratePrefixIn(loadedLaunchers, launcherPath, oldPrefix, newPrefix, "; Launcher file - Format: shortcut target") return total } MigratePrefixIn(dataMap, filePath, oldPrefix, newPrefix, headerComment) { count := 0 newMap := Map() for sc, target in dataMap { ; Match: oldPrefix followed by exactly one alphanumeric or punctuation char. ; We restrict to a single-char suffix so we never touch a user's ; multi-character shortcut that happens to start with the prefix. if (SubStr(sc, 1, StrLen(oldPrefix)) = oldPrefix && StrLen(sc) = StrLen(oldPrefix) + 1) { newSc := newPrefix . SubStr(sc, StrLen(oldPrefix) + 1) if (!dataMap.Has(newSc) && !newMap.Has(newSc)) { newMap[newSc] := target count++ continue } } newMap[sc] := target } if (count > 0) { ; Mutate the original Map in place - clear and refill so the caller's ; global variable still points at the same Map object dataMap.Clear() for k, v in newMap dataMap[k] := v RewriteFile(filePath, dataMap, headerComment) } return count } ;------------------------------------------------ ; EDIT GUI ;------------------------------------------------ ShowEditGUI(startTab := "launchers") { gEdit := Gui("+Resize +MinSize640x460", "MultiShortcuts v7.6 - Manage Shortcuts") gEdit.SetFont("s10", "Segoe UI") gEdit.BackColor := "F5F5F5" tabs := gEdit.Add("Tab3", "x8 y8 w784 h460 +BackgroundF5F5F5", ["Launchers", "Text Expansions"]) ; --- TAB 1: LAUNCHERS --- tabs.UseTab(1) gEdit.Add("Text", "x18 y48 w60 h24 +0x200", "Search:") searchL := gEdit.Add("Edit", "x80 y46 w260 h24 vSearchL") gEdit.Add("Button", "x346 y46 w24 h24", "X") .OnEvent("Click", (*) => (searchL.Value := "", RefreshLauncherTab(lvL, searchL, countL))) countL := gEdit.Add("Text", "x380 y48 w390 h20 +Right cGray vCountL", "") lvL := gEdit.Add("ListView", "x18 y76 w764 h320 Grid +LV0x10000 vLvL", ["Shortcut", "Target", "Type"]) lvL.ModifyCol(1, "AutoHdr") lvL.ModifyCol(2, "Auto") lvL.ModifyCol(3, "AutoHdr") gEdit.Add("Button", "x18 y406 w110 h26", "&Edit") .OnEvent("Click", (*) => EditSelectedRow(lvL, "launchers", gEdit, searchL, countL)) gEdit.Add("Button", "x134 y406 w110 h26", "&Delete") .OnEvent("Click", (*) => DeleteSelectedRow(lvL, "launchers", gEdit, countL)) gEdit.Add("Button", "x250 y406 w110 h26", "&Add New") .OnEvent("Click", (*) => (gEdit.Hide(), AddNewLauncher(), gEdit.Show(), RefreshLauncherTab(lvL, searchL, countL))) gEdit.Add("Button", "x366 y406 w110 h26", "&Test") .OnEvent("Click", (*) => TestSelectedRow(lvL, "launchers")) gEdit.Add("Button", "x668 y406 w114 h26", "&Reload") .OnEvent("Click", (*) => RefreshLauncherTab(lvL, searchL, countL)) lvL.OnEvent("DoubleClick", (*) => EditSelectedRow(lvL, "launchers", gEdit, searchL, countL)) lvL.OnEvent("ColClick", (lv, col) => SortLV(lv, col, "launchers", searchL, countL)) searchL.OnEvent("Change", (*) => RefreshLauncherTab(lvL, searchL, countL)) ; --- TAB 2: EXPANSIONS --- tabs.UseTab(2) gEdit.Add("Text", "x18 y48 w60 h24 +0x200", "Search:") searchE := gEdit.Add("Edit", "x80 y46 w260 h24 vSearchE") gEdit.Add("Button", "x346 y46 w24 h24", "X") .OnEvent("Click", (*) => (searchE.Value := "", RefreshExpansionTab(lvE, searchE, countE))) countE := gEdit.Add("Text", "x380 y48 w390 h20 +Right cGray vCountE", "") lvE := gEdit.Add("ListView", "x18 y76 w764 h320 Grid +LV0x10000 vLvE", ["Shortcut", "Expands To"]) lvE.ModifyCol(1, "AutoHdr") lvE.ModifyCol(2, "Auto") gEdit.Add("Button", "x18 y406 w110 h26", "&Edit") .OnEvent("Click", (*) => EditSelectedRow(lvE, "expansions", gEdit, searchE, countE)) gEdit.Add("Button", "x134 y406 w110 h26", "&Delete") .OnEvent("Click", (*) => DeleteSelectedRow(lvE, "expansions", gEdit, countE)) gEdit.Add("Button", "x250 y406 w110 h26", "&Add New") .OnEvent("Click", (*) => (gEdit.Hide(), AddNewExpansion(), gEdit.Show(), RefreshExpansionTab(lvE, searchE, countE))) gEdit.Add("Button", "x366 y406 w110 h26", "&Test") .OnEvent("Click", (*) => TestSelectedRow(lvE, "expansions")) gEdit.Add("Button", "x668 y406 w114 h26", "&Reload") .OnEvent("Click", (*) => RefreshExpansionTab(lvE, searchE, countE)) lvE.OnEvent("DoubleClick", (*) => EditSelectedRow(lvE, "expansions", gEdit, searchE, countE)) lvE.OnEvent("ColClick", (lv, col) => SortLV(lv, col, "expansions", searchE, countE)) searchE.OnEvent("Change", (*) => RefreshExpansionTab(lvE, searchE, countE)) tabs.UseTab() gEdit.Add("Text", "x8 y474 w784 h20 BackgroundE0E0E0 c444444", " Double-click a row to edit | Click column header to sort") RefreshLauncherTab(lvL, searchL, countL) RefreshExpansionTab(lvE, searchE, countE) if (startTab = "expansions") tabs.Value := 2 gEdit.OnEvent("Close", (*) => gEdit.Destroy()) gEdit.OnEvent("Escape", (*) => gEdit.Destroy()) gEdit.Show("w800 h500") } RefreshLauncherTab(lv, search, countCtrl) { global loadedLaunchers if (loadedLaunchers.Count = 0) LoadLaunchers() n := PopulateLauncherList(lv, search.Value) countCtrl.Value := n . " launcher" . (n = 1 ? "" : "s") . (search.Value != "" ? " (filtered)" : "") } RefreshExpansionTab(lv, search, countCtrl) { global loadedExpansions if (loadedExpansions.Count = 0) LoadTextExpansions() n := PopulateExpansionList(lv, search.Value) countCtrl.Value := n . " expansion" . (n = 1 ? "" : "s") . (search.Value != "" ? " (filtered)" : "") } PopulateLauncherList(lv, filter) { global loadedLaunchers lv.Delete() filter := Trim(filter ?? "") count := 0 for shortcut, target in loadedLaunchers { if (filter != "" && !InStr(shortcut, filter) && !InStr(target, filter)) continue type := InStr(target, "://") || InStr(target, "mailto:") ? "Web/Email" : (SubStr(StrLower(target), -3) = ".exe" || SubStr(StrLower(target), -3) = ".lnk") ? "App" : "Document" lv.Add("", shortcut, target, type) count++ } return count } PopulateExpansionList(lv, filter) { global loadedExpansions lv.Delete() filter := Trim(filter ?? "") count := 0 for shortcut, expansion in loadedExpansions { if (filter != "" && !InStr(shortcut, filter) && !InStr(expansion, filter)) continue lv.Add("", shortcut, StrReplace(StrReplace(expansion, "`r`n", " <- "), "`n", " <- ")) count++ } return count } SortLV(lv, col, dataType, searchCtrl, countCtrl) { global lvSortCol, lvSortAsc ; Toggle sort direction on repeated column click if (lvSortCol[dataType] = col) lvSortAsc[dataType] := !lvSortAsc[dataType] else { lvSortCol[dataType] := col lvSortAsc[dataType] := true } ; Use native Windows ListView sort - no rebuild, no flicker if (lvSortAsc[dataType]) lv.ModifyCol(col, "Sort") else lv.ModifyCol(col, "SortDesc") } EditSelectedRow(lv, dataType, parentGui, searchCtrl, countCtrl) { global loadedLaunchers, loadedExpansions, launcherPath, textExpansionPath row := lv.GetNext(0, "Focused") if (!row) row := lv.GetNext() if (!row) { MsgBox("Please select a row to edit.", "Nothing Selected", "Iconi") return } oldShortcut := lv.GetText(row, 1) oldTarget := lv.GetText(row, 2) gInline := Gui("+Owner" . parentGui.Hwnd . " +ToolWindow", "Edit: " . oldShortcut) gInline.SetFont("s10", "Segoe UI") gInline.Add("Text", "x10 y12 w110", "Shortcut:") editSC := gInline.Add("Edit", "x120 y10 w320 h24", oldShortcut) gInline.Add("Text", "x10 y44 w110", dataType = "launchers" ? "Target:" : "Expands To:") editVal := gInline.Add("Edit", "x120 y42 w320 h70 +Multi +WantReturn", oldTarget) gInline.Add("Button", "x120 y124 w100 h28", "Save") .OnEvent("Click", SaveInlineEdit) gInline.Add("Button", "x226 y124 w100 h28", "Cancel") .OnEvent("Click", (*) => gInline.Destroy()) gInline.Show("w460 h164") editSC.Focus() SaveInlineEdit(*) { newShortcut := Trim(editSC.Value) newTarget := Trim(editVal.Value) if (newShortcut = "" || newTarget = "") { MsgBox("Both fields are required.", "Validation", "Icon!") return } if (!IsValidShortcut(newShortcut)) { MsgBox("Invalid shortcut name.", "Validation", "Icon!") return } if (dataType = "launchers") { if (!IsAllowedTarget(newTarget)) return if (oldShortcut != newShortcut) loadedLaunchers.Delete(oldShortcut) loadedLaunchers[newShortcut] := newTarget RewriteFile(launcherPath, loadedLaunchers, "; Launcher file - Format: shortcut target") LoadLaunchers() RefreshLauncherTab(lv, searchCtrl, countCtrl) } else { if (oldShortcut != newShortcut) loadedExpansions.Delete(oldShortcut) loadedExpansions[newShortcut] := newTarget RewriteFile(textExpansionPath, loadedExpansions, "; Text Expansion file - Format: shortcut expansion") LoadTextExpansions() RefreshExpansionTab(lv, searchCtrl, countCtrl) } gInline.Destroy() TrayTip("Saved", newShortcut . " updated", "Iconi") } } DeleteSelectedRow(lv, dataType, parentGui, countCtrl) { global loadedLaunchers, loadedExpansions, launcherPath, textExpansionPath row := lv.GetNext(0, "Focused") if (!row) row := lv.GetNext() if (!row) { MsgBox("Please select a row to delete.", "Nothing Selected", "Iconi") return } shortcut := lv.GetText(row, 1) if (MsgBox("Delete '" . shortcut . "'?`n`nThis cannot be undone.", "Confirm Delete", "YesNo Icon!") != "Yes") return if (dataType = "launchers") { loadedLaunchers.Delete(shortcut) RewriteFile(launcherPath, loadedLaunchers, "; Launcher file - Format: shortcut target") LoadLaunchers() RefreshLauncherTab(lv, {Value:""}, countCtrl) } else { loadedExpansions.Delete(shortcut) RewriteFile(textExpansionPath, loadedExpansions, "; Text Expansion file - Format: shortcut expansion") LoadTextExpansions() RefreshExpansionTab(lv, {Value:""}, countCtrl) } TrayTip("Deleted", "'" . shortcut . "' removed", "Iconi") } TestSelectedRow(lv, dataType) { row := lv.GetNext(0, "Focused") if (!row) row := lv.GetNext() if (!row) { MsgBox("Please select a row to test.", "Nothing Selected", "Iconi") return } shortcut := lv.GetText(row, 1) target := lv.GetText(row, 2) if (dataType = "launchers") { if (MsgBox("Launch:`n" . target . "`n`nProceed?", "Test: " . shortcut, "YesNo Iconi") = "Yes") LaunchTarget(target, shortcut) } else { MsgBox("This expansion would type:`n`n" . target, "Preview: " . shortcut, "Iconi") } } ;------------------------------------------------ ; HELP WINDOW ;------------------------------------------------ ShowHelp() { global appMode if (appMode = "starter") { ShowStarterHelp() return } ShowFullHelp() } ShowStarterHelp() { global config wc := config["Triggers"]["WinCmd"] al := config["Triggers"]["AppLaunch"] gHelp := Gui("+ToolWindow", "MultiShortcuts (Starter) - Built-in Shortcuts") gHelp.SetFont("s10", "Segoe UI") gHelp.BackColor := "FFFFFF" title := gHelp.Add("Text", "x0 y0 w560 h40 +0x200 Background1A1A2E cWhite Center", " MultiShortcuts v7.6 - Starter Mode ") title.SetFont("s12 Bold", "Segoe UI") sections := [ ["; WINDOWS COMMANDS", [ [wc . "c", "Copy"], [wc . "x", "Cut"], [wc . "v", "Paste"], [wc . "z", "Undo / " . wc . "y Redo"], [wc . "a", "Select All"], [wc . "f", "Find / " . wc . "h Find & Replace"], [wc . "s", "Save / " . wc . "S Save As"], [wc . "o", "Open / " . wc . "n New"], [wc . "p", "Print / " . wc . "w Close tab"], [wc . "r", "Refresh (F5)"], [wc . "e", "Emoji picker (Win+.)"], [wc . "k", "Task Manager / " . wc . "l Lock PC / " . wc . "m Minimize"], [wc . "g", "Go to line"] ]], ["; DOCUMENT NAVIGATION", [ [wc . "t", "Top of page"], [wc . "b", "Bottom of page"], [wc . "u", "Page Up / " . wc . "d Page Down"], [wc . "L", "Line start / " . wc . "E Line end"], [wc . "<", "Snap window left / " . wc . "> Snap window right"] ]], ["`` APP LAUNCH", [ [al . "S", "Snip Tool"], [al . "C", "Calculator / " . al . "N Notepad / " . al . "P Paint"], [al . "T", "Windows Terminal / " . al . "F File Explorer"], [al . "B", "Default browser / " . al . "M Default mail"], [al . "W", "Word (if installed) / " . al . "X Excel (if installed)"] ]], ["= DATES & UTILITY", [ ["-=", "Today (April 26, 2026) / =- short (04/26/26)"], ["1-=", "1 week / 2-= 2 weeks / 1m= 1 month / 1y= 1 year"], ["r=", "F5 Refresh / f= F11 Fullscreen / d= F12 Dev tools"], ["s=", "Ctrl+S Save / t= Ctrl+T New tab"] ]] ] y := 50 for si, section in sections { heading := gHelp.Add("Text", "x10 y" . y . " w540 h22 c1A1A2E", section[1]) heading.SetFont("s10 Bold", "Segoe UI") y += 24 gHelp.Add("Text", "x10 y" . y . " w540 h1 Background808080") y += 6 for , row in section[2] { keyCtrl := gHelp.Add("Text", "x14 y" . y . " w80 h20 BackgroundE8EAF6 c1A1A2E Center", row[1]) keyCtrl.SetFont("s9 Bold", "Consolas") gHelp.Add("Text", "x102 y" . (y+1) . " w440 h20 c444444", row[2]) y += 24 } y += 10 } y += 4 unlockBg := gHelp.Add("Text", "x10 y" . y . " w540 h60 BackgroundFFFDE7") unlockText := gHelp.Add("Text", "x18 y" . (y+8) . " w524 h50 BackgroundFFFDE7 c555500", "Ready for more? * Unlock Full Features from the tray menu - `n" . "add your own shortcuts, customize anything, full editor GUI.") y += 68 gHelp.Add("Text", "x10 y" . y . " w540 h18 cAAAAAA Center", "(c) GnuKey LLC - Built-in shortcuts always work in Starter and Full modes") y += 22 gHelp.Add("Button", "x230 y" . y . " w100 h30", "Close") .OnEvent("Click", (*) => gHelp.Destroy()) gHelp.OnEvent("Close", (*) => gHelp.Destroy()) gHelp.OnEvent("Escape", (*) => gHelp.Destroy()) gHelp.Show("w560 h" . (y + 44)) } ShowFullHelp() { global config hk := config["Hotkeys"] gHelp := Gui("+ToolWindow", "Help - MultiShortcuts v7.6") gHelp.SetFont("s10", "Segoe UI") gHelp.BackColor := "FFFFFF" title := gHelp.Add("Text", "x0 y0 w500 h40 +0x200 Background1A1A2E cWhite Center", " MultiShortcuts v7.6 ") title.SetFont("s12 Bold", "Segoe UI") sections := [ ["ADD NEW SHORTCUTS", [ [hk["AddExpansion"], "Add Text Expansion"], [hk["AddLauncher"], "Add App Launcher"], [hk["AddDocumentLauncher"], "Add Document Launcher"], [hk["AddWebLauncher"], "Add Web/Email Launcher"], [hk["CaptureExpansion"], "Capture Selected Text as Expansion"] ]], ["MANAGE AND VIEW", [ [hk["ShowEditGUI"], "Edit Shortcuts (full GUI)"], [hk["ManageLaunchers"], "Manage Launchers"], [hk["ManageExpansions"], "Manage Text Expansions"], [hk["ReloadScript"], "Reload Script"] ]], ["FEATURES", [ [hk["QuickTextExpansion"], "Quick Expansion Picker"], [hk["UndoExpansion"], "Undo Last Expansion"], [hk["ImmediateBackup"], "Backup Both Files Now"], [hk["PauseResume"], "Pause / Resume All Macros"], [hk["SnipTool"], "Snip Tool (Win+Shift+S)"], [hk["ShowHelp"], "This Help Screen"] ]], ["F-KEY HUB - GESTURES", [ ["Tray: F-Key Hub", "Assign actions to F1-F12"], ["Tap F-key", "Fire assigned action"], ["Hold F-key 500ms+", "Fire long-press action"], ["Double-tap F-key", "Fire double-tap action"], ["F-key then letter", "Fire combo action (within 1 second)"], ["Tray: F-Key Mgr", "Exempt or remap F-keys"], ["Tray: Pause Hub", "Restore normal F-key behavior"] ]], ["= SHORTCUT FAMILY", [ ["-=", "Full date (April 26, 2026)"], ["=-", "Short date (04/26/26)"], ["1-=", "One week from today"], ["2-=", "Two weeks from today"], ["1m=", "One month from today"], ["1y=", "One year from today"], ["r=", "F5 - Refresh"], ["f=", "F11 - Fullscreen"], ["s=", "Ctrl+S - Save"], ["t=", "Ctrl+T - New tab"] ]], ["; WINDOWS COMMANDS", [ [config["Triggers"]["WinCmd"] . "c", "Copy / " . config["Triggers"]["WinCmd"] . "x Cut / " . config["Triggers"]["WinCmd"] . "v Paste"], [config["Triggers"]["WinCmd"] . "z", "Undo / " . config["Triggers"]["WinCmd"] . "y Redo"], [config["Triggers"]["WinCmd"] . "a", "Select All / " . config["Triggers"]["WinCmd"] . "f Find / " . config["Triggers"]["WinCmd"] . "h Find & Replace"], [config["Triggers"]["WinCmd"] . "s", "Save / " . config["Triggers"]["WinCmd"] . "S Save As / " . config["Triggers"]["WinCmd"] . "o Open / " . config["Triggers"]["WinCmd"] . "n New"], [config["Triggers"]["WinCmd"] . "p", "Print / " . config["Triggers"]["WinCmd"] . "w Close tab"], [config["Triggers"]["WinCmd"] . "r", "Refresh (F5)"], [config["Triggers"]["WinCmd"] . "e", "Emoji picker / " . config["Triggers"]["WinCmd"] . "g Go to line"], [config["Triggers"]["WinCmd"] . "k", "Task Manager / " . config["Triggers"]["WinCmd"] . "l Lock PC / " . config["Triggers"]["WinCmd"] . "m Minimize"] ]], ["; DOCUMENT NAVIGATION", [ [config["Triggers"]["WinCmd"] . "t", "Top of page"], [config["Triggers"]["WinCmd"] . "b", "Bottom of page"], [config["Triggers"]["WinCmd"] . "u", "Page Up / " . config["Triggers"]["WinCmd"] . "d Page Down"], [config["Triggers"]["WinCmd"] . "L", "Line start / " . config["Triggers"]["WinCmd"] . "E Line end"], [config["Triggers"]["WinCmd"] . "<", "Snap left / " . config["Triggers"]["WinCmd"] . "> Snap right"] ]], ["`` QUICK APP LAUNCH (case-sensitive)", [ [config["Triggers"]["AppLaunch"] . "S", "Snip Tool"], [config["Triggers"]["AppLaunch"] . "C", "Calculator"], [config["Triggers"]["AppLaunch"] . "N", "Notepad"], [config["Triggers"]["AppLaunch"] . "P", "Paint"], [config["Triggers"]["AppLaunch"] . "T", "Windows Terminal"], [config["Triggers"]["AppLaunch"] . "F", "File Explorer"], [config["Triggers"]["AppLaunch"] . "B", "Default browser"], [config["Triggers"]["AppLaunch"] . "M", "Default mail client"], [config["Triggers"]["AppLaunch"] . "W", "Word (if installed)"], [config["Triggers"]["AppLaunch"] . "X", "Excel (if installed)"] ]] ] y := 50 for si, section in sections { heading := gHelp.Add("Text", "x10 y" . y . " w480 h22 c1A1A2E", section[1]) heading.SetFont("s10 Bold", "Segoe UI") y += 24 gHelp.Add("Text", "x10 y" . y . " w480 h1 Background808080") y += 6 ; Use wider label for F-Key Hub, =, ;, and ` sections (last four) labelW := (si >= 4) ? 170 : 110 descX := (si >= 4) ? 192 : 132 descW := (si >= 4) ? 290 : 350 for , row in section[2] { label := HotkeyLabel(row[1]) keyCtrl := gHelp.Add("Text", "x14 y" . y . " w" . labelW . " h20 BackgroundE8EAF6 c1A1A2E Center", label) keyCtrl.SetFont("s9 Bold", "Consolas") gHelp.Add("Text", "x" . descX . " y" . (y+1) . " w" . descW . " h20 c444444", row[2]) y += 26 } y += 10 } gHelp.Add("Text", "x10 y" . y . " w480 h1 Background808080") y += 8 extras := ["Usage Statistics", "Export/Import CSV", "Run on Windows Startup", "Backups auto-saved to .\backups\"] for item in extras { gHelp.Add("Text", "x20 y" . y . " w460 h18 c555555", " " . item) y += 20 } y += 8 tipBg := gHelp.Add("Text", "x10 y" . y . " w480 h74 BackgroundFFFDE7") gHelp.Add("Text", "x18 y" . (y+5) . " w468 h86 BackgroundFFFDE7 c555500", "TIP: Type your shortcut anywhere - it triggers automatically.`n" . " Alt+C captures selected text instantly.`n" . " Use {date:dd/MM/yyyy} or {time:h:mm tt} for custom formats.`n" . " = family: r= refresh f= fullscreen s= save t= new tab`n" . " 1-= 1 week from today 2-= 2 weeks 1m= 1 month`n" . " Sequences: {key:{Tab}} = Tab, {key:^s} = Ctrl+S`n" . " {wait:500} = pause, {focus:Notepad} = switch window") y += 96 gHelp.Add("Text", "x10 y" . y . " w480 h1 Background808080") y += 6 gHelp.Add("Text", "x10 y" . y . " w480 h18 cAAAAAA Center", "(c) GnuKey LLC - Implementation: Claude (Anthropic) - Review: Gemini (Google)") y += 22 gHelp.Add("Button", "x190 y" . y . " w120 h30", "Close") .OnEvent("Click", (*) => gHelp.Destroy()) gHelp.OnEvent("Close", (*) => gHelp.Destroy()) gHelp.OnEvent("Escape", (*) => gHelp.Destroy()) gHelp.Show("w500 h" . (y + 44)) } ;------------------------------------------------ ; LAUNCHER LOADING - CORRECTED WITH CALLBACK FACTORY ;------------------------------------------------ MakeLaunchCallback(t, s) { ; Factory: Returns a fresh closure that captures THESE specific values return (*) => LaunchTarget(t, s) } LoadLaunchers() { global launcherPath, loadedLaunchers, registeredLaunchers ; This prevents leaks when a shortcut is renamed in the file for sc in registeredLaunchers { try Hotstring(GetHotstringOptions(sc) . sc, , "Off") } registeredLaunchers := [] loadedLaunchers.Clear() if (!FileExist(launcherPath)) { TrayTip("Launchers", "No launcher file found - creating empty one.", "Iconi") return } try { content := FileRead(launcherPath, "UTF-8") count := 0 for line in StrSplit(content, "`n", "`r") { line := Trim(line) if (line = "" || SubStr(line, 1, 1) = ";") continue if (InStr(line, " ")) { parts := StrSplit(line, " ", , 2) if (parts.Length >= 2) { shortcut := Trim(parts[1]) target := StrReplace(Trim(parts[2]), "[NEWLINE]", "`n") if (shortcut != "" && target != "" && IsValidShortcut(shortcut, false)) { ; Store in map loadedLaunchers[shortcut] := target count++ ; Use FACTORY to create a closure that captures THESE values only callback := MakeLaunchCallback(target, shortcut) try { Hotstring(GetHotstringOptions(shortcut) . shortcut, callback) registeredLaunchers.Push(shortcut) ; Fix 4: track for clean reload } catch Error as e { MsgBox("Failed to register " . shortcut . "`nError: " . e.Message, "Hotstring Error", "Icon!") } } } } } TrayTip("Launchers Loaded", count . " launchers ready (backtick + number works!)", "Iconi") } catch Error as err { MsgBox("Error loading launchers: " . err.Message . "`nFile preview: " . SubStr(content, 1, 200), "Load Error", "Icon!") } UpdateTrayTooltip() } ;------------------------------------------------ ; LAUNCH TARGET - SILENT VERSION ;------------------------------------------------ LaunchTarget(target, shortcut := "") { target := Trim(target) if (shortcut != "") TrackUsage(shortcut, "launcher") cleanTarget := Trim(target, ' "') try { if (InStr(cleanTarget, "://") || InStr(cleanTarget, "mailto:")) { Run(cleanTarget) } else { ; Extract base path (strip trailing arguments) testPath := (SubStr(cleanTarget, 1, 1) = '"') ? RegExReplace(cleanTarget, '^"([^"]+)".*', "$1") : StrSplit(cleanTarget, " ")[1] ; Warn if path looks like a file/folder that doesn't exist ; (avoids 10-30 second freeze on missing network shares) if (InStr(testPath, "\") && !FileExist(testPath)) { PlayFeedback("ErrorSound") MsgBox("Target not found - it may be offline or the path has moved.`n`n" . testPath, "Launch Error", "Icon!") return } if (FileExist(cleanTarget)) Run('"' . cleanTarget . '"') else Run(cleanTarget) } PlayFeedback("LaunchSound") } catch Error as err { PlayFeedback("ErrorSound") MsgBox("Could not launch: " . cleanTarget . "`n`nError: " . err.Message, "Launch Error", "Icon!") } } ;------------------------------------------------ ; TEXT EXPANSION LOADING ;------------------------------------------------ MakeExpansionCallback(text, sc) { return (*) => DoExpand(text, sc) } DoExpand(text, sc) { global config ; ── Step 1: protect sequence tokens before running ResolvePlaceholders ─── ; Scan character-by-character to correctly handle nested braces like ; {key:{Tab}} without confusing the placeholder resolver. seqTokens := [] protected := "" i := 1 textLen := StrLen(text) loop { if (i > textLen) break ch := SubStr(text, i, 1) if (ch = "{") { rest := SubStr(text, i) if (RegExMatch(rest, "^\{(key|wait|focus):")) { depth := 0 j := i loop { if (j > textLen) { depth := 0 ; force safe exit on malformed/unclosed brace break } c := SubStr(text, j, 1) if (c = "{") depth++ else if (c = "}") depth-- j++ if (depth = 0) break } fullTok := SubStr(text, i, j - i) seqTokens.Push(fullTok) protected .= "##SEQ" . seqTokens.Length . "##" i := j continue } } protected .= ch i++ } ; ── Step 2: resolve non-sequence placeholders ──────────────────────────── resolved := ResolvePlaceholders(protected) ; ── Step 3: restore sequence tokens ────────────────────────────────────── for idx, tok in seqTokens resolved := StrReplace(resolved, "##SEQ" . idx . "##", tok) ; ── Step 4: record history using the fully-resolved string ─────────────── RecordExpansionHistory(sc, sc, resolved) TrackUsage(sc, "expansion") PlayFeedback("ExpandSound") ; ── Step 3: split into segments on sequence tokens ─────────────────────── ; Tokens recognised: ; {key:X} – send keystroke X (any AHK Send key notation) ; {wait:N} – sleep N milliseconds ; {focus:Title} – activate window whose title contains Title ; ; A 150 ms pause is inserted automatically before every {key:} so the ; target application has time to open dialogs or move focus. ; ; {cursor} / {cursor:N} still work and are processed at the very end. ; ───────────────────────────────────────────────────────────────────────── ; Check whether any sequence tokens are present at all ; Simple check: just look for {key: or {wait: or {focus: anywhere in the string hasSequence := (InStr(resolved, "{key:") || InStr(resolved, "{wait:") || InStr(resolved, "{focus:")) if (!hasSequence) { ; ── Simple (no-sequence) path – original behaviour ────────────────── cursorPos := 0 if (RegExMatch(resolved, "\{cursor(?::(\d+))?\}", &m)) { if (m[1] != "") cursorPos := Integer(m[1]) else cursorPos := StrLen(resolved) - InStr(resolved, "{cursor}") + 1 - StrLen(m[0]) resolved := StrReplace(resolved, m[0], "") } SendExpandText(resolved, config) if (cursorPos > 0) Send("{Left " . cursorPos . "}") return } ; ── Sequence path ──────────────────────────────────────────────────────── ; Walk character-by-character through resolved, collecting text segments ; and sequence tokens, then execute them in order. cursorPos := 0 si := 1 sLen := StrLen(resolved) textAccum := "" loop { if (si > sLen) break sch := SubStr(resolved, si, 1) ; Check for a sequence token starting here if (sch = "{") { srest := SubStr(resolved, si) if (RegExMatch(srest, "^\{(key|wait|focus):")) { ; Consume full token using brace depth sdepth := 0 sj := si loop { sc2 := SubStr(resolved, sj, 1) if (sc2 = "{") sdepth++ else if (sc2 = "}") sdepth-- sj++ if (sdepth = 0 || sj > sLen + 1) break } seqTok := SubStr(resolved, si, sj - si) ; Flush any accumulated text first if (textAccum != "") { ; Handle {cursor} in accumulated text if (RegExMatch(textAccum, "\{cursor(?::(\d+))?\}", &cm)) { if (cm[1] != "") cursorPos := Integer(cm[1]) else cursorPos := StrLen(textAccum) - InStr(textAccum, "{cursor}") + 1 - StrLen(cm[0]) textAccum := StrReplace(textAccum, cm[0], "") } SendExpandText(textAccum, config) textAccum := "" } ; Parse and execute the token ; seqTok is like {key:VALUE} or {wait:500} or {focus:Title} ; Fix 5: guard against malformed tokens like {key:} with empty value if (!RegExMatch(seqTok, "^\{(key|wait|focus):(.+)\}$", &stok)) { si := sj continue ; skip malformed token silently } stokType := stok[1] stokValue := Trim(stok[2]) if (stokValue = "") { si := sj continue ; skip empty value token } if (stokType = "key") { Sleep(config["Performance"]["SequenceDelay"]) Send(stokValue) } else if (stokType = "wait") { ms := Integer(stokValue) if (ms > 0) Sleep(ms) } else if (stokType = "focus") { Sleep(config["Performance"]["SequenceDelay"]) try { WinActivate("ahk_exe " . stokValue) } catch { try WinActivate(stokValue) } try WinWaitActive(stokValue, , 0.5) } si := sj continue } } textAccum .= sch si++ } ; Flush remaining text if (textAccum != "") { if (RegExMatch(textAccum, "\{cursor(?::(\d+))?\}", &cm)) { if (cm[1] != "") cursorPos := Integer(cm[1]) else cursorPos := StrLen(textAccum) - InStr(textAccum, "{cursor}") + 1 - StrLen(cm[0]) textAccum := StrReplace(textAccum, cm[0], "") } SendExpandText(textAccum, config) } if (cursorPos > 0) Send("{Left " . cursorPos . "}") } ; Helper – sends a text chunk, choosing SendEvent for large blocks SendExpandText(txt, cfg) { ; Use clipboard paste for all expansions - works in browsers and all apps savedClip := ClipboardAll() A_Clipboard := txt if (ClipWait(1)) { Send("^v") Sleep(150) ; wait for target app to process paste before sending space Send(" ") ; send space as separate keystroke - not clipped, not trimmed } else { if (StrLen(txt) > cfg["Performance"]["LargeTextThreshold"]) SendEvent(txt . " ") else SendText(txt . " ") } A_Clipboard := savedClip } ; Placeholder registry global placeholderRegistry := Map( "clip", (fmt) => SanitizedClip(fmt), "date", (fmt) => fmt = "locale" ? FormatTime(, "ShortDate") : RegExMatch(fmt, "^\+(\d+):(.+)$", &m) ? FormatTime(DateAdd(A_Now, Integer(m[1]), "Days"), m[2]) : RegExMatch(fmt, "^\+(\d+)$", &m) ? FormatTime(DateAdd(A_Now, Integer(m[1]), "Days"), "MMMM d, yyyy") : fmt != "" ? SafeFormatTime(fmt) : FormatTime(, "yyyy-MM-dd"), "time", (fmt) => fmt != "" ? SafeFormatTime(fmt) : FormatTime(, "HH:mm"), "day", (fmt) => FormatTime(, "dddd"), "month", (fmt) => FormatTime(, "MMMM"), "year", (fmt) => FormatTime(, "yyyy"), "user", (fmt) => A_UserName, "comp", (fmt) => A_ComputerName, "cursor", (fmt) => "", "prompt", (fmt) => PromptDuringExpansion(fmt) ) ; ── Clipboard sanitization ──────────────────────────────────────────────── ; The {clip} placeholder reads from whatever happens to be on the clipboard. ; Hostile content (e.g. an attacker who got a user to copy a string from a ; malicious page) could embed shell commands or scripting URIs that, if ; pasted into the wrong target, would execute. ; ; This wrapper applies the formatter (trim/lower/upper/length) as before, ; then checks the result for known-dangerous patterns. If something looks ; risky, the user is prompted before the expansion fires. The check is ; deliberately a *warning*, not a hard block - many legitimate clipboard ; contents look unusual (URLs, file paths, code snippets), and we don't ; want false positives to silently mangle the user's text. SanitizedClip(fmt) { raw := A_Clipboard ; Apply formatter first if (fmt = "trim") out := Trim(raw) else if (fmt = "lower") out := StrLower(raw) else if (fmt = "upper") out := StrUpper(raw) else if (fmt != "" && RegExMatch(fmt, "^\d+$")) out := SubStr(raw, 1, Integer(fmt)) else out := raw ; Scan for command-injection patterns. Case-insensitive substring match. ; These are the prefixes a script would use to execute something - the ; user almost never wants these pasted from {clip}. dangerous := ["cmd.exe", "powershell", "rundll32", "mshta", "javascript:", "vbscript:", "data:text/html", "ms-msdt:", "file:///", "about:", "wscript", "cscript"] hit := "" low := StrLower(out) for , pat in dangerous { if (InStr(low, pat)) { hit := pat break } } if (hit = "") return out ; Suspicious content found - ask the user before proceeding preview := SubStr(out, 1, 80) if (StrLen(out) > 80) preview .= "..." msg := "Your clipboard contains a pattern that looks like a command:`n`n" msg .= " Detected: " . hit . "`n" msg .= " Preview: " . preview . "`n`n" msg .= "Paste this clipboard content anyway?" result := MsgBox(msg, "Clipboard Safety Check", "YesNo Icon!") if (result = "Yes") return out return "" } SafeFormatTime(fmt) { try { return FormatTime(, fmt) } catch { return "{" . fmt . "}" } } PromptDuringExpansion(fmt) { if (fmt = "") return "" parts := StrSplit(fmt, "|", , 2) question := Trim(parts[1]) default := parts.Length > 1 ? Trim(parts[2]) : "" IB := InputBox(question, "Input Required", "w360 h130", default) if (IB.Result = "Cancel") return "" return IB.Value } ResolvePlaceholders(text, depth := 0) { global placeholderRegistry ; Recursion guard: a {clip} containing {date} containing {clip}… would ; otherwise loop. Ten is well past anything legitimate; nested resolves ; in normal use bottom out within 1–2. if (depth > 10) { TrayTip("Expansion Limit", "Placeholder expansion exceeded 10 levels - stopping.", "Iconi") return "[RECURSION LIMIT]" } result := text for name, handler in placeholderRegistry { pattern := "\{" . name . ":([^}]+)\}" startPos := 1 loop { foundPos := RegExMatch(result, pattern, &m, startPos) if (!foundPos) break replacement := handler.Call(m[1]) ; If the replacement contains further placeholders, recurse ; with an incremented depth counter. if (InStr(replacement, "{") && InStr(replacement, "}")) replacement := ResolvePlaceholders(replacement, depth + 1) result := SubStr(result, 1, foundPos - 1) . replacement . SubStr(result, foundPos + StrLen(m[0])) startPos := foundPos + StrLen(replacement) } ; Bare {name} form (no formatter) if (InStr(result, "{" . name . "}")) { replacement := handler.Call("") if (InStr(replacement, "{") && InStr(replacement, "}")) replacement := ResolvePlaceholders(replacement, depth + 1) result := StrReplace(result, "{" . name . "}", replacement) } } return result } LoadTextExpansions() { global textExpansionPath, loadedExpansions, registeredExpansions ; Disable previously registered hotstrings using stored options for sc, opts in registeredExpansions { try Hotstring(opts . sc, , "Off") } registeredExpansions := Map() loadedExpansions.Clear() if (!FileExist(textExpansionPath)) return try { content := FileRead(textExpansionPath, "UTF-8") for line in StrSplit(content, "`n", "`r") { line := Trim(line) if (line = "" || SubStr(line, 1, 1) = ";") continue if (InStr(line, " ")) { parts := StrSplit(line, " ", , 2) if (parts.Length >= 2) { shortcut := Trim(parts[1]) expansion := StrReplace(parts[2], "[NEWLINE]", "`n") ; SpeedKee compatibility: $$ suffix = instant fire instantForced := false if (SubStr(shortcut, -1) = "$$") { shortcut := SubStr(shortcut, 1, StrLen(shortcut) - 2) instantForced := true } if (shortcut != "" && expansion != "" && IsValidShortcut(shortcut, false)) { try { opts := instantForced ? ":*OC:" : GetHotstringOptions(shortcut) Hotstring(opts . shortcut, MakeExpansionCallback(expansion, shortcut)) loadedExpansions[shortcut] := expansion registeredExpansions[shortcut] := opts ; store opts for clean unload } catch Error as e { ; Skip invalid hotstring } } } } } } catch Error as err { MsgBox("Error loading expansions: " . err.Message, "Load Error", "Icon!") } UpdateTrayTooltip() } ;------------------------------------------------ ; FILE INITIALIZATION ;------------------------------------------------ ;------------------------------------------------ ; AUTO-MIGRATION ; Fixes legacy {date+N:format} -> {date:+N:format} ; Runs silently on startup - safe to run every time ;------------------------------------------------ MigrateDateSyntax() { global textExpansionPath if (!FileExist(textExpansionPath)) return tmp := textExpansionPath . ".tmp" try { content := FileRead(textExpansionPath, "UTF-8") if (!InStr(content, "{date+")) return fixed := RegExReplace(content, "\{date\+(\d+):([^}]+)\}", "{date:+$1:$2}") if (fixed != content) { if (FileExist(tmp)) FileDelete(tmp) FileAppend(fixed, tmp, "UTF-8") FileCopy(tmp, textExpansionPath, 1) } } catch { ; Migration failed silently - not critical } finally { try FileDelete(tmp) } } MigratePipeSeparators() { ; Converts old pipe-separated format (shortcut|expansion) to space-separated ; Runs silently on startup - safe to run every time global textExpansionPath, launcherPath for , filePath in [textExpansionPath, launcherPath] { if (!FileExist(filePath)) continue tmp := filePath . ".tmp" try { content := FileRead(filePath, "UTF-8") if (!InStr(content, "|")) continue fixed := "" for line in StrSplit(content, "`n", "`r") { trimmed := Trim(line) if (trimmed = "" || SubStr(trimmed, 1, 1) = ";") { fixed .= line . "`n" continue } ; Only replace first | with space (old separator format) if (InStr(trimmed, "|") && !InStr(trimmed, " ")) trimmed := StrReplace(trimmed, "|", " ", , , 1) fixed .= trimmed . "`n" } if (fixed != content) { if (FileExist(tmp)) FileDelete(tmp) FileAppend(fixed, tmp, "UTF-8") FileCopy(tmp, filePath, 1) } } catch { ; Migration failed silently - not critical } finally { try FileDelete(tmp) } } } InitializeFiles() { global launcherPath, textExpansionPath try { if (!FileExist(launcherPath)) FileAppend("; Launcher file - Format: shortcut target`n; Created: " . FormatTime(, "yyyy-MM-dd HH:mm:ss") . "`n", launcherPath) if (!FileExist(textExpansionPath)) FileAppend("; Text Expansion file - Format: shortcut expansion`n; Created: " . FormatTime(, "yyyy-MM-dd HH:mm:ss") . "`n", textExpansionPath) SeedDefaultExpansions() } catch Error as err { MsgBox("Could not initialize files: " . err.Message, "Init Error", "Icon!") } } ;------------------------------------------------ ; SEED BUILT-IN SHORTCUTS ; Writes defaults on first run if not already present. ; The = family: function keys, relative dates, utilities ;------------------------------------------------ SeedDefaultExpansions() { global textExpansionPath, config wc := config["Triggers"]["WinCmd"] ; default ";" defaults := [ ; ── Date shortcuts ────────────────────────────── ["-=", "{date:MMMM d, yyyy}"], ; full date ["=-", "{date:MM/dd/yy}"], ; short date ; ── Relative date shortcuts (= family) ────────── ["1-=", "{date:+7:MMMM d, yyyy}"], ; 1 week from today ["2-=", "{date:+14:MMMM d, yyyy}"], ; 2 weeks from today ["3-=", "{date:+21:MMMM d, yyyy}"], ; 3 weeks from today ["4-=", "{date:+28:MMMM d, yyyy}"], ; 4 weeks from today ["1m=", "{date:+30:MMMM d, yyyy}"], ; ~1 month from today ["2m=", "{date:+60:MMMM d, yyyy}"], ; ~2 months from today ["3m=", "{date:+90:MMMM d, yyyy}"], ; ~3 months from today ["6m=", "{date:+180:MMMM d, yyyy}"], ; ~6 months from today ["1y=", "{date:+365:MMMM d, yyyy}"], ; 1 year from today ; ── Function key / utility shortcuts (= family) ─ ["r=", "{key:F5}"], ; Refresh ["f=", "{key:F11}"], ; Fullscreen toggle ["d=", "{key:F12}"], ; Dev tools ["n=", "{key:F2}"], ; Rename (Explorer) ["t=", "{key:^t}"], ; New tab ["w=", "{key:^w}"], ; Close tab ["z=", "{key:^z}"], ; Undo ["p=", "{key:^p}"], ; Print ["a=", "{key:^a}"], ; Select all ["s=", "{key:^s}"], ; Save ; ── ; family - Windows commands ───────────────── ; Note: Ctrl+Alt+Del is not included - Windows blocks scripts from ; sending the Secure Attention Sequence. ; ; Design rule: ; lowercase = the everyday action (copy, paste, save, find...) ; CAPITAL = a navigation jump (top, bottom, page up, line end...) ; ; This mirrors the original WinKeys design - your hands learn it once ; and the same shortcuts work everywhere MultiShortcuts runs. ; ; Clipboard & editing [wc . "c", "{key:^c}"], ; Copy [wc . "x", "{key:^x}"], ; Cut [wc . "v", "{key:^v}"], ; Paste [wc . "p", "{key:^p}"], ; Print [wc . "z", "{key:^z}"], ; Undo [wc . "y", "{key:^y}"], ; Redo [wc . "a", "{key:^a}"], ; Select All [wc . "f", "{key:^f}"], ; Find [wc . "h", "{key:^h}"], ; Find & Replace ; File ops [wc . "s", "{key:^s}"], ; Save [wc . "S", "{key:^+s}"], ; Save As [wc . "n", "{key:^n}"], ; New [wc . "o", "{key:^o}"], ; Open [wc . "w", "{key:^w}"], ; Close tab / window ; Document navigation (lowercase = jump) [wc . "t", "{key:^{Home}}"], ; Top of page (Ctrl+Home) [wc . "b", "{key:^{End}}"], ; Bottom of page (Ctrl+End) [wc . "u", "{key:{PgUp}}"], ; Page Up [wc . "d", "{key:{PgDn}}"], ; Page Down ; Window snap (arrows point the way) [wc . "<", "{key:#{Left}}"], ; Snap window left (Win+Left) [wc . ">", "{key:#{Right}}"], ; Snap window right (Win+Right) ; Utility [wc . "r", "{key:F5}"], ; Refresh [wc . "e", "{key:#.}"], ; Emoji picker (Win+.) [wc . "k", "{key:^+{Escape}}"], ; Task Manager (k for "kill") [wc . "l", "{key:#l}"], ; Lock PC [wc . "m", "{key:#{Down}}"], ; Minimize window [wc . "g", "{key:^g}"], ; Go to line ; ── ; family CAPITALS - second meanings for select letters ── [wc . "L", "{key:{Home}}"], ; Line start [wc . "E", "{key:{End}}"], ; Line end ] existing := FileExist(textExpansionPath) ? FileRead(textExpansionPath, "UTF-8") : "" for entry in defaults { sc := entry[1] exp := entry[2] if (!RegExMatch(existing, "(?m)^\Q" . sc . "\E ")) LockedFileAppend(textExpansionPath, sc . " " . exp . "`n") } ; Seed the ` family - these are app launchers, not text expansions, ; so they live in launchers.ahk SeedDefaultLaunchers() } SeedDefaultLaunchers() { global launcherPath, config al := config["Triggers"]["AppLaunch"] ; default "`" defaults := [ ; ── ` family - Quick app launch ───────────────── [al . "S", "ms-screenclip:"], ; Snip Tool (opens Snipping Tool) [al . "C", "calc.exe"], ; Calculator [al . "N", "notepad.exe"], ; Notepad [al . "T", "wt.exe"], ; Windows Terminal [al . "B", "https://www.google.com"], ; Default browser [al . "M", "mailto:"], ; Default mail client [al . "P", "mspaint.exe"], ; Paint [al . "F", "explorer.exe"], ; File Explorer (launcher version) [al . "W", "winword.exe"], ; Word (if installed) [al . "X", "excel.exe"], ; Excel (if installed) ] existing := FileExist(launcherPath) ? FileRead(launcherPath, "UTF-8") : "" for entry in defaults { sc := entry[1] exp := entry[2] if (!RegExMatch(existing, "(?m)^\Q" . sc . "\E ")) LockedFileAppend(launcherPath, sc . " " . exp . "`n") } } ;------------------------------------------------ ; INSERT TEXT AT CURSOR IN EDIT CONTROL ;------------------------------------------------ InsertIntoEdit(ctrl, text) { sel := SendMessage(0xB0, 0, 0, ctrl) selStart := sel & 0xFFFF selEnd := (sel >> 16) & 0xFFFF current := ctrl.Value ctrl.Value := SubStr(current, 1, selStart) . text . SubStr(current, selEnd + 1) newPos := selStart + StrLen(text) SendMessage(0xB1, newPos, newPos, ctrl) ctrl.Focus() } ;------------------------------------------------ ; VALIDATION AND SECURITY ;------------------------------------------------ ;------------------------------------------------ ; HOTSTRING OPTIONS HELPER ;------------------------------------------------ ; Returns the correct hotstring option string for a given shortcut. ; Rules: ; - Ends in an uppercase letter → immediate, case-sensitive (:*C:) ; - Contains any digit → immediate, case-sensitive (:*C:) ; - Contains any special char → immediate, case-sensitive (:*C:) ; (anything that is not a-z, A-Z, 0-9) ; - All lowercase letters only → space/terminator required (:C:) ; ; The C option is always included to preserve case as typed. ;------------------------------------------------ GetHotstringOptions(shortcut) { ; NOTE: $$ suffix is handled upstream (stripped before this call) ; and forces :*OC: directly - this function only sees the clean trigger. ; The O flag suppresses AHK's automatic end character so we control spacing. ; Contains any digit? if (RegExMatch(shortcut, "[0-9]")) return ":*OC:" ; Contains any special character (non-alphabetic)? if (RegExMatch(shortcut, "[^a-zA-Z]")) return ":*OC:" ; Ends in uppercase letter? lastChar := SubStr(shortcut, -1) if (RegExMatch(lastChar, "[A-Z]")) return ":*OC:" ; Everything else (pure lowercase) - require terminator (space/enter) return ":OC:" } IsValidShortcut(shortcut, showMessages := true) { ; Rules: at least 2 characters, no characters that break hotstring registration. ; When called from file-loading paths, pass showMessages := false so a corrupt ; data file silently skips bad entries instead of popping a dialog at startup. if (StrLen(shortcut) < 2) return false ; Block characters that break file format or hotstring registration blockedChars := ["|", " ", "`t", "`n", "`r"] for , ch in blockedChars { if (InStr(shortcut, ch)) { if (showMessages) { if (ch = "|") MsgBox("The character '|' is not allowed in shortcuts`n(it is used as a separator in the data files).", "Invalid Character", "Icon!") else MsgBox("Shortcuts cannot contain spaces or whitespace characters.", "Invalid Character", "Icon!") } return false } } return true } ;------------------------------------------------ ; SOUND FEEDBACK ;------------------------------------------------ PlayFeedback(soundKey) { global config if (config["Feedback"]["PlaySounds"]) try SoundPlay(config["Feedback"][soundKey]) } ;------------------------------------------------ ; RELOAD AND PAUSE ;------------------------------------------------ ReloadScript() { Reload() } ToggleMacrosPause() { global config, activeInputHook if (!A_IsSuspended) { ; Kill any active combo InputHook before suspending if (activeInputHook != "") { try activeInputHook.Stop() activeInputHook := "" } Suspend(true) TraySetIcon("shell32.dll", 131) TrayTip("PAUSED", "All macros paused - press " . HotkeyLabel(config["Hotkeys"]["PauseResume"]) . " to resume", "Iconi") PlayFeedback("ErrorSound") } else { Suspend(false) TraySetIcon("shell32.dll", 46) TrayTip("ACTIVE", "All macros are active", "Iconi") PlayFeedback("ExpandSound") } UpdateTrayTooltip() } ;================================================ ; MEGABAR - F-KEY GESTURE SYSTEM ;================================================ LoadMegaBar() { global megaBarPath, megaBarSlots megaBarSlots := Map() if (!FileExist(megaBarPath)) return try { loop read megaBarPath { line := Trim(A_LoopReadLine) if (line = "" || SubStr(line,1,1) = ";") continue parts := StrSplit(line, "|") if (parts.Length < 4) continue fkey := parts[1] gesture := parts[2] type := parts[3] target := parts[4] if (!megaBarSlots.Has(fkey)) megaBarSlots[fkey] := Map("tap","","longpress","","doubletap","","combo",Map()) if (SubStr(gesture,1,6) = "combo_") megaBarSlots[fkey]["combo"][SubStr(gesture,7)] := Map("type",type,"target",target) else megaBarSlots[fkey][gesture] := Map("type",type,"target",target) } } } SaveMegaBar() { global megaBarPath, megaBarSlots try { content := "; MegaBar config - MultiShortcuts v7.6`n" for fkey, slots in megaBarSlots { for gesture in ["tap","longpress","doubletap"] { slot := slots[gesture] if (slot != "" && slot.Has("type") && slot["target"] != "") content .= fkey . "|" . gesture . "|" . slot["type"] . "|" . slot["target"] . "`n" } if (slots.Has("combo")) { for letter, slot in slots["combo"] { if (slot["target"] != "") content .= fkey . "|combo_" . letter . "|" . slot["type"] . "|" . slot["target"] . "`n" } } } tmp := megaBarPath . ".tmp" FileAppend(content, tmp, "UTF-8") if (FileExist(megaBarPath)) FileDelete(megaBarPath) FileMove(tmp, megaBarPath) } } SetupMegaBarHotkeys() { global fkeyExempt fkeys := ["F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12"] for , fk in fkeys { if (fkeyExempt.Has(fk) && fkeyExempt[fk]) continue try { Hotkey("~*" . fk, ProcessFKDown, "On") Hotkey("~*" . fk . " up", ProcessFKUp, "On") } } } ProcessFKDown(*) { global megaBarPaused, fkeyDownTime, fkeyExempt, fkeyRemap fk := RegExReplace(A_ThisHotkey, "i)^\~\*|\s+up$", "") if (megaBarPaused) { return } if (fkeyExempt.Has(fk) && fkeyExempt[fk]) { return } if (fkeyRemap.Has(fk) && fkeyRemap[fk] != "") { Send("{" . fkeyRemap[fk] . "}") return } fkeyDownTime[fk] := A_TickCount } ProcessFKUp(*) { global megaBarPaused, fkeyDownTime, fkeyLastTap global megaBarSlots, LONGPRESS_MS, DOUBLETAP_MS, COMBO_WAIT_MS global fkeyExempt, fkeyRemap fk := RegExReplace(A_ThisHotkey, "i)^\~\*|\s+up$", "") if (megaBarPaused) { return } if (fkeyExempt.Has(fk) && fkeyExempt[fk]) { return } if (fkeyRemap.Has(fk) && fkeyRemap[fk] != "") { return } if (!megaBarSlots.Has(fk)) { return } heldMs := A_TickCount - (fkeyDownTime.Has(fk) ? fkeyDownTime[fk] : 0) if (heldMs >= LONGPRESS_MS) { if (megaBarSlots.Has(fk) && megaBarSlots[fk].Has("longpress")) { slot := megaBarSlots[fk]["longpress"] if (slot != "" && slot.Has("type") && slot["type"] != "") FireMegaBarSlot(slot) } fkeyDownTime.Delete(fk) return } slots := megaBarSlots[fk] if (slots.Has("combo") && slots["combo"].Count > 0) { global activeInputHook local ih := InputHook("L1 T" . (COMBO_WAIT_MS / 1000)) ih.UserObj := {fk: fk, comboSlots: slots["combo"]} ih.OnEnd := (hookInstance) => (activeInputHook := "", ProcessComboInput(hookInstance)) ih.Start() activeInputHook := ih return } now := A_TickCount if (fkeyLastTap.Has(fk) && (now - fkeyLastTap[fk]) <= DOUBLETAP_MS) { fkeyLastTap.Delete(fk) if (megaBarSlots.Has(fk) && megaBarSlots[fk].Has("doubletap")) { slot := megaBarSlots[fk]["doubletap"] if (slot != "" && slot.Has("type") && slot["type"] != "") FireMegaBarSlot(slot) } return } fkeyLastTap[fk] := now SetTimer(MakeTapFireCB(fk, now), -(DOUBLETAP_MS + 30)) } ProcessComboInput(ih) { ; Extract context safely from UserObj - prevents cross-talk between concurrent instances fk := ih.UserObj.fk comboSlots := ih.UserObj.comboSlots letter := ih.Input if (letter = "") { ; Timeout - fire tap action instead if (megaBarSlots.Has(fk)) { slot := megaBarSlots[fk].Has("tap") ? megaBarSlots[fk]["tap"] : "" if (slot != "" && slot.Has("type") && slot["type"] != "") FireMegaBarSlot(slot) } return } ; Check if letter matches a defined combo for comboLetter, slot in comboSlots { if (letter = comboLetter) { FireMegaBarSlot(slot) return } } ; No match - re-send the key and fire tap if assigned SendText(letter) if (megaBarSlots.Has(fk)) { slot := megaBarSlots[fk].Has("tap") ? megaBarSlots[fk]["tap"] : "" if (slot != "" && slot.Has("type") && slot["type"] != "") FireMegaBarSlot(slot) } } TapFire(fk, tapTime) { global fkeyLastTap, megaBarSlots, DOUBLETAP_MS if (fkeyLastTap.Has(fk) && fkeyLastTap[fk] != tapTime) { return } fkeyLastTap.Delete(fk) if (!megaBarSlots.Has(fk)) { return } slots := megaBarSlots[fk] if (!slots.Has("tap") || slots["tap"] = "") { return } slot := slots["tap"] if (slot.Has("type") && slot["type"] != "" && slot["target"] != "") FireMegaBarSlot(slot) } class TapFireCallback { __New(fk, tapTime) { this.fk := fk this.tapTime := tapTime } Call(*) { TapFire(this.fk, this.tapTime) } } MakeTapFireCB(fk, tapTime) { cb := TapFireCallback(fk, tapTime) return cb.Call.Bind(cb) } FireMegaBarSlot(slot) { global loadedExpansions type := slot["type"] target := slot["target"] if (type = "expansion") { if (loadedExpansions.Has(target)) SendText(loadedExpansions[target]) else SendText(target) } else { if (!IsAllowedTarget(target)) return try Run(target) catch Error as e MsgBox("F-Key Hub launch failed: " . e.Message, "F-Key Hub", "Icon!") } } ToggleMegaBar() { global megaBarPaused megaBarPaused := !megaBarPaused if (megaBarPaused) { A_TrayMenu.Rename("Pause F-Key Hub", "Resume F-Key Hub") TrayTip("F-Key Hub Paused", "F-keys restored to normal Windows behavior", "Iconi") } else { A_TrayMenu.Rename("Resume F-Key Hub", "Pause F-Key Hub") TrayTip("F-Key Hub Active", "F-key gestures restored", "Iconi") } UpdateTrayTooltip() } ;================================================ ; F-KEY MANAGER - EXEMPT & REMAP ;================================================ LoadFKeySettings() { global prefsPath, fkeyExempt, fkeyRemap fkeyExempt := Map() fkeyRemap := Map() if (!FileExist(prefsPath)) { return } try { exemptStr := IniRead(prefsPath, "FKeys", "Exempt", "") if (exemptStr != "") { for , fk in StrSplit(exemptStr, ",") fkeyExempt[Trim(fk)] := true } loop 12 { fk := "F" . A_Index val := IniRead(prefsPath, "FKeys", "Remap_" . fk, "") if (val != "") fkeyRemap[fk] := val } } } SaveFKeySettings() { global prefsPath, fkeyExempt, fkeyRemap try { ; Build exempt list exemptList := "" for fk, val in fkeyExempt if (val) exemptList .= (exemptList = "" ? "" : ",") . fk IniWrite(exemptList, prefsPath, "FKeys", "Exempt") ; Save remaps - delete key entirely if empty to keep prefs.ini clean loop 12 { fk := "F" . A_Index if (fkeyRemap.Has(fk) && fkeyRemap[fk] != "") IniWrite(fkeyRemap[fk], prefsPath, "FKeys", "Remap_" . fk) else try IniDelete(prefsPath, "FKeys", "Remap_" . fk) } } } ShowFKeyManager() { global fkeyExempt, fkeyRemap gFKM := Gui("+ToolWindow +AlwaysOnTop", "F-Key Manager - MultiShortcuts v7.6") gFKM.SetFont("s9", "Segoe UI") ; ── EXEMPT SECTION ────────────────────────────── gFKM.Add("Text", "x10 y10 w560 cGray", "Exempt F-keys: ticked keys are NEVER intercepted - always pass through to Windows and apps.") gFKM.Add("Text", "x10 y28 w560 h1 Background808080") exemptChecks := Map() x := 10 y := 38 loop 12 { fk := "F" . A_Index cb := gFKM.Add("CheckBox", "x" . x . " y" . y . " w70", fk) if (fkeyExempt.Has(fk) && fkeyExempt[fk]) cb.Value := 1 exemptChecks[fk] := cb x += 72 if (Mod(A_Index, 6) = 0) { x := 10 y += 26 } } y += 32 ; ── REMAP SECTION ─────────────────────────────── gFKM.Add("Text", "x10 y" . y . " w560 cGray", "Remap F-keys: while running, pressing source sends destination instead. Exempt keys always override.") y += 18 gFKM.Add("Text", "x10 y" . y . " w560 h1 Background808080") y += 8 fkList := ["- none -","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12"] ; Column headers gFKM.Add("Text", "x14 y" . y . " w60 cGray", "Source") gFKM.Add("Text", "x120 y" . y . " w60 cGray", "-> Destination") gFKM.Add("Text", "x270 y" . y . " w60 cGray", "Enabled") y += 18 remapRows := [] loop 6 { srcDDL := gFKM.Add("DropDownList", "x10 y" . y . " w90 Choose1", fkList) dstDDL := gFKM.Add("DropDownList", "x110 y" . y . " w130 Choose1", fkList) enableCB := gFKM.Add("CheckBox", "x260 y" . (y+2) . " w80", "Active") remapRows.Push(Map("src", srcDDL, "dst", dstDDL, "enable", enableCB)) y += 28 } ; Pre-populate remap rows from saved settings rowIdx := 1 for fk, dst in fkeyRemap { if (dst != "" && rowIdx <= remapRows.Length) { row := remapRows[rowIdx] for i, item in fkList { if (item = fk) row["src"].Choose(i) if (item = dst) row["dst"].Choose(i) } row["enable"].Value := 1 rowIdx++ } } y += 8 ; ── BUTTONS ───────────────────────────────────── gFKM.Add("Button", "x10 y" . y . " w120 h28", "Save & Apply") .OnEvent("Click", DoSaveFKM) gFKM.Add("Button", "x140 y" . y . " w80 h28", "Cancel") .OnEvent("Click", (*) => gFKM.Destroy()) gFKM.Add("Text", "x230 y" . (y+6) . " w340 cGray", "Changes apply immediately after Save.") gFKM.OnEvent("Close", (*) => gFKM.Destroy()) gFKM.OnEvent("Escape", (*) => gFKM.Destroy()) gFKM.Show("w590 h" . (y + 50)) DoSaveFKM(*) { global fkeyExempt, fkeyRemap, prefsPath ; Read exempt checkboxes fkeyExempt := Map() for fk, cb in exemptChecks if (cb.Value) fkeyExempt[fk] := true ; Read remap rows fkeyRemap := Map() for row in remapRows { srcIdx := row["src"].Value dstIdx := row["dst"].Value active := row["enable"].Value if (active && srcIdx > 1 && dstIdx > 1) { src := fkList[srcIdx] dst := fkList[dstIdx] if (src != dst) { ; Exempt keys cannot be remapped if (fkeyExempt.Has(src) && fkeyExempt[src]) { MsgBox("'" . src . "' is exempted and cannot be remapped.`n" . "Remove it from the exempt list first.", "Conflict", "Icon!") return } fkeyRemap[src] := dst } } } SaveFKeySettings() ; Re-register all F-key hotkeys with new settings loop 12 { fk := "F" . A_Index try Hotkey("~*" . fk, , "Off") try Hotkey("~*" . fk . " up", , "Off") } SetupMegaBarHotkeys() gFKM.Destroy() TrayTip("F-Key Manager", "Settings saved and applied.", "Iconi") UpdateTrayTooltip() } } ;================================================ ; MEGABAR SETUP GUI ;================================================ ShowMegaBarSetup() { global megaBarSlots, fkeyExempt gestureCtrls := Map() ; declared here so all nested functions see it as a closure LoadMegaBar() gMB := Gui("+Resize +MinSize700x520", "F-Key Hub Setup - MultiShortcuts v7.6") gMB.SetFont("s9", "Segoe UI") gMB.Add("Text", "x10 y10 w680 cGray", "Assign actions to F-keys. Each key supports: Tap, Long Press, Double Tap, and Combo (Fkey then character within 1s).") fkList := [] loop 12 fkList.Push("F" . A_Index) gMB.Add("Text", "x10 y36", "Select F-Key:") fkDDL := gMB.Add("DropDownList", "x100 y33 w80 Choose1", fkList) tabCtrl := gMB.Add("Tab3", "x10 y62 w680 h340", ["Tap", "Long Press", "Double Tap", "Combo"]) for i, gesture in ["tap","longpress","doubletap"] { tabCtrl.UseTab(i) gMB.Add("Text", "x20 y100", "Action type:") typeDDL := gMB.Add("DropDownList", "x110 y97 w160 Choose1", ["- none -","Text Expansion","App / File","Document","URL"]) gMB.Add("Text", "x20 y132", "Target:") targetEdit := gMB.Add("Edit", "x110 y129 w460 h22") browseBtn := gMB.Add("Button", "x578 y128 w80 h24", "Browse...") browseBtn.OnEvent("Click", MakeMBBrowse(targetEdit)) gMB.Add("Text", "x20 y162 w640 cGray", "For Text Expansion enter the text. For App/Document enter the full path. For URL enter https://...") gestureCtrls[gesture] := Map("type", typeDDL, "target", targetEdit) } tabCtrl.UseTab(4) gMB.Add("Text", "x20 y100 w640", "Tap F-key then press a single letter within 1 second.") gMB.Add("Text", "x20 y130", "Combo key:") comboLetter := gMB.Add("Edit", "x100 y127 w30 h22") gMB.Add("Text", "x145 y130", "Type:") comboType := gMB.Add("DropDownList", "x185 y127 w140 Choose1", ["- none -","Text Expansion","App / File","Document","URL"]) gMB.Add("Text", "x20 y162", "Target:") comboTarget := gMB.Add("Edit", "x75 y159 w500 h22") comboBrowse := gMB.Add("Button", "x583 y158 w80 h24", "Browse...") comboBrowse.OnEvent("Click", MakeMBBrowse(comboTarget)) gMB.Add("Button", "x20 y194 w100 h26", "Add Combo") .OnEvent("Click", DoAddCombo) gMB.Add("Text", "x20 y232 cGray", "Defined combos for selected F-key:") comboLV := gMB.Add("ListView", "x20 y250 w640 h120 -Multi", ["Letter","Type","Target"]) comboLV.ModifyCol(1,60) comboLV.ModifyCol(2,120) comboLV.ModifyCol(3,440) gMB.Add("Button", "x20 y378 w120 h24", "Delete Selected") .OnEvent("Click", DoDeleteCombo) tabCtrl.UseTab() gMB.Add("Button", "x10 y415 w120 h30", "Load F-Key") .OnEvent("Click", DoLoadFKey) gMB.Add("Button", "x140 y415 w120 h30", "Save F-Key") .OnEvent("Click", DoSaveFKey) gMB.Add("Button", "x270 y415 w120 h30", "Clear F-Key") .OnEvent("Click", DoClearFKey) gMB.Add("Button", "x560 y415 w130 h30", "Close") .OnEvent("Click", (*) => gMB.Destroy()) gMB.Add("Text", "x10 y455 w680 cGray", "Exempted F-keys cannot be assigned here. Use Tray -> F-Key Manager to manage exemptions.") gMB.OnEvent("Close", (*) => gMB.Destroy()) gMB.OnEvent("Escape", (*) => gMB.Destroy()) gMB.Show("w700 h490") MakeMBBrowse(editCtrl) { return (*) => (f := FileSelect(3,,"Select target file"), f != "" ? editCtrl.Value := f : "") } GetCurrentFKey() => "F" . fkDDL.Value PopulateFromSlot(fk) { if (!megaBarSlots.Has(fk)) return slots := megaBarSlots[fk] typeMap := Map("launcher",3,"expansion",2,"document",4,"url",5) for gesture, ctrl in gestureCtrls { typeCtrl := ctrl.Get("type") targetCtrl := ctrl.Get("target") slot := slots.Has(gesture) ? slots[gesture] : "" if (slot != "" && slot.Has("type")) { typeCtrl.Choose(typeMap.Has(slot["type"]) ? typeMap[slot["type"]] : 1) targetCtrl.Value := slot["target"] } else { typeCtrl.Choose(1) targetCtrl.Value := "" } } RefreshComboList(fk) } RefreshComboList(fk) { comboLV.Delete() if (!megaBarSlots.Has(fk) || !megaBarSlots[fk].Has("combo")) return for letter, slot in megaBarSlots[fk]["combo"] comboLV.Add("", letter, slot["type"], slot["target"]) } DoLoadFKey(*) => PopulateFromSlot(GetCurrentFKey()) DoSaveFKey(*) { fk := GetCurrentFKey() ; Block saving to exempted key if (fkeyExempt.Has(fk) && fkeyExempt[fk]) { MsgBox("'" . fk . "' is exempted and cannot be assigned an F-Key Hub action.`n" . "Use Tray -> F-Key Manager to remove the exemption first.", "Key Exempted", "Icon!") return } if (!megaBarSlots.Has(fk)) megaBarSlots[fk] := Map("tap","","longpress","","doubletap","","combo",Map()) typeNames := Map(1,"",2,"expansion",3,"launcher",4,"document",5,"url") for gesture, ctrl in gestureCtrls { typeCtrl := ctrl.Get("type") targetCtrl := ctrl.Get("target") typeIdx := typeCtrl.Value tgt := Trim(targetCtrl.Value) if (typeIdx > 1 && tgt != "") megaBarSlots[fk][gesture] := Map("type",typeNames[typeIdx],"target",tgt) else megaBarSlots[fk][gesture] := "" } SaveMegaBar() TrayTip("F-Key Hub Saved", fk . " gestures updated", "Iconi") } DoClearFKey(*) { fk := GetCurrentFKey() megaBarSlots[fk] := Map("tap","","longpress","","doubletap","","combo",Map()) for , ctrl in gestureCtrls { ctrl.Get("type").Choose(1) ctrl.Get("target").Value := "" } RefreshComboList(fk) SaveMegaBar() TrayTip("F-Key Hub", fk . " cleared", "Iconi") } DoAddCombo(*) { fk := GetCurrentFKey() letter := Trim(comboLetter.Value) tgt := Trim(comboTarget.Value) if (letter = "" || StrLen(letter) != 1 || tgt = "") { MsgBox("Enter a single letter and a target.", "Combo", "Icon!") return } if (!RegExMatch(letter, "^[a-zA-Z0-9]$")) { MsgBox("Combo key must be a letter (a-z, A-Z) or digit (0-9).", "Invalid Key", "Icon!") return } typeNames := Map(1,"",2,"expansion",3,"launcher",4,"document",5,"url") typeIdx := comboType.Value if (typeIdx < 2) { MsgBox("Please select an action type.", "Combo", "Icon!") return } if (!megaBarSlots.Has(fk)) megaBarSlots[fk] := Map("tap","","longpress","","doubletap","","combo",Map()) megaBarSlots[fk]["combo"][letter] := Map("type",typeNames[typeIdx],"target",tgt) comboLetter.Value := "" comboTarget.Value := "" comboType.Choose(1) RefreshComboList(fk) SaveMegaBar() } DoDeleteCombo(*) { row := comboLV.GetNext() if (!row) { return } fk := GetCurrentFKey() letter := comboLV.GetText(row,1) if (megaBarSlots.Has(fk) && megaBarSlots[fk]["combo"].Has(letter)) megaBarSlots[fk]["combo"].Delete(letter) RefreshComboList(fk) SaveMegaBar() } PopulateFromSlot("F1") } ;------------------------------------------------ ; Save stats on exit so nothing is lost OnExit((*) => SaveUsageStats()) ;------------------------------------------------ ; END OF SCRIPT ;------------------------------------------------