From b870b7a165ce326eeb01d268c56ec9e24c641039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Ortega=20Froysa?= Date: Wed, 1 Jul 2026 07:19:52 +0200 Subject: [PATCH] Initial commit. --- LICENSE | 17 ++++ README.md | 120 +++++++++++++++++++++++++ kb-block.py | 223 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 4 files changed, 362 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100755 kb-block.py create mode 100644 requirements.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f69cc4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Copyright (C) 2026 Nicolás A. Ortega Froysa + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..783650d --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Keyboard Input Blocker + +A graphical application for Linux that toggles keyboard input blocking with a +single button. Perfect if your toddler likes to press a lot of buttons during +video calls. + +## Features + +- Simple one-button toggle interface built with PyQt6 +- Disables/enables all keyboard input at the system level +- Status indicator showing current state +- Works on Wayland and X11 + +## Requirements + +- Python 3.7+ +- PyQt6 +- evdev (Linux kernel input device library) + +## Installation + +### 1. Install dependencies + +**On Ubuntu/Debian:** + +```bash +sudo apt-get install python3 python3-pip libevdev-dev +pip install -r requirements.txt +``` + +**On Fedora:** + +```bash +sudo dnf install python3 python3-pip libevdev-devel +pip install -r requirements.txt +``` + +**On Arch:** + +```bash +sudo pacman -S python3 libevdev python-pyqt6 python-evdev +``` + +### 2. Run the application + +The application requires elevated privileges to access input devices: + +```bash +sudo -E python3 kb-block.py +``` + +Or make it executable: + +```bash +chmod +x kb-block.py +sudo -E ./kb-block.py +``` + +## Usage + +1. Launch the application with `sudo -E` +2. Click **"Block Keyboard"** to disable all keyboard input + - The button turns red and shows **"Unblock Keyboard"** + - Status shows "Keyboard BLOCKED ⚠" +3. Click **"Unblock Keyboard"** to re-enable keyboard input + - The button returns to normal + - Status shows "Keyboard enabled ✓" + +**Note:** When keyboard is blocked, you'll need to use: + +- Mouse to unblock via the button +- External input devices (if available) +- Or kill the process from another terminal: `sudo killall -9 python3` (or the + specific PID) + +## How it Works + +The application uses the `evdev` library to grab input devices at the kernel level: + +- `grab()` - Prevents the keyboard from sending any events +- `ungrab()` - Restores keyboard input + +This works with both X11 and Wayland display servers. + +## Permissions Note + +The application requires `sudo` because it needs to directly access +`/dev/input/event*` devices. + +If you want to run without `sudo`, you can add your user to the `input` group: + +```bash +sudo usermod -a -G input $USER +# Log out and back in for the change to take effect +``` + +However, this is less secure as it grants all users in the input group broad +input device access. + +## Troubleshooting + +### No keyboard device found + +- Ensure you're running with `sudo -E` +- Check that keyboard input devices exist: `ls -la /dev/input/event*` + +### Permission denied" + +- Run with `sudo -E` +- Or add your user to the input group (see Permissions Note above) + +### Keyboard still responds + +- Multiple keyboard devices may exist; the app grabs the primary one +- Try unblocking and re-blocking + +## License + +This project is licensed under the Zlib license (see [LICENSE](LICENSE) file for +more information). diff --git a/kb-block.py b/kb-block.py new file mode 100755 index 0000000..17cc870 --- /dev/null +++ b/kb-block.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Nicolás Ortega Froysa All rights reserved. +# Author: Nicolás Ortega Froysa +# +# 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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..175cf1c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyQt6>=6.0.0 +evdev>=1.6.0