import cv2
import mediapipe as mp
import time
from interception import ffi, lib

def get_keyboard_id():
    for device in range(1, 20):
        if lib.interception_is_keyboard(device):
            return device
    raise Exception("Keyboard device not found")

def get_mouse_id():
    for device in range(1, 20):
        if lib.interception_is_mouse(device):
            return device
    raise Exception("Mouse device not found")

def is_finger_up(tip_y, pip_y):
    return tip_y < pip_y

def is_finger_down(tip_y, pip_y):
    return tip_y > pip_y

class HandController:
    def __init__(self):
        self.cap = cv2.VideoCapture(0)
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

        self.hands = mp.solutions.hands.Hands(
            max_num_hands=2,
            min_detection_confidence=0.7,
            min_tracking_confidence=0.7
        )
        self.mp_draw = mp.solutions.drawing_utils
        self.interception_context = lib.interception_create_context()
        if self.interception_context == ffi.NULL:
            raise Exception("Failed to initialize interception context.")
        self.device = get_keyboard_id()
        self.mouse_device = get_mouse_id()

        self.left_key_mappings = {'w': False,'shift': False,'a': False,'d': False,'space': False}
        self.right_key_mappings = {'e': False, 'q': False}
        self.previous_left_key_mappings = self.left_key_mappings.copy()
        self.previous_right_key_mappings = self.right_key_mappings.copy()

        self.finger_tips = {'thumb': 4, 'index': 8, 'middle': 12, 'ring': 16, 'pinky': 20}
        self.finger_pips = {'thumb': 3, 'index': 6, 'middle': 10, 'ring': 14, 'pinky': 18}
        self.key_codes = {
            'w': 0x11, 'a': 0x1E, 'd': 0x20, 'space': 0x39, 'shift': 0x2A,
            '1': 0x02, '2': 0x03, '3': 0x04, '4': 0x05, 'e': 0x12, 'q': 0x10
        }
        self.walk_threshold = 5
        self.run_threshold = 30
        self.prev_right_hand_pos = None
        self.mouse_sensitivity = 16

        self.current_key = 1
        self.prev_index_closed = False
        self.prev_thumb_closed = False

    def run(self):
        try:
            while self.cap.isOpened():
                success, img = self.cap.read()
                if not success:
                    print("Ignoring empty camera frame.")
                    continue

                img = cv2.flip(img, 1)
                img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

                start_time = time.time()
                results = self.hands.process(img_rgb)
                end_time = time.time()

                h, w, _ = img.shape
                self.left_hand_detected = False
                self.right_hand_detected = False

                if results.multi_hand_landmarks:
                    for idx, handLms in enumerate(results.multi_hand_landmarks):
                        hand_label = results.multi_handedness[idx].classification[0].label
                        self.mp_draw.draw_landmarks(img, handLms, mp.solutions.hands.HAND_CONNECTIONS)

                        # Extract necessary landmark positions
                        lm = {id: (int(lm.x * w), int(lm.y * h)) for id, lm in enumerate(handLms.landmark)}
                        if hand_label == 'Left':
                            self.left_hand_detected = True
                            self.check_fingers(lm)
                        elif hand_label == 'Right':
                            self.right_hand_detected = True
                            self.move_mouse(lm)
                            self.handle_right_hand_gestures(lm)

                if self.left_hand_detected or self.right_hand_detected:
                    self.perform_actions()
                else:
                    self.release_all_keys()

                if not self.right_hand_detected:
                    self.prev_right_hand_pos = None

                cv2.imshow("Hand Controller", img)
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break

        finally:
            self.release_all_keys()
            self.cap.release()
            cv2.destroyAllWindows()
            lib.interception_destroy_context(self.interception_context)

    def check_fingers(self, lm):
        self.left_key_mappings = {key: False for key in self.left_key_mappings}
        if is_finger_down(lm[self.finger_tips['index']][1], lm[self.finger_pips['index']][1]):
            self.left_key_mappings['d'] = True
        if is_finger_down(lm[self.finger_tips['ring']][1], lm[self.finger_pips['ring']][1]):
            self.left_key_mappings['a'] = True
        if is_finger_down(lm[self.finger_tips['thumb']][1], lm[self.finger_pips['thumb']][1]):
            self.left_key_mappings['space'] = True

        y_tip = lm[self.finger_tips['middle']][1]
        y_pip = lm[self.finger_pips['middle']][1]
        vertical_distance = y_tip - y_pip
        if vertical_distance > self.run_threshold:
            self.left_key_mappings['w'] = True
            self.left_key_mappings['shift'] = True
        elif vertical_distance > self.walk_threshold:
            self.left_key_mappings['w'] = True

    def perform_actions(self):
        for key in self.left_key_mappings:
            if self.left_key_mappings[key] != self.previous_left_key_mappings[key]:
                key_code = self.key_codes[key]
                state = lib.INTERCEPTION_KEY_DOWN if self.left_key_mappings[key] else lib.INTERCEPTION_KEY_UP
                stroke = ffi.new("InterceptionKeyStroke *", {'code': key_code, 'state': state, 'information': 0})
                lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke), 1)
        self.previous_left_key_mappings = self.left_key_mappings.copy()

        for key in self.right_key_mappings:
            if self.right_key_mappings[key] != self.previous_right_key_mappings[key]:
                key_code = self.key_codes[key]
                state = lib.INTERCEPTION_KEY_DOWN if self.right_key_mappings[key] else lib.INTERCEPTION_KEY_UP
                stroke = ffi.new("InterceptionKeyStroke *", {'code': key_code, 'state': state, 'information': 0})
                lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke), 1)
        self.previous_right_key_mappings = self.right_key_mappings.copy()

    def release_all_keys(self):
        for key, pressed in self.previous_left_key_mappings.items():
            if pressed:
                key_code = self.key_codes[key]
                stroke = ffi.new("InterceptionKeyStroke *", {'code': key_code, 'state': lib.INTERCEPTION_KEY_UP, 'information': 0})
                lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke), 1)
        self.previous_left_key_mappings = self.left_key_mappings.copy()

        for key, pressed in self.previous_right_key_mappings.items():
            if pressed:
                key_code = self.key_codes[key]
                stroke = ffi.new("InterceptionKeyStroke *", {'code': key_code, 'state': lib.INTERCEPTION_KEY_UP, 'information': 0})
                lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke), 1)
        self.previous_right_key_mappings = self.right_key_mappings.copy()

    def is_hand_open(self, lm):
        fingers_up = sum(is_finger_up(lm[self.finger_tips[finger]][1], lm[self.finger_pips[finger]][1])
                         for finger in self.finger_tips)
        return fingers_up > 2

    def is_fist(self, lm):
        fingers_down = sum(is_finger_down(lm[self.finger_tips[finger]][1], lm[self.finger_pips[finger]][1])
                           for finger in self.finger_tips)
        return fingers_down >= 4

    def move_mouse(self, lm):
        if self.is_hand_open(lm):
            current_x, current_y = lm[0]
            if self.prev_right_hand_pos is not None:
                delta_x = int((current_x - self.prev_right_hand_pos[0]) * self.mouse_sensitivity)
                delta_y = int((current_y - self.prev_right_hand_pos[1]) * self.mouse_sensitivity)
                stroke = ffi.new("InterceptionMouseStroke *", {
                    'state': 0,
                    'flags': lib.INTERCEPTION_MOUSE_MOVE_RELATIVE,
                    'rolling': 0,
                    'x': delta_x,
                    'y': delta_y,
                    'information': 0
                })
                lib.interception_send(self.interception_context, self.mouse_device, ffi.cast("InterceptionStroke *", stroke), 1)
            self.prev_right_hand_pos = (current_x, current_y)
        else:
            self.prev_right_hand_pos = None

    def handle_right_hand_gestures(self, lm):
        if not self.is_fist(lm):
            index_closed = is_finger_down(lm[self.finger_tips['index']][1], lm[self.finger_pips['index']][1])
            if index_closed and not self.prev_index_closed:
                self.increment_key()
                self.prev_index_closed = True
            elif not index_closed:
                self.prev_index_closed = False

            thumb_closed = is_finger_down(lm[self.finger_tips['thumb']][1], lm[self.finger_pips['thumb']][1])
            if thumb_closed and not self.prev_thumb_closed:
                self.decrement_key()
                self.prev_thumb_closed = True
            elif not thumb_closed:
                self.prev_thumb_closed = False

            middle_closed = is_finger_down(lm[self.finger_tips['middle']][1], lm[self.finger_pips['middle']][1])
            self.right_key_mappings['e'] = middle_closed

            pinky_closed = is_finger_down(lm[self.finger_tips['pinky']][1], lm[self.finger_pips['pinky']][1])
            self.right_key_mappings['q'] = pinky_closed
        else:
            self.prev_index_closed = False
            self.prev_thumb_closed = False
            self.right_key_mappings['e'] = False
            self.right_key_mappings['q'] = False

    def increment_key(self):
        old_key = self.current_key
        self.current_key = self.current_key % 4 + 1
        self.update_number_key(old_key, self.current_key)

    def decrement_key(self):
        old_key = self.current_key
        self.current_key = (self.current_key - 2) % 4 + 1
        self.update_number_key(old_key, self.current_key)

    def update_number_key(self, old_key, new_key):
        old_key_code = self.key_codes[str(old_key)]
        stroke_up = ffi.new("InterceptionKeyStroke *", {
            'code': old_key_code,
            'state': lib.INTERCEPTION_KEY_UP,
            'information': 0
        })
        lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke_up), 1)

        new_key_code = self.key_codes[str(new_key)]
        stroke_down = ffi.new("InterceptionKeyStroke *", {
            'code': new_key_code,
            'state': lib.INTERCEPTION_KEY_DOWN,
            'information': 0
        })
        lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke_down), 1)

        stroke_up_new = ffi.new("InterceptionKeyStroke *", {
            'code': new_key_code,
            'state': lib.INTERCEPTION_KEY_UP,
            'information': 0
        })
        lib.interception_send(self.interception_context, self.device, ffi.cast("InterceptionStroke *", stroke_up_new), 1)

if __name__ == "__main__":
    HandController().run()
Tambahkan apabila tangan kanan, jari kelingling turun, maka akan pencet left mouse spam ya setiap 0.25 detik, kalau berdiri baru berhenti!