import signal, sys, objc from AppKit import NSWorkspace from Foundation import NSRunLoop, NSDate, NSObject import hid VID, PID = 0x3384, 0x0009 USAGE_PAGE, USAGE = 0xFF60, 0x61 CMD_SET_FOCUSED_APP = 0x80 def find_keyboard_path(): for info in hid.enumerate(VID, PID): if info["usage_page"] == USAGE_PAGE and info["usage"] == USAGE: return info["path"] return None def send_app_name(dev, name): payload = name.encode("utf-8", errors="ignore")[:31] payload = payload.decode("utf-8", errors="ignore").encode("utf-8") data = bytes([0x00, CMD_SET_FOCUSED_APP]) + payload + b"\x00" * (32 - 1 - len(payload)) dev.write(data) dev.read(32, timeout=1000) class AppObserver(NSObject): def init(self): self = objc.super(AppObserver, self).init() self.dev = None self.last_app = None self.current_app = NSWorkspace.sharedWorkspace().frontmostApplication().localizedName() return self def appChanged_(self, notification): app = notification.userInfo()["NSWorkspaceApplicationKey"].localizedName() self.current_app = app if self.dev is not None: self.last_app = app print(f"App: {app}") try: send_app_name(self.dev, app) except Exception: print("Keyboard disconnected") self.dev = None self.last_app = None def main(): signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) ws = NSWorkspace.sharedWorkspace() observer = AppObserver.alloc().init() ws.notificationCenter().addObserver_selector_name_object_( observer, "appChanged:", "NSWorkspaceDidActivateApplicationNotification", None ) while True: connected = find_keyboard_path() is not None # Detect disconnect if observer.dev is not None and not connected: print("Keyboard disconnected") observer.dev = None observer.last_app = None # Connect and send current app if observer.dev is None and connected: try: observer.dev = hid.Device(path=find_keyboard_path()) print("Keyboard connected") observer.last_app = observer.current_app print(f"App: {observer.current_app}") send_app_name(observer.dev, observer.current_app) except Exception: observer.dev = None # Pump run loop — notifications fire here and send immediately; # 2s is enough for connect/disconnect detection NSRunLoop.currentRunLoop().runUntilDate_( NSDate.dateWithTimeIntervalSinceNow_(2.0) ) if __name__ == "__main__": main()