# gui.py import dearpygui.dearpygui as dpg import tomllib import tomli_w import threading from pathlib import Path from tkinter import filedialog, Tk import aggregate import gemini CONFIG_PATH = Path("config.toml") def load_config() -> dict: with open(CONFIG_PATH, "rb") as f: return tomllib.load(f) def save_config(config: dict): with open(CONFIG_PATH, "wb") as f: tomli_w.dump(config, f) def hide_tk_root() -> Tk: root = Tk() root.withdraw() root.wm_attributes("-topmost", True) return root class App: def __init__(self): self.config = load_config() self.files: list[str] = list(self.config["files"].get("paths", [])) self.screenshots: list[str] = list(self.config.get("screenshots", {}).get("paths", [])) self.history: list[str] = list(self.config.get("discussion", {}).get("history", [])) self.gemini_status = "idle" self.gemini_response = "" self.last_md = "" self.last_md_path: Path | None = None self.send_thread: threading.Thread | None = None self.file_rows: list[str] = [] self.shot_rows: list[str] = [] # ------------------------------------------------------------------ helpers def _flush_to_config(self): self.config["output"]["namespace"] = dpg.get_value("namespace") self.config["output"]["output_dir"] = dpg.get_value("output_dir") self.config["files"]["base_dir"] = dpg.get_value("files_base_dir") if "screenshots" not in self.config: self.config["screenshots"] = {} self.config["screenshots"]["base_dir"] = dpg.get_value("shots_base_dir") self.config["files"]["paths"] = self.files self.config["screenshots"]["paths"] = self.screenshots raw = dpg.get_value("discussion_box") self.history = [s.strip() for s in raw.split("---") if s.strip()] self.config["discussion"] = {"history": self.history} def _do_generate(self) -> tuple[str, Path]: self._flush_to_config() save_config(self.config) return aggregate.run(self.config) def _update_status(self, status: str): self.gemini_status = status dpg.set_value("gemini_status", f"Status: {status}") def _update_response(self, text: str): self.gemini_response = text dpg.set_value("gemini_response", text) def _rebuild_files_table(self): dpg.delete_item("files_table", children_only=True) for i, f in enumerate(self.files): with dpg.table_row(parent="files_table"): dpg.add_text(f) dpg.add_button( label="x", callback=self._make_remove_file_cb(i), width=24 ) def _rebuild_shots_table(self): dpg.delete_item("shots_table", children_only=True) for i, s in enumerate(self.screenshots): with dpg.table_row(parent="shots_table"): dpg.add_text(s) dpg.add_button( label="x", callback=self._make_remove_shot_cb(i), width=24 ) def _make_remove_file_cb(self, idx: int): def cb(): if idx < len(self.files): self.files.pop(idx) self._rebuild_files_table() return cb def _make_remove_shot_cb(self, idx: int): def cb(): if idx < len(self.screenshots): self.screenshots.pop(idx) self._rebuild_shots_table() return cb # ---------------------------------------------------------------- callbacks def cb_browse_output(self): root = hide_tk_root() d = filedialog.askdirectory(title="Select Output Dir") root.destroy() if d: dpg.set_value("output_dir", d) def cb_save_config(self): self._flush_to_config() save_config(self.config) self._update_status("config saved") def cb_browse_files_base(self): root = hide_tk_root() d = filedialog.askdirectory(title="Select Files Base Dir") root.destroy() if d: dpg.set_value("files_base_dir", d) def cb_add_files(self): root = hide_tk_root() paths = filedialog.askopenfilenames(title="Select Files") root.destroy() for p in paths: if p not in self.files: self.files.append(p) self._rebuild_files_table() def cb_add_wildcard(self): root = hide_tk_root() d = filedialog.askdirectory(title="Select Dir for Wildcard") root.destroy() if d: self.files.append(str(Path(d) / "**" / "*")) self._rebuild_files_table() def cb_browse_shots_base(self): root = hide_tk_root() d = filedialog.askdirectory(title="Select Screenshots Base Dir") root.destroy() if d: dpg.set_value("shots_base_dir", d) def cb_add_shots(self): root = hide_tk_root() paths = filedialog.askopenfilenames( title="Select Screenshots", filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")] ) root.destroy() for p in paths: if p not in self.screenshots: self.screenshots.append(p) self._rebuild_shots_table() def cb_add_excerpt(self): current = dpg.get_value("discussion_box") dpg.set_value("discussion_box", current + "\n---\n") def cb_clear_discussion(self): dpg.set_value("discussion_box", "") def cb_save_discussion(self): self._flush_to_config() save_config(self.config) self._update_status("discussion saved") def cb_md_only(self): try: md, path = self._do_generate() self.last_md = md self.last_md_path = path self._update_status(f"md written: {path.name}") except Exception as e: self._update_status(f"error: {e}") def cb_reset_session(self): gemini.reset_session() self._update_status("session reset") self._update_response("") def cb_generate_send(self): if self.send_thread and self.send_thread.is_alive(): return try: md, path = self._do_generate() self.last_md = md self.last_md_path = path except Exception as e: self._update_status(f"generate error: {e}") return self._update_status("sending...") user_msg = dpg.get_value("gemini_input") def do_send(): try: response = gemini.send(self.last_md, user_msg) self._update_response(response) self._update_status("done") except Exception as e: self._update_response(f"ERROR: {e}") self._update_status("error") self.send_thread = threading.Thread(target=do_send, daemon=True) self.send_thread.start() # ---------------------------------------------------------------- build ui def _build_ui(self): with dpg.window( label="Config", tag="win_config", pos=(8, 8), width=340, height=220, no_close=True ): dpg.add_input_text( label="Namespace", tag="namespace", default_value=self.config["output"]["namespace"], width=-1 ) dpg.add_input_text( label="Output Dir", tag="output_dir", default_value=self.config["output"]["output_dir"], width=-1 ) dpg.add_button(label="Browse Output Dir", callback=self.cb_browse_output, width=-1) dpg.add_separator() dpg.add_button(label="Save Config", callback=self.cb_save_config, width=-1) with dpg.window( label="Files", tag="win_files", pos=(8, 236), width=340, height=460, no_close=True ): dpg.add_input_text( label="Base Dir", tag="files_base_dir", default_value=self.config["files"]["base_dir"], width=-1 ) dpg.add_button(label="Browse Base Dir##files", callback=self.cb_browse_files_base, width=-1) dpg.add_separator() with dpg.table( tag="files_table", header_row=False, resizable=True, borders_innerV=True, scrollY=True, height=280 ): dpg.add_table_column(label="Path", width_stretch=True) dpg.add_table_column(label="", width_fixed=True, init_width_or_weight=28) self._rebuild_files_table() dpg.add_separator() with dpg.group(horizontal=True): dpg.add_button(label="Add File(s)", callback=self.cb_add_files, width=-1) with dpg.group(horizontal=True): dpg.add_button(label="Add Wildcard", callback=self.cb_add_wildcard, width=-1) with dpg.window( label="Screenshots", tag="win_screenshots", pos=(356, 8), width=340, height=460, no_close=True ): dpg.add_input_text( label="Base Dir", tag="shots_base_dir", default_value=self.config.get("screenshots", {}).get("base_dir", "."), width=-1 ) dpg.add_button(label="Browse Base Dir##shots", callback=self.cb_browse_shots_base, width=-1) dpg.add_separator() with dpg.table( tag="shots_table", header_row=False, resizable=True, borders_innerV=True, scrollY=True, height=280 ): dpg.add_table_column(label="Path", width_stretch=True) dpg.add_table_column(label="", width_fixed=True, init_width_or_weight=28) self._rebuild_shots_table() dpg.add_separator() dpg.add_button(label="Add Screenshot(s)", callback=self.cb_add_shots, width=-1) with dpg.window( label="Discussion History", tag="win_discussion", pos=(704, 8), width=340, height=460, no_close=True ): dpg.add_input_text( label="##discussion_box", tag="discussion_box", default_value="\n---\n".join(self.history), multiline=True, width=-1, height=340 ) dpg.add_separator() with dpg.group(horizontal=True): dpg.add_button(label="Add Separator", callback=self.cb_add_excerpt) dpg.add_button(label="Clear", callback=self.cb_clear_discussion) dpg.add_button(label="Save", callback=self.cb_save_discussion) with dpg.window( label="Gemini", tag="win_gemini", pos=(1052, 8), width=340, height=700, no_close=True ): dpg.add_text("Status: idle", tag="gemini_status") dpg.add_separator() dpg.add_input_text( label="##gemini_input", tag="gemini_input", multiline=True, width=-1, height=120 ) dpg.add_separator() with dpg.group(horizontal=True): dpg.add_button(label="Gen + Send", callback=self.cb_generate_send) dpg.add_button(label="MD Only", callback=self.cb_md_only) dpg.add_button(label="Reset", callback=self.cb_reset_session) dpg.add_separator() dpg.add_text("Response:") dpg.add_input_text( label="##gemini_response", tag="gemini_response", multiline=True, readonly=True, width=-1, height=-1 ) def run(self): dpg.create_context() dpg.configure_app(docking=True, docking_space=True) dpg.create_viewport( title="manual slop", width=1600, height=900 ) dpg.setup_dearpygui() dpg.show_viewport() dpg.maximize_viewport() self._build_ui() while dpg.is_dearpygui_running(): dpg.render_dearpygui_frame() dpg.destroy_context() def main(): app = App() app.run() if __name__ == "__main__": main()