0

I want to create a tool which will allow me to use some of the Vim-style commands in an application (Scrivener) that does not support it.

For example, if

  • the current mode is Command mode and
  • the user presses the button w,

then, the caret should move one character to the right. Instead of the w character, Scrivener should receive the "right arrow" signal.

To implement this, I wrote the following code (based on these 2 answers: 1, 2):

from pynput.keyboard import Key, Listener, Controller
from typing import Optional
from ctypes import wintypes, windll, create_unicode_buffer

def getForegroundWindowTitle() -> Optional[str]:
    hWnd = windll.user32.GetForegroundWindow()
    length = windll.user32.GetWindowTextLengthW(hWnd)
    buf = create_unicode_buffer(length + 1)
    windll.user32.GetWindowTextW(hWnd, buf, length + 1)
    if buf.value:
        return buf.value
    else:
        return None

class State:
    def __init__(self):
        self.mode = "Command"

state = State()
keyboard = Controller()

def on_press(key):
    pass

def on_release(key):
    if key == Key.f12:
        return False
    window_title = getForegroundWindowTitle()
    if not window_title.endswith("Scrivener"):
        return
    print("Mode: " + state.mode)
    print('{0} release'.format(
        key))
    if state.mode == "Command":
        print("1")
        if str(key) == "'w'":
            print("2")
            print("w released in command mode")
            # Press the backspace button to delete the w letter
            keyboard.press(Key.backspace)
            # Press the right arrow button
            keyboard.press(Key.right)
    if key == Key.insert:
        if state.mode == "Command":
            state.mode = "Insert"
        else:
            state.mode = "Command"

# Collect events until released
print("Press F12 to exit")

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

Whenever I press the button w in Scrivener in Command mode, two keystrokes are sent to Scrivener:

  1. Backspace to delete the already typed w character.
  2. Right arrow to move the caret.

This kinda works, but you can see the w character being displayed and deleted again (see this video).

How can I make sure that the keystroke with w does not reach Scrivener at all, if the mode is Command and currently focused window is the Scrivener application?

Glory to Russia
  • 17,289
  • 56
  • 182
  • 325
  • A (low-level) keyboard hook can filter out input, and prevent it from being passed to the foreground thread. Even if you get that sorted out using pynput, you're still going to have to deal with *fundamental* flaws of this solution (e.g. ensuring that the internal state actually matches the target application's mode, or properly handling keyboard input like Ctrl+Insert or Shift+w). – IInspectable Jul 11 '20 at 21:04
  • @IInspectable Re *internal state actually matches the target application's mode*: The target application does not have a mode. The mode exists only inside my Python application – Glory to Russia Jul 12 '20 at 08:25

1 Answers1

2

First you need to install pyHook and pywin32 library.

Then monitor the keyboard information through pyhook. If you need to intercept the keyboard information (for example, press w), return False.

Finally, through pythoncom.PumpMessages () to achieve loop monitoring. Here is the sample:

import pyHook
import pythoncom
from pynput.keyboard import Key, Listener, Controller
keyboard = Controller()
def onKeyboardEvent(event):
    if event.Key == "F12":
        exit()
    print("1")
    if event.Key == 'W':
        print("2")
        print("w released in command mode")
        # Press the right arrow button
        keyboard.press(Key.right)
        return False

    print("hook" + event.Key)
    return True

# Collect events until released
print("Press F12 to exit")
hm = pyHook.HookManager()
hm.KeyDown = onKeyboardEvent
hm.HookKeyboard()
pythoncom.PumpMessages() 
Zeus
  • 3,703
  • 3
  • 7
  • 20