initial commit, GP tools split into individual includes
This commit is contained in:
655
includes/Chrome.ahk
Normal file
655
includes/Chrome.ahk
Normal file
@@ -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:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
|
||||
. "content='IE=edge'><body></body>")
|
||||
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("<Object at 0x{:p}>", &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("<Object at 0x{:p}>", &obj) : "<blank>")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
23
includes/ChromeController.ahk
Normal file
23
includes/ChromeController.ahk
Normal file
@@ -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
|
||||
}
|
||||
39
includes/QuickSearch.ahk
Normal file
39
includes/QuickSearch.ahk
Normal file
@@ -0,0 +1,39 @@
|
||||
;; Quicksearch / Quicklinks
|
||||
;; Shortcuts:
|
||||
;; - 'can': NICE Cancer by site
|
||||
;; Search:
|
||||
;; - 'bnf <drug>': Searches BNF for <drug>
|
||||
;; - 'bnfc <drug>': Search BNFc for <drug>
|
||||
;;
|
||||
;; 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
|
||||
14
includes/S1Shortcuts.ahk
Normal file
14
includes/S1Shortcuts.ahk
Normal file
@@ -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
|
||||
88
includes/S1Utils.ahk
Normal file
88
includes/S1Utils.ahk
Normal file
@@ -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
|
||||
80
includes/TextExpansions.ahk
Normal file
80
includes/TextExpansions.ahk
Normal file
@@ -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 <current time>
|
||||
::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
|
||||
Reference in New Issue
Block a user