тепер хоткей працює, і навіть не перекривається віндовими хоткеями, єдине що логіка тепер не відпрацьовує воно має, імітувати контрол+с копіювати виділений текст відправляти на переклад і вставляти туди результат роботи нейронки а не просто копіювати вставляти текст чи відправляти буфер перед тим як скопіювало, перепиши файл повністю  #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AI-помічник-перекладач.  Глобальний хот-кей ‟Ctrl+D”:
 • На Windows перехоплення через власний LowLevelKeyboardProc +
   RegisterHotKey (MOD_NOREPEAT) ➜ інші програми поєднання НЕ бачать.
 • На інших ОС – через бібліотеку `keyboard` (suppress=True).

Автор: you 😉
"""

import ctypes
import json
import os
import sys
import threading
import time
from ctypes import wintypes
from typing import Optional

import keyboard
import pyperclip
import tkinter as tk
from PIL import Image, ImageDraw
from pystray import Icon, Menu, MenuItem
from tkinter import ttk, messagebox

from ai_client import translate_text, set_service   # ← ваш клієнт


# ─────────────────────────  Глобальні змінні  ──────────────────────────
RUNNING               = True
HOTKEY_ACTIVE         = False
CONFIG_FILE           = "config.json"
MODELS_FILE           = "models.json"
IS_WINDOWS            = sys.platform.startswith("win32")
WIN_HOTKEY_ID         = 1          # будь-який невеликий int

user32: Optional[ctypes.WinDLL] = ctypes.windll.user32 if IS_WINDOWS else None
kernel32: Optional[ctypes.WinDLL] = ctypes.windll.kernel32 if IS_WINDOWS else None


# ─────────────────────────  Конфіг / моделі  ───────────────────────────
def load_config() -> dict:
    default = {
        "service": "gemini",
        "model": "gemini-2.5-flash",
        "target_language": "українська",
        "alternative_language": "українська",
    }
    try:
        if os.path.exists(CONFIG_FILE):
            with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception as e:
        print(f"⚠️  Не вдалося завантажити config: {e}")
    return default


def save_config(cfg: dict) -> bool:
    try:
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(cfg, f, ensure_ascii=False, indent=2)
        return True
    except Exception as e:
        print(f"⚠️  Не вдалося зберегти config: {e}")
        return False


def load_models() -> dict:
    if os.path.exists(MODELS_FILE):
        try:
            with open(MODELS_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception as e:
            print(f"⚠️  Не вдалося завантажити models: {e}")
    return {}


def init_model(service: str, model: str):
    params = load_models().get(service, {}).get(model, {})
    print(f"🔧 Ініціалізовано модель {model} сервісу {service}")
    print(f"⚙️  Параметри: {params}")
    return params


# ─────────────────  Початкові налаштування  ────────────────────────────
cfg                   = load_config()
SERVICE               = cfg["service"]
MODEL                 = cfg["model"]
TARGET_LANGUAGE       = cfg["target_language"]
ALTERNATIVE_LANGUAGE  = cfg["alternative_language"]

set_service(SERVICE)
init_model(SERVICE, MODEL)


# ──────────────────────  Логіка перекладу  ─────────────────────────────
def process_text():
    global HOTKEY_ACTIVE
    if HOTKEY_ACTIVE:
        return
    HOTKEY_ACTIVE = True
    orig_clip = pyperclip.paste()
    try:
        keyboard.press_and_release("ctrl+c")
        time.sleep(0.1)
        src = pyperclip.paste().strip()
        if not src:
            print("⚠️  Порожній буфер")
            return
        print(f"📋 Отримано {len(src)} симв.")
        dst = translate_text(src, TARGET_LANGUAGE, SERVICE)
        if dst:
            pyperclip.copy(dst)
            print(f"✅ Перекладено ({len(dst)} симв.)")
            time.sleep(0.1)
            keyboard.press_and_release("ctrl+v")
        else:
            print("❌ Нема відповіді від сервісу")
    except Exception as e:
        print(f"🚫 Помилка обробки: {e}")
    finally:
        pyperclip.copy(orig_clip)
        HOTKEY_ACTIVE = False


def on_activate():
    threading.Thread(target=process_text, daemon=True).start()


# ─────────────────────  Хот-кей Windows  ───────────────────────────────
LLKHF_INJECTED = 0x00000010
WH_KEYBOARD_LL = 13
WM_KEYDOWN     = 0x0100
WM_SYSKEYDOWN  = 0x0104
WM_KEYUP       = 0x0101
VK_D           = 0x44
CTRL_MASK      = 0x0008   # bit 3 у KBDLLHOOKSTRUCT-flags

LowLevelKeyboardProc = ctypes.WINFUNCTYPE(
    ctypes.c_long, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM
)


def _low_level_proc(nCode, wParam, lParam):
    """
    LL-hook: якщо натиснуто Ctrl+D – блокуємо подальше проходження клавіші,
    власна логіка виконується при WM_KEYUP (аби не повторюватись).
    """
    if nCode >= 0:
        kb = ctypes.cast(lParam, ctypes.POINTER(wintypes.KBDLLHOOKSTRUCT)).contents
        ctrl_pressed = (user32.GetAsyncKeyState(0x11) & 0x8000) != 0  # VK_CONTROL
        is_d = kb.vkCode == VK_D
        injected = kb.flags & LLKHF_INJECTED
        if ctrl_pressed and is_d and not injected:
            if wParam == WM_KEYUP:
                on_activate()
            return 1   # 1 ➜ блокуємо клавішу для інших
    return user32.CallNextHookEx(None, nCode, wParam, lParam)


def _start_windows_hotkey() -> bool:
    """
    • Ставимо low-level-hook (щоб повністю «з’їсти» Ctrl+D).
    • Реєструємо RegisterHotKey на випадок, якщо hook не спрацьовує
      через UAC/Win-Store-apps (подвійна страховка).
    Все працює у dedicated-потоці, тому GUI / основний цикл не блокуються.
    """

    def thread_fn():
        # 1. Low-level hook
        proc      = LowLevelKeyboardProc(_low_level_proc)
        hook_id   = user32.SetWindowsHookExW(WH_KEYBOARD_LL, proc, kernel32.GetModuleHandleW(None), 0)

        # 2. RegisterHotKey (не блокує, але дає WM_HOTKEY на випадок
        #    коли low-level-hook буде відключено правилами безпеки)
        MOD_CONTROL, MOD_NOREPEAT = 0x0002, 0x4000
        user32.RegisterHotKey(None, WIN_HOTKEY_ID,
                              MOD_CONTROL | MOD_NOREPEAT, VK_D)

        msg = wintypes.MSG()
        while RUNNING:
            if user32.GetMessageW(ctypes.byref(msg), None, 0, 0) != 0:
                if msg.message == 0x0312 and msg.wParam == WIN_HOTKEY_ID:  # WM_HOTKEY
                    on_activate()
                user32.TranslateMessage(ctypes.byref(msg))
                user32.DispatchMessageW(ctypes.byref(msg))

        # при виході
        if hook_id:
            user32.UnhookWindowsHookEx(hook_id)
        user32.UnregisterHotKey(None, WIN_HOTKEY_ID)

    try:
        threading.Thread(target=thread_fn, daemon=True).start()
        print("⌨️  Ctrl+D перехоплено через WinAPI (hook + RegisterHotKey)")
        return True
    except Exception as e:
        print(f"⚠️  Не вдалося ініціалізувати Win-hotkey: {e}")
        return False


# ─────────────────────  Хот-кей не-Windows  ────────────────────────────
def _start_keyboard_hotkey() -> bool:
    try:
        keyboard.add_hotkey("ctrl+d", on_activate,
                            suppress=True, trigger_on_release=True)
        print("⌨️  Ctrl+D зареєстровано через keyboard-lib")
        return True
    except Exception as e:
        print(f"⚠️  Не вдалося зареєструвати hotkey (keyboard): {e}")
        return False


def register_hotkey() -> bool:
    if IS_WINDOWS:
        ok = _start_windows_hotkey()
        if not ok:
            ok = _start_keyboard_hotkey()
        return ok
    return _start_keyboard_hotkey()


def unregister_hotkeys():
    if IS_WINDOWS:
        try:
            user32.UnregisterHotKey(None, WIN_HOTKEY_ID)
        except Exception:
            pass
    keyboard.unhook_all_hotkeys()


# ─────────────────────  Tray-іконка та меню  ───────────────────────────
def create_tray_icon():
    img = Image.new("RGB", (64, 64), "black")
    ImageDraw.Draw(img).text((10, 25), "T", fill="white")
    return img


def update_menu(icon):
    icon.menu = Menu(
        MenuItem(f"Сервіс: {SERVICE}", None, enabled=False),
        MenuItem(f"Модель: {MODEL}", None, enabled=False),
        MenuItem(f"Мова: {TARGET_LANGUAGE}", None, enabled=False),
        MenuItem("Конфігурація", lambda: show_config_window(icon)),
        MenuItem("Вийти", lambda: exit_action(icon)),
    )
    icon.update_menu()


def setup_tray(ic: Icon):
    ic.visible = True
    update_menu(ic)
    print("🟢  Запущено.  Ctrl+D – перекласти виділене.")


# ───────────────────────────  GUI конфіг  ─────────────────────────────
def show_config_window(icon):
    def run():
        global SERVICE, MODEL, TARGET_LANGUAGE, ALTERNATIVE_LANGUAGE, cfg

        models_data = load_models()

        def update_models(*_):
            srv = service_var.get()
            model_box["values"] = list(models_data.get(srv, {}).keys())
            if model_box["values"]:
                model_box.current(0)

        def on_save():
            new_cfg = dict(
                service=service_var.get(),
                model=model_var.get(),
                target_language=target_var.get(),
                alternative_language=alt_var.get(),
            )
            if save_config(new_cfg):
                cfg.update(new_cfg)
                SERVICE, MODEL = new_cfg["service"], new_cfg["model"]
                TARGET_LANGUAGE, ALTERNATIVE_LANGUAGE = (
                    new_cfg["target_language"], new_cfg["alternative_language"]
                )
                set_service(SERVICE)
                init_model(SERVICE, MODEL)
                update_menu(icon)
                wnd.destroy()
            else:
                messagebox.showerror("Помилка", "Не вдалося зберегти файл")

        wnd = tk.Tk()
        wnd.title("Конфігурація перекладу")
        wnd.geometry("420x260")
        wnd.resizable(False, False)
        frm = ttk.Frame(wnd, padding="15")
        frm.pack(fill="both", expand=True)

        ttk.Label(frm, text="Сервіс:").grid(row=0, column=0, sticky="w")
        service_var = tk.StringVar(value=SERVICE)
        service_box = ttk.Combobox(frm, textvariable=service_var,
                                   values=list(models_data.keys()),
                                   state="readonly")
        service_box.grid(row=0, column=1, sticky="ew", pady=5)
        service_box.bind("<<ComboboxSelected>>", update_models)

        ttk.Label(frm, text="Модель:").grid(row=1, column=0, sticky="w")
        model_var = tk.StringVar(value=MODEL)
        model_box = ttk.Combobox(frm, textvariable=model_var, state="readonly")
        model_box.grid(row=1, column=1, sticky="ew", pady=5)

        ttk.Label(frm, text="Цільова мова:").grid(row=2, column=0, sticky="w")
        target_var = tk.StringVar(value=TARGET_LANGUAGE)
        ttk.Entry(frm, textvariable=target_var).grid(row=2, column=1, sticky="ew", pady=5)

        ttk.Label(frm, text="Альтернативна мова:").grid(row=3, column=0, sticky="w")
        alt_var = tk.StringVar(value=ALTERNATIVE_LANGUAGE)
        ttk.Entry(frm, textvariable=alt_var).grid(row=3, column=1, sticky="ew", pady=5)

        btns = ttk.Frame(frm)
        btns.grid(row=4, column=0, columnspan=2, pady=15)
        ttk.Button(btns, text="Зберегти", command=on_save).pack(side="right", padx=5)
        ttk.Button(btns, text="Скасувати", command=wnd.destroy).pack(side="right")

        update_models()
        wnd.mainloop()

    threading.Thread(target=run, daemon=True).start()


# ──────────────────────────  Завершення  ──────────────────────────────
def exit_action(icon):
    global RUNNING
    RUNNING = False
    unregister_hotkeys()
    icon.stop()
    print("🔴  Служба зупинена")
    os._exit(0)


# ───────────────────────────────  main  ────────────────────────────────
def main():
    if not register_hotkey():
        messagebox.showerror("Помилка", "Не вдалося ініціалізувати Ctrl+D")
        return

    icon = Icon("AI-Translator", create_tray_icon())
    threading.Thread(target=icon.run, args=(setup_tray,), daemon=True).start()

    try:
        while RUNNING:
            time.sleep(0.3)
    except KeyboardInterrupt:
        pass
    finally:
        unregister_hotkeys()
        print("🔑  Усі хот-кеї знято")


if __name__ == "__main__":
    main()