From 15b2bd622fbd58e802823f3a6a1c0c61222ad644 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 21 Feb 2026 14:12:48 -0500 Subject: [PATCH] slop intensifies --- gui.py | 646 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 321 insertions(+), 325 deletions(-) diff --git a/gui.py b/gui.py index 7bc926f..c9c6c16 100644 --- a/gui.py +++ b/gui.py @@ -1,16 +1,14 @@ # gui.py +import dearpygui.dearpygui as dpg import tomllib import tomli_w -import pygame -import pygame_gui +import threading from pathlib import Path from tkinter import filedialog, Tk -import threading import aggregate import gemini CONFIG_PATH = Path("config.toml") -WINDOW_W, WINDOW_H = 1280, 720 def load_config() -> dict: with open(CONFIG_PATH, "rb") as f: @@ -28,220 +26,32 @@ def hide_tk_root() -> Tk: class App: def __init__(self): - pygame.init() - self.screen = pygame.display.set_mode((WINDOW_W, WINDOW_H), pygame.RESIZABLE) - pygame.display.set_caption("manual slop") - self.manager = pygame_gui.UIManager((WINDOW_W, WINDOW_H)) - self.clock = pygame.time.Clock() - self.config = load_config() - self.files = list(self.config["files"].get("paths", [])) - self.screenshots = list(self.config.get("screenshots", {}).get("paths", [])) - self.history = list(self.config.get("discussion", {}).get("history", [])) + 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 = None - self.send_thread = None + self.last_md_path: Path | None = None + self.send_thread: threading.Thread | None = None - self.panels = {} - self.elements = {} - self._build_ui() + self.file_rows: list[str] = [] + self.shot_rows: list[str] = [] - def _col_rect(self, col: int, cols: int = 4, padding: int = 8) -> pygame.Rect: - w, h = self.screen.get_size() - col_w = (w - padding * (cols + 1)) // cols - x = padding + col * (col_w + padding) - return pygame.Rect(x, padding, col_w, h - padding * 2) - - def _build_ui(self): - w, h = self.screen.get_size() - p = 8 - cols = 4 - col_w = (w - p * (cols + 1)) // cols - col_h = h - p * 2 - - def col_x(i): - return p + i * (col_w + p) - - # --- CONFIG PANEL (col 0 top) --- - config_h = 220 - self.elements["lbl_config"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(0), p, col_w, 24), - text="Config", manager=self.manager) - - self.elements["namespace"] = pygame_gui.elements.UITextEntryLine( - relative_rect=pygame.Rect(col_x(0), p + 30, col_w, 32), - manager=self.manager) - self.elements["namespace"].set_text(self.config["output"]["namespace"]) - - self.elements["lbl_ns"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(0), p + 24, col_w, 24), - text="Namespace", manager=self.manager) - - self.elements["output_dir"] = pygame_gui.elements.UITextEntryLine( - relative_rect=pygame.Rect(col_x(0), p + 90, col_w, 32), - manager=self.manager) - self.elements["output_dir"].set_text(self.config["output"]["output_dir"]) - - self.elements["lbl_outdir"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(0), p + 66, col_w, 24), - text="Output Dir", manager=self.manager) - - self.elements["btn_browse_output"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(0), p + 128, col_w, 30), - text="Browse Output Dir", manager=self.manager) - - self.elements["btn_save_config"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(0), p + 164, col_w, 30), - text="Save Config", manager=self.manager) - - # --- FILES PANEL (col 0 bottom) --- - fy = p + config_h - fh = col_h - config_h - - self.elements["lbl_files"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(0), fy, col_w, 24), - text="Files", manager=self.manager) - - self.elements["files_base_dir"] = pygame_gui.elements.UITextEntryLine( - relative_rect=pygame.Rect(col_x(0), fy + 30, col_w - 90, 32), - manager=self.manager) - self.elements["files_base_dir"].set_text(self.config["files"]["base_dir"]) - - self.elements["btn_browse_files_base"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(0) + col_w - 86, fy + 30, 86, 32), - text="Browse", manager=self.manager) - - self.elements["files_list"] = pygame_gui.elements.UISelectionList( - relative_rect=pygame.Rect(col_x(0), fy + 68, col_w, fh - 140), - item_list=self.files, - manager=self.manager) - - self.elements["btn_add_files"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(0), fy + fh - 68, col_w // 2 - 2, 30), - text="Add File(s)", manager=self.manager) - - self.elements["btn_add_wildcard"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(0) + col_w // 2 + 2, fy + fh - 68, col_w // 2 - 2, 30), - text="Add Wildcard", manager=self.manager) - - self.elements["btn_remove_file"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(0), fy + fh - 34, col_w, 30), - text="Remove Selected File", manager=self.manager) - - # --- SCREENSHOTS PANEL (col 1) --- - self.elements["lbl_shots"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(1), p, col_w, 24), - text="Screenshots", manager=self.manager) - - self.elements["shots_base_dir"] = pygame_gui.elements.UITextEntryLine( - relative_rect=pygame.Rect(col_x(1), p + 30, col_w - 90, 32), - manager=self.manager) - self.elements["shots_base_dir"].set_text( - self.config.get("screenshots", {}).get("base_dir", ".")) - - self.elements["btn_browse_shots_base"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(1) + col_w - 86, p + 30, 86, 32), - text="Browse", manager=self.manager) - - self.elements["shots_list"] = pygame_gui.elements.UISelectionList( - relative_rect=pygame.Rect(col_x(1), p + 68, col_w, col_h - 140), - item_list=self.screenshots, - manager=self.manager) - - self.elements["btn_add_shots"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(1), p + col_h - 68, col_w, 30), - text="Add Screenshot(s)", manager=self.manager) - - self.elements["btn_remove_shot"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(1), p + col_h - 34, col_w, 30), - text="Remove Selected Screenshot", manager=self.manager) - - # --- DISCUSSION PANEL (col 2) --- - self.elements["lbl_discussion"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(2), p, col_w, 24), - text="Discussion History", manager=self.manager) - - self.elements["discussion_box"] = pygame_gui.elements.UITextEntryBox( - relative_rect=pygame.Rect(col_x(2), p + 30, col_w, col_h - 100), - manager=self.manager) - self.elements["discussion_box"].set_text("\n---\n".join(self.history)) - - self.elements["btn_add_excerpt"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(2), p + col_h - 66, col_w // 2 - 2, 30), - text="Add Separator", manager=self.manager) - - self.elements["btn_clear_discussion"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(2) + col_w // 2 + 2, p + col_h - 66, col_w // 2 - 2, 30), - text="Clear", manager=self.manager) - - self.elements["btn_save_discussion"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(2), p + col_h - 32, col_w, 30), - text="Save Discussion", manager=self.manager) - - # --- GEMINI PANEL (col 3) --- - self.elements["lbl_gemini"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(3), p, col_w, 24), - text="Gemini", manager=self.manager) - - self.elements["lbl_status"] = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(col_x(3), p + 28, col_w, 24), - text=f"Status: {self.gemini_status}", manager=self.manager) - - self.elements["gemini_input"] = pygame_gui.elements.UITextEntryBox( - relative_rect=pygame.Rect(col_x(3), p + 56, col_w, 120), - manager=self.manager) - - self.elements["btn_generate_send"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(3), p + 182, col_w // 3 - 2, 30), - text="Gen + Send", manager=self.manager) - - self.elements["btn_md_only"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(3) + col_w // 3 + 2, p + 182, col_w // 3 - 2, 30), - text="MD Only", manager=self.manager) - - self.elements["btn_reset_session"] = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(col_x(3) + (col_w // 3) * 2 + 4, p + 182, col_w // 3 - 2, 30), - text="Reset", manager=self.manager) - - self.elements["gemini_response"] = pygame_gui.elements.UITextBox( - relative_rect=pygame.Rect(col_x(3), p + 218, col_w, col_h - 222), - html_text="", - manager=self.manager) - - def _rebuild_ui(self): - for el in self.elements.values(): - el.kill() - self.elements.clear() - self.manager.set_window_resolution(self.screen.get_size()) - self._build_ui() - - def _refresh_files_list(self): - self.elements["files_list"].set_item_list(self.files) - - def _refresh_shots_list(self): - self.elements["shots_list"].set_item_list(self.screenshots) - - def _update_status(self, status: str): - self.gemini_status = status - self.elements["lbl_status"].set_text(f"Status: {status}") - - def _update_response(self, text: str): - self.gemini_response = text - self.elements["gemini_response"].set_text(text) + # ------------------------------------------------------------------ helpers def _flush_to_config(self): - self.config["output"]["namespace"] = self.elements["namespace"].get_text() - self.config["output"]["output_dir"] = self.elements["output_dir"].get_text() - self.config["files"]["base_dir"] = self.elements["files_base_dir"].get_text() + 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"] = self.elements["shots_base_dir"].get_text() + self.config["screenshots"]["base_dir"] = dpg.get_value("shots_base_dir") self.config["files"]["paths"] = self.files self.config["screenshots"]["paths"] = self.screenshots - raw = self.elements["discussion_box"].get_text() + raw = dpg.get_value("discussion_box") self.history = [s.strip() for s in raw.split("---") if s.strip()] self.config["discussion"] = {"history": self.history} @@ -250,145 +60,331 @@ class App: save_config(self.config) return aggregate.run(self.config) - def handle_event(self, event: pygame.event.Event): - if event.type == pygame.QUIT: - return False + def _update_status(self, status: str): + self.gemini_status = status + dpg.set_value("gemini_status", f"Status: {status}") - if event.type == pygame.VIDEORESIZE: - self._rebuild_ui() + def _update_response(self, text: str): + self.gemini_response = text + dpg.set_value("gemini_response", text) - if event.type == pygame_gui.UI_BUTTON_PRESSED: - el = event.ui_element - - if el == self.elements["btn_browse_output"]: - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Output Dir") - root.destroy() - if d: - self.elements["output_dir"].set_text(d) - - elif el == self.elements["btn_save_config"]: - self._flush_to_config() - save_config(self.config) - - elif el == self.elements["btn_browse_files_base"]: - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Files Base Dir") - root.destroy() - if d: - self.elements["files_base_dir"].set_text(d) - - elif el == self.elements["btn_add_files"]: - 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._refresh_files_list() - - elif el == self.elements["btn_add_wildcard"]: - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Dir for Wildcard") - root.destroy() - if d: - self.files.append(str(Path(d) / "**" / "*")) - self._refresh_files_list() - - elif el == self.elements["btn_remove_file"]: - selected = self.elements["files_list"].get_single_selection() - if selected and selected in self.files: - self.files.remove(selected) - self._refresh_files_list() - - elif el == self.elements["btn_browse_shots_base"]: - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Screenshots Base Dir") - root.destroy() - if d: - self.elements["shots_base_dir"].set_text(d) - - elif el == self.elements["btn_add_shots"]: - root = hide_tk_root() - paths = filedialog.askopenfilenames( - title="Select Screenshots", - filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")] + 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 ) - root.destroy() - for p in paths: - if p not in self.screenshots: - self.screenshots.append(p) - self._refresh_shots_list() - elif el == self.elements["btn_remove_shot"]: - selected = self.elements["shots_list"].get_single_selection() - if selected and selected in self.screenshots: - self.screenshots.remove(selected) - self._refresh_shots_list() + 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 + ) - elif el == self.elements["btn_add_excerpt"]: - current = self.elements["discussion_box"].get_text() - self.elements["discussion_box"].set_text(current + "\n---\n") + 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 - elif el == self.elements["btn_clear_discussion"]: - self.elements["discussion_box"].set_text("") + 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 - elif el == self.elements["btn_save_discussion"]: - self._flush_to_config() - save_config(self.config) + # ---------------------------------------------------------------- callbacks - elif el == self.elements["btn_md_only"]: - md, path = self._do_generate() - self.last_md = md - self.last_md_path = path - self._update_status(f"md written: {path.name}") + 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) - elif el == self.elements["btn_reset_session"]: - gemini.reset_session() - self._update_status("session reset") - self._update_response("") + def cb_save_config(self): + self._flush_to_config() + save_config(self.config) + self._update_status("config saved") - elif el == self.elements["btn_generate_send"]: - is_sending = self.send_thread is not None and self.send_thread.is_alive() - if is_sending: - return True - md, path = self._do_generate() - self.last_md = md - self.last_md_path = path - self._update_status("sending...") - user_msg = self.elements["gemini_input"].get_text() + 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 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") + 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() - self.send_thread = threading.Thread(target=do_send, daemon=True) - self.send_thread.start() + 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() - return True + 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): - running = True - while running: - td = self.clock.tick(30) / 1000.0 - for event in pygame.event.get(): - self.manager.process_events(event) - running = self.handle_event(event) + dpg.create_context() + dpg.configure_app(docking=True, docking_space=True) - self.manager.update(td) - self.screen.fill((30, 30, 30)) - self.manager.draw_ui(self.screen) - pygame.display.flip() + dpg.create_viewport( + title="manual slop", + width=1600, + height=900 + ) - pygame.quit() + dpg.setup_dearpygui() + dpg.show_viewport() + dpg.maximize_viewport() + self._build_ui() + + while dpg.is_dearpygui_running(): + dpg.render_dearpygui_frame() + + dpg.destroy_context() -import threading def main(): app = App()