Initial Commit - Cat mode for windows.

This commit is contained in:
serversdwn
2026-02-23 21:31:18 +00:00
commit 3e625a9669
5 changed files with 419 additions and 0 deletions

347
main.py Normal file
View File

@@ -0,0 +1,347 @@
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
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 == VK_CONTROL:
ctrl_down = True
elif kb.vkCode == VK_SHIFT:
shift_down = True
elif w_param in (WM_KEYUP, WM_SYSKEYUP):
if kb.vkCode == VK_CONTROL:
ctrl_down = False
elif kb.vkCode == VK_SHIFT:
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()