; RadProHotkey.ahk -- RadPro Hotkey Pipeline ; Configurable PowerScribe <-> RadPro AI hotkey helper ; ---------------------------------------------------------------------- ; User-facing defaults: ; ` Run all tasks from the current PowerScribe draft with Generate Template off. ; Win+` Run from the current PowerScribe draft with Generate Template on. ; Alt+` Insert the "Integrate these findings:" marker in the report editor. ; Ctrl+` Send the RadPro polished report back to PowerScribe. ; ; Configure these from the tray menu: Configure hotkeys... ; Settings are saved next to this file in RadProHotkey.ini. ; ; Requires AutoHotkey v2.0+. #Requires AutoHotkey v2.0 #SingleInstance Force SetWorkingDir(A_ScriptDir) SendMode("Input") ; ---------- user configuration ---------- DEFAULT_FORWARD_HOTKEY := "``" DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY := "#``" INTEGRATE_FINDINGS_HOTKEY := "!``" DEFAULT_REVERSE_HOTKEY := "^``" BROWSER_FORWARD_NO_TEMPLATE_HOTKEY := "^!{F3}" BROWSER_FORWARD_WITH_TEMPLATE_HOTKEY := "^!+{F3}" INTERNAL_RADPRO_COPY_HOTKEY := "^!``" RADPRO_TRAY_TITLE := "RadProAI Hotkey Helper" CONFIG_PATH := A_ScriptDir . "\RadProHotkey.ini" LOG_PATH := A_ScriptDir . "\RadProHotkey.log" EMBEDDED_RADPRO_ICON_BASE64 := "AAABAAEAICAAAAEAIACaAwAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p69AAAA09JREFUeAGslk9u1DAUxr/MBZgjpHsqegAQwwlQ4QClZdsRUtkzKftWQsO2LT0ArTgBRXCAorLvHKGcILyfPc7EkziTKY38Yvv9+z4/21EGWveZlE9VlPsmRyYnc2G8L2xr5utHgMRFeaLD8k6ZfhrG1OTAZG8ujKfOhg++xJhxVesmUJSPbYUXLrG0p1KPViWU99lzMUV5YfGPu2LSBCizdGPB2yb3bcTeGIn9VIJ2AkV5ZAFTk4dqUyNBzka+JgEPzp4653wojXL1knzoQlKvgzYSMQFf9gqcTGcvpe876iW377wfhIltEUhE27EgwIGT/rvsgEP4zVYLvFexHdXBXBCQPnr7w7ypXMeWVFiegL+znNiV6C/OpY1PsaBDZndxONWINdVsO3y0PIFMu5VpaXA1ixWsDM3MwILggyyT2HmCZ0LmmIHA64SbDn9IX64XVsrKHtMvtH4EIYj4mdRRASmTwxy4Uvivl1LP7rcmidTq8mEqy5IeTNv6gTFJn9daDCSCcAaoTM3shqwYcRN71ath02bLtDUw7YZJr8ZWIJSaAFYLIFeOs8HWoA8C4TBO9BsQ6Fs0l2PyXOKDU058DyjgkHAO8xerD0TnqrZuCIE2Q0PHSgErRhIrbzjUFIBzI2qq5BACdqGSdmdgdYCPcjdNvgCm7H3BLdEdBG5tkGz5UKLEdQeAACmu6lrpahbfltjaOrsd2A9E7ZY3nZavG6CAA8ZN4FCGqGIkjfIw69GXuh7oMPulTH9T7vWEM9ssQOu+zNEHHdXKh2HW0YNp2GyBrApfO1wrU321QQl4XQ/4ctWCb9SXcpiBwFlkXHNCFdiSENZrK0o5TE/ASmHBlyad7fx32szppxrBY8VWXMpjyhPwUR9813yTmINH37R6DbY1tqLCWhAosj+WamwSNVbGt79e4sihNmEr8A2SqNhYHks8CwLMiuyzdccmVWNl1aTHAP+6LIUcy2MoPDEBtEX23rqIhM0fogFO7ihXkwBmT6KxHZjuKZS9AU6udgJYfKk2bbjydphPqhG7KZ9LbU+aAN4cliJ7pVLPbHoqvl426Gze59TFEEuOjoBuAiGQO1tkbzXJhiodmbGZOCen1iOMx86GD77ESDJ7Z/sHAAD//zUIv0oAAAAGSURBVAMAgxUt6jOWQqkAAAAASUVORK5CYII=" ; ---------- workflow configuration ---------- PS_WINDOW_REGEX := "i)(PowerScribe (One|360)|Nuance\.(PSOne|PowerScribe360))" PS_PROCESSES := ["Nuance.PSOne.exe", "Nuance.PowerScribe360.exe", "Nuance.NMS.UI.exe"] RADPRO_TITLE_REGEX := "i)RadPro AI" PS_PASTE_SETTLE_MS := 250 RADPRO_ACTIVATE_MS := 250 CLIPBOARD_TIMEOUT_SEC := 2.5 CLIPBOARD_SENTINEL := "__RADPRO_HOTKEY_SENTINEL__" CLIPBOARD_RESTORE_MS := 1500 ACTIVATE_TIMEOUT_SEC := 0.5 COPY_BTN_X_FRACTION := 0.467 COPY_BTN_Y_FRACTION := 0.244 VIRTUAL_PROCESSES := [ "wfica32.exe","wfshell.exe","ctxapd.exe","selfservice.exe","CDViewer.exe", "vmware-view.exe","vmware-remotemks.exe","horizonclient.exe","mstsc.exe" ] NON_EDITOR_TITLE_REGEX := "i)(Worklist|Peer Review|Sign Report|Discard|Settings|Preferences|Login)" busy := false helperPaused := false activeForwardHotkey := "" activeForwardWithTemplateHotkey := "" activeIntegrateFindingsHotkey := "" activeReverseHotkey := "" settings := LoadHotkeySettings() if (!ApplyConfiguredHotkeys(settings.forward, settings.forwardWithTemplate, settings.reverse, false)) { ApplyConfiguredHotkeys(DEFAULT_FORWARD_HOTKEY, DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY, DEFAULT_REVERSE_HOTKEY, false) } RegisterIntegrateFindingsHotkey(false) SetupBranding() BuildTrayMenu() ShowStartupNotice() Log("RadProHotkey helper loaded -- configurable pipeline") ; ---------- configurable hotkey registration ---------- LoadHotkeySettings() { global CONFIG_PATH, DEFAULT_FORWARD_HOTKEY, DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY, DEFAULT_REVERSE_HOTKEY settingsText := ReadHotkeySettingsFile() forward := NormalizeHotkey(ReadHotkeySetting(settingsText, "Forward", DEFAULT_FORWARD_HOTKEY)) forwardWithTemplate := NormalizeHotkey(ReadHotkeySetting(settingsText, "ForwardWithTemplate", DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY)) reverse := NormalizeHotkey(ReadHotkeySetting(settingsText, "Reverse", DEFAULT_REVERSE_HOTKEY)) message := "" if (!ValidateUserHotkeys(forward, forwardWithTemplate, reverse, &message)) { forward := DEFAULT_FORWARD_HOTKEY forwardWithTemplate := DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY reverse := DEFAULT_REVERSE_HOTKEY Log("Invalid saved hotkeys; using defaults. " . message) } return { forward: forward, forwardWithTemplate: forwardWithTemplate, reverse: reverse } } SaveHotkeySettings(forward, forwardWithTemplate, reverse) { global CONFIG_PATH try { text := "[Hotkeys]`r`nForward=" . forward . "`r`nForwardWithTemplate=" . forwardWithTemplate . "`r`nReverse=" . reverse . "`r`n" file := FileOpen(CONFIG_PATH, "w", "UTF-8") file.Write(text) file.Close() return true } catch as e { Log("Could not save hotkey settings: " . e.Message) return false } } ReadHotkeySettingsFile() { global CONFIG_PATH try { if (!FileExist(CONFIG_PATH)) return "" return FileRead(CONFIG_PATH) } catch as e { Log("Could not read hotkey settings: " . e.Message) return "" } } ReadHotkeySetting(settingsText, key, fallback) { if (settingsText = "") return fallback normalized := StrReplace(settingsText, "`r`n", "`n") normalized := StrReplace(normalized, "`r", "`n") inHotkeysSection := false for line in StrSplit(normalized, "`n") { line := Trim(line) if (line = "" || SubStr(line, 1, 1) = ";" || SubStr(line, 1, 1) = "#") continue if (RegExMatch(line, "^\[(.+)\]$", §ionMatch)) { inHotkeysSection := StrLower(Trim(sectionMatch[1])) = "hotkeys" continue } if (!inHotkeysSection) continue if (RegExMatch(line, "i)^" . key . "\s*=\s*(.*)$", &valueMatch)) return Trim(valueMatch[1]) } return fallback } ApplyConfiguredHotkeys(forward, forwardWithTemplate, reverse, showErrors := true) { global activeForwardHotkey, activeForwardWithTemplateHotkey, activeReverseHotkey, helperPaused forward := NormalizeHotkey(forward) forwardWithTemplate := NormalizeHotkey(forwardWithTemplate) reverse := NormalizeHotkey(reverse) message := "" if (!ValidateUserHotkeys(forward, forwardWithTemplate, reverse, &message)) { if (showErrors) MsgBox(message, "RadPro Hotkey Settings", "Icon!") return false } oldForward := activeForwardHotkey oldForwardWithTemplate := activeForwardWithTemplateHotkey oldReverse := activeReverseHotkey registeredForward := false registeredForwardWithTemplate := false registeredReverse := false try { if (oldForward != "") try Hotkey(oldForward, "Off") if (oldForwardWithTemplate != "" && oldForwardWithTemplate != oldForward) try Hotkey(oldForwardWithTemplate, "Off") if (oldReverse != "" && oldReverse != oldForward) try Hotkey(oldReverse, "Off") Hotkey(forward, (*) => RunForwardWithoutTemplatePipeline(), "On") registeredForward := true Hotkey(forwardWithTemplate, (*) => RunForwardWithTemplatePipeline(), "On") registeredForwardWithTemplate := true Hotkey(reverse, (*) => RunReversePipeline(), "On") registeredReverse := true activeForwardHotkey := forward activeForwardWithTemplateHotkey := forwardWithTemplate activeReverseHotkey := reverse SetPipelineHotkeysEnabled(!helperPaused) BuildTrayMenu() return true } catch as e { if (registeredForward && forward != oldForward) try Hotkey(forward, "Off") if (registeredForwardWithTemplate && forwardWithTemplate != oldForwardWithTemplate) try Hotkey(forwardWithTemplate, "Off") if (registeredReverse && reverse != oldReverse) try Hotkey(reverse, "Off") if (oldForward != "") try Hotkey(oldForward, (*) => RunForwardWithoutTemplatePipeline(), "On") if (oldForwardWithTemplate != "") try Hotkey(oldForwardWithTemplate, (*) => RunForwardWithTemplatePipeline(), "On") if (oldReverse != "") try Hotkey(oldReverse, (*) => RunReversePipeline(), "On") activeForwardHotkey := oldForward activeForwardWithTemplateHotkey := oldForwardWithTemplate activeReverseHotkey := oldReverse SetPipelineHotkeysEnabled(!helperPaused) BuildTrayMenu() if (showErrors) MsgBox("AutoHotkey could not register that shortcut. Pick a different hotkey.`n`n" . e.Message, "RadPro Hotkey Settings", "Icon!") Log("Hotkey registration failed: " . e.Message) return false } } SetPipelineHotkeysEnabled(enable) { global activeForwardHotkey, activeForwardWithTemplateHotkey, activeIntegrateFindingsHotkey, activeReverseHotkey option := enable ? "On" : "Off" if (activeForwardHotkey != "") try Hotkey(activeForwardHotkey, option) if (activeForwardWithTemplateHotkey != "" && activeForwardWithTemplateHotkey != activeForwardHotkey) try Hotkey(activeForwardWithTemplateHotkey, option) if (activeIntegrateFindingsHotkey != "" && activeIntegrateFindingsHotkey != activeForwardHotkey && activeIntegrateFindingsHotkey != activeForwardWithTemplateHotkey) try Hotkey(activeIntegrateFindingsHotkey, option) if (activeReverseHotkey != "" && activeReverseHotkey != activeForwardHotkey) try Hotkey(activeReverseHotkey, option) } RegisterIntegrateFindingsHotkey(showErrors := true) { global activeIntegrateFindingsHotkey, INTEGRATE_FINDINGS_HOTKEY, helperPaused hk := NormalizeHotkey(INTEGRATE_FINDINGS_HOTKEY) try { if (activeIntegrateFindingsHotkey != "" && activeIntegrateFindingsHotkey != hk) try Hotkey(activeIntegrateFindingsHotkey, "Off") Hotkey(hk, (*) => RunIntegrateFindingsMarker(), helperPaused ? "Off" : "On") activeIntegrateFindingsHotkey := hk BuildTrayMenu() return true } catch as e { activeIntegrateFindingsHotkey := "" if (showErrors) MsgBox("AutoHotkey could not register Alt+Backtick for the Integrate these findings marker.`n`n" . e.Message, "RadPro Hotkey Settings", "Icon!") Log("Integrate findings hotkey registration failed: " . e.Message) return false } } ToggleHotkeysPaused(*) { global helperPaused helperPaused := !helperPaused SetPipelineHotkeysEnabled(!helperPaused) BuildTrayMenu() if (helperPaused) { Notify("Hotkeys paused" , "RadPro AI is still running in the system tray. Choose Resume hotkeys when you want the send shortcuts active again." , "warn") Log("Hotkeys paused") } else { Notify("Hotkeys resumed" , "Send text shortcuts are active again." , "info") Log("Hotkeys resumed") } } ValidateUserHotkeys(forward, forwardWithTemplate, reverse, &message) { global INTERNAL_RADPRO_COPY_HOTKEY, BROWSER_FORWARD_NO_TEMPLATE_HOTKEY, BROWSER_FORWARD_WITH_TEMPLATE_HOTKEY, INTEGRATE_FINDINGS_HOTKEY forward := NormalizeHotkey(forward) forwardWithTemplate := NormalizeHotkey(forwardWithTemplate) reverse := NormalizeHotkey(reverse) if (forward = "" || forwardWithTemplate = "" || reverse = "") { message := "All three hotkey fields are required." return false } if (forward = forwardWithTemplate || forward = reverse || forwardWithTemplate = reverse) { message := "The two PowerScribe send shortcuts and the paste-back shortcut must be different." return false } reserved := Map( NormalizeHotkey(INTERNAL_RADPRO_COPY_HOTKEY), "Ctrl+Alt+Backtick is used by RadPro AI for report routing. Choose a different hotkey.", NormalizeHotkey(BROWSER_FORWARD_NO_TEMPLATE_HOTKEY), "Ctrl+Alt+F3 is used by RadPro AI to run all tasks with Generate Template off. Choose a different hotkey.", NormalizeHotkey(BROWSER_FORWARD_WITH_TEMPLATE_HOTKEY), "Ctrl+Alt+Shift+F3 is used by RadPro AI to run with Generate Template on. Choose a different hotkey.", NormalizeHotkey(INTEGRATE_FINDINGS_HOTKEY), "Alt+Backtick is used by RadPro AI to insert the Integrate these findings marker. Choose a different hotkey." ) if (reserved.Has(forward)) { message := reserved[forward] return false } if (reserved.Has(forwardWithTemplate)) { message := reserved[forwardWithTemplate] return false } if (reserved.Has(reverse)) { message := reserved[reverse] return false } if (InStr(forward, "&") || InStr(forwardWithTemplate, "&") || InStr(reverse, "&")) { message := "Prefix hotkeys that use '&' are not supported in this settings window." return false } message := "" return true } NormalizeHotkey(hk) { hk := Trim(hk) if (hk = "") return "" hk := StrReplace(hk, " ") hk := StrReplace(hk, "Control+", "^") hk := StrReplace(hk, "Ctrl+", "^") hk := StrReplace(hk, "Alt+", "!") hk := StrReplace(hk, "Shift+", "+") hk := StrReplace(hk, "Windows+", "#") hk := StrReplace(hk, "Win+", "#") ctrl := InStr(hk, "^") ? "^" : "" alt := InStr(hk, "!") ? "!" : "" shift := InStr(hk, "+") ? "+" : "" win := InStr(hk, "#") ? "#" : "" key := StrReplace(hk, "^") key := StrReplace(key, "!") key := StrReplace(key, "+") key := StrReplace(key, "#") return ctrl . alt . shift . win . key } HumanHotkey(hk) { hk := NormalizeHotkey(hk) if (hk = "") return "Unassigned" label := "" if (InStr(hk, "^")) label .= "Ctrl+" if (InStr(hk, "!")) label .= "Alt+" if (InStr(hk, "+")) label .= "Shift+" if (InStr(hk, "#")) label .= "Win+" key := StrReplace(hk, "^") key := StrReplace(key, "!") key := StrReplace(key, "+") key := StrReplace(key, "#") if (key = "``") key := "Backtick" return label . key } BuildTrayMenu() { global activeForwardHotkey, activeForwardWithTemplateHotkey, activeIntegrateFindingsHotkey, activeReverseHotkey, helperPaused, RADPRO_TRAY_TITLE pausedLabel := helperPaused ? " - paused" : "" A_IconTip := Format("RadPro AI Hotkey Helper{1} ({2} / {3} / {4} / {5})", pausedLabel, HumanHotkey(activeForwardHotkey), HumanHotkey(activeForwardWithTemplateHotkey), HumanHotkey(activeIntegrateFindingsHotkey), HumanHotkey(activeReverseHotkey)) tray := A_TrayMenu tray.Delete() tray.Add(RADPRO_TRAY_TITLE, (*) => 0) tray.Disable(RADPRO_TRAY_TITLE) tray.Default := RADPRO_TRAY_TITLE tray.Add() statusLabel := helperPaused ? "Status: hotkeys paused" : "Status: hotkeys active" tray.Add(statusLabel, (*) => 0) tray.Disable(statusLabel) tray.Add(helperPaused ? "Resume hotkeys" : "Pause hotkeys", ToggleHotkeysPaused) tray.Add() forwardLabel := Format("Send PowerScribe draft to RadPro - Generate Template off ({1})", HumanHotkey(activeForwardHotkey)) forwardWithTemplateLabel := Format("Send PowerScribe draft to RadPro with Generate Template on ({1})", HumanHotkey(activeForwardWithTemplateHotkey)) integrateFindingsLabel := Format("Insert Integrate these findings marker ({1})", HumanHotkey(activeIntegrateFindingsHotkey)) reverseLabel := Format("Send RadPro report to PowerScribe ({1})", HumanHotkey(activeReverseHotkey)) tray.Add(forwardLabel, (*) => RunForwardWithoutTemplatePipeline()) tray.Add(forwardWithTemplateLabel, (*) => RunForwardWithTemplatePipeline()) tray.Add(integrateFindingsLabel, (*) => RunIntegrateFindingsMarker()) tray.Add(reverseLabel, (*) => RunReversePipeline()) if (helperPaused) { tray.Disable(forwardLabel) tray.Disable(forwardWithTemplateLabel) tray.Disable(integrateFindingsLabel) tray.Disable(reverseLabel) } tray.Add("Configure hotkeys...", ShowHotkeySettings) tray.Add() tray.Add("Reload script", (*) => Reload()) tray.Add("Exit", (*) => ExitApp()) } ShowStartupNotice() { global activeForwardHotkey, activeForwardWithTemplateHotkey, activeIntegrateFindingsHotkey, activeReverseHotkey Notify("RadPro AI Hotkey Helper" , Format("Running in the system tray. Right-click the RadPro icon to configure hotkeys.`n`nCurrent hotkeys:`nRun all tasks, Generate Template off: {1}`nRun with Generate Template on: {2}`nInsert Integrate these findings marker: {3}`nSend text from RadPro to PowerScribe: {4}" , HumanHotkey(activeForwardHotkey) , HumanHotkey(activeForwardWithTemplateHotkey) , HumanHotkey(activeIntegrateFindingsHotkey) , HumanHotkey(activeReverseHotkey)) , "info" , 7000) } SetupBranding() { iconPath := EnsureRadProIcon() if (iconPath != "") { try TraySetIcon(iconPath) } } EnsureRadProIcon() { global EMBEDDED_RADPRO_ICON_BASE64 iconPath := A_Temp . "\RadProAIHotkey.ico" if (FileExist(iconPath)) return iconPath size := 0 if (!DllCall("Crypt32\CryptStringToBinaryW" , "Str", EMBEDDED_RADPRO_ICON_BASE64 , "UInt", 0 , "UInt", 0x1 , "Ptr", 0 , "UIntP", &size , "Ptr", 0 , "Ptr", 0)) { Log("Could not decode embedded RadPro tray icon size") return "" } buf := Buffer(size) if (!DllCall("Crypt32\CryptStringToBinaryW" , "Str", EMBEDDED_RADPRO_ICON_BASE64 , "UInt", 0 , "UInt", 0x1 , "Ptr", buf.Ptr , "UIntP", &size , "Ptr", 0 , "Ptr", 0)) { Log("Could not decode embedded RadPro tray icon") return "" } try { file := FileOpen(iconPath, "w") file.RawWrite(buf, size) file.Close() return iconPath } catch as e { Log("Could not write embedded RadPro tray icon: " . e.Message) return "" } } ShowHotkeySettings(*) { global activeForwardHotkey, activeForwardWithTemplateHotkey, activeReverseHotkey settingsGui := Gui("+AlwaysOnTop", "RadPro Hotkey Settings") settingsGui.SetFont("s10", "Segoe UI") settingsGui.Add("Text", "w520", "You can change the default RadPro AI hotkeys for running PowerScribe text with or without Generate Template and sending the polished report back.") settingsGui.Add("Text", "y+10 w520", "Ctrl+Alt+Backtick will not work as a custom hotkey because RadPro AI uses it in the background for report routing.") settingsGui.Add("Text", "y+6 w520", "Alt+Backtick inserts the Integrate these findings marker in the report editor.") settingsGui.Add("Text", "xm y+16 w520", "Run all tasks from PowerScribe (Generate Template off)") settingsGui.Add("Hotkey", "xm y+4 w220 vForwardHotkey", activeForwardHotkey) settingsGui.Add("Text", "x+12 yp+4 w280", "Default: Backtick") settingsGui.Add("Text", "xm y+14 w520", "Run from PowerScribe with Generate Template on") settingsGui.Add("Hotkey", "xm y+4 w220 vForwardWithTemplateHotkey", activeForwardWithTemplateHotkey) settingsGui.Add("Text", "x+12 yp+4 w280", "Default: Win+Backtick") settingsGui.Add("Text", "xm y+14 w520", "Send RadPro polished report to PowerScribe") settingsGui.Add("Hotkey", "xm y+4 w220 vReverseHotkey", activeReverseHotkey) settingsGui.Add("Text", "x+12 yp+4 w280", "Default: Ctrl+Backtick") settingsGui.Add("Text", "xm y+12 w520 cRed vErrorText", "") settingsGui.Add("Button", "xm y+14 w90 Default", "Save").OnEvent("Click", (*) => SaveHotkeySettingsFromGui(settingsGui)) settingsGui.Add("Button", "x+8 yp w120", "Reset defaults").OnEvent("Click", (*) => ResetHotkeySettingsFromGui(settingsGui)) settingsGui.Add("Button", "x+8 yp w90", "Cancel").OnEvent("Click", (*) => settingsGui.Destroy()) settingsGui.OnEvent("Escape", (*) => settingsGui.Destroy()) settingsGui.Show("AutoSize Center") } SaveHotkeySettingsFromGui(settingsGui, *) { data := settingsGui.Submit(false) forward := NormalizeHotkey(data.ForwardHotkey) forwardWithTemplate := NormalizeHotkey(data.ForwardWithTemplateHotkey) reverse := NormalizeHotkey(data.ReverseHotkey) message := "" if (!ValidateUserHotkeys(forward, forwardWithTemplate, reverse, &message)) { settingsGui["ErrorText"].Value := message return } if (!ApplyConfiguredHotkeys(forward, forwardWithTemplate, reverse, true)) { settingsGui["ErrorText"].Value := "Could not register those hotkeys. Choose a different combination." return } if (!SaveHotkeySettings(forward, forwardWithTemplate, reverse)) { settingsGui["ErrorText"].Value := "Hotkeys are active for this session, but RadProHotkey.ini could not be saved." return } Notify("Hotkeys updated" , Format("Current hotkeys:`nRun all tasks, Generate Template off: {1}`nRun with Generate Template on: {2}`nSend text from RadPro to PowerScribe: {3}" , HumanHotkey(forward) , HumanHotkey(forwardWithTemplate) , HumanHotkey(reverse)) , "info") Log(Format("Hotkeys updated: forward={1}, forwardWithTemplate={2}, reverse={3}", forward, forwardWithTemplate, reverse)) settingsGui.Destroy() } ResetHotkeySettingsFromGui(settingsGui, *) { global DEFAULT_FORWARD_HOTKEY, DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY, DEFAULT_REVERSE_HOTKEY settingsGui["ForwardHotkey"].Value := DEFAULT_FORWARD_HOTKEY settingsGui["ForwardWithTemplateHotkey"].Value := DEFAULT_FORWARD_WITH_TEMPLATE_HOTKEY settingsGui["ReverseHotkey"].Value := DEFAULT_REVERSE_HOTKEY SaveHotkeySettingsFromGui(settingsGui) } ; ---------- forward path: PowerScribe -> RadPro ---------- RunIntegrateFindingsMarker(*) { global busy if (busy) return busy := true try { Log("Trigger: mode=integrate-findings-marker") KeyWait("Alt") virt := DetectVirtual() if (virt) { Notify("Cannot insert findings marker" , Format("Detected {1}. Run the helper inside the same Windows session as PowerScribe.", virt) , "error") Log(Format("Integrate findings marker abort: virtual session detected ({1})", virt)) return } if (!ActivateSourceForForward()) { Notify("Open the report editor", "Could not activate PowerScribe or the current reporting window.", "warn") Log("Integrate findings marker abort: source activation failed") return } Send("^{End}") Sleep(50) SendText("Integrate these findings:") Send("{Enter 2}") Log("Integrate findings marker inserted") } finally { busy := false } } RunForwardWithoutTemplatePipeline(*) { RunForwardPipeline(false) } RunForwardWithTemplatePipeline(*) { RunForwardPipeline(true) } RunForwardPipeline(useTemplate := false) { global busy, CLIPBOARD_SENTINEL, RADPRO_ACTIVATE_MS, BROWSER_FORWARD_NO_TEMPLATE_HOTKEY, BROWSER_FORWARD_WITH_TEMPLATE_HOTKEY if (busy) return busy := true try { templateLabel := useTemplate ? "on" : "off" Log(Format("Trigger: mode=forward, generateTemplate={1}", templateLabel)) virt := DetectVirtual() if (virt) { Notify("Cannot send PowerScribe text" , Format("Detected {1}. Run the helper inside the same Windows session as PowerScribe.", virt) , "error") Log(Format("Forward abort: virtual session detected ({1})", virt)) return } if (!ActivateSourceForForward()) { Notify("Open the report editor", "Could not activate PowerScribe or the current reporting window.", "warn") Log("Forward abort: source activation failed") return } Send("^a") if (IsRadProOrEdgeActive()) { if (useTemplate) Send(BROWSER_FORWARD_WITH_TEMPLATE_HOTKEY) else Send(BROWSER_FORWARD_NO_TEMPLATE_HOTKEY) Log(Format("Forward sent browser trigger with Generate Template {1} in active browser window", templateLabel)) return } A_Clipboard := CLIPBOARD_SENTINEL ClipWait(0.2) Sleep(200) Send("^{Insert}") copied := WaitForClipChange(700, A_TickCount + 800) if (copied = "") { Sleep(200) Send("^{Insert}") copied := WaitForClipChange(700, A_TickCount + 800) } if (copied = "") { Notify("Could not copy PowerScribe text", "Open the report editor and try again.", "error") Log("Forward abort: clipboard did not change") return } Sleep(100) radHwnd := FindRadProWindow() if (radHwnd) { try WinActivate("ahk_id " . radHwnd) try WinWaitActive("ahk_id " . radHwnd, , 1) Sleep(RADPRO_ACTIVATE_MS) if (useTemplate) Send(BROWSER_FORWARD_WITH_TEMPLATE_HOTKEY) else Send(BROWSER_FORWARD_NO_TEMPLATE_HOTKEY) Log(Format("Forward OK: RadPro activated and browser trigger sent with Generate Template {1}", templateLabel)) } else { Run("msedge.exe https://radpro.ai/radpro-ai") Log("Forward opened RadPro AI in Edge") } } finally { busy := false } } ActivateSourceForForward() { if (IsPsWindowActive()) return true psPid := GetPsPid() if (psPid) return ActivatePsByPid(psPid) try { if (WinExist("PowerScribe")) { WinActivate("PowerScribe") return !!WinWaitActive("PowerScribe", , 1) } } catch { } ; Preserve the old helper behavior for compatible non-PowerScribe reporting ; windows: keep the current foreground app if no local PS process exists. return true } IsPsWindowActive() { global PS_WINDOW_REGEX, PS_PROCESSES title := SafeWinGetTitle() if (title ~= PS_WINDOW_REGEX) return true proc := SafeWinGetProcessName() for psProc in PS_PROCESSES { if (StrLower(proc) = StrLower(psProc)) return true } return false } IsRadProOrEdgeActive() { global RADPRO_TITLE_REGEX title := SafeWinGetTitle() proc := SafeWinGetProcessName() return (title ~= RADPRO_TITLE_REGEX) || (StrLower(proc) = "msedge.exe") } ; ---------- reverse path: RadPro -> PowerScribe ---------- RunReversePipeline(*) { global busy, CLIPBOARD_RESTORE_MS, PS_PASTE_SETTLE_MS if (busy) return busy := true try { Log("Trigger: mode=paste") virt := DetectVirtual() if (virt) { Notify("Cannot send RadPro text to PowerScribe" , Format("Detected {1}. Run the helper inside the same Windows session as PowerScribe.", virt) , "error") Log(Format("Reverse abort: virtual session detected ({1})", virt)) return } savedClip := ClipboardAll() polished := CopyPolishedFromRadPro() if (polished = "") { Notify("No RadPro report copied" , "Make sure RadPro AI is open and the Polished Report is visible." , "error") Log("Reverse abort: polished report not captured") SetTimer(() => RestoreClipNow(savedClip), -300) return } Log(Format("Captured polished report: {1} characters", StrLen(polished))) psPid := GetPsPid() if (!psPid) { Notify("Open PowerScribe", "PowerScribe is not running.", "warn") Log("Reverse abort: PS not running") SetTimer(() => RestoreClipNow(savedClip), -CLIPBOARD_RESTORE_MS) return } if (!ActivatePsByPid(psPid)) { Notify("Could not open PowerScribe", "PowerScribe is running, but the helper could not bring it to the foreground.", "error") Log("Reverse abort: PS activate failed") SetTimer(() => RestoreClipNow(savedClip), -CLIPBOARD_RESTORE_MS) return } activeTitle := SafeWinGetTitle() if (activeTitle ~= NON_EDITOR_TITLE_REGEX) { Notify("Open the report editor" , "PowerScribe is open to a worklist or dialog. Open the report editor first." , "warn") Log(Format("Reverse abort: PS active but wrong title: {1}", activeTitle)) SetTimer(() => RestoreClipNow(savedClip), -CLIPBOARD_RESTORE_MS) return } plain := SanitizeText(polished) A_Clipboard := plain if (!ClipWait(0.5)) { Notify("Could not prepare text", "The helper could not place the RadPro report on the clipboard.", "error") Log("Reverse abort: ClipWait stage failed") SetTimer(() => RestoreClipNow(savedClip), -CLIPBOARD_RESTORE_MS) return } DoReplaceAndPaste(StrLen(plain)) SetTimer(() => RestoreClipNow(savedClip), -CLIPBOARD_RESTORE_MS) } finally { busy := false } } CopyPolishedFromRadPro() { global CLIPBOARD_SENTINEL, CLIPBOARD_TIMEOUT_SEC, ACTIVATE_TIMEOUT_SEC, RADPRO_ACTIVATE_MS radHwnd := FindRadProWindow() if (!radHwnd) { Log("RadPro AI window not found") return "" } A_Clipboard := CLIPBOARD_SENTINEL if (!ClipWait(0.5)) { Log("Could not stage clipboard sentinel") return "" } try WinActivate("ahk_id " . radHwnd) try { if (!WinWaitActive("ahk_id " . radHwnd, , ACTIVATE_TIMEOUT_SEC)) { Log("RadPro WinWaitActive timed out") return "" } } catch { Log("RadPro WinWaitActive threw") return "" } Sleep(RADPRO_ACTIVATE_MS) overallDeadline := A_TickCount + (CLIPBOARD_TIMEOUT_SEC * 1000) perAttemptMs := 700 ; Ctrl+Alt+Backtick is the only chord RadPro AI handles. The old second ; chord, Ctrl+Shift+C, has not been a site shortcut since May 2026 and ; opens DevTools in Chrome and Edge when nothing is ready to copy. chords := ["^!``"] for chord in chords { if (A_TickCount > overallDeadline) break Log(Format("Trying RadPro chord: {1}", chord)) Send(chord) captured := WaitForClipChange(perAttemptMs, overallDeadline) if (captured != "") { Log(Format("Clipboard captured via chord {1}: {2} chars", chord, StrLen(captured))) return captured } A_Clipboard := CLIPBOARD_SENTINEL ClipWait(0.3) } if (A_TickCount < overallDeadline) { Log("Trying UIA invoke on Copy polished report button") if (InvokeCopyButtonViaUIA(radHwnd)) { captured := WaitForClipChange(perAttemptMs * 2, overallDeadline) if (captured != "") { Log(Format("Clipboard captured via UIA invoke: {1} chars", StrLen(captured))) return captured } } } if (A_TickCount < overallDeadline) { Log("Trying coordinate click on Copy polished report button") if (ClickCopyPolishedButton(radHwnd)) { captured := WaitForClipChange(perAttemptMs * 2, overallDeadline) if (captured != "") { Log(Format("Clipboard captured via click: {1} chars", StrLen(captured))) return captured } } } Log("Timed out waiting for RadPro to replace clipboard sentinel") return "" } InvokeCopyButtonViaUIA(hwnd) { static UIA_ControlTypePropertyId := 30003 static UIA_ButtonControlTypeId := 50000 static UIA_InvokePatternId := 10000 static UIA_LegacyIAccessiblePatternId := 10018 static TreeScope_Subtree := 4 try { UIA := "" try UIA := ComObject("{ff48dba4-60ef-4201-aa87-54103eef594e}", "{30cbe57d-d9d0-452a-ab13-7ac5ac4825ee}") if (!UIA) { Log("UIA: could not instantiate CUIAutomation") return false } rootElem := UIA.ElementFromHandle(hwnd) if (!rootElem) { Log("UIA: ElementFromHandle returned null") return false } cond := UIA.CreatePropertyCondition(UIA_ControlTypePropertyId, UIA_ButtonControlTypeId) buttons := rootElem.FindAll(TreeScope_Subtree, cond) if (!buttons) { Log("UIA: no buttons found in RadPro window") return false } count := buttons.Length Log(Format("UIA: walking {1} buttons in RadPro window", count)) bestMatch := "" Loop count { i := A_Index - 1 try { btn := buttons.GetElement(i) nm := btn.CurrentName if (nm = "") continue lcn := StrLower(nm) if (InStr(lcn, "copy") = 0) continue if (InStr(lcn, "task 3") || InStr(lcn, "polished") || InStr(lcn, "synthes")) { Log(Format("UIA: invoking button '{1}'", nm)) if (TryInvokeButton(btn, UIA_InvokePatternId, UIA_LegacyIAccessiblePatternId)) return true } if (bestMatch = "") bestMatch := btn } catch { continue } } if (bestMatch != "") { try Log(Format("UIA: invoking fallback Copy button '{1}'", bestMatch.CurrentName)) if (TryInvokeButton(bestMatch, UIA_InvokePatternId, UIA_LegacyIAccessiblePatternId)) return true } Log("UIA: found no matching Copy button") } catch as e { try Log(Format("UIA error: {1}", e.Message)) return false } return false } TryInvokeButton(btn, invokeId, legacyId) { try { pat := btn.GetCurrentPattern(invokeId) if (pat) { pat.Invoke() return true } } catch { } try { pat := btn.GetCurrentPattern(legacyId) if (pat) { pat.DoDefaultAction() return true } } catch { } return false } WaitForClipChange(maxWaitMs, hardDeadlineTick) { global CLIPBOARD_SENTINEL sub := A_TickCount + maxWaitMs while (A_TickCount < sub && A_TickCount < hardDeadlineTick) { cur := A_Clipboard if (cur != "" && cur != CLIPBOARD_SENTINEL) return cur Sleep(40) } return "" } ClickCopyPolishedButton(radHwnd) { global COPY_BTN_X_FRACTION, COPY_BTN_Y_FRACTION try { WinGetPos(&wx, &wy, &ww, &wh, "ahk_id " . radHwnd) } catch { Log("WinGetPos failed for RadPro window") return false } if (ww < 600 || wh < 400) { Log(Format("RadPro window too small for modal click: {1}x{2}", ww, wh)) return false } btnX := wx + Round(ww * COPY_BTN_X_FRACTION) btnY := wy + Round(wh * COPY_BTN_Y_FRACTION) Log(Format("Clicking Copy at screen ({1},{2}) within window ({3},{4}) {5}x{6}", btnX, btnY, wx, wy, ww, wh)) prevMode := CoordMode("Mouse", "Screen") MouseGetPos(&origX, &origY) blockedHere := false try { try { BlockInput(true) blockedHere := true } MouseMove(btnX, btnY, 0) Sleep(40) Click(btnX, btnY) Sleep(60) } finally { if (blockedHere) try BlockInput(false) try MouseMove(origX, origY, 0) CoordMode("Mouse", prevMode) } return true } DoReplaceAndPaste(charCount) { global PS_PASTE_SETTLE_MS blocked := false try { try { BlockInput(true) blocked := true } Sleep(60) Send("^a") Sleep(80) Send("+{Insert}") Sleep(PS_PASTE_SETTLE_MS) } finally { if (blocked) try BlockInput(false) } Notify("Pasted into PowerScribe" , Format("Replaced the current PowerScribe text with {1} characters. Ctrl+Z to undo.", charCount) , "info") Log("Paste OK") } ; ---------- environment helpers ---------- DetectVirtual() { global VIRTUAL_PROCESSES sn := EnvGet("SESSIONNAME") if (sn != "" && sn != "Console" && !RegExMatch(sn, "i)RDP-Tcp")) return Format("session={1}", sn) if (sn != "" && RegExMatch(sn, "i)RDP-Tcp")) return Format("session={1} (RDP)", sn) fgProc := SafeWinGetProcessName() if (fgProc != "") { for proc in VIRTUAL_PROCESSES { if (StrLower(fgProc) = StrLower(proc)) return Format("foreground={1}", fgProc) } } if (HasLocalPs()) return "" for proc in VIRTUAL_PROCESSES { if (ProcessExist(proc)) return Format("client-only={1} (no local PS)", proc) } return "" } HasLocalPs() { global PS_PROCESSES for proc in PS_PROCESSES { if (ProcessExist(proc)) return true } return false } GetPsPid() { global PS_PROCESSES for proc in PS_PROCESSES { pid := ProcessExist(proc) if (pid) return pid } return 0 } ActivatePsByPid(pid) { global ACTIVATE_TIMEOUT_SEC try WinActivate("ahk_pid " . pid) try { result := WinWaitActive("ahk_pid " . pid, , ACTIVATE_TIMEOUT_SEC) return (result != 0) } catch { return false } } FindRadProWindow() { global RADPRO_TITLE_REGEX list := WinGetList() for hwnd in list { try { title := WinGetTitle("ahk_id " . hwnd) if (title ~= RADPRO_TITLE_REGEX) return hwnd } catch { } } return 0 } SanitizeText(s) { s := StrReplace(s, "`r`n", "`n") s := StrReplace(s, "`r", "`n") s := StrReplace(s, "`n", "`r`n") return s } RestoreClipNow(savedClip) { try A_Clipboard := savedClip } SafeWinGetProcessName() { try return WinGetProcessName("A") return "" } SafeWinGetTitle() { try return WinGetTitle("A") return "" } Notify(title, message, level := "info", durationMs := 4500) { static activeToast := "" try { if (IsObject(activeToast)) activeToast.Destroy() } bg := "F8FAFC" accent := "2563EB" if (level = "warn") { bg := "FFFBEB" accent := "B45309" } else if (level = "error") { bg := "FFF1F2" accent := "BE123C" } toast := Gui("+AlwaysOnTop -Caption +ToolWindow +Border", "RadPro AI") toast.BackColor := bg area := GetToastWorkArea() areaW := Max(200, area.right - area.left) margin := Min(48, Max(12, Floor(areaW / 12))) availableW := Max(160, areaW - (margin * 2)) toastW := Min(340, availableW) textW := Max(132, toastW - 28) toast.MarginX := 14 toast.MarginY := 12 if (title != "") { toast.SetFont("s10 bold c" . accent, "Segoe UI") toast.Add("Text", Format("w{1}", textW), title) } toast.SetFont("s9 c374151", "Segoe UI") toast.Add("Text", Format("y+6 w{1}", textW), message) maxH := Max(120, area.bottom - area.top - (margin * 2)) toast.Show("Hide AutoSize") try { WinGetPos(&unusedX, &unusedY, &toastActualW, &toastActualH, "ahk_id " . toast.Hwnd) } catch { toastActualW := toastW toastActualH := 160 } toastTargetH := Min(toastActualH, maxH) toast.Show(Format("Hide w{1} h{2}", toastW, toastTargetH)) try { WinGetPos(&unusedX, &unusedY, &toastActualW, &toastActualH, "ahk_id " . toast.Hwnd) } catch { toastActualW := toastW toastActualH := toastTargetH } x := Max(area.left + margin, area.right - toastActualW - margin) y := Max(area.top + margin, area.bottom - toastActualH - margin) toast.Show(Format("NoActivate x{1} y{2}", x, y)) activeToast := toast SetTimer(() => CloseToast(toast), -durationMs) } CloseToast(toast) { try toast.Destroy() } GetToastWorkArea() { try { WinGetPos(&wx, &wy, &ww, &wh, "A") if (GetMonitorWorkAreaForPoint(wx + (ww / 2), wy + (wh / 2), &area)) return area } MouseGetPos(&mx, &my) if (GetMonitorWorkAreaForPoint(mx, my, &area)) return area try { primary := MonitorGetPrimary() MonitorGetWorkArea(primary, &left, &top, &right, &bottom) return { left: left, top: top, right: right, bottom: bottom } } return { left: 0, top: 0, right: 800, bottom: 600 } } GetMonitorWorkAreaForPoint(x, y, &area) { try { count := MonitorGetCount() } catch { return false } Loop count { try { MonitorGet(A_Index, &left, &top, &right, &bottom) if (x >= left && x < right && y >= top && y < bottom) { MonitorGetWorkArea(A_Index, &workLeft, &workTop, &workRight, &workBottom) area := { left: workLeft, top: workTop, right: workRight, bottom: workBottom } return true } } } return false } Log(line) { global LOG_PATH try { ts := FormatTime(, "yyyy-MM-dd HH:mm:ss") FileAppend(Format("{1} {2}`r`n", ts, line), LOG_PATH) } }