screen-record

骑着单车滑翔發表於2024-12-06
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)  # 確保主執行緒退出後強制結束程序