Initial commit.
This commit is contained in:
Executable
+223
@@ -0,0 +1,223 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user