import ctypes import threading import queue import sys import tkinter as tk from tkinter import messagebox from ctypes import wintypes # Some Python builds don't expose common WinAPI types in ctypes.wintypes if not hasattr(wintypes, "ULONG_PTR"): wintypes.ULONG_PTR = ctypes.c_ulonglong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_ulong if not hasattr(wintypes, "LRESULT"): wintypes.LRESULT = ctypes.c_longlong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_long if not hasattr(wintypes, "WPARAM"): wintypes.WPARAM = wintypes.ULONG_PTR if not hasattr(wintypes, "LPARAM"): wintypes.LPARAM = ctypes.c_longlong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_long if not hasattr(wintypes, "INT"): wintypes.INT = ctypes.c_int try: import pystray from PIL import Image, ImageDraw TRAY_AVAILABLE = True except Exception: TRAY_AVAILABLE = False # Windows constants WH_KEYBOARD_LL = 13 WH_MOUSE_LL = 14 WM_KEYDOWN = 0x0100 WM_SYSKEYDOWN = 0x0104 WM_KEYUP = 0x0101 WM_SYSKEYUP = 0x0105 WM_QUIT = 0x0012 HC_ACTION = 0 VK_F12 = 0x7B VK_CONTROL = 0x11 VK_SHIFT = 0x10 VK_LSHIFT = 0xA0 VK_RSHIFT = 0xA1 VK_LCONTROL = 0xA2 VK_RCONTROL = 0xA3 LLKHF_INJECTED = 0x10 LLMHF_INJECTED = 0x01 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 user32 = ctypes.WinDLL("user32", use_last_error=True) kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # Function prototypes (helps avoid silent failures on some Python builds) user32.SetWindowsHookExW.argtypes = [ wintypes.INT, wintypes.HANDLE, wintypes.HINSTANCE, wintypes.DWORD, ] user32.SetWindowsHookExW.restype = wintypes.HHOOK user32.CallNextHookEx.argtypes = [wintypes.HHOOK, wintypes.INT, wintypes.WPARAM, wintypes.LPARAM] user32.CallNextHookEx.restype = wintypes.LRESULT user32.UnhookWindowsHookEx.argtypes = [wintypes.HHOOK] user32.UnhookWindowsHookEx.restype = wintypes.BOOL user32.GetMessageW.argtypes = [ctypes.POINTER(wintypes.MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT] user32.GetMessageW.restype = wintypes.BOOL user32.TranslateMessage.argtypes = [ctypes.POINTER(wintypes.MSG)] user32.DispatchMessageW.argtypes = [ctypes.POINTER(wintypes.MSG)] user32.PostThreadMessageW.argtypes = [wintypes.DWORD, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] user32.PostThreadMessageW.restype = wintypes.BOOL class KBDLLHOOKSTRUCT(ctypes.Structure): _fields_ = [ ("vkCode", wintypes.DWORD), ("scanCode", wintypes.DWORD), ("flags", wintypes.DWORD), ("time", wintypes.DWORD), ("dwExtraInfo", wintypes.ULONG_PTR), ] class MSLLHOOKSTRUCT(ctypes.Structure): _fields_ = [ ("pt", wintypes.POINT), ("mouseData", wintypes.DWORD), ("flags", wintypes.DWORD), ("time", wintypes.DWORD), ("dwExtraInfo", wintypes.ULONG_PTR), ] LowLevelKeyboardProc = ctypes.WINFUNCTYPE( wintypes.LRESULT, wintypes.INT, wintypes.WPARAM, wintypes.LPARAM ) LowLevelMouseProc = ctypes.WINFUNCTYPE( wintypes.LRESULT, wintypes.INT, wintypes.WPARAM, wintypes.LPARAM ) # Globals shared between threads cat_mode = False ui_queue = queue.Queue() quit_event = threading.Event() ctrl_down = False shift_down = False hook_thread_id = None kbd_hook = None mouse_hook = None tray_icon = None def _is_ctrl_shift_down(): return ( user32.GetAsyncKeyState(VK_CONTROL) & 0x8000 and user32.GetAsyncKeyState(VK_SHIFT) & 0x8000 ) def set_cat_mode(enabled: bool): global cat_mode if cat_mode == enabled: return cat_mode = enabled ui_queue.put(("status", cat_mode)) @LowLevelKeyboardProc def keyboard_proc(n_code, w_param, l_param): global ctrl_down, shift_down if n_code == HC_ACTION: kb = ctypes.cast(l_param, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents injected = (kb.flags & LLKHF_INJECTED) != 0 if not injected: if w_param in (WM_KEYDOWN, WM_SYSKEYDOWN): if kb.vkCode in (VK_CONTROL, VK_LCONTROL, VK_RCONTROL): ctrl_down = True elif kb.vkCode in (VK_SHIFT, VK_LSHIFT, VK_RSHIFT): shift_down = True elif w_param in (WM_KEYUP, WM_SYSKEYUP): if kb.vkCode in (VK_CONTROL, VK_LCONTROL, VK_RCONTROL): ctrl_down = False elif kb.vkCode in (VK_SHIFT, VK_LSHIFT, VK_RSHIFT): shift_down = False if cat_mode and not injected: # Emergency unlock combo: Ctrl+Shift+F12 if w_param in (WM_KEYDOWN, WM_SYSKEYDOWN): if kb.vkCode == VK_F12 and (ctrl_down and shift_down): set_cat_mode(False) return user32.CallNextHookEx(kbd_hook, n_code, w_param, l_param) return 1 # block physical key return user32.CallNextHookEx(kbd_hook, n_code, w_param, l_param) @LowLevelMouseProc def mouse_proc(n_code, w_param, l_param): if n_code == HC_ACTION: ms = ctypes.cast(l_param, ctypes.POINTER(MSLLHOOKSTRUCT)).contents injected = (ms.flags & LLMHF_INJECTED) != 0 if cat_mode and not injected: return 1 # block physical mouse/trackpad return user32.CallNextHookEx(mouse_hook, n_code, w_param, l_param) def hook_loop(): global hook_thread_id, kbd_hook, mouse_hook hook_thread_id = kernel32.GetCurrentThreadId() # For low-level hooks, hMod can be NULL; some systems misbehave when passing the EXE handle. hmod = None kbd_hook = user32.SetWindowsHookExW(WH_KEYBOARD_LL, keyboard_proc, hmod, 0) mouse_hook = user32.SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, hmod, 0) if not kbd_hook or not mouse_hook: err = ctypes.get_last_error() buf = ctypes.create_unicode_buffer(512) kernel32.FormatMessageW( FORMAT_MESSAGE_FROM_SYSTEM, None, err, 0, buf, len(buf), None, ) msg = buf.value.strip() or "Unknown error" ui_queue.put(("error", f"Hook install failed (error {err}): {msg}")) return msg = wintypes.MSG() while not quit_event.is_set() and user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) != 0: user32.TranslateMessage(ctypes.byref(msg)) user32.DispatchMessageW(ctypes.byref(msg)) if mouse_hook: user32.UnhookWindowsHookEx(mouse_hook) if kbd_hook: user32.UnhookWindowsHookEx(kbd_hook) def stop_hooks(): if hook_thread_id: user32.PostThreadMessageW(hook_thread_id, WM_QUIT, 0, 0) def create_tray_image(size=64): img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) d = ImageDraw.Draw(img) d.ellipse((8, 12, size - 8, size - 4), fill=(255, 224, 189, 255), outline=(40, 40, 40, 255)) d.polygon([(16, 16), (8, 2), (28, 12)], fill=(255, 200, 170, 255), outline=(40, 40, 40, 255)) d.polygon([(size - 16, 16), (size - 8, 2), (size - 28, 12)], fill=(255, 200, 170, 255), outline=(40, 40, 40, 255)) d.ellipse((22, 28, 28, 34), fill=(30, 30, 30, 255)) d.ellipse((size - 28, 28, size - 22, 34), fill=(30, 30, 30, 255)) d.polygon([(size // 2, 36), (size // 2 - 4, 42), (size // 2 + 4, 42)], fill=(200, 80, 80, 255)) d.line((size // 2, 42, size // 2, 48), fill=(40, 40, 40, 255), width=2) d.arc((size // 2 - 10, 44, size // 2, 54), 0, 180, fill=(40, 40, 40, 255), width=2) d.arc((size // 2, 44, size // 2 + 10, 54), 0, 180, fill=(40, 40, 40, 255), width=2) return img class CatModeApp(tk.Tk): def __init__(self): super().__init__() self.title("Cat Mode") self.geometry("320x200") self.resizable(False, False) self.protocol("WM_DELETE_WINDOW", self.on_window_close) self.status_label = tk.Label(self, text="Cat mode is OFF", font=("Segoe UI", 14)) self.status_label.pack(pady=(18, 8)) self.toggle_btn = tk.Button( self, text="Enable Cat Mode", font=("Segoe UI", 12), width=18, command=self.toggle, ) self.toggle_btn.pack(pady=6) self.info_label = tk.Label( self, text="Blocks physical keyboard/trackpad.\nInjected input still works.", font=("Segoe UI", 9), ) self.info_label.pack(pady=(6, 2)) self.hotkey_label = tk.Label( self, text="Emergency unlock: Ctrl+Shift+F12", font=("Segoe UI", 9), ) self.hotkey_label.pack(pady=(0, 6)) self.exit_btn = tk.Button(self, text="Exit", width=10, command=self.on_exit) self.exit_btn.pack(pady=4) self.after(50, self.poll_queue) def toggle(self): set_cat_mode(not cat_mode) def set_status(self, enabled: bool): if enabled: self.status_label.config(text="Cat mode is ON") self.toggle_btn.config(text="Disable Cat Mode") else: self.status_label.config(text="Cat mode is OFF") self.toggle_btn.config(text="Enable Cat Mode") if tray_icon: tray_icon.title = "Cat Mode (ON)" if enabled else "Cat Mode (OFF)" def poll_queue(self): try: while True: msg, payload = ui_queue.get_nowait() if msg == "status": self.set_status(payload) elif msg == "error": self.status_label.config(text=payload, wraplength=300, justify="center") messagebox.showerror("Cat Mode Error", payload) elif msg == "toggle": self.toggle() elif msg == "show": self.deiconify() self.lift() elif msg == "hide": self.withdraw() elif msg == "quit": self.on_exit() except queue.Empty: pass self.after(50, self.poll_queue) def on_window_close(self): if TRAY_AVAILABLE: self.withdraw() else: self.on_exit() def on_exit(self): set_cat_mode(False) quit_event.set() stop_hooks() self.destroy() def tray_loop(): global tray_icon if not TRAY_AVAILABLE: return def _toggle(icon, item): ui_queue.put(("toggle", None)) def _show(icon, item): ui_queue.put(("show", None)) def _quit(icon, item): ui_queue.put(("quit", None)) icon.stop() tray_icon = pystray.Icon( "cat_mode", create_tray_image(), "Cat Mode (OFF)", menu=pystray.Menu( pystray.MenuItem("Toggle Cat Mode", _toggle), pystray.MenuItem("Show Window", _show), pystray.MenuItem("Quit", _quit), ), ) tray_icon.run() def main(): t = threading.Thread(target=hook_loop, daemon=True) t.start() app = CatModeApp() if TRAY_AVAILABLE: threading.Thread(target=tray_loop, daemon=True).start() app.mainloop() if __name__ == "__main__": if sys.platform != "win32": print("This app is Windows-only.") sys.exit(1) main()