Last active
September 10, 2025 23:09
-
-
Save mrbarletta/370e5410db425a8df11082b30e05b7d1 to your computer and use it in GitHub Desktop.
Bluetooth keyboard adding a letter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import evdev | |
| import time | |
| import sys | |
| import signal | |
| import os | |
| import select | |
| try: | |
| from rich.console import Console | |
| from rich.panel import Panel | |
| except ImportError: | |
| print("Dependency not found. Please install 'rich' to continue.") | |
| print("Install it with: sudo pip3 install rich") | |
| sys.exit(1) | |
| try: | |
| from simple_term_menu import TerminalMenu | |
| except ImportError: | |
| print("Dependency not found. Please install 'simple-term-menu' to continue.") | |
| print("Install it with: sudo pip3 install simple-term-menu") | |
| sys.exit(1) | |
| console = Console() | |
| class BluetoothKeyboardInterceptor: | |
| def __init__(self): | |
| self.device = None | |
| self.uinput = None | |
| self.last_z_time = 0 | |
| self.z_event = None | |
| self.running = True | |
| self.stop_current_device = False | |
| # Modifier key states | |
| self.ctrl_pressed = False | |
| self.alt_pressed = False | |
| signal.signal(signal.SIGINT, self.signal_handler) | |
| signal.signal(signal.SIGTERM, self.signal_handler) | |
| def select_device(self): | |
| """Scans for keyboards and lets the user select one from a menu.""" | |
| selected_device = None | |
| while self.running and selected_device is None: | |
| try: | |
| devices = [evdev.InputDevice(path) for path in evdev.list_devices()] | |
| keyboards = [d for d in devices if 'keyboard' in d.name.lower()] | |
| menu_entries = [f"π {d.name} ({d.phys})" for d in keyboards] | |
| menu_entries.append(None) # Separator | |
| menu_entries.append("[π Rescan for devices]") | |
| menu_entries.append("[β Exit Script]") | |
| if not keyboards: | |
| console.print("[yellow]No keyboard devices found. Will try again in 5 seconds...[/]") | |
| time.sleep(5) | |
| continue | |
| terminal_menu = TerminalMenu( | |
| menu_entries, | |
| title="Please select a keyboard to monitor. Press CTRL+ALT+S to return to this menu.", | |
| ) | |
| choice_index = terminal_menu.show() | |
| if choice_index is None: # User pressed ESC or q | |
| self.running = False | |
| return None | |
| menu_choice = menu_entries[choice_index] | |
| if "Rescan" in menu_choice: | |
| continue # Loop again to rescan | |
| if "Exit" in menu_choice: | |
| self.running = False | |
| return None | |
| selected_device = keyboards[choice_index] | |
| return selected_device | |
| except Exception as e: | |
| console.print(f"β [bold red]Error finding devices:[/] {e}") | |
| time.sleep(5) | |
| return None | |
| def signal_handler(self, signum, frame): | |
| console.print(f"\n[yellow]Received signal {signum}, shutting down...[/]") | |
| self.running = False | |
| def process_key_event(self, event): | |
| key_event = evdev.categorize(event) | |
| key_code = key_event.keycode | |
| key_value = key_event.keystate # 1 = key_down, 0 = key_up, 2 = key_hold | |
| # --- Handle Stop Hotkey (CTRL+ALT+S) --- | |
| if isinstance(key_code, list): # Some keys like Ctrl can be a list | |
| key_code = key_code[0] | |
| if 'CTRL' in key_code: | |
| self.ctrl_pressed = (key_value != 0) # True if down or hold | |
| if 'ALT' in key_code: | |
| self.alt_pressed = (key_value != 0) # True if down or hold | |
| if self.ctrl_pressed and self.alt_pressed and key_code == 'KEY_S' and key_value == 1: | |
| console.print("\n[bold magenta]β©οΈ Stop command (CTRL+ALT+S) received. Returning to menu...[/]") | |
| self.stop_current_device = True | |
| return # Do not process or emit this key press | |
| # --- Handle Z + Enter Bug --- | |
| if key_code == 'KEY_Z' and key_value == 1: | |
| self.last_z_time = time.time() | |
| self.z_event = event | |
| console.print("β¨οΈ 'Z' pressed, waiting for 'Enter'...", style="dim") | |
| elif key_code == 'KEY_ENTER' and key_value == 1: | |
| current_time = time.time() | |
| if (current_time - self.last_z_time) < 0.01 and self.z_event: | |
| console.print(f"β Suppressing 'Z' (Enter pressed within {current_time - self.last_z_time:.2f}s)", style="green") | |
| self.last_z_time = 0 | |
| self.z_event = None | |
| self.uinput.write_event(event) | |
| else: | |
| if self.z_event: | |
| self.uinput.write_event(self.z_event) | |
| self.z_event = None | |
| self.uinput.write_event(event) | |
| self.last_z_time = 0 | |
| else: | |
| if self.z_event: | |
| self.uinput.write_event(self.z_event) | |
| self.z_event = None | |
| self.uinput.write_event(event) | |
| self.last_z_time = 0 | |
| def run(self): | |
| while self.running: | |
| try: | |
| self.device = self.select_device() | |
| if not self.device: | |
| self.running = False | |
| break | |
| self.stop_current_device = False | |
| console.print(Panel(f"β [bold green]Connected![/]\n[cyan]Intercepting events from:[/cyan] [bold]{self.device.name}[/]", | |
| border_style="green", subtitle="Press CTRL+ALT+S to stop")) | |
| self.device.grab() | |
| self.uinput = evdev.UInput.from_device(self.device, name='bt-keyboard-interceptor') | |
| while self.running and not self.stop_current_device: | |
| r, _, _ = select.select([self.device.fd], [], [], 0.5) | |
| if not r: | |
| continue | |
| for event in self.device.read(): | |
| if event.type == evdev.ecodes.EV_KEY: | |
| self.process_key_event(event) | |
| self.uinput.syn() | |
| except (OSError, IOError) as e: | |
| console.print(f"\nβ [bold red]Device error:[/] {e}. Keyboard disconnected.") | |
| except Exception as e: | |
| console.print(f"An unexpected error occurred: {e}", style="bold red") | |
| self.running = False | |
| finally: | |
| self.cleanup_device() | |
| if self.running and not self.stop_current_device: | |
| console.print("[yellow]π Will re-scan for devices in 5 seconds...[/]") | |
| time.sleep(5) | |
| self.cleanup() | |
| def cleanup_device(self): | |
| if self.uinput: | |
| self.uinput.close() | |
| self.uinput = None | |
| if self.device: | |
| try: | |
| self.device.ungrab() | |
| self.device.close() | |
| except OSError as e: | |
| console.print(f"Error during device cleanup: {e}", style="dim red") | |
| self.device = None | |
| # Reset modifier keys state | |
| self.ctrl_pressed = False | |
| self.alt_pressed = False | |
| def cleanup(self): | |
| console.print("\n[bold yellow]π§Ή Cleaning up...[/]") | |
| if self.z_event and self.uinput: | |
| self.uinput.write_event(self.z_event) | |
| self.uinput.syn() | |
| self.cleanup_device() | |
| console.print("[bold green]π Shutdown complete.[/]") | |
| def main(): | |
| if os.geteuid() != 0: | |
| console.print("[bold red]This script requires root privileges to access input devices.[/]") | |
| console.print("Please run with: [cyan]sudo python3 your_script_name.py[/]") | |
| sys.exit(1) | |
| interceptor = BluetoothKeyboardInterceptor() | |
| interceptor.run() | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Latest version: