Buffered hotstrings

Post your working scripts, libraries and tools.
Descolada
Posts: 1431
Joined: 23 Dec 2021, 02:30

Buffered hotstrings

05 Nov 2023, 03:32

Introduction

SendInput is the default SendMode for hotstrings and Send because of it's alleged reliability: it sends the keystrokes in a manner where user and computer-generated keystrokes can't be interspersed with it. This means that if SendInput is sending aaaaaaaaaaaa and I am holding down b, then the output will be aaaaaaaaaaaabb....

Unfortunately, if another script or program has a keyboard hook installed meaning it's listening for keystrokes, this reliability breaks down and the keys may become interspersed: aaabaaaabaaaaabb....
Additionally, some programs can't handle the rapid input of SendInput and some keys get lost. In that case we can use SendEvent or SendPlay instead, but during those Send actions the keystrokes can easily get interspersed either during the backspacing of the hotstring or during the send of the hotstring.

If I have a hotstring :SE K40:hs::hotstring, then after typing hs AHK sends 3 backspaces (to delete the trigger and the trigger key) and then sends hotstring using SendEvent (SE) with keydelay 40ms (K40). Now suppose I type hs is good without waiting for the hotstring to be expanded completely. The result might be hotstrings good if the first character i I typed gets inserted after AHK has sent 2 backspaces of the 3. Or it might be hotistrinsg
good
if I type during the sending of the word "hotstring".

Buffered hotstrings

This library attempts to buffer all user keystrokes during the Send command using an InputHook and releases the keystrokes after the Send is complete. This means that no keystrokes should get interspersed similarly to SendInput.
Achieving this requires some syntax changes to all your hotstrings:
  1. Add the directives #MaxThreadsPerHotkey 10 (10 can be a higher number as well) and #Hotstring XB0O to make all hotstrings execute a function, disable the automatic backspacing, and omit the end character.
  2. Change all instances of ::hs::hotstring to ::hs::_HS("hotstring). The _HS function will do all the backspacing and replacement sending. See the code below and read the comments to get further information.
  3. Note that global changes via the #Hotstring options directive need to be done using _HS(, "options") instead, because current global hotstring options can't be read (as far as I know).
Examples & code

Code: Select all

#requires AutoHotkey v2.0.14
; #MaxThreadsPerHotkey needs to be higher than 1, otherwise some hotstrings might get lost 
; if their activation strings were buffered.
#MaxThreadsPerHotkey 10
; Enable X (execution), B0 (no backspacing) and O (omit end-character) for all hotstrings, which are necessary for this library to work.
; Z option resets the hotstring recognizer after each replacement, as AHK does with auto-replace hotstrings
#Hotstring ZXB0O

; For demonstration purposes lets use SendEvent as the default hotstring mode.
; `#Hotstring SE` won't have the desired effect, because we are not using regular hotstrings.
_HS(, "SE")

; Regular hotstrings only need to be wrapped with _HS. This uses SendEvent with default delay 0.
::fwi::_HS("for your information")
; Regular hotstring arguments can be used; this sets the keydelay to 40ms (this works since we are using SendMode Event)
:K40:afaik::_HS("as far as I know")
; Other hotstring arguments can be used as well such as Text
:T:omg::_HS("oh my god{enter}")
; Backspacing can be limited to n backspaces with Bn
:*:because of it's::_HS("s", "B2")
; ... however that's usually not necessary, because unlike the default implementation, this one backspaces 
; only the non-matching end of the trigger string
:*:thats::_HS("that's")
; To use regular hotstrings without _HS, reverse the global changes locally (X0 disables execute, B enables backspacing, O0 reverts omitting endchar)
:X0B:btw::by the way

/**
 * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't
 * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no
 * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0`
 * Note that mouse clicks *will* interrupt sending keystrokes.
 * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options
 * will be modified according to the provided opts. 
 * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 
 * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 
 * NOT be used, correct is `_HS("hotstring", "B0")`). 
 * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing
 * deletes only the non-matching end of the trigger string (compared to the replacement string).
 * Use the `BF` option to delete the whole trigger string. 
 * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 
 * of the trigger string.
 * 
 * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring`
 * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring
 *  local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect.
 * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`.
 * 
 * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided.
 * SendMode can only be changed with this (`#Hotstring SE` has no effect).
 * @param sendFunc Optional: this can be used to define a default custom send function (if replacement
 * is left empty), or temporarily use a custom function. This could, for example, be used to send
 * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 
 * ending character is still done with the normal Send function.
 * @returns {void} 
 */
_HS(replacement?, opts?, sendFunc?) {
    static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0
        , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true
        , __Init := HotstringRecognizer.Start()
    ; Save global variables ASAP to avoid these being modified if _HS is interrupted
    local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode
        , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1)
        , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar))

    ; Only options without replacement text changes the global/default options
    if !IsSet(replacement) {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts) {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
                if o = "S" {
                    ; SendMode is reset if no SendMode is specifically provided
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                } else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                } else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B" {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                } else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    ; Musn't use Critical here, otherwise InputBuffer callbacks won't work
    ; Start capturing input for the rare case where keys are sent during options parsing
    HSInputBuffer.Start()

    TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform
    SendMode DefaultSendMode
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay , DefaultKeyDelay, "Play"
    else
        SetKeyDelay DefaultKeyDelay

    ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 
    ; be changed with local hotstring options
    if IsSet(opts) && InStr(opts, "B")
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
    ; Load local hotstring options, but don't check for backspacing
    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
            if o = "S" {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            } else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay , KeyDelay, "Play"
                else
                    SetKeyDelay KeyDelay
                continue
            } else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstringLetters := RegexReplace(ThisHotstring, "\P{L}"), 1, 1), 'Locale') {
        if IsUpper(SubStr(ThisHotstringLetters, 2), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    ; If backspacing is enabled, get the activation string length using Unicode character length 
    ; since graphemes need one backspace to be deleted but regular StrLen would report more than one
    if BS {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_"))

        if BS = 0xFFFFFFF0 {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1})
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        } else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }
    ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed
    ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer
    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs
            Send("{Raw}" EndChar)
    }
    ; Reset the recognizer, so the next step will be captured by it
    HotstringRecognizer.Reset()
    ; Release the buffer, but restore Send settings *after* it (since it also uses Send)
    HSInputBuffer.Stop()
    if InStr(A_SendMode, "Play")
        SetKeyDelay , PrevKeyDurationPlay, "Play"
    else
        SetKeyDelay PrevKeyDelay
    SendMode PrevSendMode

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
}

/**
 * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically
 * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 
 * 
 * Properties:
 * HotstringRecognizer.Content  => the current content of the recognizer
 * HotstringRecognizer.Length   => length of the content string
 * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not
 * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured
 * HotstringRecognizer.ResetKeys    => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior)
 * HotstringRecognizer.OnChange     => can be set to a callback function that is called when the recognizer content changes.
 *      The callback receives two arguments: Callback(OldContent, NewContent)
 * 
 * Methods:
 * HotstringRecognizer.Start()  => starts capturing hotstring content
 * HotstringRecognizer.Stop()   => stops capturing
 * HotstringRecognizer.Reset()  => clears the content and resets the internal foreground window
 * 
 */
