From 5549bf85907c6a8e0327a7181271921fb47ee8fd Mon Sep 17 00:00:00 2001 From: FURK4NGG <105324908+FURK4NGG@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:01:55 +0300 Subject: [PATCH] Add files via upload --- cli.py | 1225 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1225 insertions(+) create mode 100644 cli.py diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..12fae9d --- /dev/null +++ b/cli.py @@ -0,0 +1,1225 @@ +#!/usr/bin/env python3 +import os +import sys +import json +import base64 +import shutil +import tempfile +import subprocess +from pathlib import Path +from datetime import datetime + +CONFIG_PATH = Path.home() / ".config" / "capture-ai" / "config.json" +CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + +BASE_DIR = Path.home() / ".cache" / "capture-ai" +CHAT_DIR = BASE_DIR / "chats" +AI_SCRIPT = str(Path.home() / "capture-ai" / "ai.py") +GENERATED_DIR = BASE_DIR / "generated_images" + +CHAT_DIR.mkdir(parents=True, exist_ok=True) +GENERATED_DIR.mkdir(parents=True, exist_ok=True) + + +def fix_mojibake(s: str) -> str: + if not s: + return s + if ("Ã" not in s) and ("Å" not in s) and ("â" not in s): + return s + try: + return s.encode("latin-1").decode("utf-8") + except Exception: + return s + + +class ChatCLI: + def __init__(self): + self.pending_images = [] + self.pending_files = [] + + cfg = self.load_config() + last_chat_name = str(cfg.get("last_chat", "default.json") or "default.json").strip() + if not last_chat_name: + last_chat_name = "default.json" + + self.current_chat = CHAT_DIR / last_chat_name + if not self.current_chat.exists(): + self.current_chat.write_text("[]", encoding="utf-8") + + self.ensure_base_config() + self.ensure_ui_i18n_config() + self.active_model = self.get_chat_model(self.current_chat) + + def clear_screen(self): + os.system("clear") + + def _read_input(self, prompt=""): + try: + return input(prompt) + except EOFError: + return "" + + def _is_escape_input(self, s: str) -> bool: + s = str(s or "") + + if not s: + print(f"{self('o_Empty_Input')}") + self._read_input(f"{self('o_to_Menu')}") + return True + + return s == "\x1b" or s.strip().lower() in {"esc", ":q"} + + # ---------------- CONFIG ---------------- + + def load_config(self): + try: + if CONFIG_PATH.exists(): + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + return json.load(f) or {} + except Exception as e: + print(f"config read error: {e}") + return {} + + def save_config(self, cfg: dict): + try: + CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(cfg, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"config save error: {e}") + + def ensure_ui_i18n_config(self): + cfg = self.load_config() + changed = False + + if not isinstance(cfg.get("ui_language"), str) or not str(cfg.get("ui_language")).strip(): + cfg["ui_language"] = "en" + changed = True + + if not isinstance(cfg.get("ui_strings"), dict): + cfg["ui_strings"] = {} + changed = True + + if changed: + self.save_config(cfg) + + def get_ui_strings_map(self) -> dict: + cfg = self.load_config() + raw = cfg.get("ui_strings", {}) + return raw if isinstance(raw, dict) else {} + + def get_ui_lang_map(self, lang: str | None = None) -> dict: + if lang is None: + lang = self.get_ui_language() + + all_strings = self.get_ui_strings_map() + selected = all_strings.get(lang, {}) + if not isinstance(selected, dict): + selected = {} + return selected + + def __call__(self, key: str) -> str: + lang_map = self.get_ui_lang_map() + return str(lang_map.get(key, key)) + + def ensure_base_config(self): + cfg = self.load_config() + changed = False + + if "show_usage" not in cfg: + cfg["show_usage"] = True + changed = True + + if "ui_language" not in cfg: + cfg["ui_language"] = "tr" + changed = True + + if "response_style" not in cfg: + cfg["response_style"] = "" + changed = True + + if "force_ui_language" not in cfg: + cfg["force_ui_language"] = False + changed = True + + if "open_router_key" not in cfg: + cfg["open_router_key"] = "" + changed = True + + if "chat_models" not in cfg or not isinstance(cfg.get("chat_models"), dict): + cfg["chat_models"] = {} + changed = True + + if "pinned_chats" not in cfg or not isinstance(cfg.get("pinned_chats"), list): + cfg["pinned_chats"] = [] + changed = True + + if "is_mic_online" not in cfg: + cfg["is_mic_online"] = True + changed = True + + if "use_desktop_voice" not in cfg: + cfg["use_desktop_voice"] = False + changed = True + + if "stt_model_online" not in cfg: + cfg["stt_model_online"] = "openai/gpt-audio-mini" + changed = True + + if "whisper_cpp_bin" not in cfg: + cfg["whisper_cpp_bin"] = "/home/$USER/whisper.cpp/build/bin/whisper-cli" + changed = True + + if "whisper_cpp_model" not in cfg: + cfg["whisper_cpp_model"] = "/home/$USER/.local/share/whisper/ggml-tiny.bin" + changed = True + + models = self._get_models_list(cfg) + if not models: + cfg["ai_models"] = ["google/gemini-2.5-flash-image"] + changed = True + + if not str(cfg.get("last_chat", "")).strip(): + cfg["last_chat"] = "default.json" + changed = True + + if changed: + self.save_config(cfg) + + def get_available_ui_languages(self) -> list[str]: + strings = self.get_ui_strings_map() + if not isinstance(strings, dict): + return [] + + out = [] + for k in strings.keys(): + ks = str(k).strip().lower() + if ks: + out.append(ks) + + return out + + def get_ui_language(self) -> str: + cfg = self.load_config() + lang = str(cfg.get("ui_language", "tr") or "tr").strip().lower() + return lang if lang else "tr" + + def set_ui_language(self, lang: str): + lang = str(lang or "").strip().lower() + if not lang: + return + cfg = self.load_config() + cfg["ui_language"] = lang + self.save_config(cfg) + + def _get_models_list(self, cfg: dict): + models = cfg.get("ai_models", []) + if not isinstance(models, list): + models = [] + return [str(m).strip() for m in models if str(m).strip()] + + def _ensure_min_one_model(self, cfg: dict): + models = self._get_models_list(cfg) + if not models: + models = ["google/gemini-2.5-flash-image"] + cfg["ai_models"] = models + self.save_config(cfg) + return models + + def get_chat_model(self, chat_path: Path) -> str: + cfg = self.load_config() + models = self._ensure_min_one_model(cfg) + + chat_models = cfg.get("chat_models", {}) + if not isinstance(chat_models, dict): + chat_models = {} + + key = chat_path.name + m = str(chat_models.get(key) or "").strip() + + if (not m) or (m not in models): + m = str(models[0]) + chat_models[key] = m + cfg["chat_models"] = chat_models + self.save_config(cfg) + + return m + + def set_chat_model(self, chat_path: Path, model_id: str): + model_id = str(model_id).strip() + if not model_id: + return + + cfg = self.load_config() + models = self._ensure_min_one_model(cfg) + + if model_id not in models: + models.insert(0, model_id) + else: + models = [m for m in models if m != model_id] + models.insert(0, model_id) + + cfg["ai_models"] = models + + chat_models = cfg.get("chat_models", {}) + if not isinstance(chat_models, dict): + chat_models = {} + chat_models[chat_path.name] = model_id + cfg["chat_models"] = chat_models + + self.save_config(cfg) + self.active_model = model_id + + # ---------------- CHAT FILES ---------------- + + def load_chat_messages(self): + try: + with open(self.current_chat, "r", encoding="utf-8") as f: + return json.load(f) or [] + except Exception: + return [] + + def save_chat_messages(self, messages): + with open(self.current_chat, "w", encoding="utf-8") as f: + json.dump(messages, f, ensure_ascii=False, indent=2) + + def list_chat_files(self): + cfg = self.load_config() + pinned = cfg.get("pinned_chats", []) + if not isinstance(pinned, list): + pinned = [] + + files = list(CHAT_DIR.glob("*.json")) + + def mtime(p): + try: + return p.stat().st_mtime + except Exception: + return 0.0 + + def sort_key(p): + return (0 if p.name in pinned else 1, -mtime(p), p.name.lower()) + + return sorted(files, key=sort_key) + + def create_chat(self): + i = 1 + while True: + name = f"chat_{i}.json" + path = CHAT_DIR / name + if not path.exists(): + break + i += 1 + + path.write_text("[]", encoding="utf-8") + self.current_chat = path + + cfg = self.load_config() + cfg["last_chat"] = path.name + + models = self._ensure_min_one_model(cfg) + chat_models = cfg.get("chat_models", {}) + if not isinstance(chat_models, dict): + chat_models = {} + chat_models[path.name] = str(models[0]) + cfg["chat_models"] = chat_models + self.save_config(cfg) + + self.active_model = self.get_chat_model(self.current_chat) + print(f"\n✔ {self('o_New_Chat')}: {path.stem}") + self._read_input(f"{self('o_to_Menu')}") + + def delete_chat(self, file_path: Path): + if not file_path.exists(): + print("Chat bulunamadı.") + return + + try: + file_path.unlink() + except Exception as e: + print(f"Delete error: {e}") + return + + cfg = self.load_config() + + pinned = cfg.get("pinned_chats", []) + if isinstance(pinned, list) and file_path.name in pinned: + cfg["pinned_chats"] = [x for x in pinned if x != file_path.name] + + chat_models = cfg.get("chat_models", {}) + if isinstance(chat_models, dict) and file_path.name in chat_models: + del chat_models[file_path.name] + cfg["chat_models"] = chat_models + + if cfg.get("last_chat") == file_path.name: + cfg["last_chat"] = "default.json" + + self.save_config(cfg) + + if self.current_chat == file_path: + chats = self.list_chat_files() + if chats: + self.current_chat = chats[0] + else: + default_chat = CHAT_DIR / "default.json" + default_chat.write_text("[]", encoding="utf-8") + self.current_chat = default_chat + + cfg = self.load_config() + cfg["last_chat"] = self.current_chat.name + self.save_config(cfg) + + self.active_model = self.get_chat_model(self.current_chat) + print(f"\n✔ {self('o_Delete_Chat')}: {file_path.stem}") + + def rename_chat(self, old_path: Path, new_name_raw: str): + new_name = (new_name_raw or "").strip() + if not new_name: + print("Geçersiz isim.") + return + + for ch in ["/", "\\", "\n", "\r", "\t"]: + new_name = new_name.replace(ch, "_") + + new_path = old_path.with_name(new_name + ".json") + + if new_path == old_path: + print(f"{self('o_Chat_Rename_Fail')}") + return + + if new_path.exists(): + print("Bu isimde chat zaten var.") + return + + try: + old_path.rename(new_path) + except Exception as e: + print(f"Yeniden adlandırma hatası: {e}") + return + + cfg = self.load_config() + + plist = cfg.get("pinned_chats", []) + if isinstance(plist, list) and old_path.name in plist: + cfg["pinned_chats"] = [new_path.name if x == old_path.name else x for x in plist] + + if cfg.get("last_chat") == old_path.name: + cfg["last_chat"] = new_path.name + + chat_models = cfg.get("chat_models", {}) + if isinstance(chat_models, dict) and old_path.name in chat_models: + chat_models[new_path.name] = chat_models.pop(old_path.name) + cfg["chat_models"] = chat_models + + self.save_config(cfg) + + if self.current_chat == old_path: + self.current_chat = new_path + self.active_model = self.get_chat_model(self.current_chat) + + print(f"\n{self('o_New_Name')} {new_path.stem}") + + def select_chat(self): + chats = self.list_chat_files() + if not chats: + print("Hiç chat yok.") + return + + print(f"\n{self('o_Chats')}:") + for i, p in enumerate(chats, 1): + mark = "*" if p == self.current_chat else " " + print(f"{i}) {mark} {p.stem}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + if not choice.isdigit(): + return + + idx = int(choice) - 1 + if not (0 <= idx < len(chats)): + return + + self.current_chat = chats[idx] + cfg = self.load_config() + cfg["last_chat"] = self.current_chat.name + self.save_config(cfg) + self.active_model = self.get_chat_model(self.current_chat) + + print(f"\n{self('o_Active_Chat')}: {self.current_chat.stem}") + self._read_input(f"{self('o_to_Menu')}") + + + # ---------------- IMAGES / FILES ---------------- + + def save_base64_image(self, b64_data: str, ext: str = "png") -> str | None: + try: + raw = base64.b64decode(b64_data) + stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") + out = GENERATED_DIR / f"gen-{stamp}.{ext}" + out.write_bytes(raw) + return str(out.resolve()) + except Exception as e: + print(f"save_base64_image error: {e}") + return None + + def download_image_to_cache(self, url: str) -> str | None: + try: + import urllib.request + + stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") + ext = "png" + low = url.lower() + if ".jpg" in low or ".jpeg" in low: + ext = "jpg" + elif ".webp" in low: + ext = "webp" + + out = GENERATED_DIR / f"gen-{stamp}.{ext}" + urllib.request.urlretrieve(url, str(out)) + return str(out.resolve()) + except Exception as e: + print(f"download_image_to_cache error: {e}") + return None + + def add_file_prompt(self): + self.clear_screen() + print(f"{self('o_Attach_File')}") + print(f"{self('o_Type_ESC')}") + raw = self._read_input(f"\n{self('o_Drag_Drop')}: ") + + if self._is_escape_input(raw): + self.pending_images.clear() + self.pending_files.clear() + return False + + raw = raw.strip() + if not raw: + return False + + path = raw.strip().strip("'").strip('"') + try: + path = str(Path(path).expanduser().resolve()) + except Exception: + pass + + p = Path(path) + if not p.exists(): + print("Dosya bulunamadı.") + self._read_input("\nMenüye dönmek için Enter...") + return False + + import mimetypes + mime, _ = mimetypes.guess_type(str(p)) + mime = (mime or "").lower() + + if mime.startswith("image/"): + if str(p) not in self.pending_images: + self.pending_images.append(str(p)) + print(f"Eklendi (image): {p.name}") + else: + if not any(x.get("path") == str(p) for x in self.pending_files): + self.pending_files.append({ + "path": str(p), + "name": p.name, + "edit": False + }) + print(f"Eklendi (file): {p.name}") + + return True + + def _capture_photo_from_script(self): + script = Path.home() / ".config" / "scripts" / "screenprint.sh" + if not script.exists(): + print(f"Script bulunamadı: {script}") + return None + + watch_dirs = [Path.home() / "Resimler", Path.home() / "Pictures", Path("/tmp")] + + def collect_candidates(): + out = [] + for d in watch_dirs: + if not d.exists(): + continue + out += list(d.glob("screen*.*")) + out += list(d.glob("capture*.*")) + out += list(d.glob("shot*.*")) + out += list(d.glob("*.png")) + out += list(d.glob("*.jpg")) + out += list(d.glob("*.jpeg")) + return [p for p in out if p.exists() and p.is_file()] + + try: + before_files = {} + for pth in collect_candidates(): + try: + before_files[str(pth.resolve())] = pth.stat().st_mtime + except Exception: + pass + + start_ts = datetime.now().timestamp() + + p = subprocess.run( + [str(script), "only-one"], + capture_output=True, + text=True + ) + + if p.returncode != 0: + print("Fotoğraf çekme iptal edildi veya başarısız oldu.") + return None + + out = (p.stdout or "").strip() + + if out: + try: + cand = Path(out).expanduser().resolve() + if cand.exists() and cand.is_file(): + old_mtime = before_files.get(str(cand)) + new_mtime = cand.stat().st_mtime + if old_mtime is None or new_mtime > old_mtime or new_mtime >= start_ts: + return str(cand) + except Exception: + pass + + after_files = [] + for cand in collect_candidates(): + try: + resolved = str(cand.resolve()) + mtime = cand.stat().st_mtime + old_mtime = before_files.get(resolved) + + is_new = old_mtime is None + is_updated = old_mtime is not None and mtime > old_mtime + is_after_start = mtime >= start_ts + + if (is_new or is_updated) and is_after_start: + after_files.append(cand) + except Exception: + pass + + if not after_files: + return None + + newest = max(after_files, key=lambda x: x.stat().st_mtime) + return str(newest.resolve()) + + except Exception as e: + print(f"Fotoğraf çekme hatası: {e}") + return None + + def take_photo_and_queue(self): + self.clear_screen() + print(f"{self('o_Take_Photo')}") + print(f"{self('o_Press_ESC')}\n") + + path = self._capture_photo_from_script() + if not path: + self.pending_images.clear() + self.pending_files.clear() + return False + + if path not in self.pending_images: + self.pending_images.append(path) + + print(f"\nFotoğraf eklendi: {Path(path).name}") + return True + + # ---------------- AI ---------------- + + def _prompt_multiline(self): + print(f"{self('o_sub_Send_Message')}") + print(f"{self('o_Type_ESC')}\n") + + lines = [] + while True: + line = self._read_input() + + if not lines and self._is_escape_input(line): + return "" + + if line == "": + if lines: + break + return "" + + lines.append(line) + + return "\n".join(lines).strip() + + def append_user_message(self, message: str): + messages = self.load_chat_messages() + + new_message = {"role": "user", "content": message} + + if self.pending_images: + new_message["images"] = [ + str(Path(p).resolve()) for p in self.pending_images if Path(p).exists() + ] + + if self.pending_files: + new_message["files"] = [] + for f in self.pending_files: + fp = Path(str(f.get("path", ""))) + if fp.exists(): + new_message["files"].append({ + "path": str(fp.resolve()), + "name": fp.name, + "edit": bool(f.get("edit", False)) + }) + + messages.append(new_message) + self.save_chat_messages(messages) + + self.pending_images.clear() + self.pending_files.clear() + + def finalize_ai_response(self, raw_text: str): + raw_text = (raw_text or "").strip() + + try: + data = json.loads(raw_text) + except Exception: + data = {"type": "text", "content": raw_text} + + messages = self.load_chat_messages() + + bot_msg = { + "role": "bot", + "content": str(data.get("content") or "").strip() + } + + usage = data.get("usage") + if isinstance(usage, dict): + bot_msg["usage"] = usage + + saved_images = [] + seen_saved = set() + + image_candidates = [] + inline_data_urls = [] + + for key in ("image", "url"): + val = data.get(key) + if isinstance(val, str) and val.strip(): + sval = val.strip() + if sval.startswith("data:image/"): + inline_data_urls.append(sval) + else: + image_candidates.append(sval) + + imgs = data.get("images") + if isinstance(imgs, list): + for item in imgs: + if isinstance(item, str) and item.strip(): + sval = item.strip() + if sval.startswith("data:image/"): + inline_data_urls.append(sval) + else: + image_candidates.append(sval) + elif isinstance(item, dict): + u = item.get("url") + if isinstance(u, str) and u.strip(): + sval = u.strip() + if sval.startswith("data:image/"): + inline_data_urls.append(sval) + else: + image_candidates.append(sval) + + iu = item.get("image_url") + if isinstance(iu, str) and iu.strip(): + sval = iu.strip() + if sval.startswith("data:image/"): + inline_data_urls.append(sval) + else: + image_candidates.append(sval) + elif isinstance(iu, dict): + uu = iu.get("url") + if isinstance(uu, str) and uu.strip(): + sval = uu.strip() + if sval.startswith("data:image/"): + inline_data_urls.append(sval) + else: + image_candidates.append(sval) + + for url in image_candidates: + local_path = self.download_image_to_cache(url) + if local_path and local_path not in seen_saved: + seen_saved.add(local_path) + saved_images.append(local_path) + + base64_candidates = [] + + for item in inline_data_urls: + if item.startswith("data:image/") and "," in item: + base64_candidates.append(item.split(",", 1)[1].strip()) + + one_b64 = data.get("image_base64") + if isinstance(one_b64, str) and one_b64.strip(): + base64_candidates.append(one_b64.strip()) + + many_b64 = data.get("images_base64") + if isinstance(many_b64, list): + for item in many_b64: + if isinstance(item, str) and item.strip(): + base64_candidates.append(item.strip()) + elif isinstance(item, dict): + b = item.get("b64_json") + if isinstance(b, str) and b.strip(): + base64_candidates.append(b.strip()) + + for item in base64_candidates: + if item.startswith("data:image/") and "," in item: + item = item.split(",", 1)[1].strip() + local_path = self.save_base64_image(item, "png") + if local_path and local_path not in seen_saved: + seen_saved.add(local_path) + saved_images.append(local_path) + + if saved_images: + bot_msg["images"] = saved_images + + messages.append(bot_msg) + self.save_chat_messages(messages) + return bot_msg + + def call_ai(self): + cmd = [sys.executable, "-u", AI_SCRIPT, str(self.current_chat)] + + try: + env = os.environ.copy() + env["PYTHONUTF8"] = "1" + env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONUNBUFFERED"] = "1" + + proc = subprocess.run( + cmd, + capture_output=True, + text=False, + env=env + ) + + stdout_text = proc.stdout.decode("utf-8", errors="replace") + stderr_text = proc.stderr.decode("utf-8", errors="replace") + + stdout_text = fix_mojibake(stdout_text) + stderr_text = fix_mojibake(stderr_text) + + if proc.returncode != 0: + err = (stderr_text or stdout_text or "Bilinmeyen hata").strip() + self.handle_ai_error(err) + return None + + return self.finalize_ai_response(stdout_text) + + except Exception as e: + self.handle_ai_error(str(e)) + return None + + def handle_ai_error(self, error_text): + messages = self.load_chat_messages() + messages.append({"role": "bot", "content": f"❌ Hata:\n{error_text}"}) + self.save_chat_messages(messages) + print(f"\n❌ Hata:\n{error_text}") + + def print_last_bot_message(self, bot_msg: dict): + cfg = self.load_config() + show_usage = bool(cfg.get("show_usage", False)) + + text = str(bot_msg.get("content") or "").strip() + if text: + print("\nAssistant:") + print(text) + print() + + imgs = bot_msg.get("images") + if isinstance(imgs, list) and imgs: + print("Oluşan görseller:") + for p in imgs: + print(f"- {p}") + print() + + usage = bot_msg.get("usage") + if show_usage and isinstance(usage, dict): + p = int(usage.get("prompt_tokens", 0) or 0) + c = int(usage.get("completion_tokens", 0) or 0) + t = int(usage.get("total_tokens", 0) or 0) + print(f"[Input tokens: {p} | Output tokens: {c} | Total tokens: {t}]") + print() + + def send_message_flow(self): + self.clear_screen() + message = self._prompt_multiline() + if not message: + return + + self.append_user_message(message) + + self.clear_screen() + print(f"{self('o_AI_Response')}\n") + bot_msg = self.call_ai() + if bot_msg: + self.print_last_bot_message(bot_msg) + + self._read_input(f"{self('o_to_Menu')}") + + # ---------------- MENUS ---------------- + + def print_header(self): + self.active_model = self.get_chat_model(self.current_chat) + print("\n" + "=" * 60) + print(f"{self('o_Active_Chat')} : {self.current_chat.stem}") + print(f"{self('o_Active_Model')} : {self.active_model}") + print(f"{self('o_Language')} : {self.get_ui_language()}") + print("=" * 60) + + if self.pending_images or self.pending_files: + print("Bekleyen ekler:") + for p in self.pending_images: + print(f" [image] {Path(p).name}") + for f in self.pending_files: + print(f" [file ] {f.get('name')}") + print("-" * 60) + + def menu_chat(self): + while True: + self.clear_screen() + print(f"\n={self('o_Chats')}=") + print(f"1) {self('o_Change_Chat')}") + print(f"2) {self('o_New_Chat')}") + print(f"3) {self('o_Delete_Chat')}") + print(f"4) {self('o_Rename')}") + print(f"5) {self('o_Go_Back')}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if choice == "1": + self.select_chat() + + elif choice == "2": + self.create_chat() + + elif choice == "3": + chats = self.list_chat_files() + if not chats: + print("Hiç chat yok.") + continue + + print(f"\n{self('o_Delete_Chat')}:") + for i, p in enumerate(chats, 1): + mark = "*" if p == self.current_chat else " " + print(f"{i}) {mark} {p.stem}") + + sel = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if not sel.isdigit(): + continue + + idx = int(sel) - 1 + if not (0 <= idx < len(chats)): + continue + + print(f'{self("o_Delete")}("{chats[idx].stem}")? (e/h): ') + confirm = input(f'({self("o_Delete_Chat_Confirm")})').strip().lower() + + if confirm == "e": + self.delete_chat(chats[idx]) + self._read_input(f"{self('o_to_Menu')}") + + + elif choice == "4": + chats = self.list_chat_files() + if not chats: + print("Hiç chat yok.") + continue + + print(f"\n{self('o_Rename_Chat')}:") + for i, p in enumerate(chats, 1): + mark = "*" if p == self.current_chat else " " + print(f"{i}) {mark} {p.stem}") + + sel = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if not sel.isdigit(): + continue + + idx = int(sel) - 1 + if not (0 <= idx < len(chats)): + continue + + new_name = input(f"{self('o_New_Name')} ").strip() + self.rename_chat(chats[idx], new_name) + self._read_input(f"{self('o_to_Menu')}") + + elif choice == "5": + return + + def menu_models(self): + while True: + self.clear_screen() + cfg = self.load_config() + models = self._ensure_min_one_model(cfg) + + print(f"\n{self('o_AI_Model')}") + print(f"1) {self('o_Change_AI_Model')}") + print(f"2) {self('o_Add_Model')}") + print(f"3) {self('o_Delete_Model')}") + print(f"4) {self('o_Go_Back')}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if choice == "1": + print(f"\n{self('o_AI_Models')}:") + active = self.get_chat_model(self.current_chat) + for i, m in enumerate(models, 1): + mark = "*" if m == active else " " + print(f"{i}) {mark} {m}") + + sel = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if not sel.isdigit(): + continue + + idx = int(sel) - 1 + if not (0 <= idx < len(models)): + continue + + self.set_chat_model(self.current_chat, models[idx]) + print(f"\n{self('o_Active_Model')}: {models[idx]}") + self._read_input(f"{self('o_to_Menu')}") + + + elif choice == "2": + model_id = input(f"\n{self('o_Add_Model')}(exp: openai/gpt-4.1-mini): ").strip() + + if self._is_escape_input(model_id): + continue + + if model_id: + self.set_chat_model(self.current_chat, model_id) + print(f"\n✔ {self('o_Add_Model')}: {model_id}") + self._read_input(f"{self('o_to_Menu')}") + + elif choice == "3": + cfg2 = self.load_config() + models2 = self._ensure_min_one_model(cfg2) + + if len(models2) <= 1: + print(f"\n{self('o_Min_Model_Error')}") + self._read_input(f"\n{self('o_to_Menu')}") + continue + + print(f"\n{self('o_Delete_Model')}:") + for i, m in enumerate(models2, 1): + mark = "*" if m == self.get_chat_model(self.current_chat) else " " + print(f"{i}) {mark} {m}") + + sel = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if not sel.isdigit(): + continue + + idx = int(sel) - 1 + if not (0 <= idx < len(models2)): + continue + + model_id = models2[idx] + + confirm = input(f'{self("o_Delete")}("{model_id}")? (e/h): ').strip().lower() + + if confirm == "e": + models2 = [m for m in models2 if m != model_id] + cfg2["ai_models"] = models2 + + chat_models = cfg2.get("chat_models", {}) + if not isinstance(chat_models, dict): + chat_models = {} + + new_default = str(models2[0]) if models2 else None + for k, v in list(chat_models.items()): + if v == model_id: + chat_models[k] = new_default + + cfg2["chat_models"] = chat_models + + self.save_config(cfg2) + self.active_model = self.get_chat_model(self.current_chat) + + print(f"\n✔ {self('o_Delete_Model')}: {model_id}") + self._read_input(f"{self('o_to_Menu')}") + + elif choice == "4": + return + + def menu_personalization(self): + while True: + self.clear_screen() + cfg = self.load_config() + show_usage = bool(cfg.get("show_usage", True)) + + print("\nPersonalization") + print(f"1) {self('o_Token_Usage')}: {'✅' if show_usage else '❌'}") + print(f"2) {self('o_Go_Back')}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if choice == "1": + cfg["show_usage"] = not show_usage + self.save_config(cfg) + + elif choice == "2": + return + + def menu_ai_settings(self): + while True: + self.clear_screen() + cfg = self.load_config() + + force_ui_language = bool(cfg.get("force_ui_language", False)) + + print("\nAI Settings") + print(f"1) {self('o_Force_Language_Text')}({self('o_Force_Language_Hint')}): {'✅' if force_ui_language else '❌'}") + print(f"2) {self('o_Go_Back')}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if choice == "1": + cfg["force_ui_language"] = not force_ui_language + self.save_config(cfg) + + elif choice == "2": + return + + def menu_settings(self): + while True: + self.clear_screen() + print(f"\n{self('o_Settings')}") + print(f"1) {self('o_AI_Model')}") + print(f"2) {self('o_OpenRouter_API_Key')}") + print(f"3) {self('o_Response_Style')}") + print(f"4) {self('o_Personalization')}") + print(f"5) {self('o_Language')}") + print(f"6) {self('o_AI_Settings')}") + print(f"7) {self('o_Go_Back')}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + + + if choice == "1": + self.menu_models() + + elif choice == "2": + cfg = self.load_config() + current = str(cfg.get("open_router_key", "") or "") + print(f"\n{self('o_OpenRouter_API_Key')}:\n{current}\n") + print(f"\n{self('o_Type_ESC')}") + new_key = input(f"\n{self('o_OpenRouter_Placeholder')}: ").strip() + + if self._is_escape_input(new_key): + continue + + if new_key: + cfg["open_router_key"] = new_key + self.save_config(cfg) + print(f"\n✔ {self('o_Saved')}({self('o_OpenRouter_API_Key')})") + self._read_input(f"{self('o_to_Menu')}") + + elif choice == "3": + cfg = self.load_config() + current = str(cfg.get("response_style", "") or "") + print(f"\n{self('o_Response_Style')}:\n{current}\n") + print(f"\n{self('o_Type_ESC')}") + print(f"\n\n{self('o_New_Value')}") + new_style = input(f"\n({self('o_Response_Style_Placeholder')})").strip() + + if self._is_escape_input(new_style): + continue + + if new_style: + cfg["response_style"] = new_style + self.save_config(cfg) + print(f"\n✔ {self('o_Saved')}({self('o_Response_Style')})") + self._read_input(f"{self('o_to_Menu')}") + + + elif choice == "4": + self.menu_personalization() + + elif choice == "5": + self.clear_screen() + + current = self.get_ui_language() + langs = self.get_available_ui_languages() + + print(f"\n{self('o_Language')}: {current}") + + if langs: + print(f"\n{self('o_Available_Languages')}:") + for lang in langs: + mark = "*" if lang == current else " " + print(f" {mark} {lang}") + + print(f"\n{self('o_Language_Select_Hint')}") + print(f"{self('o_Type_ESC')}") + + new_lang = self._read_input(f"\n{self('o_New_Value')} ").strip().lower() + + if self._is_escape_input(new_lang): + continue + + if new_lang not in langs: + print(f"\n{self('o_Invalid_Language_Code')}") + self._read_input(f"\n{self('o_to_Menu')}") + continue + + self.set_ui_language(new_lang) + + print(f"\n✔ {self('o_Saved')} ({self('o_Language')}): {new_lang}") + self._read_input(f"{self('o_to_Menu')}") + + elif choice == "6": + self.menu_ai_settings() + + elif choice == "7": + return + + + def run(self): + while True: + self.clear_screen() + self.print_header() + print(f"1) {self('o_Send_Message')}") + print(f"2) {self('o_Take_Photo')}") + print(f"3) {self('o_Attach_File')}") + print(f"4) {self('o_Chats')}") + print(f"5) {self('o_Settings')}") + print(f"6) {self('o_Go_Back')}") + + choice = self._read_input(f"\n{self('o_Selection')}: ").strip() + + if choice == "1": + self.send_message_flow() + + elif choice == "2": + ok = self.take_photo_and_queue() + if ok: + self.send_message_flow() + + elif choice == "3": + ok = self.add_file_prompt() + if ok: + self.send_message_flow() + + elif choice == "4": + self.menu_chat() + + elif choice == "5": + self.menu_settings() + + elif choice == "6": + self.clear_screen() + print("🏁") + return + + +if __name__ == "__main__": + cli = ChatCLI() + cli.run()