import sys
import os
import time
import json
from datetime import datetime
from threading import Thread, Timer, Lock
import os
import pyautogui
from pynput import mouse, keyboard
from loguru import logger
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPainter, QPen, QColor, QGuiApplication, QRegion
import os
import signal
EVENT_LOG_FILE = "event_log.json"
os.makedirs("screenshots", exist_ok=True)
event_data = []
ctrl_pressed = False
shift_pressed = False
alt_pressed = False
# 輸入緩衝區
input_buffer = []
last_char_time = None
buffer_timeout = 0.5
# 滑鼠狀態
mouse_pressed = False
drag_path = []
drag_start = None
drag_button = None
press_time = None
double_click_threshold = 0.3
click_distance_threshold = 5
last_click_time = None
last_click_pos = None
last_click_button = None
pending_click_event = None
pending_click_timer = None
lock = Lock()
app = None
red_border_window = None
mouse_listener = None
keyboard_listener = None
logger.add("debug.log", format="{time} {level} {message}", level="DEBUG", rotation="1 MB", compression="zip")
def save_event_data():
with open(EVENT_LOG_FILE, 'w', encoding='utf-8') as f:
json.dump(event_data, f, ensure_ascii=False, indent=4)
logger.debug("Event data saved to file.")
def take_screenshot():
logger.debug("Taking screenshot...")
if red_border_window:
red_border_window.hide()
time.sleep(0.05)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
filename = f"screenshots/{timestamp}.png"
screenshot = pyautogui.screenshot()
screenshot.save(filename)
if red_border_window:
red_border_window.show()
logger.info(f"Screenshot saved as {filename}")
return filename
def flush_input_buffer():
global input_buffer
if input_buffer:
logger.debug(f"Flushing input buffer: {''.join(input_buffer)}")
screenshot_file = take_screenshot()
event_info = {
"type": "text_input",
"time": datetime.now().isoformat(),
"text": "".join(input_buffer),
"screenshot": screenshot_file
}
event_data.append(event_info)
input_buffer = []
save_event_data()
def handle_modifiers(key, pressed):
global ctrl_pressed, shift_pressed, alt_pressed
if key in [keyboard.Key.ctrl_l, keyboard.Key.ctrl_r]:
ctrl_pressed = pressed
elif key in [keyboard.Key.shift, keyboard.Key.shift_r]:
shift_pressed = pressed
elif key in [keyboard.Key.alt_l, keyboard.Key.alt_r]:
alt_pressed = pressed
logger.debug(f"Modifier changed: ctrl={ctrl_pressed}, shift={shift_pressed}, alt={alt_pressed}")
def handle_combination(key_char):
if ctrl_pressed and key_char.lower() in ["c","v","x","s","a"]:
# 這裡的截圖邏輯依舊在事件行為結束時進行,組合鍵按下後立即結束事件行為
screenshot_file = take_screenshot()
event_data.append({
"type": "key_shortcut",
"time": datetime.now().isoformat(),
"shortcut": f"ctrl+{key_char.lower()}",
"screenshot": screenshot_file
})
save_event_data()
logger.info(f"Detected shortcut: ctrl+{key_char.lower()}")
return True
return False
def handle_normal_char(key_char):
global input_buffer, last_char_time
if last_char_time is not None and (time.time() - last_char_time) > buffer_timeout:
flush_input_buffer()
input_buffer.append(key_char)
last_char_time = time.time()
logger.debug(f"Buffered char: {key_char}")
def handle_special_key(key):
# 特殊按鍵事件立即結束行為
screenshot_file = take_screenshot()
special_key_name = None
if key == keyboard.Key.enter:
special_key_name = "enter"
elif key == keyboard.Key.esc:
special_key_name = "esc"
elif key == keyboard.Key.tab:
special_key_name = "tab"
elif key == keyboard.Key.up:
special_key_name = "up"
elif key == keyboard.Key.down:
special_key_name = "down"
elif key == keyboard.Key.left:
special_key_name = "left"
elif key == keyboard.Key.right:
special_key_name = "right"
elif key in [keyboard.Key.f1, keyboard.Key.f2, keyboard.Key.f3, keyboard.Key.f4,
keyboard.Key.f5, keyboard.Key.f6, keyboard.Key.f7, keyboard.Key.f8,
keyboard.Key.f9, keyboard.Key.f10, keyboard.Key.f11, keyboard.Key.f12]:
special_key_name = str(key)
if special_key_name:
event_data.append({
"type": "key_special",
"time": datetime.now().isoformat(),
"key": special_key_name,
"screenshot": screenshot_file
})
save_event_data()
logger.info(f"Recorded special key: {special_key_name}")
def check_exit_condition():
logger.info("Detected ctrl+alt+esc, exiting...")
if mouse_listener:
mouse_listener.stop()
if keyboard_listener:
keyboard_listener.stop()
if red_border_window is not None:
red_border_window.close()
if app is not None:
app.quit()
# 強制退出所有執行緒和程序
os._exit(0)
def button_name_from_pynput(button):
if button == mouse.Button.left:
return "left"
elif button == mouse.Button.right:
return "right"
elif button == mouse.Button.middle:
return "middle"
return str(button)
def record_event(event_info):
event_data.append(event_info)
save_event_data()
def record_click_event(click_type, pos, button):
screenshot_file = take_screenshot()
event_info = {
"type": click_type,
"time": datetime.now().isoformat(),
"button": button,
"coordinates": {"x": pos[0], "y": pos[1]},
"screenshot": screenshot_file
}
record_event(event_info)
logger.info(f"{click_type.capitalize()} at {pos}, button={button}")
def record_drag_event(start_pos, end_pos, path):
screenshot_file = take_screenshot()
event_info = {
"type": "drag",
"time": datetime.now().isoformat(),
"start_coordinates": {"x": start_pos[0], "y": start_pos[1]},
"end_coordinates": {"x": end_pos[0], "y": end_pos[1]},
"path": path,
"screenshot": screenshot_file
}
record_event(event_info)
logger.info(f"Drag from {start_pos} to {end_pos} recorded.")
def record_scroll_event(x, y, dx, dy):
screenshot_file = take_screenshot()
event_info = {
"type": "scroll",
"time": datetime.now().isoformat(),
"coordinates": {"x": x, "y": y},
"scroll": {"dx": dx, "dy": dy},
"screenshot": screenshot_file
}
record_event(event_info)
logger.info(f"Scroll at ({x},{y}) dx={dx}, dy={dy}")
def finalize_single_click():
global pending_click_event, pending_click_timer
with lock:
if pending_click_event is not None:
event = pending_click_event
pending_click_event = None
pending_click_timer = None
else:
event = None
if event:
logger.debug("Finalize single click event due to timeout.")
record_click_event("single_click", event["pos"], event["button"])
def cancel_pending_single_click():
global pending_click_event, pending_click_timer
with lock:
if pending_click_timer:
pending_click_timer.cancel()
pending_click_event = None
pending_click_timer = None
def schedule_single_click(pos, button):
global pending_click_event, pending_click_timer
with lock:
logger.debug("Scheduling single click event waiting for double click threshold...")
cancel_pending_single_click()
pending_click_event = {"pos": pos, "button": button}
pending_click_timer = Timer(double_click_threshold, finalize_single_click)
pending_click_timer.start()
def on_click(x, y, button, pressed):
global mouse_pressed, drag_path, drag_start, drag_button, press_time
global last_click_time, last_click_pos, last_click_button
btn_name = button_name_from_pynput(button)
if pressed:
mouse_pressed = True
drag_start = (x, y)
drag_button = btn_name
drag_path = []
press_time = time.time()
else:
mouse_pressed = False
release_time = time.time()
dx = x - drag_start[0]
dy = y - drag_start[1]
distance = (dx*dx + dy*dy)**0.5
if distance > click_distance_threshold:
record_drag_event(drag_start, (x,y), drag_path)
else:
current_time = time.time()
if (last_click_time is not None and
(current_time - last_click_time) <= double_click_threshold and
last_click_button == btn_name):
# 雙擊事件
logger.debug("Double click detected.")
cancel_pending_single_click()
record_click_event("double_click", (x,y), btn_name)
last_click_time = None
last_click_pos = None
last_click_button = None
else:
# 單擊候選
schedule_single_click((x,y), btn_name)
last_click_time = current_time
last_click_pos = (x,y)
last_click_button = btn_name
def on_move(x, y):
global mouse_pressed, drag_path
if mouse_pressed:
drag_path.append({"x": x, "y": y, "time": datetime.now().isoformat()})
def on_scroll(x, y, dx, dy):
record_scroll_event(x, y, dx, dy)
def on_press(key):
# 處理ctrl+alt+esc退出
if key in [keyboard.Key.ctrl_l, keyboard.Key.ctrl_r,
keyboard.Key.shift, keyboard.Key.shift_r,
keyboard.Key.alt_l, keyboard.Key.alt_r]:
handle_modifiers(key, True)
else:
try:
key_char = key.char
if key_char and key_char.isprintable():
if handle_combination(key_char):
return
else:
handle_normal_char(key_char)
else:
handle_special_key(key)
except AttributeError:
handle_special_key(key)
# 檢查ctrl+alt+esc退出條件
if key == keyboard.Key.esc and ctrl_pressed and alt_pressed:
check_exit_condition()
def on_release(key):
if key in [keyboard.Key.ctrl_l, keyboard.Key.ctrl_r,
keyboard.Key.shift, keyboard.Key.shift_r,
keyboard.Key.alt_l, keyboard.Key.alt_r]:
handle_modifiers(key, False)
class RedBorderWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
screen = QGuiApplication.primaryScreen()
geometry = screen.geometry()
self.setGeometry(geometry)
self.showFullScreen()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
width = self.width()
height = self.height()
border_width = 10
pen = QPen(QColor("red"))
pen.setWidth(border_width)
painter.setPen(pen)
painter.drawRect(QRect(int(border_width/2), int(border_width/2),
int(width - border_width), int(height - border_width)))
outer_region = QRegion(0, 0, width, height)
inner_region = QRegion(border_width, border_width, width - 2*border_width, height - 2*border_width)
frame_region = outer_region.subtracted(inner_region)
self.setMask(frame_region)
def start_listeners():
logger.info("Starting mouse and keyboard listeners...")
global mouse_listener, keyboard_listener
mouse_listener = mouse.Listener(
on_click=on_click,
on_move=on_move,
on_scroll=on_scroll
)
keyboard_listener = keyboard.Listener(
on_press=on_press,
on_release=on_release
)
mouse_listener.start()
keyboard_listener.start()
mouse_listener.join()
keyboard_listener.join()
if __name__ == "__main__":
logger.info("Program started.")
app = QApplication(sys.argv)
red_border_window = RedBorderWindow()
red_border_window.show()
listener_thread = Thread(target=start_listeners)
listener_thread.start()
result = app.exec_()
logger.info("Program exited with code {}".format(result))
os._exit(0) # 確保主執行緒退出後強制結束程序