Initial commit.
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
Copyright (C) 2026 Nicolás A. 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.
|
||||||
@@ -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).
|
||||||
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()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
PyQt6>=6.0.0
|
||||||
|
evdev>=1.6.0
|
||||||
Reference in New Issue
Block a user