From df7a1f0a47ac1b9fd56c398f17aa042a459a56a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 23:17:08 +0000 Subject: [PATCH] feat: sensor-triggered window switcher --- requirements.txt | 2 + window_switcher.py | 112 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 requirements.txt create mode 100644 window_switcher.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4e672fc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyserial +pywin32 diff --git a/window_switcher.py b/window_switcher.py new file mode 100644 index 0000000..9d3f507 --- /dev/null +++ b/window_switcher.py @@ -0,0 +1,112 @@ +import serial +import serial.tools.list_ports +import win32gui +import win32con +import win32com.client +import time +import sys + +# --- Config --- +COM_PORT = "COM3" # change to your Arduino's COM port +BAUD_RATE = 9600 +TRIGGER_WORD = "DETECTED" +COOLDOWN_SEC = 1.5 # minimum seconds between switches +IGNORE_TITLES = {"", "Program Manager", "Windows Input Experience"} + + +def list_ports(): + ports = serial.tools.list_ports.comports() + if not ports: + print("No serial ports found.") + else: + print("Available ports:") + for p in ports: + print(f" {p.device} — {p.description}") + + +def get_windows(): + """Return list of (hwnd, title) for all visible, non-minimized top-level windows.""" + results = [] + + def callback(hwnd, _): + if not win32gui.IsWindowVisible(hwnd): + return + title = win32gui.GetWindowText(hwnd) + if title and title not in IGNORE_TITLES: + results.append((hwnd, title)) + + win32gui.EnumWindows(callback, None) + return results + + +def force_focus(hwnd): + """Bring window to foreground, bypassing Windows focus-steal protection.""" + shell = win32com.client.Dispatch("WScript.Shell") + shell.SendKeys('%') # simulate Alt — tricks Windows into allowing focus change + if win32gui.IsIconic(hwnd): + win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) + win32gui.SetForegroundWindow(hwnd) + + +class WindowSwitcher: + def __init__(self): + self.index = 0 + self.last_switch = 0 + + def switch(self): + now = time.time() + if now - self.last_switch < COOLDOWN_SEC: + return + + windows = get_windows() + if not windows: + print("No windows found.") + return + + self.index = (self.index + 1) % len(windows) + hwnd, title = windows[self.index] + + try: + force_focus(hwnd) + print(f"[{time.strftime('%H:%M:%S')}] → {title}") + self.last_switch = now + except Exception as e: + print(f"Could not switch to '{title}': {e}") + + +def main(): + if "--list-ports" in sys.argv: + list_ports() + return + + if "--test" in sys.argv: + # Test window switching without Arduino + print("Test mode — switching window in 2 seconds...") + time.sleep(2) + WindowSwitcher().switch() + return + + print(f"Connecting to {COM_PORT} at {BAUD_RATE} baud...") + print("Press Ctrl+C to stop.\n") + + switcher = WindowSwitcher() + + try: + ser = serial.Serial(COM_PORT, BAUD_RATE, timeout=1) + print(f"Connected. Waiting for '{TRIGGER_WORD}' signal...\n") + + while True: + line = ser.readline().decode("utf-8", errors="ignore").strip() + if line == TRIGGER_WORD: + switcher.switch() + + except serial.SerialException as e: + print(f"Serial error: {e}") + print("\nAvailable ports:") + list_ports() + except KeyboardInterrupt: + print("\nStopped.") + + +if __name__ == "__main__": + main()