hotstring
hotstring copied to clipboard
A few bugs
For the following script, I pasted your code to the bottom and ran it. Many of the tests I created are working, but a few do not.
#NoEnv
#SingleInstance force
#Warn
; these are working
; -----------------
Hotstring("btw", "by the way") ; simple text replacement
Hotstring("ahk", "autohotkey") ; case-insensitive
Hotstring("trigger1", "replace") ; case-insensitive
Hotstring("(\d+)\/(\d+)%", "percent", 3) ; regex mode --> (2/2% -> 100) or (70/100% -> 70%)
Hotstring("i)((d|w)on)(t)", "$1'$3",3) ; DONT --> DON'T, dont --> don't, dOnt --> dOn't, WONT --> WON'T
Hotstring("toLabel", "label") ; call a label
Hotstring("i)regex_(trigger)", "replace2", 3) ; regex with capturing
Hotstring("only1", "onlyOnce") ; only works one time!
; these are NOT working
; -----------------
Hotstring("Abc", "Alphabet", 2) ; case-sensitive - (BUG: does not erase the triggering text)
Hotstring("Trigger2", "replace", 2) ; case-sensitive - (BUG: the value of $ is empty)
Hotstring("i)colou?rs","$0 $0 everywhere!", 3) ; Regex, Case insensitive - (BUG: the $0 is not replaced, it uses the exact string)
Hotstring("(B|b)i(\d+)(\r\n|\n|\r)", "hsBackInX") ; back in X minutes (does not trigger at all - but is valid regex for "bi10{cr}" or "Bi5{crlf}" or "bi20{lf}")
return
label:
; $ == "toLabel"
MsgBox % "In the 'label' code... - triggered by [" . $ . "]"
return
percent:
; now $ is a match object.
sendInput, % Round(($.Value(1)/$.Value(2))*100) . "%"
return
replace($) {
; $ == 'trigger'
MsgBox % "'" . $ . "' was entered!"
}
replace2($) {
;$ is a match Object
Msgbox % $.Value(0) . " was entered."
Msgbox % $.Value(1) . " == 'trigger'"
}
onlyOnce($) {
MsgBox % "This will only be shown once..."
Hotstring("only1", "")
}
hsBackInX($) {
; global $
; addHotString()
;sendText($.1 . "ack in " . $.2 . " minutes...")
msgBox % "Back in " . $.Value(2) . "minutes..."
}
I would like to add that this library is truly great. I needed the ability to create hotstrings from a file, and this gives me the ability to simulate some of the features of defining a hotstring directly within AHK. It also let me workaround the limitations of the (very old and buggy) Regex Dynamic HotString (http://www.autohotkey.com/board/topic/114764-regex-dynamic-hotstrings/).
So, a HUGE thank you for creating and sharing this.
I have fixed the bugs, and did a little code cleanup. I hope someone finds this useful.
#NoEnv
#SingleInstance force
#Warn
;simple text
Hotstring("btw1", "by the way (case-insensitive)", 1) ; simple text replacement (case-insensitive) type: BTw1
Hotstring("Btw2", "by the way (case-sensitive)", 2) ; simple text replacement (case-sensitive) type: Btw2
Hotstring("bTw3", "by the way (case-sensitive regex)", 3) ; simple text replacement (regex case-sensitive) type: bTw3
Hotstring("i)btw4", "by the way (case-insensitive regex)", 3) ; simple text replacement (regex case-insensitive) type: btW4
Hotstring("btw5", "You typed: '$0'", 3) ; simple text replacement (regex case-insensitive with back-references) type: btw5
Hotstring("btw6", " is a TLA for 'by the way' (no clearing, case-insensitive)", 1, 0) ; no replacement (case-insensitive) type: BTW6
Hotstring("btW7", " is a TLA for 'by the way' (no clearing, case-sensitive)", 2, 0) ; no replacement (case-insensitive) type: btW7
Hotstring("bTw8", " for... (no clearing, case-sensitive regex)", 3, 0) ; no replacement (regex case-sensitive) type: bTw8
Hotstring("i)btw9", " by the way (case-insensitive regex)", 3, 0) ; no replacement (regex case-insensitive) type: btW9
Hotstring("btwA", ", here is your back-ref: '$0' (case-sensitive regex)", 3, 0) ; no replacement (regex case-sensitive with back-references) type: btwA
Hotstring("i)btwB", ", here is your back-ref: '$0' (case-insensitive regex)", 3, 0) ; no replacement (regex case-insensitive with back-references) type: BtWb
Hotstring("i)colou?rs","$0 $0 everywhere!", 3) ; Regex, Case insensitive
Hotstring("Abc", "Alphabet", 2) ; case-sensitive
Hotstring("i)((d|w)on)(t)", "$1'$3", 3) ; regex, text, replace, back-references
;label
Hotstring("toLabel", "label", 1) ; call a label
;label with back references
Hotstring("(\d+)\/(\d+)%", "percent", 3) ; regex mode --> (5/20% -> 25%) or (70/100% -> 70%)
;function
Hotstring("trigger1", "replace") ; case-insensitive
Hotstring("Trigger2", "replace", 2) ; case-sensitive
Hotstring("i)trigger3", "replace", 3) ; case-insensitive regex
;function with back references
Hotstring("i)regex_(trigger)", "replace2", 3) ; regex with capturing
; custom regex
Hotstring("(B|b)i(\d+)(\r\n|\n|\r)", "hsBackInX", 3) ; back in X minutes - valid "bi10{cr}" or "Bi5{crlf}" or "bi20{lf}"
;only run once
Hotstring("only1", "onlyOnce") ; only works one time!
;all done, wait for input
return
label:
; $ == "toLabel"
MsgBox % "In the 'label' code... - triggered by [" . $ . "]"
return
percent:
; now $ is a match object.
sendInput, % Round(($.Value(1)/$.Value(2))*100) . "%"
return
replace($) {
; $ == 'trigger'
MsgBox % "'" . $ . "' was entered!"
}
replace2($) {
;$ is a match Object
Msgbox % $.Value(0) . " was entered."
Msgbox % $.Value(1) . " == 'trigger'"
}
onlyOnce($) {
MsgBox % "This will only be shown once..."
Hotstring("only1", "")
}
hsBackInX($) {
MsgBox % "Back in " . $.Value(2) . " minutes..."
}
Hotstring(trigger, label, mode:=1, clearTrigger:=1, cond:= "") {
global $
static keysBound := false
static hotkeyPrefix := "~$"
static hotstrings := {}
static typed := ""
static keys := {
(LTrim Join
symbols: "!""#$%&'()*+,-./:;<=>?@[\]^_``{|}~",
num: "0123456789",
alpha: "abcdefghijklmnopqrstuvwxyz",
other: "BS,Return,Tab,Space",
breakKeys: "Left,Right,Up,Down,Home,End,RButton,LButton,LControl,RControl,LAlt,RAlt,AppsKey,Lwin,Rwin,WheelDown,WheelUp,f1,f2,f3,f4,f5,f6,f7,f8,f9,f6,f7,f9,f10,f11,f12",
numpad: "Numpad0,Numpad1,Numpad2,Numpad3,Numpad4,Numpad5,Numpad6,Numpad7,Numpad8,Numpad9,NumpadDot,NumpadDiv,NumpadMult,NumpadAdd,NumpadSub,NumpadEnter"
)}
static effect := {
(LTrim Join
Return: "`n",
Tab: A_Tab,
Space: A_Space,
Enter: "`n",
Dot: ".",
Div: "/",
Mult: "*",
Add: "+",
Sub: "-"
)}
if (!keysBound) {
;Binds the keys to watch for triggers
for k, v in ["symbols", "num", "alpha"]
{
;alphanumeric/symbols
v := keys[v]
Loop, Parse, v
{
Hotkey, %hotkeyPrefix%%A_LoopField%, __hotstring
}
}
v := keys.alpha
Loop, Parse, v
{
Hotkey, %hotkeyPrefix%+%A_Loopfield%, __hotstring
}
for k, v in ["other", "breakKeys", "numpad"]
{
;comma separated values
v := keys[v]
Loop, Parse, v, `,
{
Hotkey, %hotkeyPrefix%%A_LoopField%, __hotstring
}
}
;keysBound is a static variable, so now the keys won't be bound twice
keysBound := true
}
if (mode == "CALLBACK") {
;Callback for the hotkeys
Hotkey := SubStr(A_ThisHotkey, 3)
if (StrLen(Hotkey) == 2 && Substr(Hotkey, 1, 1) == "+" && Instr(keys.alpha, Substr(Hotkey, 2,1))) {
Hotkey := Substr(Hotkey, 2)
if (!GetKeyState("Capslock", "T")) {
StringUpper, Hotkey, Hotkey
}
}
shiftState := GetKeyState("Shift", "P")
uppercase := GetKeyState("Capslock", "T") ? !shiftState : shiftState
;if capslock is down, shift's function is reversed.(ie pressing shift and a key while capslock is on will provide the lowercase key)
if (uppercase && Instr(keys.alpha, Hotkey)) {
StringUpper, Hotkey, Hotkey
}
if (Instr("," . keys.breakKeys . ",", "," . Hotkey . ",")) {
typed := ""
return
}
else if Hotkey in Return,Tab,Space
{
typed .= effect[Hotkey]
}
else if (Hotkey == "BS") {
;trim typed var if Backspace was pressed
StringTrimRight, typed, typed, 1
return
}
else if (RegExMatch(Hotkey, "Numpad(.+?)", numKey)) {
if (numkey1 ~= "\d") {
typed .= numkey1
}
else {
typed .= effect[numKey1]
}
}
else {
typed .= Hotkey
}
matched := false
for k, v in hotstrings
{
matchRegex := (v.mode == 1 ? "Oi)" : "") . (v.mode == 3 ? RegExReplace(v.trigger, "\$$", "") : "\Q" . v.trigger . "\E") . "$"
if (v.mode == 3) {
if (matchRegex ~= "^[^\s\)\(\\]+?\)") {
matchRegex := "O" . matchRegex
}
else {
matchRegex := "O)" . matchRegex
}
}
if (RegExMatch(typed, matchRegex, local$)) {
matched := true
if (v.cond != "" && IsFunc(v.cond)) {
;if hotstring has a condition function
A_LoopCond := Func(v.cond)
if (A_LoopCond.MinParams >= 1) {
;if the function has atleast 1 parameters
A_LoopRetVal := A_LoopCond.(v.mode == 3 ? local$ : local$.Value(0))
}
else {
A_LoopRetVal := A_LoopCond.()
}
if (!A_LoopRetVal) {
;if the function returns a non-true value
matched := false
continue
}
}
if (v.mode == 2) {
returnValue := (local$ == "" ? local$.Value(0) : local$)
}
else {
returnValue := (local$.Count > 0 ? local$ : local$.Value(0))
}
if (v.clearTrigger) {
;delete the trigger
triggerStr := (v.mode == 3 && local$.Count > 0 ? local$.Value(0) : returnValue)
SendInput % "{BS " . StrLen(triggerStr) . "}"
}
if (IsLabel(v.label)) {
$ := returnValue
gosub, % v.label
}
else if (IsFunc(v.label)) {
callbackFunc := Func(v.label)
if (callbackFunc.MinParams >= 1) {
callbackFunc.(returnValue)
}
else {
callbackFunc.()
}
}
else {
toSend := v.label
;working out the back-references
if (local$.Count() == 0) {
StringReplace, toSend, toSend, % "$0", % local$.Value(0), All
}
Loop, % local$.Count()
{
StringReplace, toSend,toSend,% "$" . A_Index,% local$.Value(A_index),All
}
toSend := RegExReplace(toSend, "([!#\+\^\{\}])", "{$1}") ;Escape modifiers
SendInput, %toSend%
}
}
}
if (matched) {
typed := ""
}
else if (StrLen(typed) > 350) {
StringTrimLeft, typed, typed, 200
}
}
else {
if (hotstrings.HasKey(trigger) && label == "") {
;removing a hotstring
hotstrings.remove(trigger)
}
else {
;add to hotstrings object
hotstrings[trigger] := {
(LTrim Join
trigger: trigger,
label: label,
mode: mode,
clearTrigger: clearTrigger,
cond: cond
)}
}
}
return
__hotstring:
;this label is triggered every time a key is pressed
Hotstring("", "", "CALLBACK")
return
}
You know . . . you could just do a pull request and I'll accept it. Or if you're up to it, I can always transfer the ownership of this repo. I'm not maintaining it anymore as I have switched over to ubuntu and dont really use autohotkey.
@mviens , you tried to fix Hotstring("Trigger2", "replace", 2)
by doing this:
if (v.mode == 2) {
returnValue := (local$ == "" ? local$.Value(0) : local$)
}
else {
returnValue := (local$.Count > 0 ? local$ : local$.Value(0))
}
This is incorrect. It changes the spec that is laid out in the readme doc. The readme says that if mode=3 for regex mode, then the return value is a match object. You've changed it so that it only returns the match object if there is no subpatterns.
Suppose I want to get the length of the match. Your solution fails for this example, because in this regex I'm not capturing the subpattern:
Hotstring("(?:reg|regex)label", "label2", 3) ; type either "reglabel" or "regexlabel"
label2:
; $ == "toLabel"
MsgBox % "trigger=" $.Value(0) "`nLength of trigger=" $.Len()
return
I guess the reason you changed it like you did, was so that you could get this example to work:
Hotstring("i)trigger3", "replace", 3)
replace($) {
; $ == 'trigger'
MsgBox % "'" . $ . "' was entered!"
}
However, this is not supposed to work according to that spec. With mode=3, the spec says you get a match object in $
, so you should be using $.value(0)
within the func
Now if you don't like the spec, and think yours is better and more convenient for the end user, thats fine, but thats another discussion. But it would still fail for the example I showed above where a user would want to use the .Len()
method from the match object
I believe the proper fix is what I proposed in PR #9