Skip to content

Instantly share code, notes, and snippets.

@mrbarletta
Last active September 10, 2025 23:09
Show Gist options
  • Select an option

  • Save mrbarletta/370e5410db425a8df11082b30e05b7d1 to your computer and use it in GitHub Desktop.

Select an option

Save mrbarletta/370e5410db425a8df11082b30e05b7d1 to your computer and use it in GitHub Desktop.
Bluetooth keyboard adding a letter
#!/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()
@mrbarletta
Copy link
Author

Latest version:

  • Select device to listen to
  • if disconnects lets you reconnect without restart
  • Menu item to exit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment