JigsawWM icon indicating copy to clipboard operation
JigsawWM copied to clipboard

App changes text discovered by other applications

Open pchomik opened this issue 11 months ago • 8 comments

Hello,

First, big thanks for your work and this application. I was looking for tiling windows manager which is natural for me and I found :D

My environment:

  • Python 3.13
  • master source code

My configuration:

  • Only hotkeys

After launching application instant text replacement implemented via AutoHotKeys application stopped working. To discover issue I used following python code:

# text_replacer.py
from pynput.keyboard import Key, Listener
import pyautogui

replacements = {"#teststring": "Add new comment"}


def on_press(key):
    global typed_keys
    global listening
    key_str = str(key).replace("'", "")
    print(key_str)

    if key_str == macro_starter:
        typed_keys = []
        listening = True

    if listening:
        if key_str.isalpha():
            typed_keys.append(key_str)

        if key == macro_ender:
            candidate_keyword = ""
            candidate_keyword = candidate_keyword.join(typed_keys)
            if candidate_keyword != "":
                if candidate_keyword in replacements.keys():
                    pyautogui.press("backspace", presses=len(candidate_keyword) + 2)
                    pyautogui.typewrite(replacements[candidate_keyword])
                    listening = False


macro_starter = "#"
macro_ender = Key.space
listening = True
typed_keys = []

with Listener(on_press=on_press) as listener:
    listener.join()

Testing code received string ##tteessttssttrriinngg instead of #teststring. I changed my AutoHotKeys script to repeat twice all letters and is working again. Looks like your application changing input for applications like AutoHotKeys.

Could you look into this issue or at least elaborate how debug this issue? I know python and I can help but I'm still learning your project.

Best Regards

pchomik avatar Jan 29 '25 08:01 pchomik

I have also issue to type ~ char when application is running. Probably root cause is the same.

pchomik avatar Jan 29 '25 13:01 pchomik

I enabled DEBUG level and single key generates following logs:

2025-01-29 14:34:09,070 [jigsawwm.jmk.sysinout] [ThreadPoolEx] [DEBUG]  sys >>> JmkEvent(S, down, sys, 0, 0)
2025-01-29 14:34:09,070 [jigsawwm.jmk.hotkey] [ThreadPoolEx] [DEBUG]  JmkEvent(S, down, sys, 0, 0) >>> hotkey
2025-01-29 14:34:09,071 [jigsawwm.jmk.hotkey] [ThreadPoolEx] [DEBUG]  current pressed keys: {<Vk.S: 83>}
2025-01-29 14:34:09,071 [jigsawwm.jmk.sysinout] [ThreadPoolEx] [DEBUG]  JmkEvent(S, down, sys, 0, 0) >>> sys
2025-01-29 14:34:09,071 [jigsawwm.jmk.sysinout] [MainThread  ] [DEBUG]  synthesized event KBDLLHOOKDATA(vkCode=83, scanCode=31, flags=16, time=2336237125, dwExtraInfo=272760864), skipping
2025-01-29 14:34:09,144 [jigsawwm.jmk.sysinout] [ThreadPoolEx] [DEBUG]  sys >>> JmkEvent(S, up, sys, 128, 0)
2025-01-29 14:34:09,144 [jigsawwm.jmk.hotkey] [ThreadPoolEx] [DEBUG]  JmkEvent(S, up, sys, 128, 0) >>> hotkey
2025-01-29 14:34:09,145 [jigsawwm.jmk.hotkey] [ThreadPoolEx] [DEBUG]  current pressed keys: {<Vk.S: 83>}
2025-01-29 14:34:09,145 [jigsawwm.jmk.sysinout] [ThreadPoolEx] [DEBUG]  JmkEvent(S, up, sys, 128, 0) >>> sys
2025-01-29 14:34:09,145 [jigsawwm.jmk.sysinout] [MainThread  ] [DEBUG]  synthesized event KBDLLHOOKDATA(vkCode=83, scanCode=31, flags=144, time=2336237187, dwExtraInfo=272760864), skipping

pchomik avatar Jan 29 '25 13:01 pchomik

