import tkinter as tk from tkinter import filedialog from pathlib import Path import streamlit as st import streamlit_antd_components as sac from backend.file_manager import FileManager class FileNavigation: """ Builds a VS Code-like file tree using streamlit-antd-components. All filesystem ops delegated to FileManager. """ def __init__(self): self.file_manager = FileManager() self.supported_extensions = FileManager.SUPPORTED_EXTENSIONS # ---------------- FOLDER PICKER ---------------- def pick_folder(self) -> str | None: root = tk.Tk() root.withdraw() root.wm_attributes("-topmost", 1) folder = filedialog.askdirectory(title="Select project folder") root.destroy() return folder if folder else None # ---------------- TREE BUILDER ---------------- def build_tree(self, path=None) -> list: if path is None: path = Path.cwd() path = Path(path) def build_node(current_path: Path) -> sac.TreeItem: is_dir = current_path.is_dir() if is_dir: children = [] try: for item in sorted( current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()) ): if item.is_dir() or item.suffix in self.supported_extensions: children.append(build_node(item)) except PermissionError: pass return sac.TreeItem( label=current_path.name, icon="folder", children=children if children else None, ) else: return sac.TreeItem( label=current_path.name, icon="file-earmark-code", ) return [build_node(path)] # ---------------- DIALOG ---------------- @st.dialog("New file or folder") def _new_item_dialog(self, root_folder: str): root = Path(root_folder) all_dirs = self.file_manager.list_all_dirs(root_folder) def dir_label(p: Path) -> str: rel = p.relative_to(root) return "/ (root)" if str(rel) == "." else str(rel) dir_labels = [dir_label(d) for d in all_dirs] dir_paths = [str(d) for d in all_dirs] mode = st.segmented_control( "Type", options=["📄 File", "📁 Folder"], default="📄 File", label_visibility="collapsed" ) is_file = mode == "📄 File" st.markdown("---") selected_label = st.selectbox("Location", options=dir_labels, index=0) target_dir = dir_paths[dir_labels.index(selected_label)] name = st.text_input( "File name" if is_file else "Folder name", placeholder="e.g. main.py" if is_file else "e.g. utils", ) if name and name.strip(): sep = "" if target_dir.endswith("/") else "/" st.caption(f"`{target_dir}{sep}{name.strip()}`") st.markdown("---") col_cancel, col_create = st.columns(2) with col_cancel: if st.button("Cancel", use_container_width=True): st.rerun() with col_create: if st.button( "Create file" if is_file else "Create folder", type="primary", use_container_width=True, disabled=not (name and name.strip()) ): if is_file: ok, msg = self.file_manager.create_file(target_dir, name) else: ok, msg = self.file_manager.create_folder(target_dir, name) if ok: st.success(msg) st.rerun() else: st.error(msg) # ---------------- SIDEBAR RENDERER ---------------- def render_sidebar(self) -> str | None: st.sidebar.title("File Explorer") root_folder = st.session_state.get("root_folder") # --- Auto-refresh: detect external filesystem changes --- if root_folder: current_snapshot = self.file_manager.snapshot(root_folder) last_snapshot = st.session_state.get("_folder_snapshot") if last_snapshot is None: st.session_state._folder_snapshot = current_snapshot elif current_snapshot != last_snapshot: st.session_state._folder_snapshot = current_snapshot st.rerun() with st.sidebar: btn_left, btn_right = st.columns(2) with btn_left: if st.button("📂 Folder", key="pick_folder", use_container_width=True): picked = self.pick_folder() if picked: st.session_state.root_folder = picked st.session_state._folder_snapshot = None st.rerun() with btn_right: if st.button("💬 Ask AI", key="toggle_chat", use_container_width=True): st.session_state.chat_open = not st.session_state.get("chat_open", False) st.rerun() st.markdown("---") if root_folder: if st.button("Add new File/Folder", key="new_item_btn", use_container_width=True): self._new_item_dialog(root_folder) try: root_path = Path(root_folder) if root_path.exists() and root_path.is_dir(): tree_data = self.build_tree(root_path) selected = sac.tree( items=tree_data, label=" ", index=None, align="left", size="sm", icon="folder2", open_all=False, checkbox=False, show_line=False, key=f"tree_{root_folder}", ) if selected: return self._resolve_path(root_path, selected) else: st.warning("Folder not found.") except Exception as e: st.error(f"Could not load folder: {e}") else: st.text("No folder selected yet.") return None # ---------------- PATH RESOLVER ---------------- def _resolve_path(self, root_path: Path, selected_label: str) -> str | None: for p in root_path.rglob("*"): if p.name == selected_label and p.suffix in self.supported_extensions: return str(p) return None