pynput
pynput copied to clipboard
MacOS crash when starting pynput keyboard listener after qt6
Description If I create and start a listener AFTER launching a qt6 app (pyside6), the application crashes.
Platform and pynput version macOS Monterey (12.6), M1 Pro. Pynput 1.7.6, Pyside6 6.4.0, Python 3.10. I'm also using a Latin American keyboard layout.
To Reproduce https://github.com/alvaromunoz/pynput-macos-issues/blob/ba17e4b83f1eb239454d73ba96adf7f5fe71673b/nested_crash.py
import sys
from PySide6 import (
QtWidgets,
)
from pynput import keyboard
class MyListener():
def __init__(self, label: QtWidgets.QLabel):
def on_press(key):
label.setText("You pressed {0}".format(key))
def on_release(key):
label.setText("You released {0}".format(key))
self.listener = keyboard.Listener(
on_press=on_press,
on_release=on_release)
def start(self):
self.listener.start()
self.listener.wait()
print("listener has started? {0}".format(self.listener.running))
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Listener Demo 2")
central_widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
central_widget.setLayout(layout)
label_description = QtWidgets.QLabel("Press the button to crash")
layout.addWidget(label_description)
label = QtWidgets.QLabel("Press any key")
label.setEnabled(False)
layout.addWidget(label)
button = QtWidgets.QPushButton("Start listener!")
button.pressed.connect(self.start_listener)
layout.addWidget(button)
self.listener = MyListener(label)
self.setCentralWidget(central_widget)
def start_listener(self):
print("beep")
self.listener.start()
print("boop")
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
OUTPUT:
beep
zsh: trace trap
FYI: this happens with tkinter and pyqt6, too:
tkinter:
from tkinter import *
from pynput import keyboard
class MyListener():
def __init__(self):
def on_press(key):
print("You pressed {0}".format(key))
def on_release(key):
print("You released {0}".format(key))
self.listener = keyboard.Listener(
on_press=on_press,
on_release=on_release)
def start(self):
self.listener.start()
self.listener.wait()
print("listener has started? {0}".format(self.listener.running))
listener = MyListener()
root = Tk() # create parent window
def start_pynput():
print("beep")
listener.start()
print("boop")
# use Button and Label widgets to create a simple TV remote
button = Button(root, text="Start Pynput", command=start_pynput)
button.pack()
root.mainloop()
pyqt6
import sys
from PyQt6 import (
QtWidgets,
)
from pynput import keyboard
class MyListener():
def __init__(self, label: QtWidgets.QLabel):
def on_press(key):
label.setText("You pressed {0}".format(key))
def on_release(key):
label.setText("You released {0}".format(key))
self.listener = keyboard.Listener(
on_press=on_press,
on_release=on_release)
def start(self):
self.listener.start()
self.listener.wait()
print("listener has started? {0}".format(self.listener.running))
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Listener Demo 2")
central_widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
central_widget.setLayout(layout)
label_description = QtWidgets.QLabel("Press the button to crash")
layout.addWidget(label_description)
label = QtWidgets.QLabel("Press any key")
label.setEnabled(False)
layout.addWidget(label)
button = QtWidgets.QPushButton("Start listener!")
button.pressed.connect(self.start_listener)
layout.addWidget(button)
self.listener = MyListener(label)
self.setCentralWidget(central_widget)
def start_listener(self):
print("beep")
self.listener.start()
print("boop")
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Same here, pynput
works fine pyperclip
, PySide6
at the first place, but it shows error message as follow after pressing the button.
Error message: [1] 31161 trace trap /<path_to_current_folder/.venv/bin/python
working environment:
- macOS Monterey 12.6, M1 pro
- Python 3.9.13
- PySide6 6.4.0.1
- pyperclip 1.8.2
- pynput 1.7.6
here's the code
# -*- coding: utf-8 -*-
import random
import string
import time
import pyperclip
from pynput import keyboard
from PySide6 import QtCore
from PySide6 import QtWidgets
class Main(QtWidgets.QMainWindow):
signal = QtCore.Signal()
def __init__(self):
super().__init__()
def build(self):
self.signal.connect(self.get_text_from_clip)
self.button = QtWidgets.QPushButton()
self.button.clicked.connect(self.button_pressed)
self.keyboard_detect_start()
self.setCentralWidget(self.button)
self.show()
def keyboard_detect_start(self, key="<ctrl>+q"):
def for_canonical(hotkey_event):
return lambda key: hotkey_event(self.listener.canonical(key))
hotkey = keyboard.HotKey(keyboard.HotKey.parse(key), self.signal.emit)
self.listener = keyboard.Listener(
on_press=for_canonical(hotkey.press),
on_release=for_canonical(hotkey.release),
)
self.listener.start()
def get_text_from_clip(self):
controller = keyboard.Controller()
controller.press(keyboard.Key.cmd)
controller.press("c")
controller.release("c")
controller.release(keyboard.Key.cmd)
time.sleep(0.05)
print(pyperclip.paste().strip())
def button_pressed(self):
self.listener.stop()
self.listener.join()
time.sleep(0.5)
new_key = random.choices(string.ascii_lowercase)[0]
print("new_key: ", new_key)
self.keyboard_detect_start(key=f"<ctrl>+{new_key}")
if __name__ == "__main__":
app = QtWidgets.QApplication([])
main = Main()
main.build()
app.exec()
@ba361006 could you please check if #512 fixes your issue?
@alvaromunoz
Yes, #512 does fix this for me. It would be appreciated that if #512 can be merged :)
[Edited] 2022/11/22
With applying #512 , the following fail case still raises [1] 93758 trace trap <path_to_project>/.venv/bin/python
error, but somehow it works by invoking print_text_from_clipboard
via signal
So far I know is that the fail case
stop at controller = keyboard.Controller()
and raise the error mentioned above.
Firstly I thought maybe Mac will limit only one thread to have control of keyboard event, so I instantiate worker
and connect the signal
under Main
which should be at the same scope with the app
, and eventually it works.
But if I simply change the way of invoking print_text_from_clipboard
it fails, which is the only difference between fail and work case.
- Fail case
# -*- coding: utf-8 -*-
import time
import platform
import pyperclip
from pynput import keyboard
from PySide6 import QtCore
from PySide6 import QtWidgets
class Worker(QtCore.QObject):
def __init__(self):
super().__init__()
def run_method(self):
activate_key = "<ctrl>+q"
def for_canonical(hotkey_event):
return lambda key: hotkey_event(self.listener.canonical(key))
hotkey = keyboard.HotKey(keyboard.HotKey.parse(activate_key), self.print_text_from_clipboard)
self.listener = keyboard.Listener(
on_press=for_canonical(hotkey.press),
on_release=for_canonical(hotkey.release),
)
self.listener.start()
def print_text_from_clipboard(self):
# delay is needed before getting str from clip
if platform.system() == "Windows":
modifier = keyboard.Key.ctrl
elif platform.system() == "Darwin":
modifier = keyboard.Key.cmd
controller = keyboard.Controller()
controller.press(modifier)
controller.press("c")
controller.release("c")
controller.release(modifier)
time.sleep(0.05)
print("text: ", pyperclip.paste().strip())
class Main(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
def build(self):
self.worker = Worker()
self.button = QtWidgets.QPushButton()
self.button.clicked.connect(self.button_pressed)
self.setCentralWidget(self.button)
self.show()
def button_pressed(self):
self.worker.run_method()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
main = Main()
main.build()
app.exec()
- Work case
# -*- coding: utf-8 -*-
import time
import platform
import pyperclip
from pynput import keyboard
from PySide6 import QtCore
from PySide6 import QtWidgets
class Worker(QtCore.QObject):
signal = QtCore.Signal()
def __init__(self):
super().__init__()
def run_method(self):
activate_key = "<ctrl>+q"
def for_canonical(hotkey_event):
return lambda key: hotkey_event(self.listener.canonical(key))
hotkey = keyboard.HotKey(keyboard.HotKey.parse(activate_key), self.signal.emit)
self.listener = keyboard.Listener(
on_press=for_canonical(hotkey.press),
on_release=for_canonical(hotkey.release),
)
self.listener.start()
def print_text_from_clipboard(self):
# delay is needed before getting str from clip
if platform.system() == "Windows":
modifier = keyboard.Key.ctrl
elif platform.system() == "Darwin":
modifier = keyboard.Key.cmd
controller = keyboard.Controller()
controller.press(modifier)
controller.press("c")
controller.release("c")
controller.release(modifier)
time.sleep(0.05)
print("text: ", pyperclip.paste().strip())
class Main(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
def build(self):
self.worker = Worker()
self.worker.signal.connect(self.worker.print_text_from_clipboard)
self.button = QtWidgets.QPushButton()
self.button.clicked.connect(self.button_pressed)
self.setCentralWidget(self.button)
self.show()
def button_pressed(self):
self.worker.run_method()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
main = Main()
main.build()
app.exec()
Just found something interesting.
Without applying changes in #512, the following code works.
It seems that you can't invoking keyboard.Listener
via Signal
or clicked.connect
.
It reminds me that PyQt5
have some tricky stuff with QThread
related to scope problem, which means where you instantiate the QThread
or invoking moveToThread
to your customised QObject
class matters, but I can't recall the detail.
Does this code work for you? @alvaromunoz
# -*- coding: utf-8 -*-
from pynput import keyboard
from PySide6 import QtWidgets
class Main(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
def build(self):
def for_canonical(hotkey_event):
return lambda key: hotkey_event(self.listener.canonical(key))
hotkey = keyboard.HotKey(keyboard.HotKey.parse("<ctrl>+q"), lambda: print("keyboard detected"))
self.listener = keyboard.Listener(
on_press=for_canonical(hotkey.press),
on_release=for_canonical(hotkey.release),
)
self.listener.start()
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
main = Main()
main.build()
app.exec()
Same issue, MacBook Air M2 on MacOs Sonoma 14.0 Beta