Initial Commit - Cat mode for windows.
This commit is contained in:
347
main.py
Normal file
347
main.py
Normal 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()
|
||||
Reference in New Issue
Block a user