Looks like following code in jmk/sysinout.py fixed the issue of duplicate events:

    def input_event(
        self,
        _code: int,
        msgid: Union[hook.KBDLLHOOKMSGID, hook.MSLLHOOKMSGID],
        msg: Union[hook.KBDLLHOOKDATA, hook.MSLLHOOKDATA],
    ) -> bool:
        """Handles keyboard events and call callback if the combination
        had been registered
        """
        if self.is_running is False:
            return False
        if is_synthesized(msg):
            logger.info("synthesized event %s, skipping", msg)
            return False
        # Check for duplicate events using dwExtraInfo
        if hasattr(self, 'last_dwExtraInfo') and self.last_dwExtraInfo == msg.dwExtraInfo:
            logger.info("duplicate event %s, skipping", msg)
            return False
        self.last_dwExtraInfo = msg.dwExtraInfo

        # convert keyboard/mouse event to a unified virtual key representation
        vkey, pressed = None, None
        if isinstance(msgid, hook.KBDLLHOOKMSGID):
            vkey = Vk(msg.vkCode)
            if vkey == Vk.PACKET:
                return False
            if msgid == hook.KBDLLHOOKMSGID.WM_KEYDOWN:
                pressed = True
            elif msgid == hook.KBDLLHOOKMSGID.WM_KEYUP:
                pressed = False
            else:
                return False
        elif isinstance(msgid, hook.MSLLHOOKMSGID):
            if msgid == hook.MSLLHOOKMSGID.WM_LBUTTONDOWN:
                vkey = Vk.LBUTTON
                pressed = True
            elif msgid == hook.MSLLHOOKMSGID.WM_LBUTTONUP:
                vkey = Vk.LBUTTON
                pressed = False
            elif msgid == hook.MSLLHOOKMSGID.WM_RBUTTONDOWN:
                vkey = Vk.RBUTTON
                pressed = True
            elif msgid == hook.MSLLHOOKMSGID.WM_RBUTTONUP:
                vkey = Vk.RBUTTON
                pressed = False
            elif msgid == hook.MSLLHOOKMSGID.WM_MBUTTONDOWN:
                vkey = Vk.MBUTTON
                pressed = True
            elif msgid == hook.MSLLHOOKMSGID.WM_MBUTTONUP:
                vkey = Vk.MBUTTON
                pressed = False
            elif msgid == hook.MSLLHOOKMSGID.WM_XBUTTONDOWN:
                vkey = Vk.XBUTTON1 if msg.hiword() == 1 else Vk.XBUTTON2
                pressed = True
            elif msgid == hook.MSLLHOOKMSGID.WM_XBUTTONUP:
                vkey = Vk.XBUTTON1 if msg.hiword() == 1 else Vk.XBUTTON2
                pressed = False
            elif msgid == hook.MSLLHOOKMSGID.WM_MOUSEWHEEL:
                delta = msg.get_wheel_delta()
                if delta > 0:
                    vkey = Vk.WHEEL_UP
                else:
                    vkey = Vk.WHEEL_DOWN
                pressed = False
        # skip events that out of our interest
        if vkey is None or pressed is None:
            return False
        self.enqueue(self.on_input, vkey, pressed, msg.flags, msg.dwExtraInfo)
        if self.disabled:
            logger.debug("disabled due to %s, skipping %s", self.disabled_reason, msg)
            return False

        return True

but I see some problems when AutoHotKeys would like to enter some text. Text is incorrectly generated.

pchomik avatar Jan 30 '25 08:01 pchomik

I designed the jmk module with replacing AHK in mind. They are not supposed to be running at the same time. What is your use case for AHK?

klesh avatar Feb 05 '25 02:02 klesh

I have two cases:

  1. Control media player via keyboard shortcuts
  2. Automatic text completion/replacement

pchomik avatar Feb 05 '25 07:02 pchomik

Thanks for replying. Can you elaborate how do you utilize AHK to address your use cases?

  1. It is hotkey remapping?
  2. How does text completion/replacement behave? Like, when you type "foo" it would replace it with "bar" by sending three backspaces and bar?

klesh avatar Feb 06 '25 03:02 klesh

  1. Looks like remapping. I did it like so:
^+PgDn::Media_Prev
^+PgUp::Media_Next
^+space::Media_Play_Pause
  1. I'm not sure but probably you are right. When JigsawWM is working not whole text is deleted and new text is wrongly formatted if text is long. This is example how to do that in AHK:
:*T:#test1::
{
    dts := FormatTime(, "yyyy.MM.dd")
    text := "*" . dts . ": Daily note:*`n" . "* First point as example"
    SendInput(text)
}

or

:*T:#test2::
(
  line 1
  -
  line 2
  -
  line 3
  -
  line 4
)

pchomik avatar Feb 06 '25 09:02 pchomik

@pchomik Thanks for the explanation!

  1. Hotkey Remapping:
    The jmk tool supports hotkey remapping. You can find an example here: GitHub link.

  2. Text Replacement (Workaround):
    While text replacement isn’t directly supported, I achieved a similar functionality using layered keys:

    1. Configure the tab key to activate layer 1 when held down. GitHub link
    2. Assign text-sending functions to specific keys within that layer. Example: GitHub link.
    3. Usage:
      • Hold the tab key and press t to insert today’s date.
      • Hold tab and press another key (e.g., time) to insert the current time, etc.

klesh avatar Feb 07 '25 03:02 klesh