Biko's House of Horrors

Hotstrings

AutoHotkey, the scripting language built around SetWindowsHookEx, allows defining "hotstrings". A hotstring is a string that, when typed anywhere, triggers a certain action. For example, you can define:

::hello::привет

Which will replace the string "hello" with the string "привет", after typing the former followed by an "ending character" (e.g. space or enter).

You can also make it so that the ending character is not required:

:*:hello::привет

Which will trigger the replacement immediately after you type "hello".

And you can make the hotstring be recognized anywhere:

:?:hello::привет

Which will, for instance, replace "123hello" with "123привет". Without the ?, a hotstring will not be recognized in the middle of words.

And, these options can be combined:

:*?:hello::привет

Finally, a hotstring can be defined as calling a function:

:*?:hello::{
    MsgBox "test"
}

Which will show a message box when "hello" is typed, and delete the typed hotstring.

Now this is all fine, but what if we want to do this:

:*?:a::something
:*?:ab::something else

If we type "a", it's immediately replaced by "something". Typing "b" then has no effect. It makes a certain amount of sense: AutoHotkey can't know in advance that we want to type "ab", so when it sees the first "a" it immediately triggers the shorter hotstring.

However, looking through the documentation, we find this little snippet:

... consider the following hotstring:

:b0*?:11::
{
    SendInput "xx"
}

Since the above lacks the Z option, typing 111 (three consecutive 1's) would trigger the hotstring twice because the middle 1 is the last character of the first triggering but also the first character of the second triggering.

What this actually says, is that the b0 option does not reset the hotstring recognizer. Which means we can rewrite our hotstrings as:

:b0*?:a::{
    SendInput "{BS}something"
}
:b0*?:ab::{
    SendInput "{BS} else"
}

Now, when "a" is typed our custom function gets called. Since b0 is in effect the "a" is left alone, so we have to erase it ourselves (that's the {BS}, which sends a backspace keystroke), then send the replacement string. If a subsequent "b" is pressed, we again delete it and append the "else".

Okay, but what if we were to do this:

:b0*?:a::{
    SendInput "{BS}something"
}
:b0*?:aa::{
    SendInput "{BS} else"
}

This won't work because the second "a" press will still trigger the first hotstring. That's because hotstrings are processed in the order they are defined in the script.

So we just swap the order, right?

:b0*?:aa::{
    SendInput "{BS} else"
}
:b0*?:a::{
    SendInput "{BS}something"
}

That's better, but still not good enough. We're now running afoul of the original issue described in the documentation. If we type "aaaa" we'll get "something else else else", when what we really want1 is "something elsesomething else".

So we apply the fix from the docs — adding the Z option:

:b0*?Z:aa::{
    SendInput "{BS} else"
}
:b0*?:a::{
    SendInput "{BS}something"
}

Sometimes you have to reverse-engineer the documentation to get what you want.


  1. Presumably. ↩︎