commit 0fa1476753d629d3484a8a7b370f3138d9128c57 Author: Sean Cusack Date: Sat May 21 21:55:02 2022 +0100 initial commit, GP tools split into individual includes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..beb9a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +AHK-Studio Backup/ diff --git a/Launcher.ahk b/Launcher.ahk new file mode 100644 index 0000000..cb4a332 --- /dev/null +++ b/Launcher.ahk @@ -0,0 +1,23 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ;; +;; GP Tools ;; +;; ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Setup +#NoEnv +SetBatchLines, -1 +#SingleInstance, force +#HotkeyModifierTimeout 100 +SetTitleMatchMode, 2 +SetKeyDelay, 50 + + +#Include libraries\helper_library.ahk ;; nhs_number functions + + +#Include includes\S1Utils.ahk ;; Helper functions for use in S1 +#Include includes\S1Shortcuts.ahk ;; Shortcut keys for S1 actions +#Include includes\TextExpansions.ahk ;; Text expansions for autohotkey +;#Include includes\ChromeController.ahk ;; S1 chrome controls for AMGP +#Include includes\QuickSearch.ahk ;; Quicksearch / shortcut tools \ No newline at end of file diff --git a/includes/Chrome.ahk b/includes/Chrome.ahk new file mode 100644 index 0000000..c9fac8e --- /dev/null +++ b/includes/Chrome.ahk @@ -0,0 +1,655 @@ +; Chrome.ahk v1.2 +; Copyright GeekDude 2018 +; https://github.com/G33kDude/Chrome.ahk + +class Chrome +{ + static DebugPort := 9222 + + /* + Escape a string in a manner suitable for command line parameters + */ + CliEscape(Param) + { + return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """" + } + + /* + Finds instances of chrome in debug mode and the ports they're running + on. If no instances are found, returns a false value. If one or more + instances are found, returns an associative array where the keys are + the ports, and the values are the full command line texts used to start + the processes. + + One example of how this may be used would be to open chrome on a + different port if an instance of chrome is already open on the port + you wanted to used. + + ``` + ; If the wanted port is taken, use the largest taken port plus one + DebugPort := 9222 + if (Chromes := Chrome.FindInstances()).HasKey(DebugPort) + DebugPort := Chromes.MaxIndex() + 1 + ChromeInst := new Chrome(ProfilePath,,,, DebugPort) + ``` + + Another use would be to scan for running instances and attach to one + instead of starting a new instance. + + ``` + if (Chromes := Chrome.FindInstances()) + ChromeInst := {"base": Chrome, "DebugPort": Chromes.MinIndex()} + else + ChromeInst := new Chrome(ProfilePath) + ``` + */ + FindInstances() + { + static Needle := "--remote-debugging-port=(\d+)" + Out := {} + for Item in ComObjGet("winmgmts:") + .ExecQuery("SELECT CommandLine FROM Win32_Process" + . " WHERE Name = 'chrome.exe'") + if RegExMatch(Item.CommandLine, Needle, Match) + Out[Match1] := Item.CommandLine + return Out.MaxIndex() ? Out : False + } + + /* + ProfilePath - Path to the user profile directory to use. Will use the standard if left blank. + URLs - The page or array of pages for Chrome to load when it opens + Flags - Additional flags for chrome when launching + ChromePath - Path to chrome.exe, will detect from start menu when left blank + DebugPort - What port should Chrome's remote debugging server run on + */ + __New(ProfilePath:="", URLs:="about:blank", Flags:="", ChromePath:="", DebugPort:="") + { + ; Verify ProfilePath + if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D")) + throw Exception("The given ProfilePath does not exist") + this.ProfilePath := ProfilePath + + ; Verify ChromePath + if (ChromePath == "") + FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath + if (ChromePath == "") + RegRead, ChromePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Pahs\chrome.exe + if !FileExist(ChromePath) + throw Exception("Chrome could not be found") + this.ChromePath := ChromePath + + ; Verify DebugPort + if (DebugPort != "") + { + if DebugPort is not integer + throw Exception("DebugPort must be a positive integer") + else if (DebugPort <= 0) + throw Exception("DebugPort must be a positive integer") + this.DebugPort := DebugPort + } + + ; Escape the URL(s) + for Index, URL in IsObject(URLs) ? URLs : [URLs] + URLString .= " " this.CliEscape(URL) + + Run, % this.CliEscape(ChromePath) + . " --remote-debugging-port=" this.DebugPort + . (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "") + . (Flags ? " " Flags : "") + . URLString + ,,, OutputVarPID + this.PID := OutputVarPID + } + + /* + End Chrome by terminating the process. + */ + Kill() + { + Process, Close, % this.PID + } + + /* + Queries chrome for a list of pages that expose a debug interface. + In addition to standard tabs, these include pages such as extension + configuration pages. + */ + GetPageList() + { + http := ComObjCreate("WinHttp.WinHttpRequest.5.1") + http.open("GET", "http://127.0.0.1:" this.DebugPort "/json") + http.send() + return this.Jxon_Load(http.responseText) + } + + /* + Returns a connection to the debug interface of a page that matches the + provided criteria. When multiple pages match the criteria, they appear + ordered by how recently the pages were opened. + + Key - The key from the page list to search for, such as "url" or "title" + Value - The value to search for in the provided key + MatchMode - What kind of search to use, such as "exact", "contains", "startswith", or "regex" + Index - If multiple pages match the given criteria, which one of them to return + fnCallback - A function to be called whenever message is received from the page + */ + GetPageBy(Key, Value, MatchMode:="exact", Index:=1, fnCallback:="") + { + Count := 0 + for n, PageData in this.GetPageList() + { + if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive + || (MatchMode = "contains" && InStr(PageData[Key], Value)) + || (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1) + || (MatchMode = "regex" && PageData[Key] ~= Value)) + && ++Count == Index) + return new this.Page(PageData.webSocketDebuggerUrl, fnCallback) + } + } + + /* + Shorthand for GetPageBy("url", Value, "startswith") + */ + GetPageByURL(Value, MatchMode:="startswith", Index:=1, fnCallback:="") + { + return this.GetPageBy("url", Value, MatchMode, Index, fnCallback) + } + + /* + Shorthand for GetPageBy("title", Value, "startswith") + */ + GetPageByTitle(Value, MatchMode:="startswith", Index:=1, fnCallback:="") + { + return this.GetPageBy("title", Value, MatchMode, Index, fnCallback) + } + + /* + Shorthand for GetPageBy("type", Type, "exact") + + The default type to search for is "page", which is the visible area of + a normal Chrome tab. + */ + GetPage(Index:=1, Type:="page", fnCallback:="") + { + return this.GetPageBy("type", Type, "exact", Index, fnCallback) + } + + /* + Connects to the debug interface of a page given its WebSocket URL. + */ + class Page + { + Connected := False + ID := 0 + Responses := [] + + /* + wsurl - The desired page's WebSocket URL + fnCallback - A function to be called whenever message is received + */ + __New(wsurl, fnCallback:="") + { + this.fnCallback := fnCallback + this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False) + + ; TODO: Throw exception on invalid objects + if IsObject(wsurl) + wsurl := wsurl.webSocketDebuggerUrl + + wsurl := StrReplace(wsurl, "localhost", "127.0.0.1") + this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this} + this.ws.__New(wsurl) + + while !this.Connected + Sleep, 50 + } + + /* + Calls the specified endpoint and provides it with the given + parameters. + + DomainAndMethod - The endpoint domain and method name for the + endpoint you would like to call. For example: + PageInst.Call("Browser.close") + PageInst.Call("Schema.getDomains") + + Params - An associative array of parameters to be provided to the + endpoint. For example: + PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value + , "landscape": Chrome.Jxon_True() ; Boolean Value + , "pageRanges: "1-5, 8, 11-13"}) ; String value + PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"}) + + WaitForResponse - Whether to block until a response is received from + Chrome, which is necessary to receive a return value, or whether + to continue on with the script without waiting for a response. + */ + Call(DomainAndMethod, Params:="", WaitForResponse:=True) + { + if !this.Connected + throw Exception("Not connected to tab") + + ; Use a temporary variable for ID in case more calls are made + ; before we receive a response. + ID := this.ID += 1 + this.ws.Send(Chrome.Jxon_Dump({"id": ID + , "params": Params ? Params : {} + , "method": DomainAndMethod})) + + if !WaitForResponse + return + + ; Wait for the response + this.responses[ID] := False + while !this.responses[ID] + Sleep, 50 + + ; Get the response, check if it's an error + response := this.responses.Delete(ID) + if (response.error) + throw Exception("Chrome indicated error in response",, Chrome.Jxon_Dump(response.error)) + + return response.result + } + + /* + Run some JavaScript on the page. For example: + + PageInst.Evaluate("alert(""I can't believe it's not IE!"");") + PageInst.Evaluate("document.getElementsByTagName('button')[0].click();") + */ + Evaluate(JS) + { + response := this.Call("Runtime.evaluate", + ( LTrim Join + { + "expression": JS, + "objectGroup": "console", + "includeCommandLineAPI": Chrome.Jxon_True(), + "silent": Chrome.Jxon_False(), + "returnByValue": Chrome.Jxon_False(), + "userGesture": Chrome.Jxon_True(), + "awaitPromise": Chrome.Jxon_False() + } + )) + + if (response.exceptionDetails) + throw Exception(response.result.description,, Chrome.Jxon_Dump(response.exceptionDetails)) + + return response.result + } + + /* + Waits for the page's readyState to match the DesiredState. + + DesiredState - The state to wait for the page's ReadyState to match + Interval - How often it should check whether the state matches + */ + WaitForLoad(DesiredState:="complete", Interval:=100) + { + while this.Evaluate("document.readyState").value != DesiredState + Sleep, Interval + } + + /* + Internal function triggered when the script receives a message on + the WebSocket connected to the page. + */ + Event(EventName, Event) + { + ; If it was called from the WebSocket adjust the class context + if this.Parent + this := this.Parent + + ; TODO: Handle Error events + if (EventName == "Open") + { + this.Connected := True + BoundKeepAlive := this.BoundKeepAlive + SetTimer, %BoundKeepAlive%, 15000 + } + else if (EventName == "Message") + { + data := Chrome.Jxon_Load(Event.data) + + ; Run the callback routine + fnCallback := this.fnCallback + if (newData := %fnCallback%(data)) + data := newData + + if this.responses.HasKey(data.ID) + this.responses[data.ID] := data + } + else if (EventName == "Close") + { + this.Disconnect() + } + else if (EventName == "Error") + { + throw Exception("Websocket Error!") + } + } + + /* + Disconnect from the page's debug interface, allowing the instance + to be garbage collected. + + This method should always be called when you are finished with a + page or else your script will leak memory. + */ + Disconnect() + { + if !this.Connected + return + + this.Connected := False + this.ws.Delete("Parent") + this.ws.Disconnect() + + BoundKeepAlive := this.BoundKeepAlive + SetTimer, %BoundKeepAlive%, Delete + this.Delete("BoundKeepAlive") + } + + class WebSocket + { + __New(WS_URL) + { + static wb + + ; Create an IE instance + Gui, +hWndhOld + Gui, New, +hWndhWnd + this.hWnd := hWnd + Gui, Add, ActiveX, vWB, Shell.Explorer + Gui, %hOld%: Default + + ; Write an appropriate document + WB.Navigate("about:") + while (WB.ReadyState < 4) + sleep, 50 + this.document := WB.document + + ; Add our handlers to the JavaScript namespace + this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this) + this.document.parentWindow.ahk_event := this._Event.Bind(this) + this.document.parentWindow.ahk_ws_url := WS_URL + + ; Add some JavaScript to the page to open a socket + Script := this.document.createElement("script") + Script.text := "ws = new WebSocket(ahk_ws_url);`n" + . "ws.onopen = function(event){ ahk_event('Open', event); };`n" + . "ws.onclose = function(event){ ahk_event('Close', event); };`n" + . "ws.onerror = function(event){ ahk_event('Error', event); };`n" + . "ws.onmessage = function(event){ ahk_event('Message', event); };" + this.document.body.appendChild(Script) + } + + ; Called by the JS in response to WS events + _Event(EventName, Event) + { + this["On" EventName](Event) + } + + ; Sends data through the WebSocket + Send(Data) + { + this.document.parentWindow.ws.send(Data) + } + + ; Closes the WebSocket connection + Close(Code:=1000, Reason:="") + { + this.document.parentWindow.ws.close(Code, Reason) + } + + ; Closes and deletes the WebSocket, removing + ; references so the class can be garbage collected + Disconnect() + { + if this.hWnd + { + this.Close() + Gui, % this.hWnd ": Destroy" + this.hWnd := False + } + } + } + } + + Jxon_Load(ByRef src, args*) + { + static q := Chr(34) + + key := "", is_key := false + stack := [ tree := [] ] + is_arr := { (tree): 1 } + next := q . "{[01234567890-tfn" + pos := 0 + while ( (ch := SubStr(src, ++pos, 1)) != "" ) + { + if InStr(" `t`n`r", ch) + continue + if !InStr(next, ch, true) + { + ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n")) + col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) + + msg := Format("{}: line {} col {} (char {})" + , (next == "") ? ["Extra data", ch := SubStr(src, pos)][1] + : (next == "'") ? "Unterminated string starting at" + : (next == "\") ? "Invalid \escape" + : (next == ":") ? "Expecting ':' delimiter" + : (next == q) ? "Expecting object key enclosed in double quotes" + : (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'" + : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" + : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" + : [ "Expecting JSON value(string, number, [true, false, null], object or array)" + , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1] + , ln, col, pos) + + throw Exception(msg, -1, ch) + } + + is_array := is_arr[obj := stack[1]] + + if i := InStr("{[", ch) + { + val := (proto := args[i]) ? new proto : {} + is_array? ObjPush(obj, val) : obj[key] := val + ObjInsertAt(stack, 1, val) + + is_arr[val] := !(is_key := ch == "{") + next := q . (is_key ? "}" : "{[]0123456789-tfn") + } + + else if InStr("}]", ch) + { + ObjRemoveAt(stack, 1) + next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" + } + + else if InStr(",:", ch) + { + is_key := (!is_array && ch == ",") + next := is_key ? q : q . "{[0123456789-tfn" + } + + else ; string | number | true | false | null + { + if (ch == q) ; string + { + i := pos + while i := InStr(src, q,, i+1) + { + val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") + static end := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(val, end) != "\") + break + } + if !i ? (pos--, next := "'") : 0 + continue + + pos := i ; update pos + + val := StrReplace(val, "\/", "/") + , val := StrReplace(val, "\" . q, q) + , val := StrReplace(val, "\b", "`b") + , val := StrReplace(val, "\f", "`f") + , val := StrReplace(val, "\n", "`n") + , val := StrReplace(val, "\r", "`r") + , val := StrReplace(val, "\t", "`t") + + i := 0 + while i := InStr(val, "\",, i+1) + { + if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0 + continue 2 + + ; \uXXXX - JSON unicode escape sequence + xxxx := Abs("0x" . SubStr(val, i+2, 4)) + if (A_IsUnicode || xxxx < 0x100) + val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6) + } + + if is_key + { + key := val, next := ":" + continue + } + } + + else ; number | true | false | null + { + val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) + + ; For numerical values, numerify integers and keep floats as is. + ; I'm not yet sure if I should numerify floats in v2.0-a ... + static number := "number", integer := "integer" + if val is %number% + { + if val is %integer% + val += 0 + } + ; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo, + ; SOMETIMES return strings due to certain optimizations. Since it + ; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a + else if (val == "true" || val == "false") + val := %value% + 0 + ; AHK_H has built-in null, can't do 'val := %value%' where value == "null" + ; as it would raise an exception in AHK_H(overriding built-in var) + else if (val == "null") + val := "" + ; any other values are invalid, continue to trigger error + else if (pos--, next := "#") + continue + + pos += i-1 + } + + is_array? ObjPush(obj, val) : obj[key] := val + next := obj==tree ? "" : is_array ? ",]" : ",}" + } + } + + return tree[1] + } + + Jxon_Dump(obj, indent:="", lvl:=1) + { + static q := Chr(34) + + if IsObject(obj) + { + static Type := Func("Type") + if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "") + throw Exception("Object type not supported.", -1, Format("", &obj)) + + prefix := SubStr(A_ThisFunc, 1, InStr(A_ThisFunc, ".",, 0)) + fn_t := prefix "Jxon_True", obj_t := this ? %fn_t%(this) : %fn_t%() + fn_f := prefix "Jxon_False", obj_f := this ? %fn_f%(this) : %fn_f%() + + if (&obj == &obj_t) + return "true" + else if (&obj == &obj_f) + return "false" + + is_array := 0 + for k in obj + is_array := k == A_Index + until !is_array + + static integer := "integer" + if indent is %integer% + { + if (indent < 0) + throw Exception("Indent parameter must be a postive integer.", -1, indent) + spaces := indent, indent := "" + Loop % spaces + indent .= " " + } + indt := "" + Loop, % indent ? lvl : 0 + indt .= indent + + this_fn := this ? Func(A_ThisFunc).Bind(this) : A_ThisFunc + lvl += 1, out := "" ; Make #Warn happy + for k, v in obj + { + if IsObject(k) || (k == "") + throw Exception("Invalid object key.", -1, k ? Format("", &obj) : "") + + if !is_array + out .= ( ObjGetCapacity([k], 1) ? %this_fn%(k) : q . k . q ) ;// key + . ( indent ? ": " : ":" ) ; token + padding + out .= %this_fn%(v, indent, lvl) ; value + . ( indent ? ",`n" . indt : "," ) ; token + indent + } + + if (out != "") + { + out := Trim(out, ",`n" . indent) + if (indent != "") + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) + } + + return is_array ? "[" . out . "]" : "{" . out . "}" + } + + ; Number + else if (ObjGetCapacity([obj], 1) == "") + return obj + + ; String (null -> not supported by AHK) + if (obj != "") + { + obj := StrReplace(obj, "\", "\\") + , obj := StrReplace(obj, "/", "\/") + , obj := StrReplace(obj, q, "\" . q) + , obj := StrReplace(obj, "`b", "\b") + , obj := StrReplace(obj, "`f", "\f") + , obj := StrReplace(obj, "`n", "\n") + , obj := StrReplace(obj, "`r", "\r") + , obj := StrReplace(obj, "`t", "\t") + + static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]" + while RegExMatch(obj, needle, m) + obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0]))) + } + + return q . obj . q + } + + Jxon_True() + { + static obj := {} + return obj + } + + Jxon_False() + { + static obj := {} + return obj + } +} \ No newline at end of file diff --git a/includes/ChromeController.ahk b/includes/ChromeController.ahk new file mode 100644 index 0000000..c071d79 --- /dev/null +++ b/includes/ChromeController.ahk @@ -0,0 +1,23 @@ +;; Relies on chrome.ahk library, get it from https://github.com/G33kDude/Chrome.ahk, put it in a Chrome subfolder +#Include includes\Chrome.ahk + + + +;; A function to get the patient demographics of the currently loaded patient in AskMyGP in Chrome +;; Supply patient_name and patient_nhs empty variables when called - these will be populated +;; Chrome must be running in debug mode, do this by adding ' --remote-debugging-port=9222' to the shortcut +;; You must restart chrome completely (kill task or restart pc) to relaunch it in debug +GetPatientDetails(ByRef patient_name, ByRef patient_nhs) { + + page := Chrome.GetPageByURL("https://nhs.askmygp.uk", "startswith") + ;; this happens if we cannot find the tab + if !IsObject(page) { + MsgBox, Couldn't attach to AskMyGP Tab + ExitApp, 1 + } + + patient_name_parts := StrSplit(page.Evaluate("document.querySelector('.patient-banner').children[0].children[0].textContent").value, ",", , 2) + patient_name := patient_name_parts[2] " " patient_name_parts[1] + patient_nhs := SubStr(page.Evaluate("document.querySelector('.patient-banner').children[0].children[3].textContent").value, 16) + return +} \ No newline at end of file diff --git a/includes/QuickSearch.ahk b/includes/QuickSearch.ahk new file mode 100644 index 0000000..fb4c827 --- /dev/null +++ b/includes/QuickSearch.ahk @@ -0,0 +1,39 @@ +;; Quicksearch / Quicklinks +;; Shortcuts: +;; - 'can': NICE Cancer by site +;; Search: +;; - 'bnf ': Searches BNF for +;; - 'bnfc ': Search BNFc for +;; +;; TODO: Add other shortcut URLs, add support for third word and URL escaping + +^\:: +InputBox, vQuery, BNF Search, , , 200, 100 + +;; Need some sort of select statemnt here, probably better to check wordcount first, if just 1 then a shortcut +if (vQuery == "can") { + Run, chrome.exe "https://www.nice.org.uk/guidance/ng12/chapter/Recommendations-organised-by-site-of-cancer" + +;; Sarches +} else { + vBNF := "https://www.medicinescomplete.com/#/search/bnf/" + vBNFC := "https://www.medicinescomplete.com/#/search/bnfc/" + search_split := StrSplit(vQuery, A_Space, 2) + search_type := search_split[1] + search_query := search_split[2] + search_url := "" + + if ( search_type = "bnf" ) { + search_url := vBNF + } else if ( search_type = "bnfc" ) { + search_url := vBNFC + } + + if ( search_url = "" ) { + MsgBox Not sure what search type: %search_type% + } else { + Run, chrome.exe %search_url%%search_query% + } + +} +return \ No newline at end of file diff --git a/includes/S1Shortcuts.ahk b/includes/S1Shortcuts.ahk new file mode 100644 index 0000000..928ab6b --- /dev/null +++ b/includes/S1Shortcuts.ahk @@ -0,0 +1,14 @@ +;; #=Win ^=Ctrl +=Shift !=Alt +;; F13-F20 are bound to macro keys on mouse + +F13:: +Send, !f ;; Alt+F to file a blood report +Return + +F14:: +Send, !o ;; Alt+O to press okay +Return + +F19:: +Send !h ;; Alt+H +Return \ No newline at end of file diff --git a/includes/S1Utils.ahk b/includes/S1Utils.ahk new file mode 100644 index 0000000..4111289 --- /dev/null +++ b/includes/S1Utils.ahk @@ -0,0 +1,88 @@ +;; Timings for sleep commands are based on what reliably works on my work machine - may need adjusting if not working well +;; TODO: a search that loads appointment sarching instead of the patient record + + +;; Searches for the NHS number in SystmOne's F10 menu +;; Uses the clipboard as this is a method used by other AMGP actions/hotkeys +SearchForPatientByNHS(ByRef patient_nhs) { + + ;; Trim and remove spaces + patient_nhs := RTrim(patient_nhs, "`r`n") + patient_nhs := RegExReplace(patient_nhs, "[^\d+]") + + ;; Checks to see if nhs_number is 10 digits, no checksum calculations currently + if(!valid_nhs_number(patient_nhs)) { + MsgBox, Invalid NHS number supplied - |%patient_nhs%| ;*[mybreak1] + return + } + + Clipboard := patient_nhs + SearchForPatientFromClipboard() + return +} + +;; Searches for clipboard contents in SystmOne's F10 menu (no NHS nuumbebr validation) +SearchForPatientFromClipboard() { + WinActivate, % "SystmOne GP" + Sleep 100 + Send {F10} + Sleep 350 + Send ^v + Sleep 50 + Send {ENTER} + Sleep 200 + Send {ENTER} + return +} + + + +;; Med3, Not fit for work, Free Text, in S1. To do this Assign med3 to F12 menu under letter M +^M:: +Send {F12} +Sleep 200 +SendRaw {m} +Sleep 1000 +Send {Space} +Send {Tab 4} +Send {Space} +Send {Tab} +return + +;; Copy email notes into record +^+E:: +ClipBoard := RegExReplace(ClipBoard, "\R+\R", "`r`n") ; Removes blank lines +Send ^w +Sleep 600 +Send {Shift down} +Send {Tab 16} +Send {Shift up} +Sleep 200 +Send {Shift down} +Send {Home} +Send {Shift up} +Send {Del} +Send {Control up} +Send E-mail +Send {Enter} +Send {Tab} +Send {Alt down}o{Alt up} +Sleep 50 +Send ^v +Send {BS} +Send {Home} +Send {PgUp 2} +Send {Shift down} +Send {End} +Send {Down 2} +Send {Shift up} +Send {Del 2} +Send {End} +Send {PgDn 2} +return + +^G:: ;; Px +Send {Space 50} +Send +{Home} +Send {Del} +return diff --git a/includes/TextExpansions.ahk b/includes/TextExpansions.ahk new file mode 100644 index 0000000..23ba82a --- /dev/null +++ b/includes/TextExpansions.ahk @@ -0,0 +1,80 @@ +;; --------------------------- +;; Text Based Shortcuts +;; --------------------------- + +;; cnr. -> Called, no answer at 9:33 AM +;; clm. -> Called, no answer at 9:34 AM. Left message on voicemail +;; msc. -> Hello, I have tried to call you but unfortunately couldn't reach you. I will try once more later on + +;; dtt. -> 2022 01 12 - +;; rdt. -> 12/01/2022 +;; eld. -> 12/01/2022 \n \n Time: 09:35 (for EOL meds) + +;; sig. -> - Signed +;; ftf. -> TCI for F2F assessment, appt given for today. + +;; atsp. -> Asked to see patient while visiting another at same residence. + +;; pmc. -> Pennygate Medical Centre, 109 Ladies Lane, Hindley, Wigan +;; pmctel. -> 01942 807 500 + +; ps. -> EPS prescription signed :) + + +;; Replaces the letters 'cnr' with: Called, no reply +::cnr.:: +FormatTime, CurrentDateTime,, h:mm tt +SendInput Called, no answer at %CurrentDateTime% +return + +;; Replaces the letters 'clm' with: Called, left message +::clm.:: +FormatTime, CurrentDateTime,, h:mm tt +SendInput Called, no answer at %CurrentDateTime%. Left message on voicemail +return + +;; Replaces the letters 'missc' with: Called, left message (for patient message) +::msc.:: +FormatTime, CurrentDateTime,, h:mm tt +SendInput Hello, I have tried to call you but unfortunately couldn't reach you. I will try once more later on +return + +::dtt.:: +FormatTime, CurrentDate,, yyyy MM dd '- ' +SendInput %CurrentDate% +return + +::rdt.:: +FormatTime, CurrentDate,, dd/MM/yyyy +SendInput %CurrentDate% +return + +::sig.:: +SendInput {Space}- Signed +return + +::eld.:: +FormatTime, CurrentDate,, dd/MM/yyyy +FormatTime, CurrentTime,, HH:mm +SendInput %CurrentDate%{Enter}{Enter}Time: %CurrentTime% +return + +::ftf.:: +SendInput TCI for F2F assessment, appt given for today. +return + +::atsp.:: +SendInput Asked to see patient while visiting another at same residence. +return + +::pmc.:: +SendInput Pennygate Medical Centre, 109 Ladies Lane, Hindley, Wigan +return + +::pmctel.:: +SendInput 01942 807 500 +return + +::ps.:: +SendInput EPS prescription signed :) +return diff --git a/libraries/helper_library.ahk b/libraries/helper_library.ahk new file mode 100644 index 0000000..88a69bb --- /dev/null +++ b/libraries/helper_library.ahk @@ -0,0 +1,33 @@ +integer_of_length(num, num_len) { + num := RegExReplace(num, "[^\d+]") + return RegExMatch(num, "^\d{" . num_len . "}$") +} + +;; Checks valid 10 digit number supplied (doesnt do checksums) +valid_nhs_number(patient_nhs) { + result := integer_of_length(patient_nhs, 10) + return result +} + +test_valid_nhs_number() { + test1 := valid_nhs_number(1234567890) ;; pass + test2 := valid_nhs_number("123 456 7890") ;; pass + test3 := valid_nhs_number("123456789") ;; fail + + if (test1 and test2 and not test3 ) { + MsgBox,,"Test","Test Passed: valid_nhs_number" + } else { + MsgBox,,"Test","Test Failed: valid_nhs_number" + } +} + +; Returns bool based on presence of 'match' string in window title +check_in_win_title(match) { + WinGetTitle, Title, A + If InStr(Title, match) + { + return true + } else { + return false + } +} \ No newline at end of file