class HotstringRecognizer {
    static Content := "", Length := 0, IsActive := 0, OnChange := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New() {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)})
        ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set:((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook), 
            get:((hook, *) => hook.MinSendLevel).Bind(this.__Hook)})
        HotstringRecognizer.DefineProp("ResetKeys", 
            {set:((this, dummy, value, *) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 
            get:((this, *) => this.__ResetKeys).Bind(this)})
    }

    static Start() {
        this.Reset()
        if !this.HasProp("__HotIfIsActive") {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    static Reset(ih:=0, vk:=0, *) => (vk = 8 ? this.__SetContent(SubStr(this.Content, 1, -1)) : this.__SetContent(""), this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr"))

    static __AddChar(ih, char) {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.__SetContent("")  
        this.__SetContent(this.Content char), this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*) {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) {
        switch arg1, 0 {
            case "MouseReset":
                if IsSet(arg2) {
                    HotIf(this.__HotIfIsActive)
                    if arg2 {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))  
                    } else {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")  
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
    static __SetContent(Value) {
        if this.OnChange && HasMethod(this.OnChange) && this.Content !== Value
            SetTimer(this.OnChange.Bind(this.Content, Value), -1)
        this.Content := Value
    }
}

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
        , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1

        ; Restore the state of any modifier keys before input buffering was started
        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send modifierList

        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send("{Blind}" key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}
Updates:
01.12.23: Added Bn option to _HS: B0 disables backspacing, Bn backspaces n characters, B-n leaves n characters of the trigger word, B deletes the whole trigger word. Fixed a bug where using Text/Raw mode didn't send the end character.
07.02.24: Added Z option to #Hotstring, to cause the hotstring recognizer to reset after triggering _HS.
12.02.24: Added a way to define a custom send function (eg Clip).
18.02.24: Fixed * option backspacing too much. Improved options parsing logic. Improved backspacing logic: now the entire trigger string won't be deleted if the beginning of the replacement matches the beginning of the trigger.
30.03.24. Implemented case conformity by introducing the HotstringRecognizer class, which tries to recreate AHKs internal hotstring recognizer mechanism (is not 100% the same).
07.06.24. Fixed issue where the end-character not being suppressed could cause problems (eg Tab moving focus from the target control) by adding O option to global #Hotstring directive. Requires at least AutoHotkey v2.0.14, which fixed a bug that allows this modification to be used.
21.07.24. Improved comments.
Last edited by Descolada on 21 Jul 2024, 13:49, edited 11 times in total.
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

05 Nov 2023, 13:19

Kudos and thanks for sharing!

I've been dealing with this same frustration, but didn't know how to fix it. You can see my post about it here: viewtopic.php?f=82&t=122332

I did the same experiment with your tool, and it does seem to work. Using this hotstring.

Code: Select all

:X0B*:zx::llllllllllllllllllllllllllllllllllllllllllllllllllllllllll
and then "strumming" zxcv with my left hand, I get this:

Code: Select all

lllllllllllcllllllllllllllllllllllvlllllllllllllllllllllllll
llllllllllllllclllllllllllllllllllllllllllllllllvlllllllllll
lllllllllllllllllllllllllllcllllllllllllllllllllllllllvlllll
lllllllllllllllllllclllllllllllllllllllllllllllllllllllllllv
But using this one:

Code: Select all

:*:zx::_HS("llllllllllllllllllllllllllllllllllllllllllllllllllllllllll")
I get this:

Code: Select all

llllllllllllllllllllllllllllllllllllllllllllllllllllllllllcv
llllllllllllllllllllllllllllllllllllllllllllllllllllllllllcv
llllllllllllllllllllllllllllllllllllllllllllllllllllllllllcv
llllllllllllllllllllllllllllllllllllllllllllllllllllllllllcfv
Very nice! :thumbup:

Next day edit: @Descolada
I don't know if you are interested in "feature ideas," but I have a couple for you. Check out this post viewtopic.php?f=83&t=122531
The topic is about changing a hotstring so that only the right-most needed characters are actually changed. For example this

Code: Select all

:*:because of it's::because of its
gets changed to this

Code: Select all

:B0*:because of it's::{BS 2}s 
Maybe this could be added as an option (i.e. the second parameter options) (?)

Setting up hotstrings like this also has the benefit of "case-conformation." If the auto-corrected word is at the beginning of a sentence, then you (usually) don't have to worry about making the replacement start with a capital, because the left-most characters don't get removed.

EDIT a couple of days later: Upon studying your code, I realized the "modularity" of it. The sweet buffering goodness happens in the bottom part. I was able to use your function as a model, and integrate the InputBuffer class in my own function: viewtopic.php?f=83&t=120220&p=543210#p542691 I'll update it soon.
name := "ste(phen|ve) kunkel"
Descolada
Posts: 1431
Joined: 23 Dec 2021, 02:30

Re: Buffered hotstrings

01 Dec 2023, 13:52

@kunkel321, I only saw your first reply without the edits, which is why I'm answering just now :)
If I understood correctly then you propose that instead of :B0*:because of it's::{BS 2}s something like :B2*:because of it's::s would be possible. Unfortunately that can't be implemented because AHK throws an error with that syntax, thus the _HS function also can't interpret the option. Additionally, the native B option shouldn't be changed from B0 because otherwise the backspacing can't be buffered by the InputBuffer.

The closest I could come is :*:because of it's::_HS("s", "B2"), though it's not much shorter than :*:because of it's::_HS("{BS 2}s", "B0"). I've updated the main post with that feature.
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

01 Dec 2023, 14:34

Hey very cool! Thanks for adding the feature!
EDIT: @Descolada Hey have you experimented with InputBuffer and the issue in AutoHokey (and probably other languages) about needing a "post-paste delay?" Each time SendInput '^v' is used in a loop, you have to put a sleep, to give the clipboard time to catch up. Or maybe it's not the clipboard per se, but whatever happens inside Windows when the clipboard is actually entered into the receiving edit field.

I'm referring to scenarios like the below code. With the below example, I have sleep 10, which works fine in Notepad. That is, it generates "one two three four five." Without the sleep, the entire 5 loops happen in just a couple of milliseconds, before Windows can do anything, so it generates "five."

Pasting into MS Outlook is the worst. With a 10ms delay, you are likely to get "five five five five five." For Outlook I find that about 350ms are needed for any reliability.

I did try integrating InputBuffer in the loop, but no luck. My guess is that it didn't work because InputHook() Only monitors what the user physically types, and can't monitor when back-to-back ^vs, or anything else, is sent programmatically. There might be other factors that are relevant too, IDK.

Code: Select all

#SingleInstance
#Requires AutoHotkey v2.0

list := ["one", "two", "three", "four", "five"]

!q::
{	for idx, item in list {
		A_Clipboard := ""
		A_Clipboard := item " "
		ClipWait 1 ; Wait for clipboard to contain data
		SendInput '^v' ; Send paste command
		sleep 10
	} 
}
name := "ste(phen|ve) kunkel"
Jasonosaj
Posts: 58
Joined: 02 Feb 2022, 15:02
Location: California

Re: Buffered hotstrings

06 Dec 2023, 16:54

Nice script! Below is a quick and dirty script to convert standard hotkeys to _HS syntax

Code: Select all

lines := StrSplit(clipboard, "`n")

for index, line in lines {
    ; If the line is a comment or does not contain a hotstring, keep it as is
    if (SubStr(line, 1, 1) = ";" or !RegExMatch(line, "::")) {
        continue
    }

    ; Parse the line to extract the hotstring parameters
    RegExMatch(line, "i)^(:[^:]*:)([^:]*::)([^;]*)(;.*)?$", match)

    prefix := match1
    inputString := match2
    outputString := StrReplace(match3,"`r","")
    comment := match4

    ; Handle the cases for "*", "T", and "C" in the prefix
    prefix := StrReplace(prefix, "T", "")
    prefix := StrReplace(prefix, "B0", "*") ; address :B0*:
    prefix := StrReplace(prefix, "X", "*") ; address :B0*:
    prefix := StrReplace(prefix, "?", "?") ; address :?:
    Loop 5
        prefix := StrReplace(prefix, "**", "*") ; address :C*:
    ; if (InStr(prefix, "*") || InStr(prefix, "C")) {
    ;     prefix := StrReplace(prefix, ":B0:", ":*:")  ; Replace ":B0:" with ":*:"
    ; }

    ; If the hotstring contains "SendInput", extract the number of backspaces and the text to send
    if (RegExMatch(outputString, "i){BS (\d+)}(.*)", sendInputMatch)) {
        backspaces := sendInputMatch1
        text := sendInputMatch2
        outputString := "_HS(""" . Trim(text) . """, ""B" . backspaces . """)"
    } else {
        outputString := "_HS(""" . Trim(outputString) . """)"
    }

    ; Format the new hotstring according to your specifications
    newHotstring := prefix . inputString . outputString

    ; Replace the line with the new hotstring
    lines[index] := newHotstring . (comment ? " " . comment : "")
}

; Join the processed lines back into a single string and write it back to the clipboard
clipboard := ""
for index, line in lines {
    clipboard .= line . "`n"
}
clipboard := RTrim(clipboard, "`n")  ; Remove the trailing newline

User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

28 Mar 2024, 12:12

Hi @Descolada (or other knowledgeable person),
Can you comment on this...

I have a tool that reads boilerplate text entries from the keys of an ini [section] then types and/or pastes the text. The setup includes what I've summarized in the below pseudo code...

Code: Select all

;PSEUDO CODE.
Array := [{key:"paste me",value:"I get get pasted."}
		,{key:"",value:"I get get typed out."}
		,{key:"",value:"I also get typed out."}] ; <--- Array of keyvalue pairs... just an example.. might be bad syntax. 

for Item in Array {
	if item.key has string "paste"
		paste item.value with Send '^v'
	else ; item key doesn't have 'paste' so start input hook and type it. 
	{
		static HSInputBuffer := InputBuffer() ; <--- potentially gets created many times !!!
		HSInputBuffer.Start()
		send item.value
	}
}
HSInputBuffer.Stop() ; loop (and ini section) is finished, so release any buffered text and end capture. 

InputBuffer Class code is down here... 
My Question: Is it okay to create the InputBuffer() object over and over like this, or is that bad practice?

I did it like this, because many of my boilerplate ini [section] contain only "paste me" items. I only what the buffer started when the relevant part of the If/Then code is followed.

I did a couple of test runs with my actual code, and it does work quite nicely... But is it bad to create the start the buffer multiple times like this?
name := "ste(phen|ve) kunkel"
Descolada
Posts: 1431
Joined: 23 Dec 2021, 02:30

Re: Buffered hotstrings

28 Mar 2024, 12:55

@kunkel321, if your code uses the static keyword then the variable gets initialized just once (on the first call), so it shouldn't be an issue. And even if you created it multiple times then it shouldn't cause issues, just be a bit less efficient as all the internal mechanisms get created and destroyed unnecessarily.
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

28 Mar 2024, 14:14

Descolada wrote:
28 Mar 2024, 12:55
@kunkel321, if your code uses the static keyword then the variable gets initialized just once (on the first call), so it shouldn't be an issue. And even if you created it multiple times then it shouldn't cause issues, just be a bit less efficient as all the internal mechanisms get created and destroyed unnecessarily.
Awesome -- Thank you!
Edit: Wait... It already has 'static'. So you mean like it already is -- yes?
name := "ste(phen|ve) kunkel"
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

28 Mar 2024, 16:30

@Descolada (or somebody) I discovered an interesting quirk... Not sure if it's a problem with the IB class, with AHK, with Win 10, or just something I'm doing wrong...

EDIT: I'll leave the rest of this reply for archival purposes, but Descolada explained the problem with me setup in his below reply.

Please try the below code, (types into a text field like Notepad) and see if you get the same. When I run it, after it runs, the keyboard is unresponsive. Presumably the IB class is still "hooking" the kb input. However the MButton hotkey msbBox message suggests that it is not.

Thoughts?
EDIT: Put different sample text so it's easier to follow...

Code: Select all

#SingleInstance
#Requires AutoHotkey v2+
; buffer testing
; Right click systray icon ---> exit to exit.
!+b::
{
	myArr := [{key:"paste",value:"11111111111111111111111111111111111111111111111111 "}
			,{key:"",value:"a`n"}
			,{key:"paste",value:"22222222222222222222222222222222222222222222222222 "}
			,{key:"",value:"b`n"}
			,{key:"paste",value:"33333333333333333333333333333333333333333333333333 "}
			,{key:"",value:"c`n"}
			,{key:"paste",value:"44444444444444444444444444444444444444444444444444 "}
			,{key:"",value:"d`n"}] 
			
	for Item in myArr {
		if inStr(item.key, "paste") {
			A_Clipboard := item.value
			Send '^v'
			sleep 200
		}
		else ; item key doesn't have 'paste' so start input hook and type it. 
		{
			static HSInputBuffer := InputBuffer() ; <--- potentially gets created many times !!!
			HSInputBuffer.Start()
			send item.value
		}
	}
	HSInputBuffer.Stop() ; loop (and ini section) is finished, so release any buffered text and end capture. 
}

Mbutton::msgbox 'count ' InputBuffer.ActiveCount '`nlevel ' InputBuffer.SendLevel '`nisRel ' InputBuffer.IsReleasing 

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 *")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := []

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1
        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send(key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}
Edit again: I noticed your info:
* Notes:
* * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
So it occurred to me to put this above the .Stop....

Code: Select all

	While InputBuffer.IsReleasing = 1
		sleep 50
	HSInputBuffer.Stop() ; loop (and ini section) is finished, so release any buffered text and end capture. 
That does not fix it though...

Edit again....
I guess maybe using a timeout fixes it...

Code: Select all

static HSInputBuffer := InputBuffer(,,500)
:D
Last edited by kunkel321 on 29 Mar 2024, 13:05, edited 1 time in total.
name := "ste(phen|ve) kunkel"
Descolada
Posts: 1431
Joined: 23 Dec 2021, 02:30

Re: Buffered hotstrings

29 Mar 2024, 00:48

@kunkel321, InputBuffer.Start() increases the internal reference count of active capture sessions, and Stop() decreases it. Once the reference count is 0 (meaning no other part of the code is depending on it being active any longer) the buffer is released.

If you want to buffer the whole section of Ctrl+V / Send, then the following would make more sense:

Code: Select all

	static HSInputBuffer := InputBuffer()
	HSInputBuffer.Start()
	for Item in myArr {
		if inStr(item.key, "paste") {
			A_Clipboard := item.value
			Send '^v'
			sleep 200
		}
		else ; item key doesn't have 'paste' so start input hook and type it. 
		{
			send item.value
		}
	}
    HSInputBuffer.Stop()
Note that the default SendMode is Input, so if it doesn't revert to Event then there'll be small timegaps where user input can still leak through because the keyboard hook is temporarily removed.

If you only want to buffer while using Send, then use

Code: Select all

	static HSInputBuffer := InputBuffer()
	for Item in myArr {
		if inStr(item.key, "paste") {
			A_Clipboard := item.value
			Send '^v'
			sleep 200
		}
		else ; item key doesn't have 'paste' so start input hook and type it. 
		{
			HSInputBuffer.Start()
			send item.value
			HSInputBuffer.Stop()
		}
	}
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

29 Mar 2024, 13:03

Thanks for the explanation Descolada... I guess maybe that was a no-brainer. LOL.
name := "ste(phen|ve) kunkel"
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

08 May 2024, 12:22

Here is a version of Hotstring Helper for use with the _HS() function.
Image
AutoCorrect_HS.zip
(1.27 MiB) Downloaded 75 times
The zip contains this script:
Spoiler
As well as a portable copy of AutoHotkey.exe v2.0.12 that has been renamed to match the AutoCorrect_HS.ahk file.
And a word list that is used by the Exam Pane of HH_HS.
And a homemade "HS" icon. :)

Don't complile the ahk file, or HH_HS won't be able to append new hotstrings. Most of the features of HH_HS are consistent with the version in the manual here
https://github.com/kunkel321/AutoCorrect2/blob/main/hh2%20and%20AC2%20Manual.pdf And most of the manual (though not all), is dedicated to hotstring helper. The first part will be relevant. Disregard the last part.

Special thanks to @Andymbody for updating the RegEx that allows HH_HS to parse hotstrings on-launch. (select a hotstring with your mouse, then press Win+H) Note also that the script will accept a hotstring as a commandline parameter, and parse it into HH_HS.

Regarding the code: This is Descolada's code from above, with the HH_HS code pasted right in. Near the top of his code is this:

Code: Select all

#Hotstring ZXB0
; Above items are from Descolada's original stript.
I pasted my code right below that. My point is that, if he updates his above stript(s), you can again copy and paste the relevant HH_HS code into his, if you want to.

Note also, at about line 1176 is a big "ASCI Art" thing that looks like "_HS()." This marks the end of the HH_HS code. Everything below that, is unchanged Descolada code. With the exception of .... At the start of the hotstrings section, is this:

Code: Select all

; Put this so that it deliniates where your _HS() function calls start (I.e. your autocorrect entries.)
ACitemsStartAt := A_LineNumber + 5 ; <--- Used by loop in validity function.
That needs to be put back in place too. Lastly, I moved Descolada's sample hotstrings to the very bottom.

I've recommend checking out the "user preference options" (I.e. variable assignments) near the top of the HH code and also check out the various "window specific" hotkeys, just so you know what the things are.

I'll edit this post if I think of any other important points.
name := "ste(phen|ve) kunkel"
Jasonosaj
Posts: 58
Joined: 02 Feb 2022, 15:02
Location: California

Re: Buffered hotstrings

01 Jun 2024, 23:50

@Descolada - I've run into a problem with hotstrings that use your _HS() function and the inputbuffer and hoststringrecognizer classes.

I have several acronyms that expand to their full text. In particular, when the acronym is only two letters, I notice that the script is sending extra backspace, and therefore, I am losing the space in front of the trigger, which is replaced with the replacement substring. So, the hotstring wc::_HS("workers' compensation'"), if used in the sentence, "I hate wc," I would expect to get "I hate workers' compensation." Instead, however, I get "I hateorkers' compensation" The cause of the problem seems to be when I depress "space" IMMEDIATELLY after depressing "c" and while "c" is only just starting to release. I suspect there is overlap between the {space down} and {c up}, but I cannot for the life of me figure out how to get the script to work correctly after tinkering with various send options and key delays. Any thoughts?
Descolada
Posts: 1431
Joined: 23 Dec 2021, 02:30

Re: Buffered hotstrings

02 Jun 2024, 02:02

@Jasonosaj can you perhaps share an example script that demonstrates the problem? I tried replicating it without success:

1) Script running the hotstring

Code: Select all

#requires AutoHotkey v2
#MaxThreadsPerHotkey 10
#Hotstring ZXB0

_HS(, "SE")

::wc::_HS("workers' compensation'")

/**
 * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't
 * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no
 * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0`
 * Note that mouse clicks *will* interrupt sending keystrokes.
 * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options
 * will be modified according to the provided opts. 
 * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 
 * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 
 * NOT be used, correct is `_HS("hotstring", "B0")`). 
 * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing
 * deletes only the non-matching end of the trigger string (compared to the replacement string).
 * Use the `BF` option to delete the whole trigger string. 
 * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 
 * of the trigger string.
 * 
 * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring`
 * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring
 *  local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect.
 * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`.
 * 
 * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided.
 * SendMode can only be changed with this (`#Hotstring SE` has no effect).
 * @param sendFunc Optional: this can be used to define a default custom send function (if replacement
 * is left empty), or temporarily use a custom function. This could, for example, be used to send
 * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 
 * ending character is still done with the normal Send function.
 * @returns {void} 
 */
_HS(replacement?, opts?, sendFunc?) {
    static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0
        , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true
        , __Init := HotstringRecognizer.Start()
    ; Save global variables ASAP to avoid these being modified if _HS is interrupted
    local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode
        , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1)
        , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar))

    ; Only options without replacement text changes the global/default options
    if !IsSet(replacement) {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts) {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
                if o = "S" {
                    ; SendMode is reset if no SendMode is specifically provided
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                } else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                } else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B" {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                } else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    ; Musn't use Critical here, otherwise InputBuffer callbacks won't work
    ; Start capturing input for the rare case where keys are sent during options parsing
    HSInputBuffer.Start()

    TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform
    SendMode DefaultSendMode
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay , DefaultKeyDelay, "Play"
    else
        SetKeyDelay DefaultKeyDelay

    ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 
    ; be changed with local hotstring options
    if IsSet(opts) && InStr(opts, "B")
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
    ; Load local hotstring options, but don't check for backspacing
    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
            if o = "S" {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            } else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay , KeyDelay, "Play"
                else
                    SetKeyDelay KeyDelay
                continue
            } else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstringLetters := RegexReplace(ThisHotstring, "\P{L}"), 1, 1), 'Locale') {
        if IsUpper(SubStr(ThisHotstringLetters, 2), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    ; If backspacing is enabled, get the activation string length using Unicode character length 
    ; since graphemes need one backspace to be deleted but regular StrLen would report more than one
    if BS {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_")) + !Omit

        if BS = 0xFFFFFFF0 {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1})
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        } else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }
    ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed
    ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer
    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs
            Send("{Raw}" EndChar)
    }
    ; Reset the recognizer, so the next step will be captured by it
    HotstringRecognizer.Reset()
    ; Release the buffer, but restore Send settings *after* it (since it also uses Send)
    HSInputBuffer.Stop()
    if InStr(A_SendMode, "Play")
        SetKeyDelay , PrevKeyDurationPlay, "Play"
    else
        SetKeyDelay PrevKeyDelay
    SendMode PrevSendMode

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
}

/**
 * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically
 * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 
 * 
 * Properties:
 * HotstringRecognizer.Content  => the current content of the recognizer
 * HotstringRecognizer.Length   => length of the content string
 * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not
 * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured
 * HotstringRecognizer.ResetKeys    => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior)
 * HotstringRecognizer.OnChange     => can be set to a callback function that is called when the recognizer content changes.
 *      The callback receives two arguments: Callback(OldContent, NewContent)
 * 
 * Methods:
 * HotstringRecognizer.Start()  => starts capturing hotstring content
 * HotstringRecognizer.Stop()   => stops capturing
 * HotstringRecognizer.Reset()  => clears the content and resets the internal foreground window
 * 
 */
class HotstringRecognizer {
    static Content := "", Length := 0, IsActive := 0, OnChange := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New() {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)})
        ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set:((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook), 
            get:((hook, *) => hook.MinSendLevel).Bind(this.__Hook)})
        HotstringRecognizer.DefineProp("ResetKeys", 
            {set:((this, dummy, value, *) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 
            get:((this, *) => this.__ResetKeys).Bind(this)})
    }

    static Start() {
        this.Reset()
        if !this.HasProp("__HotIfIsActive") {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    static Reset(ih:=0, vk:=0, *) => (vk = 8 ? this.__SetContent(SubStr(this.Content, 1, -1)) : this.__SetContent(""), this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr"))

    static __AddChar(ih, char) {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.__SetContent("")  
        this.__SetContent(this.Content char), this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*) {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) {
        switch arg1, 0 {
            case "MouseReset":
                if IsSet(arg2) {
                    HotIf(this.__HotIfIsActive)
                    if arg2 {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))  
                    } else {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")  
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
    static __SetContent(Value) {
        if this.OnChange && HasMethod(this.OnChange) && this.Content !== Value
            SetTimer(this.OnChange.Bind(this.Content, Value), -1)
        this.Content := Value
    }
}

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
        , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1

        ; Restore the state of any modifier keys before input buffering was started
        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send modifierList

        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send("{Blind}" key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}
2) Script triggering the hotstring in two different ways:

Code: Select all

#Requires AutoHotkey v2

F1::{
    SendLevel 1
    SendEvent "i hate wc "
}
F2::{
    SendLevel 1
    SendEvent "i hate w{c down}{space down}{c up}{space up}"
}
Jasonosaj
Posts: 58
Joined: 02 Feb 2022, 15:02
Location: California

Re: Buffered hotstrings

03 Jun 2024, 14:32

Apologies for including a ton of extraneous stuff - better you have the full script than I screw up the review by omitting something... Also, it is probably worth noting that the problem is PARTICULARLY noticeable in the Office environment (Office 365). ALSO, it's probably worth noting that I ran your version and encountered the same problem in my very limited test.

Code: Select all


#Requires AutoHotkey v2+
#SingleInstance Force
SetWorkingDir(A_ScriptDir)
SetTitleMatchMode("RegEx")

pathDefaultEditor := (FileExist("C:\Users\" . A_UserName . "\AppData\Local\Programs\Microsoft VS Code\Code.exe"))
    ? "C:\Users\" . A_UserName . "\AppData\Local\Programs\Microsoft VS Code\Code.exe"
    : "Notepad.exe"

urlGitRepo := "https://github.com/kunkel321/AutoCorrect2"

activeScriptsFolder := A_ScriptDir "\Auto Launch"

folderLib := activeScriptsFolder '\Lib'
folderGitCopy := folderLib '\GitHub'
folderMedia := activeScriptsFolder '\Media\Icons'
folderLogs := activeScriptsFolder '\Logs'

filenameThisScript := A_ScriptName
filenameUserHotstrings := "UserHotstringsFile.ahk"
filenameWordlist := 'GitHubComboList249k.txt'
filenameACLogger := "AutoCorrectsLog.ahk"
filenameACLog := "AClog.txt"
filenameMCLogger := "MClogger.ahk"
filenameMCLog := "MCLog.txt"

pathThisScript := A_ScriptFullPath
pathUserHotstrings := folderLib '\' filenameUserHotstrings
pathWordList := folderLib '\' filenameWordlist
pathACLogger := folderLogs '\' filenameACLogger
pathACLog := folderLogs '\' filenameACLog
pathMCLogger := folderLogs '\' filenameMCLogger
pathMCLog := folderLogs '\' filenameMCLog

mapKeyFolderContents := map()

mapKeyFolderContents[folderGitCopy] := []

mapKeyFolderContents[folderLib] := [
    filenameUserHotstrings,
    filenameWordlist
]

mapKeyFolderContents[folderLogs] := [
    filenameACLogger,
    filenameACLog,
    filenameMCLogger,
    filenameMCLog
]

mapKeyFolderContents[folderMedia] := []

loop files, folderGitCopy "\Icons\*.ico"
    mapKeyFolderContents[folderMedia].push(A_LoopFileName)


for key in mapKeyFolderContents
{
    if !DirExist(key) {
        DirCreate(key)
    }

    for each, item in mapKeyFolderContents[key] {
        if !FileExist(key . '\' . item) {
            try {
               FileCopy(folderGitCopy . '\' . item, key . '\' . item)
            } catch {
                try {
                    FileAppend("", key . '\' . item)
                } catch {
                    MsgBox("Failed to create " key . '\' . item)
                }
            }
        }
    }
}

hhNameForGUITitleBar            := "HotString Helper 2"

hhGUIHotkey                     := "#h"

myPilcrow                       := "¶"
myDot                           := "• "
myTab                           := "⟹ "

DefaultBoilerPlateOpts          := ""
DefaultAutoCorrectOpts          := "*"

myPrefix                        := ";"
mySuffix                        := ""

addFirstLetters                 := 5
tooSmallLen                     := 2

AutoLookupFromValidityCheck     := 0
AutoCommentFixesAndMisspells    := 1

hhGUIColor                      := "F5F5DC"
hhFontColor                     := "c003366"

myGreen                         := 'c1D7C08'
myRed                           := 'cB90012'
myBigFont                       := 's13'

HeightSizeIncrease              := 300
WidthSizeIncrease               := 400

bb                              := Gui('', 'Validity Report')

AutoEnterNewEntry               := 1

savedUpText                     := ""
keepForLog                      := ""

logIsRunning                    := 0
intervalCounter                 := 0
saveIntervalMinutes             := 5
saveIntervalMinutes             := saveIntervalMinutes * 60 * 1000
IntervalsBeforeStopping         := 2

ExamPaneOpen                    := 0
ControlPaneOpen                 := 0

targetWindow                    := ""

lastTrigger                     := ""
origTriggerTypo                 := ""
OrigReplacment                  := ""
IsMultiLine                     := 0

tArrStep                        := []
rArrStep                        := []



TraySetIcon(folderMedia . "\Psicon.ico")

acMenu := A_TrayMenu
acMenu.Delete
acMenu.SetColor("Silver")

acMenu.Add("Edit This Script", handleEditScript)
acMenu.Add("Reload Script", (*) => Reload())
acMenu.Add("List Lines Debug", (*) => ListLines())
acMenu.Add("Exit Script", (*) => ExitApp())

acMenu.SetIcon("Edit This Script", folderMedia . "\edit-Blue.ico")
acMenu.SetIcon("Reload Script", folderMedia . "\repeat-Blue.ico")
acMenu.SetIcon("List Lines Debug", folderMedia . "\ListLines-Blue.ico")
acMenu.SetIcon("Exit Script", folderMedia . "\exit-Blue.ico")

hh := Gui('', hhNameForGUITitleBar)

hh.BackColor := hhGuiColor
FontColor := (hhFontColor != "") ? "c" . hhFontColor : ""
hFactor := 0, wFactor := 0

hh.Opt("-MinimizeBox +alwaysOnTop")
hh.SetFont("s11 " . hhFontColor)

hh.AddText('y4 w30', 'Options')
hhTriggerLabel := hh.AddText('x+40 w250', 'Trigger String')
hhOptionsEdit := hh.AddEdit('cDefault yp+20 xm+2 w70 h24')
hhTriggerEdit := hh.AddEdit('cDefault x+18 w' . wFactor + 280, '')

hh.SetFont('s9')

hhReplacementLabel := hh.AddText('xm', 'Replacement')
hhBiggerButton := hh.AddButton('vSizeTog x+75 yp-5 h8 +notab', 'Make Bigger')
hhShowSymbolsButton := hh.AddButton('vSymTog x+5 h8 +notab', '+ Symbols')

hh.SetFont('s11')
hhReplacementEdit := hh.AddEdit('cDefault vReplaceString +Wrap y+1 xs h' . hFactor + 100 . ' w' . wFactor + 370, '')

hhCommentLabel := hh.AddText('xm y' . hFactor + 182, 'Comment')
hhMakeFunctionToggle := hh.AddCheckbox('vFunc, x+70 y' . hFactor + 182, 'Make Function')
hhMakeFunctionToggle.Value := 1
hhCommentEdit := hh.AddEdit('cGreen vComStr xs y' . hFactor + 200 . ' w' . wFactor + 370)

hhAppendButton := hh.AddButton('xm y' . hFactor + 234, 'Append')

hhCheckButton := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Check')

hhExamButton := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Exam')
hhSpellButton := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Spell')

hhOpenButton := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Open')
hhCancelButton := hh.AddButton('+notab x+5 y' . hFactor + 234, 'Cancel')

hh.SetFont('s10')
hhLeftTrimButton := hh.AddButton('vbutLtrim xm h50  w' . (wFactor + 182 / 6), '>>')

hh.SetFont('s14')
hhTypoLabel := hh.AddText('vTypoLabel -wrap +center cBlue x+1 w' . (wFactor + 182 * 5 / 3), hhNameForGUITitleBar)

hh.SetFont('s10')
hhRightTrimButton := hh.AddButton('vbutRtrim x+1 h50 w' . (wFactor + 182 / 6), '<<')

hh.SetFont('s11')
hhBeginRadio := hh.AddRadio('vBegRadio y+-18 x' . (wFactor + 182 / 3), '&Beginnings')
hhMidRadio := hh.AddRadio('vMidRadio x+5', '&Middles')
hhEndRadio := hh.AddRadio('vEndRadio x+5', '&Endings')

hhUndoButton := hh.AddButton('xm y+3 h26 w' . (wFactor + 182 * 2), 'Undo (+Reset)')
hhUndoButton.Enabled := false

hh.SetFont('s12')
hhMisspellsListLabel := hh.AddText('vTrigLabel center y+4 h25 xm w' . wFactor + 182, 'Misspells')
hhFixesListLabel := hh.AddText('vReplLabel center h25 x+5 w' . wFactor + 182, 'Fixes')
hhTriggerMatchesEdit := hh.AddEdit('cDefault vTrigMatches y+1 xm h' . hFactor + 300 . ' w' . wFactor + 182,)
hhReplacementMatchesEdit := hh.AddEdit('cDefault vReplMatches x+5 h' . hFactor + 300 . ' w' . wFactor + 182,)


hh.SetFont('bold s10')
hhWordlistFilenameButton := hh.AddText('vWordList center xm y+1 h14 w' . wFactor * 2 + 364, filenameWordlist)
hhSecretControlPanelButton := hh.AddText(' center cBlue ym+270 h25 xm w' . wFactor + 370, 'Secret Control Panel!')

hh.SetFont('s10')

hhACLogHandlerButton := hh.AddButton('vbutRunAcLog xm h25 w' . wFactor + 370, 'Open AutoCorrection Log')
hhMCLogHandlerButton := hh.AddButton('vbutRunMcLog xm h25 w' . wFactor + 370, 'Open Manual Correction Log')
hhCountHoststringsAndFixesButton := hh.AddButton('vbutFixRep xm h25 w' . wFactor + 370, 'Count HotStrings and Potential Fixes')


hh.OnEvent("Close", hhButtonCancelHandler)

hhAppendButton.OnEvent("Click", hhAppendHandler)
hhCheckButton.OnEvent("Click", hhCheckHandler)
hhExamButton.OnEvent("Click", hhExamHandler)
hhSpellButton.OnEvent("Click", hhSpellHandler)
hhOpenButton.OnEvent("Click", hhButtonOpenHandler)
hhCancelButton.OnEvent("Click", hhButtonCancelHandler)
hhBiggerButton.OnEvent("Click", hhSizeToggleHandler)
hhMakeFunctionToggle.OnEvent('click', hhSaveAsFunctionHandler)
hhShowSymbolsButton.OnEvent("Click", hhToggleSymbolsHandler)
hhLeftTrimButton.OnEvent('click', hhTrimHandler)
hhRightTrimButton.OnEvent('click', hhTrimHandler)
hhExamButton.OnEvent("ContextMenu", hhSubFuncExamControlHandler)
hhTriggerEdit.OnEvent('Change', hhTriggerChangedHandler)
hhReplacementEdit.OnEvent('Change', hhActivateFilterHandler)
hhBeginRadio.OnEvent('click', hhActivateFilterHandler)
hhMidRadio.OnEvent('click', hhActivateMiddleHandler)
hhEndRadio.OnEvent('click', hhActivateFilterHandler)
hhUndoButton.OnEvent('Click', hhUndoHandler)
hhWordlistFilenameButton.OnEvent('DoubleClick', hhWordlistHandler)
hhACLogHandlerButton.OnEvent("click", (*) => hhRunLoggerHandler("hhACLogHandler"))
hhMCLogHandlerButton.OnEvent("click", (*) => hhRunLoggerHandler("hhMCLogHandler"))
hhCountHoststringsAndFixesButton.OnEvent('Click', hhStringsAndFixesHandler)


hhToggleExamButtonHandler(Visibility := False)
hhToggleButtonsControlHandler(Visibility := False)

#HotIf WinActive(hhNameForGUITitleBar)

    $Enter::
    {
        If (hh['SymTog'].text = "Hide Symb")
            return
        Else if (hhReplacementEdit.Focused)
        {
            Send("{Enter}")
            Return
        }
        Else
            hhAppendHandler()
    }

    +Left::
    {
        hhTriggerEdit.Focus()
        Send("{Home}")
    }

    Esc::
    {
        hh.Hide()
        A_Clipboard := ClipboardOld
    }

    ^z::hhUndoHandler()

    ^+z::GoReStart()

    ^Up::
    ^WheelUp::
    {
        hhOptionsEdit.SetFont('s15')
        hhTriggerEdit.SetFont('s15')
        hhReplacementEdit.SetFont('s15')
    }

    ^Down::
    ^WheelDown::
    {
        hhOptionsEdit.SetFont('s11')
        hhTriggerEdit.SetFont('s11')
        hhReplacementEdit.SetFont('s11')
    }

#HotIf

Hotkey(hhGUIHotkey, hhAddNewHotstring)

hhRunLoggerHandler(buttonIdentifier)
{
    if (buttonIdentifier = "hhACLogHandler")
        Run("'" pathDefaultEditor "' '" pathACLogger "'")
    else if (buttonIdentifier = "hhMCLogHandler")
        Run("'" pathDefaultEditor "' '" pathMCLogger "'")
}

hhToggleButtonsControlHandler(Visibility := False)
{
    Global hhSecretControlPanelButton, hhACLogHandlerButton, hhMCLogHandlerButton, hhCountHoststringsAndFixesButton
    ControlCmds := [
        hhSecretControlPanelButton,
        hhACLogHandlerButton,
        hhMCLogHandlerButton,
        hhCountHoststringsAndFixesButton
    ]
    for ctrl in ControlCmds
    {
        ctrl.Visible := Visibility
    }
}

hhToggleExamButtonHandler(Visibility := False)
{
    examCmds := [
        hhLeftTrimButton,
        hhTypoLabel,
        hhRightTrimButton,
        hhBeginRadio,
        hhMidRadio,
        hhEndRadio,
        hhUndoButton,
        hhFixesListLabel,
        hhMisspellsListLabel,
        hhTriggerMatchesEdit,
        hhReplacementMatchesEdit,
        hhWordlistFilenameButton
    ]
    for ctrl in examCmds
    {
        ctrl.Visible := Visibility
    }
}

hhAddNewHotstring(*)
{
    hhTriggerLabel.SetFont(hhFontColor)

    Global ClipboardOld := ClipboardAll()

    Global triggerBeforeTrimming
    Global replacementBeforeTrimming

    Global replacementBeforeThisTrim

    hsRegex := "(?Jim)^:(?<Opts>[^:]+)*:(?<Trig>[^:]+)::(?:f\((?<Repl>[^,)]*)[^)]*\)|(?<Repl>[^;\v]+))?(?<fCom>\h*;\h*(?:\bFIXES\h*\d+\h*WORDS?\b)?(?:\h;)?\h*(?<mCom>.*))?$"
    hhReplacementMatchesEdit.CurrMatches := ""
    A_Clipboard := ""

    Send("^c")
    Errorlevel := !ClipWait(0.3)

    hotstringFromClipboard := Trim(A_Clipboard, " `t`n`r")

    If RegExMatch(hotstringFromClipboard, hsRegex, &hotstringFromRegex)
    {
        hhTriggerEdit.text := hotstringFromRegex.Trig, hhOptionsEdit.Value := hotstringFromRegex.Opts

        sleep(200)

        hhCurrentTrigger := triggerBeforeTrimming := hhCurrentTrigger.text := Trim(hotstringFromRegex.Trig, '"')
        hhCurrentReplacement := replacementBeforeThisTrim := replacementBeforeTrimming := hhReplacementEdit.text := Trim(hotstringFromRegex.Repl, '"')
        hhCommentEdit.text := hotstringFromRegex.mCom

        hhBeginRadio.Value := InStr(hotstringFromRegex.Opts, "*") ? (InStr(hotstringFromRegex.Opts, "?") ? 0 : 1) : 0
        hhMidRadio.Value := InStr(hotstringFromRegex.Opts, "*") ? (InStr(hotstringFromRegex.Opts, "?") ? 1 : 0) : 1
        hhEndRadio.Value := InStr(hotstringFromRegex.Opts, "*") ? 0 : (InStr(hotstringFromRegex.Opts, "?") ? 1 : 0)

        ExamineWords(hhCurrentTrigger, hhCurrentReplacement)
    }
    Else
    {
        NormalStartup(A_Clipboard, A_Clipboard)
    }

    hhUndoButton.Enabled := false

    Loop tArrStep.Length
        tArrStep.pop
    Loop rArrStep.Length
        rArrStep.pop
}

NormalStartup(stringTrigger, stringReplacement)
{
    Global IsMultiLine := 0
    Global targetWindow := WinActive("A")
    Global origTriggerTypo := ""
    Global DefaultBoilerPlateOpts
    Global DefaultAutoCorrectOpts
    Global hhReplacementEdit, hhTriggerEdit, hhOptionsEdit, hhCommentEdit

    If ((StrLen(stringTrigger) - StrLen(StrReplace(stringTrigger, " ")) > 2) || InStr(stringReplacement, "`n"))
    {
        DefaultOpts := DefaultBoilerPlateOpts
        hhReplacementEdit.value := stringTrigger
        IsMultiLine := 1
        hhMakeFunctionToggle.Value := 0
        If (addFirstLetters > 0)
        {
            initials := ""
            HotStrSug := StrReplace(stringTrigger, "`n", " ")
            Loop Parse, HotStrSug, A_Space, A_Tab
            {
                If (Strlen(A_LoopField) > tooSmallLen)
                    initials .= SubStr(A_LoopField, "1", "1")
                If (StrLen(initials) = addFirstLetters)
                    break
            }
            initials := StrLower(initials)
            DefaultHotStr := myPrefix . initials . mySuffix
        }
        else
        {
            DefaultHotStr := myPrefix . mySuffix
        }
    }
    Else If (stringTrigger = "")
    {
        hhCommentEdit.Text := hhReplacementEdit.Text := hhTriggerEdit.Text := hhOptionsEdit.Text := ""
        hhEndRadio.Value := hhMidRadio.Value := hhBeginRadio.Value := 0
        hhActivateFilterHandler()
        hh.Show('Autosize yCenter')
        Return
    }
    else
    {
        If (AutoEnterNewEntry = 1)
            origTriggerTypo := stringTrigger
        DefaultHotStr := Trim(StrLower(stringTrigger))
        hhReplacementEdit.value := Trim(StrLower(stringTrigger))
        DefaultOpts := DefaultAutoCorrectOpts
    }

    hhOptionsEdit.text := DefaultOpts
    hhTriggerEdit.value := DefaultHotStr
    hhReplacementEdit.Opt("-Readonly")
    hhAppendButton.Enabled := true
    If ExamPaneOpen = 1
        hhActivateFilterHandler()
    hh.Show('Autosize yCenter')
}

ExamineWords(stringTrigger, stringReplacement)
{
    Global overlapBeginningSubstring := ""
    Global typo := ""
    Global fix := ""
    Global overlapEndSubstring := ""

    SubTogSize(0, 0)
    hh.Show('Autosize yCenter')

    originalTrigger := stringTrigger, originalReplacement := stringReplacement
    
    
    If originalTrigger = originalReplacement
    {
        deltaString := "[ " originalTrigger " | " originalReplacement " ]"
        found := false
    }
    else
    {
        originalTriggerLength := strLen(stringTrigger), originalReplacementLength := strLen(stringReplacement)
        
        LoopNum := min(originalTriggerLength, originalReplacementLength)

        arrayOriginalTrigger := StrSplit(stringTrigger), arrayOriginalReplacement := StrSplit(stringReplacement)
        
        Loop LoopNum
        {
            If (arrayOriginalTrigger[A_Index] = arrayOriginalReplacement[A_Index])
                overlapBeginningSubstring .= arrayOriginalTrigger[A_Index]
            else
                break
        }

        Loop LoopNum
        {
            esubT := (arrayOriginalTrigger[(originalTriggerLength - A_Index) + 1])
            esubR := (arrayOriginalReplacement[(originalReplacementLength - A_Index) + 1])
            If (esubT = esubR)
                overlapEndSubstring := esubT . overlapEndSubstring
            else
                break
        }

        If (strLen(overlapBeginningSubstring) + strLen(overlapEndSubstring)) > LoopNum
        {
            delta := (originalTriggerLength > originalReplacementLength)
                ? " [ " . subStr(overlapEndSubstring, 1, (originalTriggerLength - originalReplacementLength)) . " |  ] "
                : " [  |  " . subStr(overlapEndSubstring, 1, (originalReplacementLength - originalTriggerLength)) . " ] "
        }
        Else
        {
            typo := StrReplace(originalTrigger, overlapBeginningSubstring, "")
            fix := StrReplace(originalReplacement, overlapBeginningSubstring, "")
            delta := " [ " . typo . " | " . fix . " ] "
        }
        deltaString := overlapBeginningSubstring . delta . overlapEndSubstring
    }

    hhTypoLabel.text := deltaString

    ViaExamButt := "Yes"

    hhActivateFilterHandler(ViaExamButt)

    If (hhExamButton.text = "Exam")
    {
        hhExamButton.text := "Done"
        If (hFactor != 0)
        {
            hh['SizeTog'].text := "Make Bigger"
            SubTogSize(0, 0)
        }
        hhToggleExamButtonHandler(True)
    }

    hh.Show('Autosize yCenter')
}

hhSizeToggleHandler(*)
{
    Global hFactor := ""
    Global Visibility
    Global ExamPaneOpen
    Global ControlPaneOpen

    If (hh['SizeTog'].text = "Make Bigger")
    {
        hh['SizeTog'].text := "Make Smaller"
        If (hhExamButton.text = "Done")
        {
            hhToggleExamButtonHandler(Visibility := False)
            hhToggleButtonsControlHandler(Visibility := False)
            ExamPaneOpen := ControlPanelOpen := 0
            hhExamButton.text := "Exam"
        }
        hFactor := HeightSizeIncrease
        SubTogSize(hFactor, WidthSizeIncrease)
    } Else { 
        hh['SizeTog'].text := "Make Bigger"
        hFactor := 0
        SubTogSize(0, 0)
    }
    hh.Show('Autosize yCenter')
    return
}

SubTogSize(hFactor, wFactor)
{
    hhTriggerEdit.Move(, , wFactor + 280,)
    hhReplacementEdit.Move(, , wFactor + 372, hFactor + 100)
    hhCommentLabel.Move(, hFactor + 182, ,)
    hhCommentEdit.move(, hFactor + 200, wFactor + 367,)
    hhMakeFunctionToggle.Move(, hFactor + 182, ,)
    hhAppendButton.Move(, hFactor + 234, ,)
    hhCheckButton.Move(, hFactor + 234, ,)
    hhExamButton.Move(, hFactor + 234, ,)
    hhSpellButton.Move(, hFactor + 234, ,)
    hhOpenButton.Move(, hFactor + 234, ,)
    hhCancelButton.Move(, hFactor + 234, ,)
}

hhSubFuncExamControlHandler(*)
{
    Global ControlPaneOpen

    hhExamButton.text := (ControlPaneOpen = 1)
        ? "Exam"
            : "Done"

    If (hFactor = HeightSizeIncrease && ControlPaneOpen = 0)
    {
        hhSizeToggleHandler()
        hh['SizeTog'].text := "Make Bigger"
    }

    (ControlPaneOpen = 1) ? hhToggleButtonsControlHandler(False)
        : hhToggleButtonsControlHandler(True)

    ControlPaneOpen := (ControlPaneOpen = 1)
        ? 0
            : 1

    hhToggleExamButtonHandler(False)
    hh.Show('Autosize yCenter')
}

hhExamHandler(*)
{
    Global ExamPaneOpen
    Global ControlPaneOpen

    If ((ExamPaneOpen = 0) and (ControlPaneOpen = 0) and GetKeyState("Shift")) || ((ExamPaneOpen = 1) and (ControlPaneOpen = 0) and GetKeyState("Shift"))
    {
        hhSubFuncExamControlHandler()
    }
    Else If ((ExamPaneOpen = 0) and (ControlPaneOpen = 0))
    {
        hhExamButton.text := "Done"
        If (hFactor = HeightSizeIncrease)
        {
            hhSizeToggleHandler()
            hh['SizeTog'].text := "Make Bigger"
        }

        ExamineWords(hhTriggerEdit.text, hhReplacementEdit.text)
        hhActivateFilterHandler()
        hhToggleButtonsControlHandler(False)
        hhToggleExamButtonHandler(True)
        ExamPaneOpen := 1
    }
    Else
    {
        hhExamButton.text := "Exam"
        hhToggleButtonsControlHandler(False)
        hhToggleExamButtonHandler(False)
        ExamPaneOpen := 0
        ControlPaneOpen := 0
    }
    hh.Show('Autosize yCenter')
}

hhToggleSymbolsHandler(*)
{
    If (hh['SymTog'].text = "+ Symbols")
    {
        hh['SymTog'].text := "- Symbols"
        togReplaceString := hhReplacementEdit.text
        togReplaceString := StrReplace(StrReplace(togReplaceString, "`r`n", "`n"), "`n", myPilcrow . "`n")
        togReplaceString := StrReplace(togReplaceString, A_Space, myDot)
        togReplaceString := StrReplace(togReplaceString, A_Tab, myTab)
        hhReplacementEdit.value := togReplaceString
        hhReplacementEdit.Opt("+Readonly")
        hhAppendButton.Enabled := false
    } Else {
        hh['SymTog'].text := "+ Symbols"
        togReplaceString := hhReplacementEdit.text
        togReplaceString := StrReplace(togReplaceString, myPilcrow . "`r", "`r")
        togReplaceString := StrReplace(togReplaceString, myDot, A_Space)
        togReplaceString := StrReplace(togReplaceString, myTab, A_Tab)
        hhReplacementEdit.value := togReplaceString
        hhReplacementEdit.Opt("-Readonly")
        hhAppendButton.Enabled := true
    }
    hh.Show('Autosize yCenter')
    return
}

hhTriggerChangedHandler(*)
{
    Static triggerBeforeThisTrim := ""
    triggerAfterThisTrim := hhTriggerEdit.text

    If (triggerAfterThisTrim != triggerBeforeThisTrim && ExamPaneOpen = 1)
    {
        If (triggerBeforeThisTrim = SubStr(triggerAfterThisTrim, 2,))
        {
            tArrStep.push(hhTriggerEdit.text)
            rArrStep.push(hhReplacementEdit.text)
            hhReplacementEdit.Value := SubStr(triggerAfterThisTrim, 1, 1) . hhReplacementEdit.text
        }
        If (triggerBeforeThisTrim = SubStr(triggerAfterThisTrim, 1, StrLen(triggerAfterThisTrim) - 1))
        {
            tArrStep.push(hhTriggerEdit.text)
            rArrStep.push(hhReplacementEdit.text)
            hhReplacementEdit.text := hhReplacementEdit.text . SubStr(triggerAfterThisTrim, -1,)
        }
        triggerBeforeThisTrim := triggerAfterThisTrim
    }
    hhUndoButton.Enabled := true
    hhActivateFilterHandler()
}

hhSaveAsFunctionHandler(*)
{
    hhOptionsEdit.text := StrReplace(StrReplace(hhOptionsEdit.text, "B0", ""), "X", "")
}

hhAppendHandler(*)
{
    CombinedValidMsg := ValidationFunction(hhOptionsEdit.text, hhTriggerEdit.text, hhReplacementEdit.text)

    If (!InStr(CombinedValidMsg, "-Okay.", , , 3))
        biggerMsgBox(hhOptionsEdit.text, hhTriggerEdit.text, hhReplacementEdit.text, CombinedValidMsg, 1)
    else
    {
        Appendit()
        return
    }
}

hhCheckHandler(*)
{
    CombinedValidMsg := ValidationFunction(hhOptionsEdit.text, hhTriggerEdit.text, hhReplacementEdit.text)
    biggerMsgBox(hhOptionsEdit.text, hhTriggerEdit.text, hhReplacementEdit.text, CombinedValidMsg, 0)
    Return
}

biggerMsgBox(options, trigger, replacement, thisMess, bbShowAppendButton := 0)
{
    Global hhGUIColor, hhFontColor, myBigFont, AutoLookupFromValidityCheck
    A_Clipboard := thisMess

    if (IsObject(bb))
        bb.Destroy()

    Global bb := Gui(, 'Validity Report')
    bb.SetFont('s11 ' hhFontColor)
    bb.BackColor := hhGUIColor, hhGUIColor

    (bbMessageBoxTitle := bb.Add('Text', , 'For proposed new item:')).Focus()

    bb.SetFont(myBigFont)
    proposedHS := ':' options ':' trigger '::' replacement
    bbNewHotstringLabel := bb.Add('Text', (strLen(proposedHS) > 90 ? 'w600 ' : '') 'xs yp+22', proposedHS)

    bb.SetFont('s11 ')
    bbShowAppendButton = 0 ? bb.Add('Text', , "===Validation Check Results===") : ''

    bb.SetFont(myBigFont)
    arrayValidityCheckResults := StrSplit(thisMess, "*|*")
    bbHotstringValidityMessage := (InStr(arrayValidityCheckResults[2], "`n", , , 10))
        ? subStr(arrayValidityCheckResults[2], 1, inStr(arrayValidityCheckResults[2], "`n", , , 10)) "`n## Too many conflicts to show in form ##"
            : arrayValidityCheckResults[2]

    edtSharedSettings := ' -VScroll ReadOnly -E0x200 Background'

    bbOptionsEdit := bb.Add('Edit', (inStr(arrayValidityCheckResults[1], '-Okay.') ? myGreen : myRed) edtSharedSettings hhGUIColor, arrayValidityCheckResults[1])
    bbTriggerEdit := bb.Add('Edit', (strLen(bbHotstringValidityMessage) > 104 ? ' w600 ' : ' ') (inStr(bbHotstringValidityMessage, '-Okay.') ? myGreen : myRed) edtSharedSettings hhGUIColor, bbHotstringValidityMessage)

    bbReplacementEdit := bb.Add('Edit', (strLen(arrayValidityCheckResults[3]) > 104 ? ' w600 ' : ' ') (inStr(arrayValidityCheckResults[3], '-Okay.') ? myGreen : myRed) edtSharedSettings hhGUIColor, arrayValidityCheckResults[3])
    
    bb.SetFont('s11 ' hhFontColor)
    bbAppendWithConflictLabel := (bbShowAppendButton = 1) ? bb.Add('Text', , "==============================`nAppend HotString Anyway?") : ''
    
    bbAppendButton := bb.Add('Button', , 'Append Anyway')
    
    if (bbShowAppendButton != 1)
        bbAppendButton.Visible := False
        
    bbCloseButton := bb.Add('Button', 'x+5 Default', 'Close')
        
    If not inStr(bbHotstringValidityMessage, '-Okay.')
        bbAutoLookUpToggle := bb.Add('Checkbox', 'x+5 Checked' AutoLookupFromValidityCheck, 'Auto Lookup`nin editor')
        
    bb.Show('yCenter x' (A_ScreenWidth / 2))
        
    WinSetAlwaysontop(1, "A")

    bbTriggerEdit.OnEvent('Focus', findInScript)
    bbAppendButton.OnEvent('Click', (*) => Appendit())
    bbAppendButton.OnEvent('Click', (*) => bb.Destroy())
    bbCloseButton.OnEvent('Click', (*) => bb.Destroy())
    bb.OnEvent('Escape', (*) => bb.Destroy())
    }

findInScript(*)
{
    Global filenameThisScript
    Global pathDefaultEditor
    Global AutoLookupFromValidityCheck

    If (AutoLookupFromValidityCheck = 0)
        Return

    A_Clipboard := ""
    activeWin := WinActive("A")
    activeControl := ControlGetFocus("ahk_ID " activeWin)
    
    if (GetKeyState("LButton", "P"))
        KeyWait("LButton", "U")

    SendInput("^c")
    If !ClipWait(1, 0)
        Return

    if WinExist(filenameThisScript)
        WinActivate(filenameThisScript)
    else
    {
        Run('"' pathDefaultEditor "' '" filenameThisScript "'")
        If !WinWait(filenameThisScript, , 5)
        {
            Msgbox("Failed to open " filenameThisScript " in your editor.")
            Return
        }
        
        else
            WinActivate(filenameThisScript)
    }
    If RegExMatch(A_Clipboard, "^\d{2,}")
        SendInput("^g" . A_Clipboard)
    else
    {
        SendInput("^f")
        sleep(200)
        SendInput("^v")
    }
    WinActivate("ahk_ID " activeWin)
    ControlFocus("ahk_ID " activeWin, "ahk_ID " activeControl)
}

ValidationFunction(paramOpts, paramTrigger, paramReplacement)
{
    Global ACitemsStartAt

    hhActivateFilterHandler()

    validOpts := (paramOpts = "") ? "Okay." : CheckOptions(paramOpts)

    validHot := ""

    if (paramTrigger = "" || paramTrigger = myPrefix || paramTrigger = mySuffix)
        validHot := "HotString box should not be empty."
    else if InStr(paramTrigger, ":")
        validHot := "Don't include colons."
    else
    {
        Loop Parse, Fileread(A_ScriptName), "`n", "`r"
        {
            if (A_Index < ACitemsStartAt || SubStr(trim(A_LoopField, " `t"), 1, 1) != ":")
                continue
            if RegExMatch(A_LoopField, "i):(?P<Opts>[^:]+)*:(?P<Trig>[^:]+)", &loo)
            {
                validHot .= CheckDupeTriggers(A_Index, A_Loopfield, paramTrigger, paramOpts, loo.Trig, loo.Opts)
                validHot .= CheckMiddleConflicts(A_Index, A_Loopfield, paramTrigger, paramOpts, loo.Trig, loo.Opts)
                validHot .= CheckPotentialMiddleConflicts(A_Index, A_Loopfield, paramTrigger, paramOpts, loo.Trig, loo.Opts)
                validHot .= CheckPotentialBeginningEndConflicts(A_Index, A_Loopfield, paramTrigger, paramOpts, loo.Trig, loo.Opts)
                validHot .= CheckWordBeginningConflicts(A_Index, A_Loopfield, paramTrigger, paramOpts, loo.Trig, loo.Opts)
                validHot .= CheckWordEndingConflicts(A_Index, A_Loopfield, paramTrigger, paramOpts, loo.Trig, loo.Opts)
                continue
            }
            else
            {
                continue
            }
        }
    }

    if (validHot = "")
        validHot := "Okay."

    validRep := (paramReplacement = "")
        ? "Replacement string box should not be empty."
        : (SubStr(paramReplacement, 1, 1) = ":")
            ? "Don't include the colons."
            : (paramReplacement = paramTrigger)
                ? "Replacement string SAME AS Trigger string."
                : "Okay."

CombinedValidMsg := "OPTIONS BOX `n-"       
        . validOpts
        . "*|*HOTSTRING BOX `n-"
        . validHot
        . "*|*REPLACEMENT BOX `n-"
        . validRep

    Return CombinedValidMsg
}

CheckOptions(tMyDefaultOpts)
{
    NeedleRegEx := "(\*|B0|\?|SI|C|K[0-9]{1,3}|SE|X|SP|O|R|T)"
    WithNeedlesRemoved := RegExReplace(tMyDefaultOpts, NeedleRegEx, "")
    If (WithNeedlesRemoved = "")
        return "Okay."
    else
    {
        OptTips := inStr(WithNeedlesRemoved, ":") ? "Don't include the colons.`n" : ""
        OptTips .= "
			(
			...Tips from AHK v1 docs...
			* - ending char not needed
			? - trigger inside other words
			B0 - no backspacing
			SI - send input mode
			C - case-sensitive
			K(n) - set key delay
			SE - send event mode
			X - execute command
			SP - send play mode
			O - omit end char
			R - send raw
			T - super raw
			)"
        return "Invalid Hotsring Options found.`n---> " WithNeedlesRemoved "`n" OptTips
    }
}

CheckDupeTriggers(LineNum, Line, newTrigger, newOptions, loopTrigger, loopOptions)
{
    Return ((newTrigger = loopTrigger) && (newOptions = loopOptions))
        ? "`nDuplicate trigger string found at line " LineNum ".`n---> " Line
        : ""
}

CheckMiddleConflicts(LineNum, Line, newTrigger, newOptions, loopTrigger, loopOptions)
{
    Return ((InStr(loopTrigger, newTrigger) and inStr(loopOptions, "*") and inStr(loopOptions, "?"))
        || (InStr(newTrigger, loopTrigger) and inStr(newOptions, "*") and inStr(newOptions, "?")))
        ? "`nWord-Middle conflict found at line " LineNum ", where one of the strings will be nullified by the other.`n---> " Line
        : ""
}

CheckPotentialMiddleConflicts(LineNum, Line, newTrigger, newOptions, loopTrigger, loopOptions)
{
    Return ((loopTrigger = newTrigger) and inStr(loopOptions, "*") and not inStr(loopOptions, "?") and inStr(newOptions, "?") and not inStr(newOptions, "*"))
    || ((loopTrigger = newTrigger) and inStr(loopOptions, "?") and not inStr(loopOptions, "*") and inStr(newOptions, "*") and not inStr(newOptions, "?"))
        ? "`nDuplicate trigger found at line " LineNum ", but maybe okay, because one is word-beginning and other is word-ending.`n---> " Line
        : ""
}

CheckPotentialBeginningEndConflicts(LineNum, Line, newTrigger, newOptions, loopTrigger, loopOptions)
{
    Return ((inStr(loopOptions, "*") and (loopTrigger = subStr(newTrigger, 1, strLen(loopTrigger))))
        || (inStr(newOptions, "*") and (newTrigger = subStr(loopTrigger, 1, strLen(newTrigger)))))
        ? "`nWord Beginning conflict found at line " LineNum ", where one of the strings is a subset of the other.  Whichever appears last will never be expanded.`n---> " Line
        : ""
}

CheckWordBeginningConflicts(LineNum, Line, newTrigger, newOptions, loopTrigger, loopOptions)
{
    Return ((inStr(loopOptions, "?") and loopTrigger = subStr(newTrigger, -strLen(loopTrigger)))
        || (inStr(newOptions, "?") and newTrigger = subStr(loopTrigger, -strLen(newTrigger))))
        ? "`nWord Ending conflict found at line " LineNum ", where one of the strings is a superset of the other.  The longer of the strings should appear before the other, in your code.`n---> " Line
        : ""
}

CheckWordEndingConflicts(LineNum, Line, newTrigger, newOptions, loopTrigger, loopOptions)
{
    Return ((inStr(loopOptions, "?") and loopTrigger = subStr(newTrigger, -strLen(loopTrigger)))
        || (inStr(newOptions, "?") and newTrigger = subStr(loopTrigger, -strLen(newTrigger))))
        ? "`nWord Ending conflict found at line " LineNum ", where one of the strings is a superset of the other.  The longer of the strings should appear before the other, in your code.`n---> " Line
        : ""
}

Appendit()
{
    Global pathUserHotstrings
    Global targetWindow
    Global rMatches, tMatches

    stringComment := wholeStr := ""

    If (rMatches > 0) and (AutoCommentFixesAndMisspells = 1)
    {
        stringComment :=    " Fixes " 
                            . rMatches 
                            . " words "
                            . (tMatches > 3) 
                                    ?   "but misspells " 
                                        . tMatches 
                                        . " words !!! " 
                                    :   (hhTriggerMatchesEdit.Value != "") 
                                            ?   "but misspells " 
                                                . SubStr(StrReplace(hhTriggerMatchesEdit.Value, "`n", " (), "), 1, -2) 
                                                . ". "
                                            :   ""

    }

    stringComment := "`t`;`t" . ((stringComment != "") ? stringComment . " // " . hhCommentEdit.text : hhCommentEdit.text)

    WholeStr :=     "`n:" 
                    . hhOptionsEdit.text
                    . ":" 
                    . hhTriggerEdit.text
                    . "::" 
                    . ((hhMakeFunctionToggle.Value = 1) ? '_F("' : "") 
                    . ((InStr(hhReplacementEdit.text, "`n")) ? StrReplace(hhReplacementEdit.text,"`n","`n") : hhReplacementEdit.text)
                    . ((hhMakeFunctionToggle.Value = 1) ? '")' : "")
                    . stringComment
    
    FileAppend(WholeStr, pathUserHotstrings)
    Reload()
}

ChangeActiveEditField(*)
{
    Global origTriggerTypo := trim(origTriggerTypo)

    Send("^c")
    Errorlevel := !ClipWait(0.3)

    hasSpace := (subStr(A_Clipboard, -1) = " ") ? " " : ""
    A_Clipboard := trim(A_Clipboard)

    If (origTriggerTypo = A_Clipboard) and (origTriggerTypo = hhTriggerEdit.text)
    {
        If (bb != 0)
            bb.Hide()
        hh.Hide()
        WinWaitActive(targetWindow)
        Send(hhReplacementEdit.text hasSpace)
    }
}

hhSpellHandler(*)
{
    Global tReplaceString

    tReplaceString := hhReplacementEdit.text
    If (tReplaceString = "")
        MsgBox("Replacement Text not found.", , 4096)
    else
    {
        googleSugg := GoogleAutoCorrect(tReplaceString)
        If (googleSugg = "")
            MsgBox("No suggestions found.", , 4096)
        Else
        {
            msgResult := MsgBox(googleSugg "`n`n######################`nChange Replacement Text?", "Google Suggestion", "OC 4096")
            if (msgResult = "OK")
            {
                hhReplacementEdit.value := googleSugg
                hhActivateFilterHandler()
            }
            else
                return
        }
    }
}

GoogleAutoCorrect(word)
{
    objReq := ComObject('WinHttp.WinHttpRequest.5.1')
    objReq.Open('GET', 'https://www.google.com/search?q=' word)
    objReq.SetRequestHeader('User-Agent'
        , 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)')
    objReq.Send(), HTML := objReq.ResponseText
    If RegExMatch(HTML, 'value="(.*?)"', &A)
        If RegExMatch(HTML, ';spell=1.*?>(.*?)<\/a>', &B)
            Return B[1] || A[1]
}

hhButtonOpenHandler(*)
{
    Global hh
    Global pathDefaultEditor
    Global filenameThisScript
    Global ClipboardOld

    hh.Hide()
    A_Clipboard := ClipboardOld
    Try
        Run("'" pathDefaultEditor "' '" filenameThisScript "'")
    Catch
        Try
            Run("'" pathDefaultEditor "' '" A_ScriptFullPath "'")
        Catch
            WinWaitActive(filenameThisScript)
    Sleep(1000)
    Send("{Ctrl Down}{End}{Ctrl Up}{Home}")
}

hhWordlistHandler(*)
{
    Global filenameWordlist
    Global pathWordList
    Global pathDefaultEditor
    Global filenameThisScript
    Global ClipboardOld

    hh.Hide()
    A_Clipboard := ClipboardOld
    Try
        RunWait("'" pathDefaultEditor "' '" A_ScriptFullPath "'")
    WinWaitActive(filenameThisScript)
    Sleep(1000)
    SendInput("^f")
    Sleep(100)
    SendInput(filenameWordlist)
    Sleep(250)
    Run(folderLib)
}

hhButtonCancelHandler(*)
{
    Global ClipboardOld, tArrStep, rArrStep
    hh.Hide()
    hhTriggerEdit.value := hhReplacementEdit.value := hhOptionsEdit.value := hhCommentEdit.value := ""
    tArrStep := rArrStep := []
    A_Clipboard := ClipboardOld
}

hhTrimHandler(direction := "",*)
{
    global tArrStep, rArrStep

    tArrStep.push(hhTriggerEdit.value), rArrStep.push(hhReplacementEdit.value)

    hhTriggerEdit.value := (direction.name = "butRTrim") ? subStr(hhTriggerEdit.value, 1, strLen(hhTriggerEdit.value) - 1) : subStr(hhTriggerEdit.value, 2)
    hhReplacementEdit.value := (direction.name = "butRTrim") ? subStr(hhReplacementEdit.value, 1, strLen(hhReplacementEdit.value) - 1) : subStr(hhReplacementEdit.value, 2)
    hhUndoButton.Enabled := true
    
    hhTriggerChangedHandler()
}

hhUndoHandler(*)
{
    Global tArrStep, rArrStep
    If GetKeyState("Shift")
        GoReStart()
    else If (tArrStep.Length > 0) and (rArrStep.Length > 0)
    {
        hhTriggerEdit.value := tArrStep.Pop()
        hhReplacementEdit.value := rArrStep.Pop()
        hhActivateFilterHandler()
    }
    else
    {
        hhUndoButton.Enabled := false
    }
}

GoReStart(*)
{
    Global triggerBeforeTrimming, replacementBeforeTrimming, tArrStep, rArrStep

    hhTriggerEdit.Value := (triggerBeforeTrimming) ? triggerBeforeTrimming : ""
    hhReplacementEdit.Value := (replacementBeforeTrimming) ? replacementBeforeTrimming : ""
    tArrStep := rArrStep := []

    hhUndoButton.Enabled := false

    hhActivateFilterHandler()
}

hhActivateMiddleHandler(*)
{
    Static clickLast := 0
    Static clickCurrent := A_TickCount
    if (clickCurrent - clickLast < 500)
    {
        hhMidRadio.Value := 0
        hhOptionsEdit.text := strReplace(strReplace(hhOptionsEdit.text, "?", ""), "*", "")
    }
    clickLast := A_TickCount
    hhActivateFilterHandler()
}

hhActivateFilterHandler(ViaExamButt := "No", *)
{
    Global rMatches := 0
    Global tMatches := 0
    Global pathWordList
    Global hhFontColor

    hhCurrentOptions := hhOptionsEdit.text

    hhCurrentTrigger := Trim(hhTriggerEdit.Value)
    hhCurrentTrigger := (hhCurrentTrigger != "") ? hhCurrentTrigger : " "

    rFind := Trim(hhReplacementEdit.Value, "`n`t ")
    rFind := (rFind != "") ? rFind : " "

    tFilt := ''
    rFilt := ''

    If (ViaExamButt = "Yes")
    {
        If (InStr(hhCurrentOptions, "*") and InStr(hhCurrentOptions, "?"))
            hhMidRadio.value := 1, hhBeginRadio.value := 0, hhEndRadio.value := 0
        Else If (InStr(hhCurrentOptions, "*") and !InStr(hhCurrentOptions, "?"))
            hhMidRadio.value := 0, hhBeginRadio.value := 1, hhEndRadio.value := 0
        Else If (!InStr(hhCurrentOptions, "*") and InStr(hhCurrentOptions, "?"))
            hhMidRadio.value := 0, hhBeginRadio.value := 0, hhEndRadio.value := 1
        Else 
            hhMidRadio.value := 0, hhBeginRadio.value := 0, hhEndRadio.value := 0
    }

    Loop Read, pathWordList
    {
        If InStr(A_LoopReadLine, hhCurrentTrigger)
        {
            switch
            {
                case (hhMidRadio.value = 1):
                    tFilt .= A_LoopReadLine '`n'
                    tMatches++
                case (hhEndRadio.value = 1 && InStr(SubStr(A_LoopReadLine, -StrLen(hhCurrentTrigger)), hhCurrentTrigger)):
                    tFilt .= A_LoopReadLine '`n'
                    tMatches++
                case (hhBeginRadio.value = 1 && InStr(SubStr(A_LoopReadLine, 1, StrLen(hhCurrentTrigger)), hhCurrentTrigger)):
                    tFilt .= A_LoopReadLine '`n'
                    tMatches++
                case (A_LoopReadLine = hhCurrentTrigger):
                    tFilt := hhCurrentTrigger
                    tMatches++
            }
        }
    }

    if (hhMidRadio.value = 1)
    {
        hhCurrentOptions := hhCurrentOptions . "*?"
    }
    else if (hhEndRadio.value = 1)
    {
        hhCurrentOptions := StrReplace(hhCurrentOptions, "*", "")
        hhCurrentOptions := "?" . hhCurrentOptions
    }
    else if (hhBeginRadio.value = 1)
    {
        hhCurrentOptions := StrReplace(hhCurrentOptions, "?", "")
        hhCurrentOptions := (InStr(hhCurrentOptions, "*")) ? hhCurrentOptions : "*" . hhCurrentOptions
    }
    if (inStr(hhCurrentOptions, "**"))
        hhOptionsEdit.text := hhCurrentOptions

    hhTriggerMatchesEdit.Value := tFilt
    hhMisspellsListLabel.Text := "Misspells [" . tMatches . "]"

    hhTriggerLabel.Text := (tMatches > 0) ? "Misspells [" . tMatches . "] words" : "No Misspellings found."
    hhTriggerLabel.SetFont((tMatches > 0) ? "cRed" : hhFontColor)

    Loop Read pathWordList
    {
        If InStr(A_LoopReadLine, rFind)
        {
            switch
            {
                case (hhMidRadio.value = 1):
                    rFilt .= A_LoopReadLine "`n"
                    rMatches++
                case (hhEndRadio.value = 1 && InStr(SubStr(A_LoopReadLine, -StrLen(rFind)), rFind)):
                    rFilt .= A_LoopReadLine "`n"
                    rMatches++
                case (hhBeginRadio.value = 1 && InStr(SubStr(A_LoopReadLine, 1, StrLen(rFind)), rFind)):
                    rFilt .= A_LoopReadLine "`n"
                    rMatches++
                case (A_LoopReadLine = rFind):
                    rFilt := rFind
                    rMatches++
            }
        }
    }

    hhReplacementMatchesEdit.Value := rFilt
    hhFixesListLabel.Text := "Fixes [" . rMatches . "]"
}

#HotIf WinActive(filenameThisScript)
    ^s::
    {
        Send("^s")
        MsgBox("Reloading...", "", "T0.3")
        Sleep(250)
        Reload()
        MsgBox("I'm reloaded.")
    }
#HotIf

^+e::
handleEditScript(*)
{
    Global pathDefaultEditor
    Global filenameThisScript

    Try
        Run('"' pathDefaultEditor '" "' filenameThisScript '"')
    Catch
        try
            Run('"' pathDefaultEditor '" "' A_ScriptFullPath '"')
        Catch
            Msgbox('cannot run ' filenameThisScript)
}

!+F3::MsgBox(lastTrigger, "Trigger", 0)

#MaxThreadsPerHotkey 20


_F(replacement?, opts?, sendFunc?)
{
    static HSInputBuffer := InputBuffer()
    static DefaultOmit := false
    static DefaultSendMode := A_SendMode
    static DefaultKeyDelay := 0
    static DefaultTextMode := ""
    static DefaultBS := 0xFFFFFFF0
    static DefaultCustomSendFunc := ""
    static DefaultCaseConform := true
    static __Init := HotstringRecognizer.Start()

    local Omit
    local TextMode
    local PrevKeyDelay := A_KeyDelay
    local PrevKeyDurationPlay := A_KeyDurationPlay
    local PrevSendMode := A_SendMode
    local ThisHotkey := A_ThisHotkey
    local EndChar := A_EndChar
    local Trigger := RegExReplace(ThisHotkey, "^:[^:]*:", , , 1)
    local ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger) - StrLen(EndChar))

    Global keepForLog

    if !IsSet(replacement)
    {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts)
        {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len
            {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i + 1, 1)
                if o = "S"
                {
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                }
                else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i + 1)
                {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                }
                else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B"
                {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                }
                else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    HSInputBuffer.Start()

    TextMode := DefaultTextMode
    BS := DefaultBS
    Omit := DefaultOmit
    CustomSendFunc := sendFunc ?? DefaultCustomSendFunc
    CaseConform := DefaultCaseConform

    SendMode(DefaultSendMode)
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay(DefaultKeyDelay, "Play")
    else
        SetKeyDelay(DefaultKeyDelay)

    if IsSet(opts) && InStr(opts, "B")
    {
        BSCount := ""
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
        
    }

    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts)
    {
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len
        {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i + 1, 1)
            if o = "S"
            {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            }
            else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i + 1)
            {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay(KeyDelay, "Play")
                else
                    SetKeyDelay(KeyDelay)
                continue
            }
            else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstringLetters := RegexReplace(ThisHotstring, "\P{L}"), 1, 1), 'Locale')
    {
        if IsUpper(SubStr(ThisHotstringLetters, 2), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    if BS
    {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_")) + !Omit

        if BS = 0xFFFFFFF0
        {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {
                CompareString: replacement,
                GraphemeLength: 0,
                Pos: 1
            })
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        }
        else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }

    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else
    {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit
            Send("{Raw}" EndChar)
    }

    HotstringRecognizer.Reset()

    HSInputBuffer.Stop()

    if InStr(A_SendMode, "Play")
        SetKeyDelay(PrevKeyDurationPlay, "Play")
    else
        SetKeyDelay(PrevKeyDelay)

    SendMode(PrevSendMode)

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
    KeepForLog := A_ThisHotkey "`n"
    SetTimer(keepText, -1)

}

class HotstringRecognizer
{
    static Content := "", Length := 0, IsActive := 0, OnChange := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New()
    {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {
            Call: this.__Hotstring.Bind(this)
        })
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set: ((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook),
            get: ((hook, *) => hook.MinSendLevel).Bind(this.__Hook)
        })
        HotstringRecognizer.DefineProp("ResetKeys", {
            set: ((this, dummy, value, *) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this),
            get: ((this, *) => this.__ResetKeys).Bind(this)
        })
    }

    static Start()
    {
        this.Reset()
        if !this.HasProp("__HotIfIsActive")
        {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset"))
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    static Reset(ih := 0, vk := 0, *) => (vk = 8 ? this.__SetContent(SubStr(this.Content, 1, -1)) : this.__SetContent(""), this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr"))

    static __AddChar(ih, char)
    {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.__SetContent("")
        this.__SetContent(this.Content char), this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*)
    {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*)
    {
        switch arg1, 0
        {
            case "MouseReset":
                if IsSet(arg2)
                {
                    HotIf(this.__HotIfIsActive)
                    if arg2
                    {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))
                    }
                    else
                    {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
    static __SetContent(Value)
    {
        if this.OnChange && HasMethod(this.OnChange) && this.Content !== Value
            SetTimer(this.OnChange.Bind(this.Content, Value), -1)
        this.Content := Value
    }
}

class InputBuffer
{
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := [
            "LButton",
            "RButton",
            "MButton",
            "XButton1",
            "XButton2",
            "WheelUp",
            "WheelDown"
        ]
        , ModifierKeys := [
            "LShift",
            "RShift",
            "LCtrl",
            "RCtrl",
            "LAlt",
            "RAlt",
            "LWin",
            "RWin"
        ]
    static __New() => this.DefineProp("Default", {
        value: InputBuffer()
    })
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0)
    {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd
        {
            if keybd is String
            {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown := this.BufferKey.Bind(this, , , , "Down")
            this.InputHook.OnKeyUp := this.BufferKey.Bind(this, , , , "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "")
    {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start()
    {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse
        {
            HotIf(this.HotIfIsActive)
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for keystroke in this.MouseButtons
            {
                if InStr(keystroke, "Wheel")
                    HotKey(keystroke, this.BufferMouse.Bind(this), opts)
                else
                {
                    HotKey(keystroke, this.BufferMouse.Bind(this, , "Down"), opts)
                    HotKey(keystroke " Up", this.BufferMouse.Bind(this), opts)
                }
            }
            HotIf()
        }
    }
    Release()
    {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        PrevSendLevel := A_SendLevel
        SendLevel(this.SendLevel - 1)

        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send(modifierList)

        while this.Buffer.Length
        {
            keystroke := this.Buffer.RemoveAt(1)
            sent.Push(keystroke)
            if InStr(keystroke, "{Click ")
                clickSent := true
            if (keystroke = "{ Down}")
                continue 
            else if (keystroke != "{ Up}")
                send("{Blind}" keystroke)
        }
        SendLevel(PrevSendLevel)

        if this.Mouse && clickSent
        {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true)
    {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse
        {
            HotIf(this.HotIfIsActive)
            for keystroke in this.MouseButtons
                HotKey(keystroke, "Off")
            HotIf()
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}


#MaxThreadsPerHotkey 5
keepText(*)
{
    Global savedUpText
    Global intervalCounter
    Global logIsRunning
    Global saveIntervalMinutes
    Global KeepForLog

    CtrlZ := Chr(26)
    EndKeys := "{Backspace}," CtrlZ
    lih := InputHook("B V I1 E M T1", EndKeys)

    lih.Start()
    lih.Wait()

    hyphen := (lih.EndReason = "EndKey") ? " << " : " -- "
    savedUpText .= A_YYYY "-" A_MM "-" A_DD "`t`t" hyphen "`t`t" KeepForLog
    intervalCounter := 0
    If logIsRunning = 0
        setTimer(Appender, saveIntervalMinutes)
}
#MaxThreadsPerHotkey 1

Appender(*)
{
    Global savedUpText
    Global logIsRunning
    Global intervalCounter
    Global IntervalsBeforeStopping
    Global filenameACLogger
    Global pathACLogger

    If (savedUpText != "")
        FileAppend(savedUpText, pathACLogger)
    savedUpText := ''
    logIsRunning := 1
    intervalCounter += 1
    If (intervalCounter >= IntervalsBeforeStopping)
    {
        setTimer(Appender, 0)
        intervalCounter := logIsRunning := 0
    }
}

OnExit(Appender)

^F3::
hhStringsAndFixesHandler(*)
{
    ThisFile := FileRead(A_ScriptFullPath)
    thisOptions := '', regulars := 0, begins := 0, middles := 0, ends := 0, fixes := 0, entries := 0
    Loop Parse ThisFile, '`n'
    {
        If (SubStr(Trim(A_LoopField), 1, 1) = ':')
        {
            entries++
            thisOptions := SubStr(Trim(A_LoopField), 1, InStr(A_LoopField, ':', , , 2))

            If InStr(thisOptions, '*') and InStr(thisOptions, '?')
                middles++
            Else If InStr(thisOptions, '*')
                begins++
            Else If InStr(thisOptions, '?')
                ends++
            Else
                regulars++
            If RegExMatch(A_LoopField, 'Fixes\h*\K\d+', &fn)
                fixes += fn[]

        }
    }
    ends := numberFormat(ends)
    begins := numberFormat(begins)
    middles := numberFormat(middles)
    regulars := numberFormat(regulars)
    entries := numberFormat(entries)
    fixes := numberFormat(fixes)

    MsgBox('   Totals`n==========================='
        '`n    Regular Autocorrects:`t' regulars
        '`n    Word Beginnings:`t`t' begins
        '`n    Word Middles:`t`t' middles
        '`n    Word Ends:`t`t' ends
        '`n==========================='
        '`n   Total Entries:`t`t' entries
        '`n   Potential Fixes:`t`t' fixes
        , 'Report for ' A_ScriptName, 64 + 4096)

}

numberFormat(num)
{
    parts := StrSplit(num, ",")
    if (parts.Length() > 1)
    {
        intPart := parts[1]
        decimalPart := parts[2]
        formattedIntPart := RegExReplace(intPart, "(\d)(?=(\d{3})+$)", "$1,")
        return formattedIntPart "." decimalPart
    }
    else
    {
        return RegExReplace(num, "(\d)(?=(\d{3})+$)", "$1,")
    }
}

UnzipFile(file, folder := "")
{
    if (folder = "")
        folder := A_ScriptDir

    RunWait("powershell -command Expand-Archive -Path " file " -DestinationPath " folder)
}

CopyFilesAndFolders(SourcePattern, DestinationFolder, DoOverwrite := false)
{
    ErrorCount := 0
    try
        FileCopy(SourcePattern, DestinationFolder, DoOverwrite)
    catch as Err
        ErrorCount := Err.Extra
    Loop Files, SourcePattern, "D"
    {
        try
            DirCopy(A_LoopFilePath, DestinationFolder "\" A_LoopFileName, DoOverwrite)
        catch
        {
            ErrorCount += 1
            MsgBox("Could not copy " A_LoopFilePath " into " DestinationFolder)
        }
    }
    return ErrorCount
}



:B0:Savitr::
:B0:Vaisyas::
:B0:Wheatley::
:B0:arraign::
:B0:bialy::
:B0:callsign::
:B0:champaign::
:B0:coign::
:B0:condign::
:B0:consign::
:B0:coreign::
:B0:cosign::
:B0:countersign::
:B0:deign::
:B0:deraign::
:B0:eloign::
:B0:ensign::
:B0:feign::
:B0:indign::
:B0:kc::
:B0:malign::
:B0:miliary::
:B0:minyanim::
:B0:pfennig::
:B0:reign::
:B0:sice::
:B0:sign::
:B0:verisign::
:B0?:align::
:B0?:assign::
:B0?:benign::
:B0?:campaign::
:B0?:design::
:B0?:foreign::
:B0?:resign::
:B0?:sovereign::

{
    return
}

:XB0K2:wc::_F("workers` compensation")

#Hotstring ZXB0

_F(,"SE")

ACitemsStartAt := A_LineNumber + 4

#INCLUDE "%A_ScriptDir%\Auto Launch\Lib\UserHotstringsFile.ahk" ; *i 

::cleint::_F("client")
:*?:schg::_F("sch")
:*:rre::_F("re")
:*:wizrads::_F("wizards")
:?:iwill::_F("i will")
:?*:riws::_F("rwis")
::ot::_F("to")
:C?:ptcs::_F("PTCS")
:?:aind::_F("ained")
:*:hwa::_F("wha")
:?*:curret::_F("current")
:?*:cliet::_F("client")
:*?:crpt::_F("cript")
:C*:asj::_F("ADJ")
:C:ddd::_F("degenerative disc disease")
:*:noic::_F("notic")
:?:fiels::_F("files")
:*:tth::_F("th")
:*:doot::_F("foot")
:*:adiv::_F("advi")
:?:teung::_F("ting")
:*:lfet::_F("left")
:*:oop::_F("out-of-pocket")
:*:porba::_F("proba")
:*:aprk::_F("park")
:?:uracne::_F("urance")
:?:osn::_F("son")
:*?:crimn::_F("crimin")
:*:ocne::_F("once")
:*?:rodc::_F("rodu")
:*:mpt::_F("not")
::tere::_F("there")
:*:joasn::_F("Jason")
::afall::_F("a fall")
:?*:amry::_F("army")
:*:gmial::_F("gmail")
:?:haty::_F("hat")
:*:counter s::_F("counters")
:C*:jaos::_F("Jaso")
:*C:merus::_F("Merus")
:C:wcj::_F("workers' compensation judge")
::tets::_F("test")
:*:daisy cha::_F("daisy-cha")
:*:mss::_F("mess")
:C:ltc::_F("LTC")
:*:tiral::_F("trial")
:*:ptu::_F("put")
:*:decemebr::_F("December")
:*:rgi::_F("rig")
:*:oddess::_F("Odyss")
:?:zuer::_F("zure")
:*?:eeiv::_F("eiv")
:?:ndn::_F("nd")
:C:wcab::_F("Workers' Compensation Appeals Board")
:*?:covg::_F("cog")
::sicj::_F("such")
:*:complee::_f("complet")
:?:eacky::_f("eaky")
:*:totalli::_f("totali")
::bene::_f("been")
:*:calender::_f("calendar")
:*:thah::_f("tha")
:?:actony::_f("ectomy")
::wc::_F("workers' compensation")

User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

03 Jun 2024, 14:49

To get it to run, Descolada will need to comment-out 1 #Include near the bottom, and 5 icon references which are near each other. It does run after that. I can't seem to get the effect you are describing, though. You said space has to be pressed immediately after the wc, but does it reliably do the error, or is it only some of the time?

Edit: Hey are using Ntepa's CAse COrrector tool? If so, have you tried commenting that out, to see if it fixes things?
name := "ste(phen|ve) kunkel"
Jasonosaj
Posts: 58
Joined: 02 Feb 2022, 15:02
Location: California

Re: Buffered hotstrings

03 Jun 2024, 15:02

Good call - sorry about that! In terms of the case corrector, no. It caused no end of mischief and I got rid of it a while ago.
ezgif-7-5a2dad6eda.gif
ezgif-7-5a2dad6eda.gif (44.13 KiB) Viewed 1246 times
kunkel321 wrote:
03 Jun 2024, 14:49
To get it to run, Descolada will need to comment-out 1 #Include near the bottom, and 5 icon references which are near each other. It does run after that. I can't seem to get the effect you are describing, though. You said space has to be pressed immediately after the wc, but does it reliably do the error, or is it only some of the time?

Edit: Hey are using Ntepa's CAse COrrector tool? If so, have you tried commenting that out, to see if it fixes things?
User avatar
kunkel321
Posts: 1293
Joined: 30 Nov 2015, 21:19

Re: Buffered hotstrings

03 Jun 2024, 15:09

Note also this
viewtopic.php?f=14&t=129476&p=570119#p570119
The topic was discussed in a thread... But now I can't find the discussion.

So maybe try upgrading to the latest AutoHotkey v2. Also see if changing O and/or * in the options makes a difference(?)

EDIT: Here's that discussion I was remembering
viewtopic.php?f=83&t=120220&p=570114&hilit=%3Asmith%3A%3ASmith#p570114
name := "ste(phen|ve) kunkel"
Descolada
Posts: 1431
Joined: 23 Dec 2021, 02:30

Re: Buffered hotstrings

06 Jun 2024, 21:40

@Jasonosaj try this variant that fixes the issue mentioned by kunkel321 (requires AutoHotkey v2.0.14+):

Code: Select all

#requires AutoHotkey v2
#MaxThreadsPerHotkey 10
#Hotstring ZXB0O

_HS(, "SE")

::wc::_HS("workers' compensation'")


/**
 * Sends a hotstring and buffers user keyboard input while sending, which means keystrokes won't
 * become interspersed or get lost. This requires that the hotstring has the X (execute) and B0 (no
 * backspacing) options enabled: these can be globally enabled with `#Hotstring XB0`
 * Note that mouse clicks *will* interrupt sending keystrokes.
 * @param replacement The hotstring to be sent. If no hotstring is provided then instead _HS options
 * will be modified according to the provided opts. 
 * @param opts Optional: hotstring options that will either affect all the subsequent _HS calls (if 
 * no replacement string was provided), or can be used to disable backspacing (`:B0:hs::hotstring` should 
 * NOT be used, correct is `_HS("hotstring", "B0")`). 
 * Additionally, differing from the default AHK hotstring syntax, the default option of backspacing
 * deletes only the non-matching end of the trigger string (compared to the replacement string).
 * Use the `BF` option to delete the whole trigger string. 
 * Also, using `Bn` backspaces only n characters and `B-n` leaves n characters from the beginning 
 * of the trigger string.
 * 
 * * Hotstring settings that modify the hotstring recognizer (eg Z, EndChars) must be changed with `#Hotstring`
 * * Hotstring settings that modify SendMode or speed must be changed with `_HS(, "opts")` or with hotstring
 *  local options such as `:K40:hs::hotstring`. In this case `#Hotstring` has no effect.
 * * O (omit EndChar) argument default option needs to be changed with `_HS(, "O")` AND with `#Hotstring O`.
 * 
 * Note that if changing global settings then the SendMode will be reset to InputThenEvent if no SendMode is provided.
 * SendMode can only be changed with this (`#Hotstring SE` has no effect).
 * @param sendFunc Optional: this can be used to define a default custom send function (if replacement
 * is left empty), or temporarily use a custom function. This could, for example, be used to send
 * via the Clipboard. This only affects sending the replacement text: backspacing and sending the 
 * ending character is still done with the normal Send function.
 * @returns {void} 
 */
_HS(replacement?, opts?, sendFunc?) {
    static HSInputBuffer := InputBuffer(), DefaultOmit := false, DefaultSendMode := A_SendMode, DefaultKeyDelay := 0
        , DefaultTextMode := "", DefaultBS := 0xFFFFFFF0, DefaultCustomSendFunc := "", DefaultCaseConform := true
        , __Init := HotstringRecognizer.Start()
    ; Save global variables ASAP to avoid these being modified if _HS is interrupted
    local Omit, TextMode, PrevKeyDelay := A_KeyDelay, PrevKeyDurationPlay := A_KeyDurationPlay, PrevSendMode := A_SendMode
        , ThisHotkey := A_ThisHotkey, EndChar := A_EndChar, Trigger := RegExReplace(ThisHotkey, "^:[^:]*:",,,1)
        , ThisHotstring := SubStr(HotstringRecognizer.Content, -StrLen(Trigger)-StrLen(EndChar))

    ; Only options without replacement text changes the global/default options
    if !IsSet(replacement) {
        if IsSet(sendFunc)
            DefaultCustomSendFunc := sendFunc
        if IsSet(opts) {
            i := 1, opts := StrReplace(opts, " "), len := StrLen(opts)
            While i <= len {
                o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
                if o = "S" {
                    ; SendMode is reset if no SendMode is specifically provided
                    DefaultSendMode := o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : (i--, "Input")
                    i += 2
                    continue
                } else if o = "O"
                    DefaultOmit := o_next != "0"
                else if o = "*"
                    DefaultOmit := o_next != "0"
                else if o = "K" && RegExMatch(opts, "i)^[-0-9]+", &KeyDelay, i+1) {
                    i += StrLen(KeyDelay[0]) + 1, DefaultKeyDelay := Integer(KeyDelay[0])
                    continue
                } else if o = "T"
                    DefaultTextMode := o_next = "0" ? "" : "{Text}"
                else if o = "R"
                    DefaultTextMode := o_next = "0" ? "" : "{Raw}"
                else if o = "B" {
                    ++i, DefaultBS := RegExMatch(opts, "i)^[fF]|^[-0-9]+", &BSCount, i) ? (i += StrLen(BSCount[0]), BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
                    continue
                } else if o = "C"
                    DefaultCaseConform := o_next = "0" ? 1 : 0
                i += IsNumber(o_next) ? 2 : 1
            }
        }
        return
    }
    if !IsSet(replacement)
        return
    ; Musn't use Critical here, otherwise InputBuffer callbacks won't work
    ; Start capturing input for the rare case where keys are sent during options parsing
    HSInputBuffer.Start()

    TextMode := DefaultTextMode, BS := DefaultBS, Omit := DefaultOmit, CustomSendFunc := sendFunc ?? DefaultCustomSendFunc, CaseConform := DefaultCaseConform
    SendMode DefaultSendMode
    if InStr(DefaultSendMode, "Play")
        SetKeyDelay , DefaultKeyDelay, "Play"
    else
        SetKeyDelay DefaultKeyDelay

    ; The only opts currently accepted is "B" or "B0" to enable/disable backspacing, since this can't 
    ; be changed with local hotstring options
    if IsSet(opts) && InStr(opts, "B")
        BS := RegExMatch(opts, "i)[fF]|[-0-9]+", &BSCount) ? (BSCount[0] = "f" ? 0xFFFFFFFF : Integer(BSCount[0])) : 0xFFFFFFF0
    ; Load local hotstring options, but don't check for backspacing
    if RegExMatch(ThisHotkey, "^:([^:]+):", &opts) { 
        opts := StrReplace(opts[1], " "), i := 1, len := StrLen(opts)
        While i <= len {
            o := SubStr(opts, i, 1), o_next := SubStr(opts, i+1, 1)
            if o = "S" {
                SendMode(o_next = "E" ? "Event" : o_next = "I" ? "InputThenPlay" : o_next = "P" ? "Play" : "Input")
                i += 2
                continue
            } else if o = "O"
                Omit := o_next != "0"
            else if o = "*"
                Omit := o_next != "0"
            else if o = "K" && RegExMatch(opts, "[-0-9]+", &KeyDelay, i+1) {
                i += StrLen(KeyDelay[0]) + 1, KeyDelay := Integer(KeyDelay[0])
                if InStr(A_SendMode, "Play")
                    SetKeyDelay , KeyDelay, "Play"
                else
                    SetKeyDelay KeyDelay
                continue
            } else if o = "T"
                TextMode := o_next = "0" ? "" : "{Text}"
            else if o = "R"
                TextMode := o_next = "0" ? "" : "{Raw}"
            else if o = "C"
                CaseConform := o_next = "0" ? 1 : 0
            i += IsNumber(o_next) ? 2 : 1
        }
    }

    if CaseConform && ThisHotstring && IsUpper(SubStr(ThisHotstringLetters := RegexReplace(ThisHotstring, "\P{L}"), 1, 1), 'Locale') {
        if IsUpper(SubStr(ThisHotstringLetters, 2), 'Locale')
            replacement := StrUpper(replacement), Trigger := StrUpper(Trigger)
        else
            replacement := (BS < 0xFFFFFFF0 ? replacement : StrUpper(SubStr(replacement, 1, 1))) SubStr(replacement, 2), Trigger := StrUpper(SubStr(Trigger, 1, 1)) SubStr(Trigger, 2)
    }

    ; If backspacing is enabled, get the activation string length using Unicode character length 
    ; since graphemes need one backspace to be deleted but regular StrLen would report more than one
    if BS {
        MaxBS := StrLen(RegExReplace(Trigger, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_"))

        if BS = 0xFFFFFFF0 {
            BoundGraphemeCallout := GraphemeCallout.Bind(info := {CompareString: replacement, GraphemeLength:0, Pos:1})
            RegExMatch(Trigger, "s)((?:(?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X)(?CBoundGraphemeCallout)")
            BS := MaxBS - info.GraphemeLength, replacement := SubStr(replacement, info.Pos)
        } else
            BS := BS = 0xFFFFFFFF ? MaxBS : BS > 0 ? BS : MaxBS + BS
    }
    ; Send backspacing + TextMode + replacement string + optionally EndChar. SendLevel isn't changed
    ; because AFAIK normal hotstrings don't add the replacements to the end of the hotstring recognizer
    if TextMode || !CustomSendFunc
        Send((BS ? "{BS " BS "}" : "") TextMode replacement (Omit ? "" : (TextMode ? EndChar : "{Raw}" EndChar)))
    else {
        Send((BS ? "{BS " BS "}" : ""))
        CustomSendFunc(replacement)
        if !Omit ; This could also be send with CustomSendFunc, but some programs (eg Chrome) sometimes trim spaces/tabs
            Send("{Raw}" EndChar)
    }
    ; Reset the recognizer, so the next step will be captured by it
    HotstringRecognizer.Reset()
    ; Release the buffer, but restore Send settings *after* it (since it also uses Send)
    HSInputBuffer.Stop()
    if InStr(A_SendMode, "Play")
        SetKeyDelay , PrevKeyDurationPlay, "Play"
    else
        SetKeyDelay PrevKeyDelay
    SendMode PrevSendMode

    GraphemeCallout(info, m, *) => SubStr(info.CompareString, info.Pos, len := StrLen(m[0])) == m[0] ? (info.Pos += len, info.GraphemeLength++, 1) : -1
}

/**
 * Mimics the internal hotstring recognizer as close as possible. It is *not* automatically
 * cleared if a hotstring is activated, as AutoHotkey doesn't provide a way to do that. 
 * 
 * Properties:
 * HotstringRecognizer.Content  => the current content of the recognizer
 * HotstringRecognizer.Length   => length of the content string
 * HotstringRecognizer.IsActive => whether HotstringRecognizer is active or not
 * HotstringRecognizer.MinSendLevel => minimum SendLevel that gets captured
 * HotstringRecognizer.ResetKeys    => gets or sets the keys that reset the recognizer (by default the arrow keys, Home, End, Next, Prior)
 * HotstringRecognizer.OnChange     => can be set to a callback function that is called when the recognizer content changes.
 *      The callback receives two arguments: Callback(OldContent, NewContent)
 * 
 * Methods:
 * HotstringRecognizer.Start()  => starts capturing hotstring content
 * HotstringRecognizer.Stop()   => stops capturing
 * HotstringRecognizer.Reset()  => clears the content and resets the internal foreground window
 * 
 */
class HotstringRecognizer {
    static Content := "", Length := 0, IsActive := 0, OnChange := 0, __ResetKeys := "{Left}{Right}{Up}{Down}{Next}{Prior}{Home}{End}"
        , __hWnd := DllCall("GetForegroundWindow", "ptr"), __Hook := 0
    static GetHotIfIsActive(*) => this.IsActive

    static __New() {
        this.__Hook := InputHook("V L0 I" A_SendLevel)
        this.__Hook.KeyOpt(this.__ResetKeys "{Backspace}", "N")
        this.__Hook.OnKeyDown := this.Reset.Bind(this)
        this.__Hook.OnChar := this.__AddChar.Bind(this)
        Hotstring.DefineProp("Call", {Call:this.__Hotstring.Bind(this)})
        ; These two throw critical recursion errors if defined with the normal syntax and AHK is ran in debugging mode
        HotstringRecognizer.DefineProp("MinSendLevel", {
            set:((hook, this, value, *) => hook.MinSendLevel := value).Bind(this.__Hook), 
            get:((hook, *) => hook.MinSendLevel).Bind(this.__Hook)})
        HotstringRecognizer.DefineProp("ResetKeys", 
            {set:((this, dummy, value, *) => (this.__ResetKeys := value, this.__Hook.KeyOpt(this.__ResetKeys, "N"), Value)).Bind(this), 
            get:((this, *) => this.__ResetKeys).Bind(this)})
    }

    static Start() {
        this.Reset()
        if !this.HasProp("__HotIfIsActive") {
            this.__HotIfIsActive := this.GetHotIfIsActive.Bind(this)
            Hotstring("MouseReset", Hotstring("MouseReset")) ; activate or deactivate the relevant mouse hooks
        }
        this.__Hook.Start()
        this.IsActive := 1
    }
    static Stop() => (this.__Hook.Stop(), this.IsActive := 0)
    static Reset(ih:=0, vk:=0, *) => (vk = 8 ? this.__SetContent(SubStr(this.Content, 1, -1)) : this.__SetContent(""), this.Length := 0, this.__hWnd := DllCall("GetForegroundWindow", "ptr"))

    static __AddChar(ih, char) {
        hWnd := DllCall("GetForegroundWindow", "ptr")
        if this.__hWnd != hWnd
            this.__hWnd := hwnd, this.__SetContent("")  
        this.__SetContent(this.Content char), this.Length += 1
        if this.Length > 100
            this.Length := 50, this.Content := SubStr(this.Content, 52)
    }
    static __MouseReset(*) {
        if Hotstring("MouseReset")
            this.Reset()
    }
    static __Hotstring(BuiltInFunc, arg1, arg2?, arg3*) {
        switch arg1, 0 {
            case "MouseReset":
                if IsSet(arg2) {
                    HotIf(this.__HotIfIsActive)
                    if arg2 {
                        Hotkey("~*LButton", this.__MouseReset.Bind(this))
                        Hotkey("~*RButton", this.__MouseReset.Bind(this))  
                    } else {
                        Hotkey("~*LButton")
                        Hotkey("~*RButton")  
                    }
                    HotIf()
                }
            case "Reset":
                this.Reset()
        }
        return (Func.Prototype.Call)(BuiltInFunc, arg1, arg2?, arg3*)
    }
    static __SetContent(Value) {
        if this.OnChange && HasMethod(this.OnChange) && this.Content !== Value
            SetTimer(this.OnChange.Bind(this.Content, Value), -1)
        this.Content := Value
    }
}

/**
 * InputBuffer can be used to buffer user input for keyboard, mouse, or both at once. 
 * The default InputBuffer (via the main class name) is keyboard only, but new instances
 * can be created via InputBuffer().
 * 
 * InputBuffer(keybd := true, mouse := false, timeout := 0)
 *      Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default 
 *      InputHook settings are used, and if they are a string then they are used as the Option 
 *      arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
 *      InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
 * 
 * InputBuffer.Start()               => initiates capturing input
 * InputBuffer.Release()             => releases buffered input and continues capturing input
 * InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
 * InputBuffer.ActiveCount           => current number of Start() calls
 *                                      Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
 * InputBuffer.SendLevel             => SendLevel of the InputHook
 *                                      InputBuffers default capturing SendLevel is A_SendLevel+2, 
 *                                      and key release SendLevel is A_SendLevel+1.
 * InputBuffer.IsReleasing           => whether Release() is currently in action
 * InputBuffer.Buffer                => current buffered input in an array
 * 
 * Notes:
 * * Mouse input can't be buffered while AHK is doing something uninterruptible (eg busy with Send)
 */
class InputBuffer {
    Buffer := [], SendLevel := A_SendLevel + 2, ActiveCount := 0, IsReleasing := 0, ModifierKeyStates := Map()
        , MouseButtons := ["LButton", "RButton", "MButton", "XButton1", "XButton2", "WheelUp", "WheelDown"]
        , ModifierKeys := ["LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LWin", "RWin"]
    static __New() => this.DefineProp("Default", {value:InputBuffer()})
    static __Get(Name, Params) => this.Default.%Name%
    static __Set(Name, Params, Value) => this.Default.%Name% := Value
    static __Call(Name, Params) => this.Default.%Name%(Params*)
    __New(keybd := true, mouse := false, timeout := 0) {
        if !keybd && !mouse
            throw Error("At least one input type must be specified")
        this.Timeout := timeout
        this.Keybd := keybd, this.Mouse := mouse
        if keybd {
            if keybd is String {
                if RegExMatch(keybd, "i)I *(\d+)", &lvl)
                    this.SendLevel := Integer(lvl[1])
            }
            this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 B0")
            this.InputHook.NotifyNonText  := true
            this.InputHook.VisibleNonText := false
            this.InputHook.OnKeyDown      := this.BufferKey.Bind(this,,,, "Down")
            this.InputHook.OnKeyUp        := this.BufferKey.Bind(this,,,, "Up")
            this.InputHook.KeyOpt("{All}", "N S")
        }
        this.HotIfIsActive := this.GetActiveCount.Bind(this)
    }
    BufferMouse(ThisHotkey, Opts := "") {
        savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen")
        MouseGetPos(&X, &Y)
        ThisHotkey := StrReplace(ThisHotkey, "Button")
        this.Buffer.Push(Format("{Click {1} {2} {3} {4}}", X, Y, ThisHotkey, Opts))
        CoordMode("Mouse", savedCoordMode)
    }
    BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
    Start() {
        this.ActiveCount += 1
        SetTimer(this.Stop.Bind(this), -this.Timeout)

        if this.ActiveCount > 1
            return

        this.Buffer := [], this.ModifierKeyStates := Map()
        for modifier in this.ModifierKeys
            this.ModifierKeyStates[modifier] := GetKeyState(modifier)

        if this.Keybd
            this.InputHook.Start()
        if this.Mouse {
            HotIf this.HotIfIsActive 
            if this.Mouse is String && RegExMatch(this.Mouse, "i)I *(\d+)", &lvl)
                this.SendLevel := Integer(lvl[1])
            opts := this.Mouse is String ? this.Mouse : ("I" this.SendLevel)
            for key in this.MouseButtons {
                if InStr(key, "Wheel")
                    HotKey key, this.BufferMouse.Bind(this), opts
                else {
                    HotKey key, this.BufferMouse.Bind(this,, "Down"), opts
                    HotKey key " Up", this.BufferMouse.Bind(this), opts
                }
            }
            HotIf ; Disable context sensitivity
        }
    }
    Release() {
        if this.IsReleasing || !this.Buffer.Length
            return []

        sent := [], clickSent := false, this.IsReleasing := 1
        if this.Mouse
            savedCoordMode := A_CoordModeMouse, CoordMode("Mouse", "Screen"), MouseGetPos(&X, &Y)

        ; Theoretically the user can still input keystrokes between ih.Stop() and Send, in which case
        ; they would get interspersed with Send. So try to send all keystrokes, then check if any more 
        ; were added to the buffer and send those as well until the buffer is emptied. 
        PrevSendLevel := A_SendLevel
        SendLevel this.SendLevel - 1

        ; Restore the state of any modifier keys before input buffering was started
        modifierList := ""
        for modifier, state in this.ModifierKeyStates
            if GetKeyState(modifier) != state
                modifierList .= "{" modifier (state ? " Down" : " Up") "}"
        if modifierList
            Send modifierList

        while this.Buffer.Length {
            key := this.Buffer.RemoveAt(1)
            sent.Push(key)
            if InStr(key, "{Click ")
                clickSent := true
            Send("{Blind}" key)
        }
        SendLevel PrevSendLevel

        if this.Mouse && clickSent {
            MouseMove(X, Y)
            CoordMode("Mouse", savedCoordMode)
        }
        this.IsReleasing := 0
        return sent
    }
    Stop(release := true) {
        if !this.ActiveCount
            return

        sent := release ? this.Release() : []

        if --this.ActiveCount
            return

        if this.Keybd
            this.InputHook.Stop()

        if this.Mouse {
            HotIf this.HotIfIsActive 
            for key in this.MouseButtons
                HotKey key, "Off"
            HotIf ; Disable context sensitivity
        }

        return sent
    }
    GetActiveCount(HotkeyName) => this.ActiveCount
}

Return to “Scripts and Functions (v2)”

Who is online

Users browsing this forum: No registered users and 35 guests