-1

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

Sepu Ling
  • 1
  • 2
  • What "wrong value"? Does the caps-lock led switch? And does `xset q` on a separate terminal show the proper result? – musicamante Sep 02 '23 at 17:59
  • Works fine for me on arch-linux. A simpler command is `subprocess.check_output('xset q | grep "Caps Lock"', shell=True).split()[3] == b'on'`. In a PyQt application, this should be run with a `QTimer` rather than a blocking while-loop. – ekhumoro Sep 02 '23 at 20:22
  • Using QTimer to run detection status every millisecond? But I don't want to do this, it's too resource intensive – Sepu Ling Sep 02 '23 at 23:59
  • @ekhumoro Your simplified command works on the console but doesn't work in pyqt – Sepu Ling Sep 03 '23 at 00:05
  • @SepuLing It works perfectly fine for me in PyQt (using a QTimer). You must be doing something wrong, but you will have to show the actual code you are running if you want help. – ekhumoro Sep 03 '23 at 00:11
  • @ekhumoro ```import subprocess new_status = subprocess.check_output('xset q | grep "Caps Lock"', shell=True).split()[3] == b'on' print(new_status) ``` output: True True True True True True I wanna it True False True False True False – Sepu Ling Sep 03 '23 at 00:17
  • @ekhumoro Are you referring to using a QTimer to run the detection status every millisecond? – Sepu Ling Sep 03 '23 at 00:20
  • @musicamante Every time the updateCapsLockStatus() function is run, it should output a different value, such as True False True False, but it outputs True True True True True – Sepu Ling Sep 03 '23 at 00:25
  • @SepuLing Please answer to *all* questions I asked. – musicamante Sep 03 '23 at 00:55
  • @musicamante I don't have the caps lock LED, but the status of the LED returned with the command is normal. Yes, on a separate terminal `xset q` shows the correct results. – Sepu Ling Sep 03 '23 at 01:02
  • @SepuLing Depending on the system, the caps lock state may be toggled by different events. On Linux (at least on the distribution I'm using), it's disabled only when the caps lock key is *released*. So, the simple solution would be to always call the function on key release, which will give valid results in both cases. Btw, why are you using keyboard listener? What do you mean by "the keyboard indicator widget cannot be run on my kali system"? – musicamante Sep 03 '23 at 01:21
  • @musicamante Because I don't know how to use pyqt to monitor keyboard caps lock events, because there are too few documents. Literal meaning https://imgur.com/8M7Twyo Thank you, I changed on_press to on_release and it works – Sepu Ling Sep 03 '23 at 01:40
  • @SepuLing Oh, you meant the "plasmoid" keyboard indicator widget, so you need a system wide indicator, not just one for your application, right? Then, yes, you can only rely on key release, but since you're probably having the program kept alive during the session, you could slightly improve it by keeping an internal status. In any case, you should probably file a report on that distro. – musicamante Sep 03 '23 at 01:57

1 Answers1

0

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.

musicamante
  • 41,230
  • 6
  • 33
  • 58