Caps Lock key always on when pressed in Linux

407 Views Asked by At

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

2

There are 2 best solutions below

0
musicamante On BEST ANSWER

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.

0
Sepu Ling On

My solution:

from platform import system
from sys import argv, exit

from PyQt6.QtCore import QThread, Qt, pyqtSignal
from PyQt6.QtGui import QMouseEvent, QPalette, QColor, QFont
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from pynput import keyboard


class Listener(QThread):
    caps = pyqtSignal(bool)

    def __init__(self, main_window):
        super().__init__()
        self.listener = keyboard.Listener(
            on_press=self.press, on_release=self.release)
        self.started.connect(self.listener.start)
        self.main_window = main_window

    def press(self, key):
        if key == keyboard.Key.caps_lock:
            self.caps.emit(True)
        elif not self.main_window.isHidden():
            self.main_window.show()

    def release(self, key):
        if key == keyboard.Key.caps_lock:
            self.caps.emit(False)
        elif not self.main_window.isHidden():
            self.main_window.hide()


class CapsLockDetector(QLabel):
    capsStatus = False

    def __init__(self, main_window):
        super().__init__()
        self.listener = Listener(main_window)
        self.listener.caps.connect(self.handleCaps)
        self.listener.start()
        self.main_window = main_window

    def handleCaps(self, pressed):
        if not pressed or not self.capsStatus:
            self.updateCapsLockStatus()

    def updateCapsLockStatus(self):
        new_status = 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.check_output('xset q | grep "Caps Lock"', shell=True).split()[3] == b'on'
            print(new_status)

        self.main_window.show()
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("color: white;")
        font = QFont("Consolas", 30)
        self.setFont(font)
        self.setText("Caps lock: " + ("OFF" if not new_status else "ON"))
        self.capsStatus = new_status


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.initUI()

    def initUI(self):
        caps_detector = CapsLockDetector(self)
        self.setCentralWidget(caps_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.show()

    def mousePressEvent(self, a0: QMouseEvent | None) -> None:
        self.hide()


app = QApplication(argv)
windows = MainWindow()
windows.hide()
exit(app.exec())

https://github.com/kaixinol/caps-lock-indicator