188 lines
6.8 KiB
Python
188 lines
6.8 KiB
Python
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 |