import dataclasses as dc
import functools
from typing import Callable, Union
from . import hotkey_context
from .flow import ahk_call, _wrap_callback
from .sending import _get_send_mode
__all__ = [
"Hotstring",
"get_hotstring_end_chars",
"get_hotstring_mouse_reset",
"reset_hotstring",
"set_hotstring_end_chars",
"set_hotstring_mouse_reset",
]
[docs]def hotstring(
ctx,
trigger: str,
repl: Union[str, Callable] = None,
*args,
case_sensitive=False,
conform_to_case=True,
replace_inside_word=False,
wait_for_end_char=True,
omit_end_char=False,
backspacing=True,
priority=0,
text=False,
mode=None,
key_delay=None,
reset_recognizer=False,
):
"""hotstring(trigger: str, repl: Union[str, Callable] = None, *args, **options)
Register a hotstring.
By default, the hotstring is triggered when the user types the given
*trigger* text and presses one of the end chars which initially consist of
the following: ``-()[]{}':;"/\\,.?!\\n \\t``. If *repl* is an instance of
:class:`str`, the user's input will be replaced with *repl*. If *repl* is a
callable, it will be called when the hotstring is triggered.
When the hotstring is triggered and *repl* is a callable, *repl* is called
with the :class:`Hotstring` instance as the *hotstring* argument if the
function supports it.
The optional positional *args* will be passed to the *repl* when it is
called. If you want the *repl* to be called with keyword arguments use
:func:`functools.partial`.
To change the end chars use the :func:`set_hotstring_end_chars` function.
The following keyword-only arguments set the hotstring *options*:
- **case_sensitive** – if true, the user must type the text with the exact
case to trigger the hotstring. Defaults to ``False``.
- **conform_to_case** – if false, the replacement is typed exactly as given
in *repl*. Otherwise, the following rules apply:
- If the user types the trigger text in all caps, the replacement text is
produced in all caps.
- If the user types the first letter in caps, the first letter of the
replacement is also capitalized.
- If the user types the case in any other way, the replacement is produced
exactly as given in *repl*.
Defaults to ``True`` for case-insensitive hotstrings. Conversely,
case-sensitive hotstrings never conform to the case of the trigger text.
- **replace_inside_word** – if true, the hotstring will be triggered even
when it is inside another word; that is, when the character typed
immediately before it is alphanumeric::
ahkpy.hotstring("al", "airline",
replace_inside_word=True)
Given the code above, typing "practical " produces "practicairline ".
Defaults to ``False``.
- **wait_for_end_char** – if false, an `end chars
<#ahkpy.get_hotstring_end_chars>`_ is not required to trigger the
hotstring. Defaults to ``True``.
- **omit_end_char** – if true and *wait_for_end_char* is true, then the
hotstring waits for the user to type an end char and produces the
replacement with the end char omitted. Defaults to ``False``.
- **backspacing** – if false, skips removing the user input that triggered
the hotstring before producing the *repl*::
ahkpy.hotstring("<em>", "</em>{Left 5}",
wait_for_end_char=False,
backspacing=False)
Given the code above, typing ``<em>`` produces ``<em>|</em>``, where ``|``
is the keyboard cursor.
Defaults to ``True``.
- **priority** (:class:`int`) – the priority of the `AHK thread
<https://www.autohotkey.com/docs/misc/Threads.htm>`_ where *repl* will be
executed if it's a callable. It must be an integer between -2147483648 and
2147483647. Defaults to 0.
- **text** – if true, sends the replacement text raw, without translating
each character to a keystroke. For details, see `Text mode
<https://www.autohotkey.com/docs/commands/Send.htm#SendText>`_. Defaults
to ``False``.
- **mode** – the method by which auto-replace hotstrings send their
keystrokes. Defaults to one currently set in :attr:`Settings.send_mode`.
For the list of valid modes refer to :func:`~ahkpy.send`.
- **key_delay** (:class:`float`) – the delay between keystrokes produced by
auto-backspacing and auto-replacement. Defaults to 0 for Event and Play
modes. For more information refer to :func:`~ahkpy.send`.
- **reset_recognizer** – if true, resets the hotstring recognizer after each
triggering of the hotstring. To illustrate, consider the following
hotstring::
@ahkpy.hotstring(
"11",
backspacing=False,
wait_for_end_char=False,
replace_inside_word=True,
)
def eleven():
ahkpy.send_event("xx", level=0)
Since the above lacks the *reset_recognizer* option, typing ``111`` (three
consecutive 1's) triggers the hotstring twice because the middle 1 is the
*last* character of the first triggering but also the first character of
the second triggering. By setting *reset_recognizer* to ``True``, you
would have to type four 1's instead of three to trigger the hotstring
twice.
Defaults to ``False``.
If *repl* is given, returns an instance of :class:`Hotstring`.
Otherwise, the method works as a decorator.
:command: `Hotstring
<https://www.autohotkey.com/docs/commands/Hotstring.htm>`_
.. TODO: Review this docstring.
"""
def hotstring_decorator(repl):
if callable(repl) and args:
repl = functools.partial(repl, *args)
nonlocal mode, key_delay
mode = _get_send_mode(mode, key_delay)
if key_delay is None and mode != "input":
# Wanted to use Settings.key_delay for default, but zero delay
# is a more suitable default for hotstrings.
key_delay = 0
hs = Hotstring(trigger, case_sensitive, replace_inside_word, context=ctx)
hs.update(
repl=repl,
conform_to_case=conform_to_case,
wait_for_end_char=wait_for_end_char,
omit_end_char=omit_end_char,
backspacing=backspacing,
priority=priority,
text=text,
mode=mode,
key_delay=key_delay,
reset_recognizer=reset_recognizer,
)
# Enable the hotstring in case another hotstring with the same
# 'string' existed before, but was disabled.
hs.enable()
return hs
if repl is None:
return hotstring_decorator
return hotstring_decorator(repl)
[docs]@dc.dataclass(frozen=True)
class Hotstring:
"""Hotstring(trigger: str, case_sensitive: bool, replace_inside_word: bool, context: ahkpy.HotkeyContext)
The immutable object that represents a registered hotstring.
Creating an instance of :class:`!Hotstring` doesn't register it in AHK. Use
the :meth:`HotkeyContext.hotstring` method instead.
Hotstrings in AutoHotkey are defined by the trigger string,
case-sensitivity, word-sensitivity (*replace_inside_word*), and the context
in which they are created. For example, the following creates only one
hotstring::
hs1 = ahkpy.hotstring("btw", "by the way", case_sensitive=False)
hs2 = ahkpy.hotstring("BTW", "by the way", case_sensitive=False)
This is because *case_sensitive* option is set to ``False`` (the default),
so that the hotstring will trigger regardless of the case it was typed in.
Conversely, the following creates two separate hotstrings::
hs1 = ahkpy.hotstring("btw", "by the way", case_sensitive=True)
hs2 = ahkpy.hotstring("BTW", "by the way", case_sensitive=True)
"""
trigger: str
case_sensitive: bool
replace_inside_word: bool
context: 'hotkey_context.HotkeyContext'
__slots__ = ("trigger", "case_sensitive", "replace_inside_word", "context")
# There are no 'repl' and option fields in Hotstring object. See the
# reasoning in the Hotkey class.
# Case sensitivity and conformity transitions:
# CO <-> C1
# C1 <-- C --> C0
def __post_init__(self):
if hasattr(self.trigger, "lower") and not self.case_sensitive:
object.__setattr__(self, "trigger", self.trigger.lower())
[docs] def enable(self):
"""Enable the hotkey."""
with self.context._manager():
ahk_call("Hotstring", f":{self._id_options()}:{self.trigger}", "", "On")
[docs] def disable(self):
"""Disable the hotkey."""
with self.context._manager():
ahk_call("Hotstring", f":{self._id_options()}:{self.trigger}", "", "Off")
[docs] def toggle(self):
"""Enable the hotstring if it's disabled or do the opposite."""
with self.context._manager():
ahk_call("Hotstring", f":{self._id_options()}:{self.trigger}", "", "Toggle")
def _id_options(self):
case_option = "C" if self.case_sensitive else ""
replace_inside_option = "?" if self.replace_inside_word else "?0"
return f"{case_option}{replace_inside_option}"
[docs] def update(
self, *, repl=None, conform_to_case=None, wait_for_end_char=None, omit_end_char=None, backspacing=None,
priority=None, text=None, mode=None, key_delay=None, reset_recognizer=None,
):
"""Update the hotstring's *repl* and options.
For more information about the arguments refer to
:meth:`HotkeyContext.hotstring`.
"""
if callable(repl):
repl = _wrap_callback(
repl,
("hotstring",),
_bare_hotstring_handler,
functools.partial(_hotstring_handler, hotstring=self),
)
options = []
if self.case_sensitive:
options.append("C")
elif conform_to_case:
options.append("C0")
elif conform_to_case is not None:
options.append("C1")
if self.replace_inside_word:
options.append("?")
else:
options.append("?0")
if wait_for_end_char is False:
options.append("*")
elif omit_end_char:
options.append("*0")
options.append("O")
else:
if wait_for_end_char:
options.append("*0")
if omit_end_char is False:
options.append("O0")
if backspacing:
options.append("B")
elif backspacing is not None:
options.append("B0")
if key_delay is not None:
if key_delay > 0:
key_delay = int(key_delay * 1000)
options.append(f"K{key_delay}")
if priority is not None:
options.append(f"P{priority}")
if text:
options.append("T")
elif text is not None:
options.append("T0")
if mode == "input":
options.append("SI")
elif mode == "play":
options.append("SP")
elif mode == "event":
options.append("SE")
elif mode is not None:
raise ValueError(f"{mode!r} is not a valid send mode")
if reset_recognizer:
options.append("Z")
elif reset_recognizer is not None:
options.append("Z0")
option_str = "".join(options)
with self.context._manager():
ahk_call("Hotstring", f":{option_str}:{self.trigger}", repl)
def _bare_hotstring_handler(func):
func()
def _hotstring_handler(func, hotstring):
func(hotstring=hotstring)
[docs]def reset_hotstring():
"""Reset the hotstring recognizer.
The script will begin waiting for an entirely new hotstring, eliminating
from consideration anything the user has typed previously.
:command: `Hotstring("Reset")
<https://www.autohotkey.com/docs/commands/Hotstring.htm#Reset>`_
"""
ahk_call("Hotstring", "Reset")
[docs]def get_hotstring_end_chars():
"""Retrieve the set of end chars.
The default end chars are the following: ``-()[]{}':;"/\\,.?!\\n \\t``.
:command: `Hotstring("EndChars")
<https://www.autohotkey.com/docs/commands/Hotstring.htm#EndChars>`_
"""
return ahk_call("Hotstring", "EndChars")
[docs]def set_hotstring_end_chars(chars: str):
"""Change the end chars.
The end chars can only be changed globally for all hostrings at once.
:command: `Hotstring("EndChars", NewValue)
<https://www.autohotkey.com/docs/commands/Hotstring.htm#EndChars>`_
"""
ahk_call("Hotstring", "EndChars", str(chars))
[docs]def get_hotstring_mouse_reset():
"""Get whether mouse clicks reset the hotstring recognizer.
By default, any click of the left or right mouse button will reset the
hotstring recognizer.
:command: `Hotstring("MouseReset")
<https://www.autohotkey.com/docs/commands/Hotstring.htm#MouseReset>`_
"""
return ahk_call("Hotstring", "MouseReset")
[docs]def set_hotstring_mouse_reset(value: bool):
"""Set whether mouse clicks reset the hotstring recognizer.
:command: `Hotstring("MouseReset", NewValue)
<https://www.autohotkey.com/docs/commands/Hotstring.htm#MouseReset>`_
"""
ahk_call("Hotstring", "MouseReset", bool(value))