diff --git a/.gitignore b/.gitignore index ff738e4..a20e18c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ credentials.toml +__pycache__ +uv.lock diff --git a/gemini.py b/gemini.py index 0477d00..2761f9b 100644 --- a/gemini.py +++ b/gemini.py @@ -1,38 +1,35 @@ # gemini.py import tomllib from pathlib import Path -import google.generativeai as genai +from google import genai +from google.genai import types -_cache = None -_model = None +_client = None _chat = None def _load_key() -> str: with open("credentials.toml", "rb") as f: return tomllib.load(f)["gemini"]["api_key"] -def _ensure_model(): - global _model - if _model is None: - genai.configure(api_key=_load_key()) - _model = genai.GenerativeModel("gemini-1.5-pro") +def _ensure_client(): + global _client + if _client is None: + _client = genai.Client(api_key=_load_key()) def _ensure_chat(): global _chat if _chat is None: - _ensure_model() - _chat = _model.start_chat(history=[]) + _ensure_client() + _chat = _client.chats.create(model="gemini-2.0-flash") def send(md_content: str, user_message: str) -> str: - global _cache, _chat + global _chat _ensure_chat() - full_message = f"\n{md_content}\n\n\n{user_message}" response = _chat.send_message(full_message) return response.text def reset_session(): - global _cache, _model, _chat - _cache = None - _model = None + global _client, _chat + _client = None _chat = None diff --git a/gui.py b/gui.py index 69d0dc1..7bc926f 100644 --- a/gui.py +++ b/gui.py @@ -1,9 +1,8 @@ # gui.py import tomllib import tomli_w -import imgui import pygame -from imgui.integrations.pygame import PygameRenderer +import pygame_gui from pathlib import Path from tkinter import filedialog, Tk import threading @@ -11,6 +10,7 @@ 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: @@ -26,550 +26,373 @@ def hide_tk_root() -> Tk: root.wm_attributes("-topmost", True) return root -class State: +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.namespace_buf = self.config["output"]["namespace"] - self.output_dir_buf = self.config["output"]["output_dir"] - self.files_base_dir_buf = self.config["files"]["base_dir"] - self.screenshots_base_dir_buf = self.config.get("screenshots", {}).get("base_dir", ".") - 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.history_edit_bufs = [h for h in self.history] - - self.gemini_input_buf = "" - self.gemini_response = "" self.gemini_status = "idle" + self.gemini_response = "" self.last_md = "" self.last_md_path = None self.send_thread = None - def flush_to_config(self): - self.config["output"]["namespace"] = self.namespace_buf - self.config["output"]["output_dir"] = self.output_dir_buf - self.config["files"]["base_dir"] = self.files_base_dir_buf + self.panels = {} + self.elements = {} + self._build_ui() + + 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) + + 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() if "screenshots" not in self.config: self.config["screenshots"] = {} - self.config["screenshots"]["base_dir"] = self.screenshots_base_dir_buf + self.config["screenshots"]["base_dir"] = self.elements["shots_base_dir"].get_text() self.config["files"]["paths"] = self.files self.config["screenshots"]["paths"] = self.screenshots - self.config["discussion"] = {"history": self.history_edit_bufs} + raw = self.elements["discussion_box"].get_text() + self.history = [s.strip() for s in raw.split("---") if s.strip()] + self.config["discussion"] = {"history": self.history} - def sync_history_bufs(self): - while len(self.history_edit_bufs) < len(self.history): - self.history_edit_bufs.append("") - self.history_edit_bufs = self.history_edit_bufs[:len(self.history)] + def _do_generate(self) -> tuple[str, Path]: + self._flush_to_config() + save_config(self.config) + return aggregate.run(self.config) -def draw_config_panel(state: State): - imgui.begin("Config") + def handle_event(self, event: pygame.event.Event): + if event.type == pygame.QUIT: + return False - changed, val = imgui.input_text("Namespace", state.namespace_buf, 256) - if changed: - state.namespace_buf = val + if event.type == pygame.VIDEORESIZE: + self._rebuild_ui() - changed, val = imgui.input_text("Output Dir", state.output_dir_buf, 512) - if changed: - state.output_dir_buf = val + if event.type == pygame_gui.UI_BUTTON_PRESSED: + el = event.ui_element - if imgui.button("Save Config"): - state.flush_to_config() - save_config(state.config) + 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) - imgui.end() + elif el == self.elements["btn_save_config"]: + self._flush_to_config() + save_config(self.config) -def draw_files_panel(state: State): - imgui.begin("Files") + 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) - changed, val = imgui.input_text("Base Dir##files", state.files_base_dir_buf, 512) - if changed: - state.files_base_dir_buf = val + 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() - if imgui.button("Browse Base Dir##files"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Files Base Dir") - root.destroy() - if d: - state.files_base_dir_buf = d + 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() - imgui.separator() + 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() - to_remove = None - for i, f in enumerate(state.files): - imgui.text(f) - imgui.same_line() - if imgui.button(f"Remove##file{i}"): - to_remove = i - if to_remove is not None: - state.files.pop(to_remove) + 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) - if imgui.button("Add File"): - root = hide_tk_root() - paths = filedialog.askopenfilenames(title="Select Files") - root.destroy() - for p in paths: - if p not in state.files: - state.files.append(p) + 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", "*.*")] + ) + root.destroy() + for p in paths: + if p not in self.screenshots: + self.screenshots.append(p) + self._refresh_shots_list() - if imgui.button("Add Wildcard##files"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Dir for Wildcard") - root.destroy() - if d: - state.files.append(str(Path(d) / "**" / "*")) + 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() - imgui.end() + elif el == self.elements["btn_add_excerpt"]: + current = self.elements["discussion_box"].get_text() + self.elements["discussion_box"].set_text(current + "\n---\n") -def draw_screenshots_panel(state: State): - imgui.begin("Screenshots") + elif el == self.elements["btn_clear_discussion"]: + self.elements["discussion_box"].set_text("") - changed, val = imgui.input_text("Base Dir##shots", state.screenshots_base_dir_buf, 512) - if changed: - state.screenshots_base_dir_buf = val + elif el == self.elements["btn_save_discussion"]: + self._flush_to_config() + save_config(self.config) - if imgui.button("Browse Base Dir##shots"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Screenshots Base Dir") - root.destroy() - if d: - state.screenshots_base_dir_buf = d + 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}") - imgui.separator() + elif el == self.elements["btn_reset_session"]: + gemini.reset_session() + self._update_status("session reset") + self._update_response("") - to_remove = None - for i, s in enumerate(state.screenshots): - imgui.text(s) - imgui.same_line() - if imgui.button(f"Remove##shot{i}"): - to_remove = i - if to_remove is not None: - state.screenshots.pop(to_remove) + 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() - if imgui.button("Add Screenshot"): - 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 state.screenshots: - state.screenshots.append(p) + 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") - imgui.end() + self.send_thread = threading.Thread(target=do_send, daemon=True) + self.send_thread.start() -def draw_discussion_panel(state: State): - imgui.begin("Discussion History") + return True - if imgui.button("Add Excerpt"): - state.history.append("") - state.history_edit_bufs.append("") + 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) - imgui.separator() + self.manager.update(td) + self.screen.fill((30, 30, 30)) + self.manager.draw_ui(self.screen) + pygame.display.flip() - state.sync_history_bufs() - to_remove = None - for i in range(len(state.history)): - imgui.text(f"Excerpt {i + 1}") - imgui.same_line() - if imgui.button(f"Remove##hist{i}"): - to_remove = i - continue - changed, val = imgui.input_text_multiline( - f"##histbuf{i}", - state.history_edit_bufs[i], - 2048, - width=-1, - height=120 - ) - if changed: - state.history_edit_bufs[i] = val - imgui.separator() + pygame.quit() - if to_remove is not None: - state.history.pop(to_remove) - state.history_edit_bufs.pop(to_remove) - imgui.end() - -def draw_gemini_panel(state: State): - imgui.begin("Gemini") - - imgui.text(f"Status: {state.gemini_status}") - - if state.last_md_path: - imgui.text(f"Last MD: {state.last_md_path}") - - imgui.separator() - - changed, val = imgui.input_text_multiline( - "##gemini_input", - state.gemini_input_buf, - 4096, - width=-1, - height=100 - ) - if changed: - state.gemini_input_buf = val - - is_sending = state.send_thread is not None and state.send_thread.is_alive() - - if is_sending: - imgui.begin_disabled() - - if imgui.button("Generate MD + Send"): - state.flush_to_config() - save_config(state.config) - - md, path = aggregate.run(state.config) - state.last_md = md - state.last_md_path = path - state.gemini_status = "sending..." - - user_msg = state.gemini_input_buf - - def do_send(): - try: - response = gemini.send(state.last_md, user_msg) - state.gemini_response = response - state.gemini_status = "done" - except Exception as e: - state.gemini_response = f"ERROR: {e}" - state.gemini_status = "error" - - state.send_thread = threading.Thread(target=do_send, daemon=True) - state.send_thread.start() - - if is_sending: - imgui.end_disabled() - - imgui.same_line() - - if imgui.button("Generate MD Only"): - state.flush_to_config() - save_config(state.config) - md, path = aggregate.run(state.config) - state.last_md = md - state.last_md_path = path - state.gemini_status = "md generated" - - imgui.same_line() - - if imgui.button("Reset Session"): - gemini.reset_session() - state.gemini_status = "session reset" - state.gemini_response = "" - - imgui.separator() - imgui.text("Response:") - imgui.input_text_multiline( - "##gemini_response", - state.gemini_response, - max_value_length=65536, - width=-1, - height=-1, - flags=imgui.INPUT_TEXT_READ_ONLY - ) - - imgui.end() +import threading def main(): - pygame.init() - screen = pygame.display.set_mode( - (1280, 720), - pygame.RESIZABLE - ) - pygame.display.set_caption("manual slop") - - imgui.create_context() - io = imgui.get_io() - io.display_size = (1280, 720) - - renderer = PygameRenderer() - - state = State() - clock = pygame.time.Clock() - - while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - return - if event.type == pygame.VIDEORESIZE: - io.display_size = (event.w, event.h) - renderer.process_event(event) - - imgui.new_frame() - - vp = imgui.get_main_viewport() - imgui.set_next_window_position(vp.pos.x, vp.pos.y) - imgui.set_next_window_size(vp.size.x, vp.size.y) - imgui.begin( - "##root", - flags=( - imgui.WINDOW_NO_TITLE_BAR - | imgui.WINDOW_NO_RESIZE - | imgui.WINDOW_NO_MOVE - | imgui.WINDOW_NO_SCROLLBAR - | imgui.WINDOW_NO_SAVED_SETTINGS - ) - ) - - avail_w, avail_h = imgui.get_content_region_available() - col_w = avail_w / 4 - - imgui.begin_child("##left", width=col_w, height=avail_h, border=True) - draw_config_panel_inline(state) - imgui.separator() - draw_files_panel_inline(state) - imgui.end_child() - - imgui.same_line() - - imgui.begin_child("##mid", width=col_w, height=avail_h, border=True) - draw_screenshots_panel_inline(state) - imgui.end_child() - - imgui.same_line() - - imgui.begin_child("##right_top", width=col_w, height=avail_h, border=True) - draw_discussion_panel_inline(state) - imgui.end_child() - - imgui.same_line() - - imgui.begin_child("##gemini", width=col_w, height=avail_h, border=True) - draw_gemini_panel_inline(state) - imgui.end_child() - - imgui.end() - - screen.fill((30, 30, 30)) - imgui.render() - renderer.render(imgui.get_draw_data()) - pygame.display.flip() - clock.tick(30) - - -def draw_config_panel_inline(state: State): - imgui.text("Config") - imgui.separator() - - changed, val = imgui.input_text("Namespace", state.namespace_buf, 256) - if changed: - state.namespace_buf = val - - changed, val = imgui.input_text("Output Dir", state.output_dir_buf, 512) - if changed: - state.output_dir_buf = val - - if imgui.button("Browse Output Dir"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Output Dir") - root.destroy() - if d: - state.output_dir_buf = d - - if imgui.button("Save Config"): - state.flush_to_config() - save_config(state.config) - - -def draw_files_panel_inline(state: State): - imgui.text("Files") - imgui.separator() - - changed, val = imgui.input_text("Base Dir##files", state.files_base_dir_buf, 512) - if changed: - state.files_base_dir_buf = val - - if imgui.button("Browse Base Dir##files"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Files Base Dir") - root.destroy() - if d: - state.files_base_dir_buf = d - - imgui.separator() - - to_remove = None - for i, f in enumerate(state.files): - imgui.text(f[:40] + "..." if len(f) > 40 else f) - imgui.same_line() - if imgui.button(f"x##file{i}"): - to_remove = i - if to_remove is not None: - state.files.pop(to_remove) - - if imgui.button("Add File(s)##files"): - root = hide_tk_root() - paths = filedialog.askopenfilenames(title="Select Files") - root.destroy() - for p in paths: - if p not in state.files: - state.files.append(p) - - if imgui.button("Add Wildcard##files"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Dir for Wildcard") - root.destroy() - if d: - state.files.append(str(Path(d) / "**" / "*")) - - -def draw_screenshots_panel_inline(state: State): - imgui.text("Screenshots") - imgui.separator() - - changed, val = imgui.input_text("Base Dir##shots", state.screenshots_base_dir_buf, 512) - if changed: - state.screenshots_base_dir_buf = val - - if imgui.button("Browse Base Dir##shots"): - root = hide_tk_root() - d = filedialog.askdirectory(title="Select Screenshots Base Dir") - root.destroy() - if d: - state.screenshots_base_dir_buf = d - - imgui.separator() - - to_remove = None - for i, s in enumerate(state.screenshots): - imgui.text(s[:40] + "..." if len(s) > 40 else s) - imgui.same_line() - if imgui.button(f"x##shot{i}"): - to_remove = i - if to_remove is not None: - state.screenshots.pop(to_remove) - - if imgui.button("Add Screenshot(s)"): - 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 state.screenshots: - state.screenshots.append(p) - - -def draw_discussion_panel_inline(state: State): - imgui.text("Discussion History") - imgui.separator() - - if imgui.button("Add Excerpt"): - state.history.append("") - state.history_edit_bufs.append("") - - imgui.separator() - - state.sync_history_bufs() - to_remove = None - for i in range(len(state.history)): - imgui.text(f"Excerpt {i + 1}") - imgui.same_line() - if imgui.button(f"x##hist{i}"): - to_remove = i - continue - changed, val = imgui.input_text_multiline( - f"##histbuf{i}", - state.history_edit_bufs[i], - 2048, - width=-1, - height=120 - ) - if changed: - state.history_edit_bufs[i] = val - imgui.separator() - - if to_remove is not None: - state.history.pop(to_remove) - state.history_edit_bufs.pop(to_remove) - - -def draw_gemini_panel_inline(state: State): - imgui.text("Gemini") - imgui.separator() - imgui.text(f"Status: {state.gemini_status}") - - if state.last_md_path: - imgui.text(f"Last MD: {str(state.last_md_path)[:40]}") - - imgui.separator() - - changed, val = imgui.input_text_multiline( - "##gemini_input", - state.gemini_input_buf, - 4096, - width=-1, - height=120 - ) - if changed: - state.gemini_input_buf = val - - is_sending = state.send_thread is not None and state.send_thread.is_alive() - - if is_sending: - imgui.begin_disabled() - - if imgui.button("Generate + Send"): - state.flush_to_config() - save_config(state.config) - md, path = aggregate.run(state.config) - state.last_md = md - state.last_md_path = path - state.gemini_status = "sending..." - user_msg = state.gemini_input_buf - - def do_send(): - try: - response = gemini.send(state.last_md, user_msg) - state.gemini_response = response - state.gemini_status = "done" - except Exception as e: - state.gemini_response = f"ERROR: {e}" - state.gemini_status = "error" - - state.send_thread = threading.Thread(target=do_send, daemon=True) - state.send_thread.start() - - if is_sending: - imgui.end_disabled() - - imgui.same_line() - - if imgui.button("MD Only"): - state.flush_to_config() - save_config(state.config) - md, path = aggregate.run(state.config) - state.last_md = md - state.last_md_path = path - state.gemini_status = "md generated" - - imgui.same_line() - - if imgui.button("Reset"): - gemini.reset_session() - state.gemini_status = "session reset" - state.gemini_response = "" - - imgui.separator() - imgui.text("Response:") - - _, _ = imgui.input_text_multiline( - "##gemini_response", - state.gemini_response, - max_value_length=65536, - width=-1, - height=-1, - flags=imgui.INPUT_TEXT_READ_ONLY - ) - + app = App() + app.run() if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index 4e18733..cff310c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,10 @@ -# pyproject.toml [project] name = "manual_slop" version = "0.1.0" requires-python = ">=3.11" dependencies = [ - "imgui[pygame]", - "pygame", - "google-generativeai", + "dearpygui", + "google-genai", "tomli-w" ] + diff --git a/uv.lock b/uv.lock deleted file mode 100644 index a389a8b..0000000 --- a/uv.lock +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" - -[[package]] -name = "aggregator" -version = "0.1.0" -source = { virtual = "." }