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