git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commitc3732bcfa4f07632364bfce62f9f87699b25c377
parentc6ae187d89
authorLucas Galante <[email protected]>
date2026-06-05 20:13
feat: integrate Breadcrumb widget and add text bounds support to PageContent

 Makefile             |  14 +++
 src/main.rs          | 335 ++++++++++++++++++++++++++++++++++++++-------------
 src/pages/browse.rs  | 189 ++++++++++++++++++++++-------
 src/pages/mod.rs     |  18 ++-
 src/pages/network.rs |  24 +++-
 5 files changed, 448 insertions(+), 132 deletions(-)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a29c71b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+	cargo build --release
+
+install: build
+	mkdir -p ~/.local/bin
+	install -m 755 target/release/clear-filesystem-interface ~/.local/bin/clear-filesystem-interface
+
+run:
+	cargo run
+
+clean:
+	cargo clean
diff --git a/src/main.rs b/src/main.rs
index 93302b0..b4a8c64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,7 @@ use clear_ui::engine::{Application, LogicalPosition, LogicalSize, WindowSettings
 use clear_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Widget};
 
 use pages::Page;
+use pages::browse::is_project_dir;
 
 // ── State ───────────────────────────────────────────────────────────
 
@@ -31,6 +32,7 @@ struct FilesystemApp {
     // Command-line chooser options
     select_mode: bool,
     select_directory: bool,
+    save_mode: bool,
 
     // Rendering resources
     widgets: Vec<AppWidget>,
@@ -45,6 +47,7 @@ struct FilesystemApp {
     cursor_x: f32,
     cursor_y: f32,
     paginator: clear_ui::widget::Paginator,
+    just_initialized: bool,
 }
 
 // ── Messages ────────────────────────────────────────────────────────
@@ -73,6 +76,9 @@ fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
 
 impl FilesystemApp {
     fn rebuild_layout(&mut self) {
+        self.browse.save_name_box.prepare_text(&mut self.font_system);
+        self.browse.search_box.prepare_text(&mut self.font_system);
+
         let mut widgets = Vec::new();
         let mut text_items = Vec::new();
 
@@ -87,9 +93,9 @@ impl FilesystemApp {
         let mut paginator_pc = pages::PageContent::new();
         clear_ui::layout::render_widget(&mut paginator_pc, &mut self.paginator, 0.0, 0.0, self.width as f32, self.height as f32);
 
-        let has_sidebar = !self.select_mode;
-        let sidebar_w = if has_sidebar { 56.0 } else { 0.0 };
-        let browse_x = if has_sidebar { 73.0 } else { 16.0 };
+        let has_sidebar = true;
+        let sidebar_w = if has_sidebar { self.paginator.sidebar_w() } else { 0.0 };
+        let browse_x = if has_sidebar { sidebar_w + 17.0 } else { 16.0 };
         let usable_w = self.width as f32 - sidebar_w - (if has_sidebar { 1.0 } else { 0.0 }) - 32.0;
         let browse_w = usable_w * 0.55;
         let preview_w = usable_w * 0.45;
@@ -104,9 +110,9 @@ impl FilesystemApp {
         };
 
         if has_sidebar {
-            // 2. Add Content Background (where x >= 56.0) from the paginator to pc first
+            // 2. Add Content Background (where x >= sidebar_w) from the paginator to pc first
             for rect in paginator_pc.rects.iter() {
-                if rect.1 >= 56.0 {
+                if rect.1 >= sidebar_w {
                     pc.rects.push(*rect);
                 }
             }
@@ -119,7 +125,7 @@ impl FilesystemApp {
         // 3. Draw Page content
         match self.current_page {
             Page::Browse => {
-                let browse_pc = pages::browse::view(&mut self.browse, browse_x, content_y, browse_w, content_h);
+                let browse_pc = pages::browse::view(&mut self.browse, browse_x, content_y, browse_w, content_h, self.select_mode);
                 let preview_pc = pages::preview::view(&self.preview, preview_x, content_y, preview_w, content_h);
 
                 pc.rects.extend(browse_pc.rects);
@@ -162,18 +168,15 @@ impl FilesystemApp {
         }
 
         if has_sidebar {
-            // 4. Add Sidebar Background and tabs (where x < 56.0) from the paginator to pc last
+            // 4. Add Sidebar Background and tabs (where x < sidebar_w) from the paginator to pc last
             for rect in paginator_pc.rects.iter() {
-                if rect.1 < 56.0 {
+                if rect.1 < sidebar_w {
                     pc.rects.push(*rect);
                 }
             }
 
             // Sidebar Divider Line (accent border)
-            pc.rect([0.36, 0.56, 0.38, 1.0], 56.0, 0.0, 1.0, self.height as f32);
-
-            // Sidebar Title rendered vertically
-            pc.text("C\nL\nE\nA\nR", 18.0, 16.0, 12.0, [0.36, 0.56, 0.38, 1.0]);
+            pc.rect([0.36, 0.56, 0.38, 1.0], sidebar_w, 0.0, 1.0, self.height as f32);
 
             // Add the paginator's texts (tab labels) on top of the sidebar background
             pc.texts.extend(paginator_pc.texts);
@@ -183,39 +186,11 @@ impl FilesystemApp {
         if self.select_mode {
             let bar_y = self.height as f32 - select_bar_h - 16.0;
             // Divider line
-            pc.rect([0.15, 0.20, 0.16, 1.0], 16.0, bar_y, self.width as f32 - 32.0, 1.0);
-
-            // Determine currently selected path
-            let selected_path = if let Some(idx) = self.browse.selected {
-                self.browse.entries.get(idx).map(|e| e.path.clone())
-            } else {
-                None
-            };
+            pc.rect([0.15, 0.20, 0.16, 1.0], browse_x, bar_y, usable_w, 1.0);
 
-            let disp_path = if self.select_directory {
-                selected_path.filter(|p| p.is_dir()).unwrap_or_else(|| self.browse.current_dir.clone())
-            } else {
-                selected_path.unwrap_or_else(|| self.browse.current_dir.clone())
-            };
-
-            let path_str = disp_path.to_string_lossy().to_string();
-            let label_text = if self.select_directory { "Selected Directory:" } else { "Selected File:" };
-            
             let accent = [0.36, 0.56, 0.38, 1.0];
             let text_fg = [0.83, 0.83, 0.83, 1.0];
 
-            pc.text(label_text, 24.0, bar_y + 18.0, 12.0, accent);
-            
-            // Limit path display string length
-            let max_chars = ((self.width as f32 - 380.0) / 7.0).max(10.0) as usize;
-            let path_truncated = if path_str.chars().count() > max_chars {
-                let truncated: String = path_str.chars().skip(path_str.chars().count() - max_chars + 3).collect();
-                format!("...{}", truncated)
-            } else {
-                path_str
-            };
-            pc.text(&path_truncated, 160.0, bar_y + 18.0, 12.0, text_fg);
-
             // Cancel button
             let cancel_x = self.width as f32 - 180.0;
             pc.button(
@@ -230,10 +205,11 @@ impl FilesystemApp {
                 Message::SelectCancel,
             );
 
-            // Open/Select button
+            // Open/Select/Save button
             let open_x = self.width as f32 - 100.0;
+            let button_label = if self.save_mode { "Save" } else { "Select" };
             pc.button(
-                "Select",
+                button_label,
                 open_x,
                 bar_y + 10.0,
                 80.0,
@@ -294,11 +270,12 @@ impl FilesystemApp {
                     (btn.label_color[1] * 255.0) as u8,
                     (btn.label_color[2] * 255.0) as u8,
                 ),
+                bounds: None,
             });
         }
 
         // Add raw texts
-        for (text, size, x, y, col, _font) in &pc.texts {
+        for (text, size, x, y, col, _font, bounds) in &pc.texts {
             text_items.push(TextItem {
                 buffer: make_text_buffer(&mut self.font_system, text, *size),
                 x: *x,
@@ -308,6 +285,7 @@ impl FilesystemApp {
                     (col[1] * 255.0) as u8,
                     (col[2] * 255.0) as u8,
                 ),
+                bounds: *bounds,
             });
         }
 
@@ -326,18 +304,20 @@ impl Application for FilesystemApp {
     fn new(_qh: &QueueHandle<clear_ui::engine::EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
         // Parse command line arguments
         let args: Vec<String> = std::env::args().collect();
-        let select_mode = args.iter().any(|arg| arg == "--select" || arg == "--select-dir");
         let select_directory = args.iter().any(|arg| arg == "--select-dir");
+        let save_mode = args.iter().any(|arg| arg == "--save");
+        let select_mode = save_mode || args.iter().any(|arg| arg == "--select" || arg == "--select-dir");
 
         // Use signature green theme for the content background of the paginator
         clear_ui::color::set_page_low_color([0.10, 0.165, 0.11, 1.0]);
+        clear_ui::scale::set_scale_factor(1.0);
 
         let browse = pages::browse::BrowseState::default();
         let current_dir = browse.current_dir.clone();
 
         let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
         let paginator = clear_ui::widget::Paginator::new(56.0, pages_names)
-            .with_tab_y_offset(120.0)
+            .with_sidebar_label("CLEAR")
             .with_tabs_rotated(true);
 
         let mut app = Self {
@@ -348,6 +328,7 @@ impl Application for FilesystemApp {
             preview: pages::preview::PreviewState::default(),
             select_mode,
             select_directory,
+            save_mode,
             widgets: Vec::new(),
             text_items: Vec::new(),
             font_system: FontSystem::new(),
@@ -360,13 +341,15 @@ impl Application for FilesystemApp {
             cursor_x: 0.0,
             cursor_y: 0.0,
             paginator,
+            just_initialized: true,
         };
 
         // Start initial directory loading
         let sender_clone = sender.clone();
+        let path_clone = current_dir.clone();
         tokio::spawn(async move {
-            let entries = pages::browse::read_directory(&current_dir);
-            let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(entries)));
+            let entries = pages::browse::read_directory(&path_clone);
+            let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
         });
 
         app.rebuild_layout();
@@ -375,7 +358,9 @@ impl Application for FilesystemApp {
 
     fn settings(&self) -> WindowSettings {
         if self.select_mode {
-            let title = if self.select_directory {
+            let title = if self.save_mode {
+                "Save File"
+            } else if self.select_directory {
                 "Select Directory"
             } else {
                 "Select File"
@@ -410,16 +395,25 @@ impl Application for FilesystemApp {
                 self.needs_rebuild = true;
             }
             Message::Browse(msg) => {
+                let is_file_double_click = if let pages::browse::BrowseMessage::NavigateTo(idx) = &msg {
+                    self.browse.entries.get(*idx).map(|e| !e.is_dir || (self.select_mode && !self.select_directory && is_project_dir(&e.path))).unwrap_or(false)
+                } else {
+                    false
+                };
+
                 let is_navigation = match &msg {
-                    pages::browse::BrowseMessage::NavigateTo(_) | pages::browse::BrowseMessage::NavigateToPath(_) => true,
+                    pages::browse::BrowseMessage::NavigateTo(idx) => {
+                        self.browse.entries.get(*idx).map(|e| e.is_dir && !is_project_dir(&e.path)).unwrap_or(false)
+                    }
+                    pages::browse::BrowseMessage::NavigateToPath(_) => true,
                     _ => false,
                 };
-                let handle = pages::browse::update(&mut self.browse, msg);
+                let (target_path, handle) = pages::browse::update(&mut self.browse, msg);
                 if is_navigation {
                     let sender_clone = self.sender.clone();
                     tokio::spawn(async move {
                         let entries = handle.await.unwrap_or_default();
-                        let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(entries)));
+                        let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(target_path, entries)));
                     });
                 }
 
@@ -433,8 +427,23 @@ impl Application for FilesystemApp {
                     pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::SetPath { path });
                 }
 
+                if let Some(idx) = self.browse.selected {
+                    if let Some(entry) = self.browse.entries.get(idx) {
+                        if self.select_mode {
+                            self.browse.save_name_box.text = entry.name.clone();
+                            if self.browse.save_name_box.editing {
+                                self.browse.save_name_box.edit_buffer = entry.name.clone();
+                            }
+                        }
+                    }
+                }
+
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
+
+                if is_file_double_click && self.select_mode {
+                    self.update(Message::SelectOpen, needs_rebuild, _exit);
+                }
             }
             Message::Preview(msg) => {
                 pages::preview::update(&mut self.preview, msg);
@@ -443,25 +452,53 @@ impl Application for FilesystemApp {
             }
             Message::KeyboardEvent(_) => {}
             Message::SelectOpen => {
-                let selected_path = if let Some(idx) = self.browse.selected {
-                    self.browse.entries.get(idx).map(|e| e.path.clone())
-                } else {
-                    None
-                };
                 if self.select_directory {
+                    let selected_path = if let Some(idx) = self.browse.selected {
+                        self.browse.entries.get(idx).map(|e| e.path.clone())
+                    } else {
+                        None
+                    };
                     let path = selected_path.filter(|p| p.is_dir()).unwrap_or_else(|| self.browse.current_dir.clone());
                     println!("{}", path.display());
                     std::process::exit(0);
-                } else if let Some(path) = selected_path {
-                    if path.is_dir() {
-                        let sender_clone = self.sender.clone();
-                        tokio::spawn(async move {
-                            let entries = pages::browse::read_directory(&path);
-                            let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(entries)));
-                        });
+                } else {
+                    let filename = if self.browse.save_name_box.editing {
+                        self.browse.save_name_box.edit_buffer.trim()
+                    } else {
+                        self.browse.save_name_box.text.trim()
+                    };
+                    if !filename.is_empty() {
+                        let path = self.browse.current_dir.join(filename);
+                        if path.is_dir() && !is_project_dir(&path) {
+                            let sender_clone = self.sender.clone();
+                            let path_clone = path.clone();
+                            tokio::spawn(async move {
+                                let entries = pages::browse::read_directory(&path_clone);
+                                let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
+                            });
+                        } else {
+                            println!("{}", path.display());
+                            std::process::exit(0);
+                        }
                     } else {
-                        println!("{}", path.display());
-                        std::process::exit(0);
+                        let selected_path = if let Some(idx) = self.browse.selected {
+                            self.browse.entries.get(idx).map(|e| e.path.clone())
+                        } else {
+                            None
+                        };
+                        if let Some(path) = selected_path {
+                            if path.is_dir() && !is_project_dir(&path) {
+                                let sender_clone = self.sender.clone();
+                                let path_clone = path.clone();
+                                tokio::spawn(async move {
+                                    let entries = pages::browse::read_directory(&path_clone);
+                                    let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
+                                });
+                            } else {
+                                println!("{}", path.display());
+                                std::process::exit(0);
+                            }
+                        }
                     }
                 }
             }
@@ -472,6 +509,15 @@ impl Application for FilesystemApp {
     }
 
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+        if self.just_initialized {
+            self.just_initialized = false;
+            if self.save_mode {
+                self.browse.save_name_box.focus();
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
+
         if self.paginator.tick(dt) {
             *needs_rebuild = true;
             self.needs_rebuild = true;
@@ -496,6 +542,7 @@ impl Application for FilesystemApp {
             self.width = size.width as u32;
             self.height = size.height as u32;
             self.scale_factor = scale;
+            clear_ui::scale::set_scale_factor(scale as f32);
             self.rebuild_layout();
         }
 
@@ -509,7 +556,11 @@ impl Application for FilesystemApp {
     }
 
     fn clear_color(&self) -> [f32; 4] {
-        [0.10, 0.165, 0.11, 1.0]
+        let mut color = [0.10, 0.165, 0.11, 1.0];
+        if let Some(opacity) = clear_ui::color::read_opacity_if_configured() {
+            color[3] = opacity;
+        }
+        color
     }
 
     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
@@ -526,10 +577,21 @@ impl Application for FilesystemApp {
             if self.browse.search_box.cursor_moved(pos.x, pos.y) {
                 changed = true;
             }
+            if self.select_mode {
+                if self.browse.save_name_box.cursor_moved(pos.x, pos.y) {
+                    changed = true;
+                }
+            }
             if self.browse.list_box.cursor_moved(pos.x, pos.y) {
                 changed = true;
             }
+            if self.browse.breadcrumb.on_cursor_moved(pos.x, pos.y) {
+                changed = true;
+            }
         } else if self.current_page == Page::Network {
+            if self.network.breadcrumb.on_cursor_moved(pos.x, pos.y) {
+                changed = true;
+            }
             if self.network.graph.is_dragging() {
                 if self.network.graph.drag_update(pos.x, pos.y) {
                     changed = true;
@@ -558,7 +620,6 @@ impl Application for FilesystemApp {
             self.needs_rebuild = true;
         }
     }
-
     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
         if button != MouseButton::Left && button != MouseButton::Right {
             return None;
@@ -566,9 +627,13 @@ impl Application for FilesystemApp {
 
         let mut changed = false;
 
-        if self.paginator.mouse_input(button, state, pos.x, pos.y) {
+        eprintln!("[DEBUG] MOUSE INPUT: {:?} {:?} pos=({}, {})", button, state, pos.x, pos.y);
+        let pag_match = self.paginator.mouse_input(button, state, pos.x, pos.y);
+        eprintln!("[DEBUG] Paginator matched: {}", pag_match);
+        if pag_match {
             if self.paginator.take_click() {
                 let idx = self.paginator.selected_page();
+                eprintln!("[DEBUG] Paginator page changed to idx: {}", idx);
                 if idx < Page::ALL.len() {
                     clear_ui::widget::focus::clear_focus();
                     self.current_page = Page::ALL[idx];
@@ -584,14 +649,78 @@ impl Application for FilesystemApp {
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
+            if self.select_mode {
+                if self.browse.save_name_box.mouse_input(button, state, pos.x, pos.y) {
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                }
+            }
             if self.browse.list_box.mouse_input(button, state, pos.x, pos.y) {
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
+            if button == MouseButton::Left && state == ElementState::Pressed {
+                if self.browse.breadcrumb.hit_test(pos.x, pos.y) {
+                    if self.browse.breadcrumb.mouse_input(button, state, pos.x, pos.y) {
+                        if let Some(seg) = self.browse.breadcrumb.path_click() {
+                            let mut target_path = std::path::PathBuf::new();
+                            let mut current_idx = 0;
+                            for component in self.browse.current_dir.components() {
+                                target_path.push(component);
+                                if component == std::path::Component::RootDir {
+                                    if seg == 0 {
+                                        break;
+                                    }
+                                } else {
+                                    current_idx += 1;
+                                    if current_idx == seg {
+                                        break;
+                                    }
+                                }
+                            }
+                            let sender_clone = self.sender.clone();
+                            let path_clone = target_path.clone();
+                            tokio::spawn(async move {
+                                let entries = pages::browse::read_directory(&path_clone);
+                                let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
+                            });
+                            *needs_rebuild = true;
+                            self.needs_rebuild = true;
+                        }
+                    }
+                }
+            }
         } else if self.current_page == Page::Network {
             if button == MouseButton::Left {
                 if state == ElementState::Pressed {
-                    if self.network.graph.mouse_input(button, state, pos.x, pos.y) {
+                    if self.network.breadcrumb.hit_test(pos.x, pos.y) {
+                        if self.network.breadcrumb.mouse_input(button, state, pos.x, pos.y) {
+                            if let Some(seg) = self.network.breadcrumb.path_click() {
+                                let mut target_path = std::path::PathBuf::new();
+                                let mut current_idx = 0;
+                                for component in self.browse.current_dir.components() {
+                                    target_path.push(component);
+                                    if component == std::path::Component::RootDir {
+                                        if seg == 0 {
+                                            break;
+                                        }
+                                    } else {
+                                        current_idx += 1;
+                                        if current_idx == seg {
+                                            break;
+                                        }
+                                    }
+                                }
+                                let sender_clone = self.sender.clone();
+                                let path_clone = target_path.clone();
+                                tokio::spawn(async move {
+                                    let entries = pages::browse::read_directory(&path_clone);
+                                    let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
+                                });
+                                changed = true;
+                            }
+                        }
+                    } else if self.network.graph.mouse_input(button, state, pos.x, pos.y) {
                         if self.network.graph.is_dragging() {
                             self.network.graph.drag_begin(pos.x, pos.y);
                         }
@@ -617,9 +746,17 @@ impl Application for FilesystemApp {
                     let entry_idx = node_sel - offset;
                     if self.browse.selected != Some(entry_idx) {
                         self.browse.selected = Some(entry_idx);
-                        let selected_path = self.browse.entries.get(entry_idx).map(|e| e.path.clone());
-                        if let Some(path) = selected_path {
-                            pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::SetPath { path });
+                        if let Some(entry) = self.browse.entries.get(entry_idx) {
+                            if self.select_mode {
+                                self.browse.save_name_box.text = entry.name.clone();
+                                if self.browse.save_name_box.editing {
+                                    self.browse.save_name_box.edit_buffer = entry.name.clone();
+                                }
+                            }
+                            let selected_path = Some(entry.path.clone());
+                            if let Some(path) = selected_path {
+                                pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::SetPath { path });
+                            }
                         }
                         changed = true;
                     }
@@ -643,23 +780,31 @@ impl Application for FilesystemApp {
                     if let Some(parent) = self.browse.current_dir.parent() {
                         let parent_path = parent.to_path_buf();
                         let sender_clone = self.sender.clone();
+                        let path_clone = parent_path.clone();
                         tokio::spawn(async move {
-                            let entries = pages::browse::read_directory(&parent_path);
-                            let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(entries)));
+                            let entries = pages::browse::read_directory(&path_clone);
+                            let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
                         });
                         changed = true;
                     }
                 } else if dbl_idx >= offset {
                     let entry_idx = dbl_idx - offset;
                     if let Some(entry) = self.browse.entries.get(entry_idx) {
-                        if entry.is_dir {
+                        if entry.is_dir && !is_project_dir(&entry.path) {
                             let path = entry.path.clone();
                             let sender_clone = self.sender.clone();
+                            let path_clone = path.clone();
                             tokio::spawn(async move {
-                                let entries = pages::browse::read_directory(&path);
-                                let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(entries)));
+                                let entries = pages::browse::read_directory(&path_clone);
+                                let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(path_clone, entries)));
                             });
                             changed = true;
+                        } else if self.select_mode {
+                            self.browse.save_name_box.text = entry.name.clone();
+                            if self.browse.save_name_box.editing {
+                                self.browse.save_name_box.edit_buffer = entry.name.clone();
+                            }
+                            return Some(Message::SelectOpen);
                         }
                     }
                 }
@@ -696,8 +841,14 @@ impl Application for FilesystemApp {
 
         if state == ElementState::Pressed {
             let clicked_search = self.current_page == Page::Browse && self.browse.search_box.hit_test(pos.x, pos.y);
+            let clicked_save_name = self.select_mode && self.current_page == Page::Browse && self.browse.save_name_box.hit_test(pos.x, pos.y);
             if !clicked_search {
                 self.browse.search_box.unfocus();
+            }
+            if !clicked_save_name {
+                self.browse.save_name_box.unfocus();
+            }
+            if !clicked_search && !clicked_save_name {
                 clear_ui::widget::focus::clear_focus();
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
@@ -766,6 +917,20 @@ impl Application for FilesystemApp {
             }
         }
 
+        // If the save_name_box is focused, forward key inputs to it
+        if self.select_mode && self.browse.save_name_box.editing {
+            if self.browse.save_name_box.keyboard_input(event) {
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+                if event.logical_key == clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Enter) {
+                    self.browse.save_name_box.unfocus();
+                    clear_ui::widget::focus::clear_focus();
+                    return Some(Message::SelectOpen);
+                }
+                return None;
+            }
+        }
+
         // Global key navigation
         if self.current_page == Page::Browse {
             let key_char = match &event.logical_key {
@@ -787,13 +952,19 @@ impl Application for FilesystemApp {
                 clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Enter) => {
                     if let Some(idx) = self.browse.selected {
                         if let Some(entry) = self.browse.entries.get(idx) {
-                            if entry.is_dir {
+                            if entry.is_dir && !is_project_dir(&entry.path) {
                                 return Some(Message::Browse(pages::browse::BrowseMessage::NavigateTo(idx)));
                             } else if self.select_mode && !self.select_directory {
-                                println!("{}", entry.path.display());
-                                std::process::exit(0);
+                                if self.save_mode {
+                                    return Some(Message::SelectOpen);
+                                } else {
+                                    println!("{}", entry.path.display());
+                                    std::process::exit(0);
+                                }
                             }
                         }
+                    } else if self.save_mode {
+                        return Some(Message::SelectOpen);
                     }
                 }
                 clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Escape) => {
@@ -820,7 +991,7 @@ impl Application for FilesystemApp {
                     Some("l") => {
                         if let Some(idx) = self.browse.selected {
                             if let Some(entry) = self.browse.entries.get(idx) {
-                                if entry.is_dir {
+                                if entry.is_dir && !is_project_dir(&entry.path) {
                                     return Some(Message::Browse(pages::browse::BrowseMessage::NavigateTo(idx)));
                                 }
                             }
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index 31f386f..9246637 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -3,7 +3,8 @@ use std::os::unix::fs::PermissionsExt;
 use std::path::{Path, PathBuf};
 
 use crate::pages::PageContent;
-use clear_ui::widget::Widget;
+use clear_ui::widget::{Widget, Breadcrumb};
+use clear_ui::layout::Section;
 
 // ── Data ────────────────────────────────────────────────────────────
 
@@ -26,20 +27,42 @@ pub struct BrowseState {
     pub search_box: clear_ui::widget::TextBox,
     pub list_box: clear_ui::widget::ScrollingList,
     pub selected: Option<usize>,
+    pub breadcrumb: Breadcrumb,
+    pub save_name_box: clear_ui::widget::TextBox,
 }
 
 impl Default for BrowseState {
     fn default() -> Self {
         let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
-        Self {
+        let mut breadcrumb = Breadcrumb::new();
+        breadcrumb.set_network_opacity(0.95);
+        let mut state = Self {
             current_dir: PathBuf::from(home),
             all_entries: Vec::new(),
             entries: Vec::new(),
             show_hidden: false,
-            search_box: clear_ui::widget::TextBox::new(String::new()).with_label("Search files..."),
+            search_box: clear_ui::widget::TextBox::new(String::new()).with_max_width(None),
             list_box: clear_ui::widget::ScrollingList::new(28.0, 2.0),
             selected: None,
+            breadcrumb,
+            save_name_box: clear_ui::widget::TextBox::new(String::new()).with_max_width(None),
+        };
+        state.list_box.scroll_box.show_border = false;
+        state.update_breadcrumb();
+        state
+    }
+}
+
+impl BrowseState {
+    pub fn update_breadcrumb(&mut self) {
+        let mut segments = Vec::new();
+        for component in self.current_dir.components() {
+            let s = component.as_os_str().to_string_lossy().to_string();
+            if s != "/" && !s.is_empty() {
+                segments.push(s);
+            }
         }
+        self.breadcrumb.set_path(&segments);
     }
 }
 
@@ -49,7 +72,7 @@ pub enum BrowseMessage {
     SelectEntry(usize),
     NavigateTo(usize),
     NavigateToPath(PathBuf),
-    DirectoryLoaded(Vec<DirEntry>),
+    DirectoryLoaded(PathBuf, Vec<DirEntry>),
     ToggleHidden,
 }
 
@@ -59,6 +82,10 @@ pub enum BrowseNavigation {
     Down,
 }
 
+pub fn is_project_dir(path: &Path) -> bool {
+    path.is_dir() && path.join("state.json").exists()
+}
+
 // ── Helpers ─────────────────────────────────────────────────────────
 
 pub fn read_directory(path: &Path) -> Vec<DirEntry> {
@@ -163,7 +190,7 @@ pub fn next_selection_index(state: &BrowseState, direction: BrowseNavigation) ->
 
 // ── View ────────────────────────────────────────────────────────────
 
-pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
+pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_mode: bool) -> PageContent {
     let mut pc = PageContent::new();
     let text_dim = [0.53, 0.53, 0.60, 1.0];
     let heading_fg = [0.56, 0.83, 0.56, 1.0]; // Color::from_rgb8(0x8f, 0xd4, 0x8f)
@@ -172,35 +199,92 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32) -> Page
     let accent_fg = [0.36, 0.56, 0.38, 1.0]; // Color::from_rgb8(0x5c, 0x90, 0x60)
     let text_fg = [0.83, 0.83, 0.83, 1.0];
 
-    // Breadcrumb path
-    let path_str = state.current_dir.to_string_lossy().to_string();
-    pc.text(&path_str, cx + 12.0, cy + 12.0, 11.0, text_dim);
+    // Render the Breadcrumb widget into PageContent
+    clear_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 12.0, cy + 6.0, cw - 24.0, 24.0);
 
-    // Search textbox
-    let search_w = cw - 80.0;
-    let search_h = 28.0;
-    let search_y = cy + 28.0 + state.search_box.top_room();
-    state.search_box.set_row_rect(cx + 12.0, search_w);
-    clear_ui::layout::render_widget(&mut pc, &mut state.search_box, cx + 12.0, search_y, search_w, search_h);
+    // 1. Files Section layout
+    let outer_x = cx + 12.0;
+    let outer_y = cy + 42.0;
+    let outer_w = cw - 24.0;
 
-    // Count label next to search
-    let count_str = format!(
-        "{} items{}",
-        state.entries.len(),
-        if state.show_hidden { " (.)" } else { "" }
-    );
-    pc.text(&count_str, cx + 12.0 + search_w + 8.0, search_y + 8.0, 11.0, text_dim);
+    // Search Section starts at the bottom
+    let search_sec_h = 56.0;
+    let search_sec_y = cy + ch - search_sec_h;
+
+    // Files Section height takes the remaining space above Search Section
+    let outer_h = search_sec_y - outer_y - 12.0;
+
+    let mut files_sec = Section::new(&mut pc, outer_x - 8.0, outer_y - 12.0, outer_w + 16.0, "Files");
+    files_sec.content_y = outer_y + outer_h - 12.0;
+    files_sec.finish(&mut pc);
 
-    // File list scroll box
-    let list_x = cx + 12.0;
-    let list_y = search_y + search_h + 16.0;
-    let list_w = cw - 24.0;
-    let list_h = ch - (list_y - cy) - 12.0;
+    // Inner file list scroll box (inset inside the Files Section)
+    let list_x = outer_x + 4.0;
+    let list_y = outer_y + 4.0;
+    let list_w = outer_w - 8.0;
+    let list_h = outer_h - 8.0;
 
     clear_ui::layout::render_widget(&mut pc, &mut state.list_box, list_x, list_y, list_w, list_h);
 
     state.list_box.update_bounds(state.entries.len(), list_y, list_h);
 
+    // Count label in the bottom right corner of the scrolling list
+    let count_str = format!(
+        "{} items{}",
+        state.entries.len(),
+        if state.show_hidden { " (.)" } else { "" }
+    );
+    let count_text_w = count_str.len() as f32 * 6.0;
+    let count_x = list_x + list_w - count_text_w - 24.0;
+    let count_y = list_y + list_h - 18.0;
+    pc.text(&count_str, count_x, count_y, 11.0, text_dim);
+
+    if select_mode {
+        // Two columns at the bottom: Search and File Name
+        let sec_w = (outer_w - 12.0) / 2.0;
+
+        // Left column: Search
+        let mut search_sec = Section::new(&mut pc, outer_x - 8.0, search_sec_y - 12.0, sec_w + 16.0, "Search");
+        search_sec.content_y = search_sec_y + search_sec_h - 12.0;
+        search_sec.finish(&mut pc);
+
+        let search_x = outer_x + 12.0;
+        let search_y = search_sec_y + 14.0;
+        let search_w = sec_w - 24.0;
+        let search_h = 28.0;
+
+        state.search_box.set_row_rect(search_x, search_w);
+        clear_ui::layout::render_widget(&mut pc, &mut state.search_box, search_x, search_y, search_w, search_h);
+
+        // Right column: File Name
+        let filename_sec_x = outer_x + sec_w + 12.0;
+        let mut filename_sec = Section::new(&mut pc, filename_sec_x - 8.0, search_sec_y - 12.0, sec_w + 16.0, "File Name");
+        filename_sec.content_y = search_sec_y + search_sec_h - 12.0;
+        filename_sec.finish(&mut pc);
+
+        let filename_x = filename_sec_x + 12.0;
+        let filename_y = search_sec_y + 14.0;
+        let filename_w = sec_w - 24.0;
+        let filename_h = 28.0;
+
+        state.save_name_box.set_row_rect(filename_x, filename_w);
+        clear_ui::layout::render_widget(&mut pc, &mut state.save_name_box, filename_x, filename_y, filename_w, filename_h);
+    } else {
+        // 2. Search Section borders (Full Width)
+        let mut search_sec = Section::new(&mut pc, outer_x - 8.0, search_sec_y - 12.0, outer_w + 16.0, "Search");
+        search_sec.content_y = search_sec_y + search_sec_h - 12.0;
+        search_sec.finish(&mut pc);
+
+        // Inner search textbox
+        let search_x = outer_x + 12.0;
+        let search_y = search_sec_y + 14.0;
+        let search_w = outer_w - 24.0;
+        let search_h = 28.0;
+
+        state.search_box.set_row_rect(search_x, search_w);
+        clear_ui::layout::render_widget(&mut pc, &mut state.search_box, search_x, search_y, search_w, search_h);
+    }
+
     for (idx, entry) in state.entries.iter().enumerate() {
         if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 2.0) {
             let is_selected = state.selected == Some(idx);
@@ -308,46 +392,44 @@ fn apply_filters(state: &mut BrowseState) {
     state.selected = if state.entries.is_empty() { None } else { Some(0) };
 }
 
-pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> tokio::task::JoinHandle<Vec<DirEntry>> {
+pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> (PathBuf, tokio::task::JoinHandle<Vec<DirEntry>>) {
     match msg {
         BrowseMessage::SearchChanged(q) => {
             state.search_box.text = q;
             apply_filters(state);
-            tokio::spawn(async { Vec::new() })
+            (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
         BrowseMessage::SelectEntry(i) => {
             state.selected = Some(i);
-            tokio::spawn(async { Vec::new() })
+            (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
         BrowseMessage::NavigateTo(idx) => {
             if let Some(entry) = state.entries.get(idx) {
-                if entry.is_dir {
+                if entry.is_dir && !is_project_dir(&entry.path) {
                     let path = entry.path.clone();
-                    return tokio::spawn(async move { read_directory(&path) });
+                    let p = path.clone();
+                    return (path, tokio::spawn(async move { read_directory(&p) }));
                 }
             }
-            tokio::spawn(async { Vec::new() })
+            (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
         BrowseMessage::NavigateToPath(path) => {
             let p = path.clone();
-            tokio::spawn(async move { read_directory(&p) })
+            (path, tokio::spawn(async move { read_directory(&p) }))
         }
-        BrowseMessage::DirectoryLoaded(entries) => {
+        BrowseMessage::DirectoryLoaded(path, entries) => {
+            state.current_dir = path;
             state.all_entries = entries;
             state.search_box.text.clear();
             state.search_box.edit_buffer.clear();
             apply_filters(state);
-            if let Some(first) = state.all_entries.first() {
-                if let Some(parent) = first.path.parent() {
-                    state.current_dir = parent.to_path_buf();
-                }
-            }
-            tokio::spawn(async { Vec::new() })
+            state.update_breadcrumb();
+            (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
         BrowseMessage::ToggleHidden => {
             state.show_hidden = !state.show_hidden;
             apply_filters(state);
-            tokio::spawn(async { Vec::new() })
+            (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
     }
 }
@@ -368,7 +450,7 @@ mod tests {
                     modified: String::new(),
                 })
                 .collect(),
-            search_box: clear_ui::widget::TextBox::new(String::new()),
+            search_box: clear_ui::widget::TextBox::new(String::new()).with_max_width(None),
             list_box: clear_ui::widget::ScrollingList::new(28.0, 2.0),
             ..BrowseState::default()
         }
@@ -437,4 +519,27 @@ mod tests {
         apply_filters(&mut state);
         assert_eq!(state.entries.len(), 2);
     }
+
+    #[test]
+    fn test_is_project_dir_detection() {
+        let unique_dir = std::env::temp_dir().join(format!("clear_test_dir_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)));
+        std::fs::create_dir_all(&unique_dir).unwrap();
+        
+        // Initially, path is a directory but doesn't have state.json
+        assert!(!is_project_dir(&unique_dir));
+        
+        // Create state.json
+        let file_path = unique_dir.join("state.json");
+        std::fs::write(&file_path, "{}").unwrap();
+        
+        // Now it should be recognized as a project dir
+        assert!(is_project_dir(&unique_dir));
+        
+        // If it's a file rather than a directory, even if named state.json, it shouldn't be a project dir itself
+        assert!(!is_project_dir(&file_path));
+
+        // Clean up
+        let _ = std::fs::remove_file(&file_path);
+        let _ = std::fs::remove_dir(&unique_dir);
+    }
 }
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 32ded8e..e686be7 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -53,7 +53,7 @@ pub struct ContentButton {
 
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
-    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>)>,
+    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
     pub buttons: Vec<ContentButton>,
 }
 
@@ -71,11 +71,11 @@ impl PageContent {
     }
 
     pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
-        self.texts.push((content.to_string(), size, x, y, color, None));
+        self.texts.push((content.to_string(), size, x, y, color, None, None));
     }
 
     pub fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
-        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string())));
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
     }
 
     pub fn button(
@@ -139,10 +139,18 @@ impl RenderTarget for PageContent {
     }
 
     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
-        self.texts.push((content.to_string(), size, x, y, color, None));
+        self.texts.push((content.to_string(), size, x, y, color, None, None));
     }
 
     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
-        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string())));
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
+    }
+
+    fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, None, bounds));
+    }
+
+    fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
     }
 }
diff --git a/src/pages/network.rs b/src/pages/network.rs
index e6a6195..e51f1ea 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,10 +1,11 @@
 use std::path::{Path, PathBuf};
 use crate::pages::PageContent;
 use crate::pages::browse::{BrowseState, DirEntry};
-use clear_ui::widget::{Graph, GraphNode, Widget};
+use clear_ui::widget::{Graph, GraphNode, Widget, Breadcrumb};
 
 pub struct NetworkState {
     pub graph: Graph,
+    pub breadcrumb: Breadcrumb,
     pub last_dir: PathBuf,
 }
 
@@ -20,8 +21,12 @@ impl Default for NetworkState {
         graph.set_uniform_background(false);
         graph.set_network_opacity(0.95);
 
+        let mut breadcrumb = Breadcrumb::new();
+        breadcrumb.set_network_opacity(0.95);
+
         Self {
             graph,
+            breadcrumb,
             last_dir: PathBuf::new(),
         }
     }
@@ -98,10 +103,23 @@ impl NetworkState {
 pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
     let mut pc = PageContent::new();
 
+    // Render the Breadcrumb widget into PageContent
+    clear_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 4.0, cy + 6.0, cw - 16.0, 24.0);
+
     // Check if directory changed, or if last_dir is empty, and repopulate
     if state.last_dir != browse.current_dir || state.graph.get_nodes().is_empty() {
         state.populate_graph(&browse.current_dir, &browse.entries);
 
+        // Update breadcrumb path
+        let mut segments = Vec::new();
+        for component in browse.current_dir.components() {
+            let s = component.as_os_str().to_string_lossy().to_string();
+            if s != "/" && !s.is_empty() {
+                segments.push(s);
+            }
+        }
+        state.breadcrumb.set_path(&segments);
+
         // Map browse selection to graph node selection if any
         let has_parent = browse.current_dir.parent().is_some();
         let offset = if has_parent { 2 } else { 1 };
@@ -131,8 +149,8 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw
         }
     }
 
-    // Render the Graph widget into PageContent
-    clear_ui::layout::render_widget(&mut pc, &mut state.graph, cx, cy, cw, ch);
+    // Render the Graph widget into PageContent, shifted down by 28.0 to leave room for the breadcrumb
+    clear_ui::layout::render_widget(&mut pc, &mut state.graph, cx, cy + 28.0, cw, ch - 28.0);
 
     pc
 }