""" Setup tab — one-click installation for Claude Code integration. Includes configuration path management and backup/restore. """ import os import threading from tkinter import filedialog, messagebox import customtkinter as ctk from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key from core.i18n import t from core.logger import log class SetupTab(ctk.CTkFrame): def __init__(self, master, store): super().__init__(master, fg_color="transparent") self.store = store # Header self.header_label = ctk.CTkLabel( self, text=t("claude_integration"), font=ctk.CTkFont(size=20, weight="bold") ) self.header_label.pack(padx=20, pady=(20, 5)) self.desc_label = ctk.CTkLabel( self, text=t("claude_desc"), text_color="#9ca3af", justify="center" ) self.desc_label.pack(padx=20, pady=(0, 15)) # Status card self.status_frame = ctk.CTkFrame(self) self.status_frame.pack(fill="x", padx=20, pady=10) self.status_title = ctk.CTkLabel( self.status_frame, text=t("status"), font=ctk.CTkFont(size=14, weight="bold"), anchor="w" ) self.status_title.pack(fill="x", padx=15, pady=(10, 5)) self._status_labels: dict[str, ctk.CTkLabel] = {} self._status_text_labels: dict[str, ctk.CTkLabel] = {} status_items = [ ("shared_dir", "status_shared_dir"), ("servers_json", "status_servers_json"), ("ssh_script", "status_ssh_script"), ("encryption", "status_encryption"), ("skill_installed", "status_skill"), ("ssh_key_exists", "status_ssh_key"), ] for key, i18n_key in status_items: row = ctk.CTkFrame(self.status_frame, fg_color="transparent") row.pack(fill="x", padx=15, pady=2) indicator = ctk.CTkLabel(row, text="\u25cf", width=20, text_color="#6b7280") indicator.pack(side="left") text_label = ctk.CTkLabel(row, text=t(i18n_key), anchor="w") text_label.pack(side="left", fill="x", expand=True) self._status_labels[key] = indicator self._status_text_labels[key] = (text_label, i18n_key) # Buttons btn_frame = ctk.CTkFrame(self, fg_color="transparent") btn_frame.pack(fill="x", padx=20, pady=15) self.install_all_btn = ctk.CTkButton( btn_frame, text=t("install_everything"), font=ctk.CTkFont(size=14, weight="bold"), height=40, fg_color="#22c55e", hover_color="#16a34a", command=self._install_all ) self.install_all_btn.pack(fill="x", pady=(0, 10)) # Individual buttons row ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent") ind_frame.pack(fill="x") self.ssh_py_btn = ctk.CTkButton(ind_frame, text=t("install_ssh_py"), width=100, fg_color="#6b7280", command=self._install_script) self.ssh_py_btn.pack(side="left", padx=(0, 5)) self.skill_btn = ctk.CTkButton(ind_frame, text=t("install_skill"), width=100, fg_color="#6b7280", command=self._install_skill) self.skill_btn.pack(side="left", padx=5) self.ssh_key_btn = ctk.CTkButton(ind_frame, text=t("install_ssh_key"), width=100, fg_color="#6b7280", command=self._gen_key) self.ssh_key_btn.pack(side="left", padx=5) self.refresh_btn = ctk.CTkButton(ind_frame, text=t("refresh"), width=80, fg_color="#3b82f6", command=self._refresh_status) self.refresh_btn.pack(side="right") # ── Monitoring section ───────────────────────── monitor_frame = ctk.CTkFrame(self) monitor_frame.pack(fill="x", padx=20, pady=(5, 5)) self.monitor_title = ctk.CTkLabel( monitor_frame, text=t("monitoring"), font=ctk.CTkFont(size=14, weight="bold"), anchor="w" ) self.monitor_title.pack(fill="x", padx=15, pady=(10, 5)) interval_row = ctk.CTkFrame(monitor_frame, fg_color="transparent") interval_row.pack(fill="x", padx=15, pady=(0, 10)) self.interval_label = ctk.CTkLabel(interval_row, text=t("check_interval"), anchor="w") self.interval_label.pack(side="left", padx=(0, 10)) self._interval_buttons: dict[int, ctk.CTkButton] = {} current_interval = store.get_check_interval() for seconds, key in [(30, "interval_30s"), (60, "interval_60s"), (120, "interval_120s"), (300, "interval_300s")]: is_active = (seconds == current_interval) btn = ctk.CTkButton( interval_row, text=t(key), width=60, height=28, fg_color="#3b82f6" if is_active else "#6b7280", hover_color="#2563eb" if is_active else "#4b5563", command=lambda s=seconds: self._set_interval(s) ) btn.pack(side="left", padx=2) self._interval_buttons[seconds] = btn # ── Configuration section ───────────────────── config_frame = ctk.CTkFrame(self) config_frame.pack(fill="x", padx=20, pady=(5, 5)) self.config_title = ctk.CTkLabel( config_frame, text=t("configuration"), font=ctk.CTkFont(size=14, weight="bold"), anchor="w" ) self.config_title.pack(fill="x", padx=15, pady=(10, 5)) # Config path row path_row = ctk.CTkFrame(config_frame, fg_color="transparent") path_row.pack(fill="x", padx=15, pady=5) self.config_label = ctk.CTkLabel(path_row, text=t("config_label"), anchor="w", width=60) self.config_label.pack(side="left") self._path_label = ctk.CTkLabel( path_row, text=store.get_config_path(), anchor="w", text_color="#9ca3af", font=ctk.CTkFont(family="Consolas", size=11) ) self._path_label.pack(side="left", fill="x", expand=True, padx=(5, 10)) self.change_path_btn = ctk.CTkButton( path_row, text=t("change_path"), width=100, fg_color="#6b7280", command=self._change_config_path ) self.change_path_btn.pack(side="right") # Backup row backup_row = ctk.CTkFrame(config_frame, fg_color="transparent") backup_row.pack(fill="x", padx=15, pady=(5, 10)) self.backup_btn = ctk.CTkButton( backup_row, text=t("backup_now"), width=100, fg_color="#3b82f6", command=self._backup_now ) self.backup_btn.pack(side="left", padx=(0, 10)) self._backup_var = ctk.StringVar(value=t("select_backup")) backups = store.list_backups() values = backups if backups else [t("no_backups")] self._backup_menu = ctk.CTkOptionMenu( backup_row, variable=self._backup_var, values=values, width=250 ) self._backup_menu.pack(side="left", padx=(0, 10)) self.restore_btn = ctk.CTkButton( backup_row, text=t("restore"), width=80, fg_color="#ef4444", hover_color="#dc2626", command=self._restore_backup ) self.restore_btn.pack(side="left") # Log self.log = ctk.CTkTextbox(self, height=150, font=ctk.CTkFont(family="Consolas", size=11), state="disabled") self.log.pack(fill="both", expand=True, padx=20, pady=(5, 20)) # Initial status check self._refresh_status() def _set_interval(self, seconds: int): self.store.set_check_interval(seconds) for s, btn in self._interval_buttons.items(): if s == seconds: btn.configure(fg_color="#3b82f6", hover_color="#2563eb") else: btn.configure(fg_color="#6b7280", hover_color="#4b5563") def _log(self, text: str): self.log.configure(state="normal") self.log.insert("end", text + "\n") self.log.configure(state="disabled") self.log.see("end") def _refresh_status(self): status = check_status() for key, label in self._status_labels.items(): if status.get(key, False): label.configure(text="\u25cf", text_color="#22c55e") # green else: label.configure(text="\u25cf", text_color="#ef4444") # red def _install_all(self): self.install_all_btn.configure(state="disabled", text=t("installing_all")) def _do(): try: results = install_all() for msg in results: self.after(0, lambda m=msg: self._log(m)) self.after(0, self._refresh_status) self.after(0, lambda: self._log("\n" + t("install_done"))) except Exception as e: log.error(f"install_all failed: {e}") self.after(0, lambda: self._log(f"ERROR: {e}")) finally: self.after(0, lambda: self.install_all_btn.configure(state="normal", text=t("install_everything"))) threading.Thread(target=_do, daemon=True).start() def _install_script(self): msg = install_ssh_script() self._log(msg) self._refresh_status() def _install_skill(self): msg = install_skill() self._log(msg) self._refresh_status() def _gen_key(self): try: msg = generate_ssh_key() self._log(msg) except Exception as e: log.error(f"generate_ssh_key failed: {e}") self._log(f"ERROR: {e}") self._refresh_status() # ── Configuration methods ───────────────────────── def _change_config_path(self): path = filedialog.askopenfilename( title=t("select_servers_json"), filetypes=[("JSON files", "*.json"), ("All files", "*.*")], initialdir=os.path.dirname(self.store.get_config_path()) ) if path: self.store.set_config_path(path) self._path_label.configure(text=path) self._log(t("config_changed").format(path=path)) def _backup_now(self): try: name = self.store.create_backup() self._log(t("backup_created").format(name=name)) self._refresh_backups() except Exception as e: self._log(t("backup_failed").format(e=e)) def _restore_backup(self): selected = self._backup_var.get() if not selected or selected in (t("select_backup"), t("no_backups")): self._log(t("no_backup_selected")) return if not messagebox.askyesno(t("restore_backup_title"), t("restore_confirm").format(name=selected)): return try: self.store.restore_backup(selected) self._log(t("restored").format(name=selected)) except Exception as e: self._log(t("restore_failed").format(e=e)) def _refresh_backups(self): backups = self.store.list_backups() if backups: self._backup_menu.configure(values=backups) self._backup_var.set(backups[0]) else: self._backup_menu.configure(values=[t("no_backups")]) self._backup_var.set(t("no_backups"))