224 lines
7.3 KiB
Python
Executable File
224 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (C) 2026 Nicolás Ortega Froysa <nicolas@ortegas.org> All rights reserved.
|
|
# Author: Nicolás Ortega Froysa <nicolas@ortegas.org>
|
|
#
|
|
# This software is provided 'as-is', without any express or implied
|
|
# warranty. In no event will the authors be held liable for any damages
|
|
# arising from the use of this software.
|
|
#
|
|
# Permission is granted to anyone to use this software for any purpose,
|
|
# including commercial applications, and to alter it and redistribute it
|
|
# freely, subject to the following restrictions:
|
|
#
|
|
# 1. The origin of this software must not be misrepresented; you must not
|
|
# claim that you wrote the original software. If you use this software
|
|
# in a product, an acknowledgment in the product documentation would be
|
|
# appreciated but is not required.
|
|
#
|
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
|
# misrepresented as being the original software.
|
|
#
|
|
# 3. This notice may not be removed or altered from any source
|
|
# distribution.
|
|
|
|
"""
|
|
Keyboard Input Blocker for X11/Wayland
|
|
|
|
A graphical application that toggles keyboard input on/off.
|
|
Requires: PyQt6, evdev
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QLabel, QMessageBox
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
from PyQt6.QtGui import QFont
|
|
import evdev
|
|
from evdev import InputDevice, list_devices
|
|
|
|
|
|
class KeyboardBlockerThread(QThread):
|
|
"""Thread to handle keyboard grabbing in the background."""
|
|
|
|
error_occurred = pyqtSignal(str)
|
|
grabbed = pyqtSignal()
|
|
released = pyqtSignal()
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.keyboard_device = None
|
|
self.is_blocking = False
|
|
self.should_stop = False
|
|
|
|
def find_keyboard_device(self):
|
|
"""Find the keyboard input device."""
|
|
devices = [InputDevice(path) for path in list_devices()]
|
|
|
|
# Look for keyboard devices
|
|
keyboard_devices = []
|
|
for device in devices:
|
|
try:
|
|
caps = device.capabilities()
|
|
# Check if device has KEY capability (keyboards have this)
|
|
if evdev.ecodes.EV_KEY in caps:
|
|
# Prefer devices with common keyboard names
|
|
if any(name in device.name.lower() for name in ['keyboard', 'input', 'wacom']):
|
|
keyboard_devices.append(device)
|
|
except:
|
|
continue
|
|
|
|
if keyboard_devices:
|
|
return keyboard_devices[0]
|
|
|
|
# Fallback: return first device with KEY capability
|
|
for device in devices:
|
|
try:
|
|
caps = device.capabilities()
|
|
if evdev.ecodes.EV_KEY in caps:
|
|
return device
|
|
except:
|
|
continue
|
|
|
|
return None
|
|
|
|
def grab_keyboard(self):
|
|
"""Grab the keyboard device to block input."""
|
|
try:
|
|
self.keyboard_device = self.find_keyboard_device()
|
|
|
|
if not self.keyboard_device:
|
|
self.error_occurred.emit(
|
|
"No keyboard device found. You may need to run with sudo."
|
|
)
|
|
return False
|
|
|
|
# Grab the device (this requires appropriate permissions)
|
|
self.keyboard_device.grab()
|
|
self.is_blocking = True
|
|
self.grabbed.emit()
|
|
return True
|
|
|
|
except PermissionError:
|
|
self.error_occurred.emit(
|
|
"Permission denied. Please run with elevated privileges (sudo)."
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Error grabbing keyboard: {str(e)}")
|
|
return False
|
|
|
|
def release_keyboard(self):
|
|
"""Release the keyboard device to allow input."""
|
|
try:
|
|
if self.keyboard_device:
|
|
self.keyboard_device.ungrab()
|
|
self.is_blocking = False
|
|
self.released.emit()
|
|
return True
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Error releasing keyboard: {str(e)}")
|
|
return False
|
|
|
|
|
|
class KeyboardBlockerApp(QMainWindow):
|
|
"""Main application window for keyboard blocker."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.blocker_thread = KeyboardBlockerThread()
|
|
self.is_keyboard_blocked = False
|
|
|
|
# Connect thread signals
|
|
self.blocker_thread.grabbed.connect(self.on_keyboard_grabbed)
|
|
self.blocker_thread.released.connect(self.on_keyboard_released)
|
|
self.blocker_thread.error_occurred.connect(self.on_error)
|
|
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
"""Initialize the user interface."""
|
|
self.setWindowTitle("Keyboard Input Blocker")
|
|
self.setGeometry(100, 100, 400, 200)
|
|
|
|
# Create central widget and layout
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
layout = QVBoxLayout(central_widget)
|
|
|
|
# Title label
|
|
title_label = QLabel("Keyboard Input Blocker")
|
|
title_font = QFont()
|
|
title_font.setPointSize(14)
|
|
title_font.setBold(True)
|
|
title_label.setFont(title_font)
|
|
layout.addWidget(title_label)
|
|
|
|
# Status label
|
|
self.status_label = QLabel("Status: Keyboard enabled")
|
|
layout.addWidget(self.status_label)
|
|
|
|
# Toggle button
|
|
self.toggle_button = QPushButton("Block Keyboard")
|
|
self.toggle_button.setMinimumHeight(50)
|
|
self.toggle_button.setFont(QFont("Times", 12))
|
|
self.toggle_button.clicked.connect(self.toggle_keyboard)
|
|
layout.addWidget(self.toggle_button)
|
|
|
|
# Info label
|
|
info_label = QLabel(
|
|
"Click the button to toggle keyboard input blocking.\n"
|
|
"Note: This application may require elevated privileges (sudo)."
|
|
)
|
|
info_font = QFont()
|
|
info_font.setPointSize(9)
|
|
info_label.setFont(info_font)
|
|
layout.addWidget(info_label)
|
|
|
|
layout.addStretch()
|
|
|
|
def toggle_keyboard(self):
|
|
"""Toggle keyboard blocking on/off."""
|
|
if self.is_keyboard_blocked:
|
|
self.blocker_thread.release_keyboard()
|
|
else:
|
|
self.blocker_thread.grab_keyboard()
|
|
|
|
def on_keyboard_grabbed(self):
|
|
"""Handle keyboard grabbed signal."""
|
|
self.is_keyboard_blocked = True
|
|
self.toggle_button.setText("Unblock Keyboard")
|
|
self.toggle_button.setStyleSheet("background-color: #ff6b6b;")
|
|
self.status_label.setText("Status: Keyboard BLOCKED ⚠")
|
|
|
|
def on_keyboard_released(self):
|
|
"""Handle keyboard released signal."""
|
|
self.is_keyboard_blocked = False
|
|
self.toggle_button.setText("Block Keyboard")
|
|
self.toggle_button.setStyleSheet("")
|
|
self.status_label.setText("Status: Keyboard enabled ✓")
|
|
|
|
def on_error(self, message):
|
|
"""Handle error signal."""
|
|
QMessageBox.critical(self, "Error", message)
|
|
|
|
def closeEvent(self, a0):
|
|
"""Ensure keyboard is released when closing."""
|
|
if self.is_keyboard_blocked:
|
|
self.blocker_thread.release_keyboard()
|
|
self.blocker_thread.quit()
|
|
self.blocker_thread.wait()
|
|
a0.accept()
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
app = QApplication(sys.argv)
|
|
window = KeyboardBlockerApp()
|
|
window.show()
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|