Since the keyboard indicator widget cannot be run on my kali system, I decided to write one myself using pyqt. I found that it would be normal if I separated the program and ran it, but not with pyqt6. It runs normally on Windows, but a very strange problem occurs on Linux. Even if I keep pressing caps lock repeatedly, it still returns the same wrong value.
import subprocess
from time import sleep
while(True):
print(subprocess.run("xset q | grep \"Caps Lock\" | awk -F': ' '{gsub(/[0-9]/,\"\",$3); print $3}'",
stdout=subprocess.PIPE,
shell=True,
text=True).stdout.strip() == 'on')
sleep(0.3)
# pip install PyQt6 pynput
from platform import system
from sys import argv, exit
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPalette, QColor, QFont
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from pynput import keyboard
class CapsLockDetector(QMainWindow):
def __init__(self):
super().__init__()
self.status_label = None
self.initUI()
self.setupKeyboardHook()
def initUI(self):
self.setWindowTitle('Caps Lock Detector')
self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint)
self.setGeometry(0, 0, 400, 120)
palette = self.palette()
palette.setColor(QPalette.ColorRole.Window, QColor(10, 10, 10))
self.setPalette(palette)
screen_geometry = QApplication.primaryScreen().geometry()
self.move(screen_geometry.x(), screen_geometry.y())
self.status_label = QLabel(self)
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setCentralWidget(self.status_label)
self.status_label.setStyleSheet("color: white;")
font = QFont("Consolas", 40)
self.status_label.setFont(font)
self.updateCapsLockStatus()
def setupKeyboardHook(self):
listener = keyboard.Listener(on_press=self.on_key_press)
listener.start()
def on_key_press(self, key):
if key == keyboard.Key.caps_lock:
self.updateCapsLockStatus()
def updateCapsLockStatus(self):
new_status: bool = None
if system() == "Windows":
import ctypes
hllDll = ctypes.WinDLL("User32.dll")
VK_CAPITAL = 0x14
new_status = hllDll.GetKeyState(VK_CAPITAL) not in [0, 65408]
elif system() == "Linux":
import subprocess
new_status = subprocess.run("xset q | grep \"Caps Lock\" | awk -F': ' '{gsub(/[0-9]/,\"\",$3); print $3}'",
stdout=subprocess.PIPE,
shell=True,
text=True).stdout.strip() == 'on'
print(new_status)
self.show()
self.status_label.setText("OFF" if not new_status else "ON")
def mousePressEvent(self, event):
self.hide()
if __name__ == '__main__':
app = QApplication(argv)
window = CapsLockDetector()
window.show()
exit(app.exec())
I wanna my program returns the correct value
On Linux (with X11/Xorg), the caps lock is switched off only when the key is released. In fact, there is a related report that also includes a patch, but, unfortunately, it was never merged even after ten years!
So, the safest solution is to rely on the key release only:
def setupKeyboardHook(self):
listener = keyboard.Listener(on_release=self.on_key_release)
listener.start()
def on_key_release(self, key):
if key == keyboard.Key.caps_lock:
self.updateCapsLockStatus()
Now, since this is intended for, possibly, a long running program and you may still want to see the current status updated as soon as possible, you could keep an internal status and only update it when necessary.
But, before that, there is an extremely important aspect that you need to keep in mind, even for your original implementation: pynput
works by using a separate thread, while widgets are not thread-safe.
Note that when something is "not thread-safe", it doesn't necessarily mean that it won't work, but it's not safe. Trying to access UI elements from a separate thread may work fine in some situations, but often results in unexpected results, graphical issues, inconsistent behavior or even fatal crash. That's why it's always discouraged, and QThread with proper signals is the only safe way to make threads communicate with the UI.
The proper way to use a keyboard listener like this is to move it to its own thread.
class Listener(QThread):
caps = pyqtSignal(bool)
def __init__(self):
super().__init__()
self.listener = keyboard.Listener(
on_press=self.press, on_release=self.release)
self.started.connect(self.listener.start)
def press(self, key):
if key == keyboard.Key.caps_lock:
self.caps.emit(True)
def release(self, key):
if key == keyboard.Key.caps_lock:
self.caps.emit(False)
class CapsLockDetector(QMainWindow):
capsStatus = False
def __init__(self):
...
self.listener = Listener()
self.listener.caps.connect(self.handleCaps)
self.listener.start()
def handleCaps(self, pressed):
if not pressed or not self.capsStatus:
self.updateCapsLockStatus()
def updateCapsLockStatus(self):
...
self.capsStatus = new_status
Note that using a QMainWindow for this doesn't make a lot of sense, and you could just directly subclass QLabel.