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

commitc6ae187d8993d76fdfe1d07340d20ed68e33a5c3
parent784f8f4719
authorLucas Galante <[email protected]>
date2026-06-03 15:28
Update and align interface features

 Cargo.lock                                |  26 ++
 src/main.rs                               | 488 +++++++++++++++++++++++++-----
 src/pages/mod.rs                          |  17 +-
 src/pages/network.rs                      | 229 ++++++++++++++
 src/pages/preview.rs                      | 107 +++++--
 src/pages/{keybindings.rs => settings.rs} |  82 +++--
 6 files changed, 802 insertions(+), 147 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 4b2d00c..4677852 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -206,6 +206,7 @@ dependencies = [
  "raw-window-handle",
  "resvg",
  "serde",
+ "serde_json",
  "smithay-client-toolkit",
  "tokio",
  "wayland-backend",
@@ -700,6 +701,12 @@ dependencies = [
  "hashbrown 0.17.1",
 ]
 
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
 [[package]]
 name = "jni-sys"
 version = "0.3.1"
@@ -1313,6 +1320,19 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -2285,3 +2305,9 @@ name = "zeno"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/src/main.rs b/src/main.rs
index 30cda81..93302b0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,5 @@
 mod pages;
 
-use std::path::PathBuf;
 use wayland_client::QueueHandle;
 use glyphon::{Attrs, Buffer, FontSystem, Metrics};
 
@@ -25,8 +24,13 @@ struct AppWidget {
 struct FilesystemApp {
     current_page: Page,
     browse: pages::browse::BrowseState,
+    network: pages::network::NetworkState,
+    settings: pages::settings::SettingsState,
     preview: pages::preview::PreviewState,
-    keybindings: pages::keybindings::KeybindingsState,
+
+    // Command-line chooser options
+    select_mode: bool,
+    select_directory: bool,
 
     // Rendering resources
     widgets: Vec<AppWidget>,
@@ -40,6 +44,7 @@ struct FilesystemApp {
     page_buttons: Vec<pages::ContentButton>,
     cursor_x: f32,
     cursor_y: f32,
+    paginator: clear_ui::widget::Paginator,
 }
 
 // ── Messages ────────────────────────────────────────────────────────
@@ -49,8 +54,9 @@ pub enum Message {
     SwitchPage(Page),
     Browse(pages::browse::BrowseMessage),
     Preview(pages::preview::PreviewMessage),
-    Keybindings(pages::keybindings::KeybindingsMessage),
     KeyboardEvent(KeyEvent),
+    SelectOpen,
+    SelectCancel,
 }
 
 // ── Helpers ─────────────────────────────────────────────────────────
@@ -75,61 +81,42 @@ impl FilesystemApp {
 
         let mut pc = pages::PageContent::new();
 
-        // 1. Draw Sidebar Background
-        let sidebar_bg = [0.086, 0.141, 0.094, 1.0]; // matching 0x16, 0x24, 0x18
-        pc.rect(sidebar_bg, 0.0, 0.0, 200.0, self.height as f32);
-
-        // Sidebar Divider Line (accent border)
-        pc.rect([0.36, 0.56, 0.38, 1.0], 200.0, 0.0, 1.0, self.height as f32);
-
-        // Sidebar Title
-        pc.text("Clear Filesystem", 16.0, 16.0, 15.0, [0.36, 0.56, 0.38, 1.0]);
-
-        // Sidebar buttons
-        let mut btn_y = 48.0;
-        for page in Page::ALL {
-            let active = page == self.current_page;
-            let bg = if active {
-                [0.16, 0.29, 0.18, 1.0]
-            } else {
-                [0.12, 0.18, 0.13, 1.0]
-            };
-            let fg = if active {
-                [0.56, 0.83, 0.56, 1.0]
-            } else {
-                [0.60, 0.60, 0.67, 1.0]
-            };
-
-            let label = format!("{}  {}", page.icon(), page.label());
+        // 1. Render the Paginator widget to separate PageContent first
+        let page_idx = Page::ALL.iter().position(|&p| p == self.current_page).unwrap_or(0);
+        self.paginator.set_selected_page(page_idx);
+        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);
 
-            pc.button(
-                &label,
-                12.0,
-                btn_y,
-                176.0,
-                36.0,
-                bg,
-                if active { bg } else { [0.18, 0.28, 0.20, 1.0] },
-                fg,
-                Message::SwitchPage(page),
-            );
-
-            btn_y += 42.0;
-        }
-
-        // 2. Draw Content Background
-        let content_bg = [0.10, 0.165, 0.11, 1.0]; // matching 0x1a, 0x2a, 0x1c
-        pc.rect(content_bg, 201.0, 0.0, self.width as f32 - 201.0, self.height as f32);
-
-        // 3. Draw Page content
-        let usable_w = self.width as f32 - 201.0 - 32.0;
+        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 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;
-        let browse_x = 217.0;
         let preview_x = browse_x + browse_w + 12.0;
         let content_y = 16.0;
-        let content_h = self.height as f32 - 32.0;
 
+        let select_bar_h = 48.0;
+        let content_h = if self.select_mode {
+            self.height as f32 - 32.0 - select_bar_h
+        } else {
+            self.height as f32 - 32.0
+        };
+
+        if has_sidebar {
+            // 2. Add Content Background (where x >= 56.0) from the paginator to pc first
+            for rect in paginator_pc.rects.iter() {
+                if rect.1 >= 56.0 {
+                    pc.rects.push(*rect);
+                }
+            }
+        } else {
+            // Full window page background
+            let bg_color = clear_ui::color::page_low_color();
+            pc.rect(bg_color, 0.0, 0.0, self.width as f32, self.height as f32);
+        }
+
+        // 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);
@@ -143,13 +130,119 @@ impl FilesystemApp {
                 pc.texts.extend(preview_pc.texts);
                 pc.buttons.extend(preview_pc.buttons);
             }
-            Page::Keybindings => {
-                let keys_pc = pages::keybindings::view(&self.keybindings, browse_x, content_y, usable_w, content_h);
+            Page::Network => {
+                let network_pc = pages::network::view(&mut self.network, &self.browse, browse_x, content_y, browse_w, content_h);
+                let preview_pc = pages::preview::view(&self.preview, preview_x, content_y, preview_w, content_h);
 
-                pc.rects.extend(keys_pc.rects);
-                pc.texts.extend(keys_pc.texts);
-                pc.buttons.extend(keys_pc.buttons);
+                pc.rects.extend(network_pc.rects);
+                pc.texts.extend(network_pc.texts);
+                pc.buttons.extend(network_pc.buttons);
+
+                pc.rects.extend(preview_pc.rects);
+                pc.texts.extend(preview_pc.texts);
+                pc.buttons.extend(preview_pc.buttons);
             }
+            Page::Settings => {
+                let settings_pc = pages::settings::view(&mut self.settings, browse_x, content_y, usable_w, content_h);
+
+                pc.rects.extend(settings_pc.rects);
+                pc.texts.extend(settings_pc.texts);
+                pc.buttons.extend(settings_pc.buttons);
+
+                // Sync color selector changes back to global node color
+                let col = self.settings.color_selector.color;
+                let r_f = col[0] as f32 / 255.0;
+                let g_f = col[1] as f32 / 255.0;
+                let b_f = col[2] as f32 / 255.0;
+                let linear_col = clear_ui::color::to_linear([r_f, g_f, b_f, 1.0]);
+                if clear_ui::color::node_color() != linear_col {
+                    clear_ui::color::set_node_color(linear_col);
+                }
+            }
+        }
+
+        if has_sidebar {
+            // 4. Add Sidebar Background and tabs (where x < 56.0) from the paginator to pc last
+            for rect in paginator_pc.rects.iter() {
+                if rect.1 < 56.0 {
+                    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]);
+
+            // Add the paginator's texts (tab labels) on top of the sidebar background
+            pc.texts.extend(paginator_pc.texts);
+        }
+
+        // Draw bottom selection bar if select_mode is enabled
+        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
+            };
+
+            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(
+                "Cancel",
+                cancel_x,
+                bar_y + 10.0,
+                70.0,
+                28.0,
+                [0.25, 0.12, 0.12, 0.5],
+                [0.35, 0.15, 0.15, 0.8],
+                text_fg,
+                Message::SelectCancel,
+            );
+
+            // Open/Select button
+            let open_x = self.width as f32 - 100.0;
+            pc.button(
+                "Select",
+                open_x,
+                bar_y + 10.0,
+                80.0,
+                28.0,
+                accent,
+                [0.46, 0.66, 0.48, 1.0],
+                [0.10, 0.16, 0.11, 1.0],
+                Message::SelectOpen,
+            );
         }
 
         // 4. Translate PageContent into rendering quads and TextItems
@@ -231,25 +324,42 @@ impl Application for FilesystemApp {
     type Message = Message;
 
     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");
+
+        // 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]);
+
         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_tabs_rotated(true);
+
         let mut app = Self {
             current_page: Page::Browse,
             browse,
+            network: pages::network::NetworkState::default(),
+            settings: pages::settings::SettingsState::default(),
             preview: pages::preview::PreviewState::default(),
-            keybindings: pages::keybindings::KeybindingsState,
+            select_mode,
+            select_directory,
             widgets: Vec::new(),
             text_items: Vec::new(),
             font_system: FontSystem::new(),
             needs_rebuild: true,
-            width: 1200,
-            height: 720,
+            width: if select_mode { 900 } else { 1200 },
+            height: if select_mode { 500 } else { 720 },
             scale_factor: 1.0,
             sender: sender.clone(),
             page_buttons: Vec::new(),
             cursor_x: 0.0,
             cursor_y: 0.0,
+            paginator,
         };
 
         // Start initial directory loading
@@ -264,13 +374,29 @@ impl Application for FilesystemApp {
     }
 
     fn settings(&self) -> WindowSettings {
-        WindowSettings {
-            title: "Clear Filesystem Interface".to_string(),
-            app_id: "clear-filesystem-interface".to_string(),
-            width: 1200,
-            height: 720,
-            fullscreen: false,
-            min_size: Some((1020, 600)),
+        if self.select_mode {
+            let title = if self.select_directory {
+                "Select Directory"
+            } else {
+                "Select File"
+            };
+            WindowSettings {
+                title: title.to_string(),
+                app_id: "clear-filesystem-chooser".to_string(),
+                width: 900,
+                height: 500,
+                fullscreen: false,
+                min_size: Some((800, 400)),
+            }
+        } else {
+            WindowSettings {
+                title: "Clear Filesystem Interface".to_string(),
+                app_id: "clear-filesystem-interface".to_string(),
+                width: 1200,
+                height: 720,
+                fullscreen: false,
+                min_size: Some((1020, 600)),
+            }
         }
     }
 
@@ -278,6 +404,8 @@ impl Application for FilesystemApp {
         match msg {
             Message::SwitchPage(page) => {
                 self.current_page = page;
+                let page_idx = Page::ALL.iter().position(|&p| p == page).unwrap_or(0);
+                self.paginator.set_selected_page(page_idx);
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
@@ -309,32 +437,60 @@ impl Application for FilesystemApp {
                 self.needs_rebuild = true;
             }
             Message::Preview(msg) => {
-                let nav_path = match &msg {
-                    pages::preview::PreviewMessage::NavigateTo(path) => Some(path.clone()),
-                    _ => None,
-                };
                 pages::preview::update(&mut self.preview, msg);
-                if let Some(path) = nav_path {
-                    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)));
-                    });
-                }
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
-            Message::Keybindings(msg) => {
-                pages::keybindings::update(&mut self.keybindings, msg);
+            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 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 {
+                        println!("{}", path.display());
+                        std::process::exit(0);
+                    }
+                }
+            }
+            Message::SelectCancel => {
+                std::process::exit(1);
+            }
+        }
+    }
+
+    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+        if self.paginator.tick(dt) {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+
+        if self.current_page == Page::Settings {
+            if self.settings.color_selector.tick(dt) {
+                let col = self.settings.color_selector.color;
+                let r_f = col[0] as f32 / 255.0;
+                let g_f = col[1] as f32 / 255.0;
+                let b_f = col[2] as f32 / 255.0;
+                let linear_col = clear_ui::color::to_linear([r_f, g_f, b_f, 1.0]);
+                clear_ui::color::set_node_color(linear_col);
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
-            Message::KeyboardEvent(_) => {}
         }
     }
 
-    fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
-
     fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
         if self.needs_rebuild || self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale {
             self.width = size.width as u32;
@@ -362,6 +518,10 @@ impl Application for FilesystemApp {
 
         let mut changed = false;
 
+        if self.paginator.cursor_moved(pos.x, pos.y) {
+            changed = true;
+        }
+
         if self.current_page == Page::Browse {
             if self.browse.search_box.cursor_moved(pos.x, pos.y) {
                 changed = true;
@@ -369,6 +529,20 @@ impl Application for FilesystemApp {
             if self.browse.list_box.cursor_moved(pos.x, pos.y) {
                 changed = true;
             }
+        } else if self.current_page == Page::Network {
+            if self.network.graph.is_dragging() {
+                if self.network.graph.drag_update(pos.x, pos.y) {
+                    changed = true;
+                }
+            } else {
+                if self.network.graph.on_cursor_moved(pos.x, pos.y) {
+                    changed = true;
+                }
+            }
+        } else if self.current_page == Page::Settings {
+            if self.settings.color_selector.cursor_moved(pos.x, pos.y) {
+                changed = true;
+            }
         }
 
         // Always check if buttons hover state changed
@@ -390,6 +564,21 @@ impl Application for FilesystemApp {
             return None;
         }
 
+        let mut changed = false;
+
+        if self.paginator.mouse_input(button, state, pos.x, pos.y) {
+            if self.paginator.take_click() {
+                let idx = self.paginator.selected_page();
+                if idx < Page::ALL.len() {
+                    clear_ui::widget::focus::clear_focus();
+                    self.current_page = Page::ALL[idx];
+                }
+            }
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+            return None;
+        }
+
         if self.current_page == Page::Browse {
             if self.browse.search_box.mouse_input(button, state, pos.x, pos.y) {
                 *needs_rebuild = true;
@@ -399,6 +588,110 @@ impl Application for FilesystemApp {
                 *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.graph.is_dragging() {
+                            self.network.graph.drag_begin(pos.x, pos.y);
+                        }
+                        changed = true;
+                    }
+                } else if state == ElementState::Released {
+                    if self.network.graph.is_dragging() {
+                        self.network.graph.drag_end();
+                        changed = true;
+                    } else {
+                        if self.network.graph.mouse_input(button, state, pos.x, pos.y) {
+                            changed = true;
+                        }
+                    }
+                }
+            }
+
+            // Sync selection from graph to browse state
+            let has_parent = self.browse.current_dir.parent().is_some();
+            let offset = if has_parent { 2 } else { 1 };
+            if let Some(node_sel) = self.network.graph.selected_node() {
+                if node_sel >= offset {
+                    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 });
+                        }
+                        changed = true;
+                    }
+                } else {
+                    if self.browse.selected.is_some() {
+                        self.browse.selected = None;
+                        changed = true;
+                    }
+                }
+            } else {
+                if self.browse.selected.is_some() {
+                    self.browse.selected = None;
+                    changed = true;
+                }
+            }
+
+            // Sync double click navigation
+            if let Some(dbl_idx) = self.network.graph.double_clicked_node() {
+                self.network.graph.clear_double_clicked_node();
+                if has_parent && dbl_idx == 0 {
+                    if let Some(parent) = self.browse.current_dir.parent() {
+                        let parent_path = parent.to_path_buf();
+                        let sender_clone = self.sender.clone();
+                        tokio::spawn(async move {
+                            let entries = pages::browse::read_directory(&parent_path);
+                            let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(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 {
+                            let path = entry.path.clone();
+                            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)));
+                            });
+                            changed = true;
+                        }
+                    }
+                }
+            }
+
+            if changed {
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        } else if self.current_page == Page::Settings {
+            if self.settings.color_selector.mouse_input(button, state, pos.x, pos.y) {
+                if self.settings.color_selector.take_click() {
+                    let col = self.settings.color_selector.color;
+                    let r_f = col[0] as f32 / 255.0;
+                    let g_f = col[1] as f32 / 255.0;
+                    let b_f = col[2] as f32 / 255.0;
+                    let linear_col = clear_ui::color::to_linear([r_f, g_f, b_f, 1.0]);
+                    clear_ui::color::set_node_color(linear_col);
+                }
+                changed = true;
+            }
+            // If user clicked outside color selector focus area, unfocus it
+            if state == ElementState::Pressed && !self.settings.color_selector.hit_test(pos.x, pos.y) {
+                self.settings.color_selector.unfocus();
+                clear_ui::widget::focus::clear_focus();
+                changed = true;
+            }
+
+            if changed {
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
         }
 
         if state == ElementState::Pressed {
@@ -430,6 +723,11 @@ impl Application for FilesystemApp {
                 *needs_rebuild = true;
                 self.needs_rebuild = true;
             }
+        } else if self.current_page == Page::Network {
+            if self.network.graph.mouse_wheel(delta, pos.x as f32, pos.y as f32) {
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
         }
     }
 
@@ -438,6 +736,22 @@ impl Application for FilesystemApp {
             return None;
         }
 
+        if self.current_page == Page::Settings && self.settings.color_selector.editing {
+            if self.settings.color_selector.keyboard_input(event) {
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+                if !self.settings.color_selector.editing {
+                    let col = self.settings.color_selector.color;
+                    let r_f = col[0] as f32 / 255.0;
+                    let g_f = col[1] as f32 / 255.0;
+                    let b_f = col[2] as f32 / 255.0;
+                    let linear_col = clear_ui::color::to_linear([r_f, g_f, b_f, 1.0]);
+                    clear_ui::color::set_node_color(linear_col);
+                }
+                return None;
+            }
+        }
+
         // If the search textbox is focused, forward key inputs to it
         if self.current_page == Page::Browse && self.browse.search_box.editing {
             if self.browse.search_box.keyboard_input(event) {
@@ -475,10 +789,18 @@ impl Application for FilesystemApp {
                         if let Some(entry) = self.browse.entries.get(idx) {
                             if entry.is_dir {
                                 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);
                             }
                         }
                     }
                 }
+                clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Escape) => {
+                    if self.select_mode {
+                        std::process::exit(1);
+                    }
+                }
                 clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Backspace) => {
                     if let Some(parent) = self.browse.current_dir.parent() {
                         return Some(Message::Browse(pages::browse::BrowseMessage::NavigateToPath(parent.to_path_buf())));
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 1aab8b1..32ded8e 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -1,32 +1,37 @@
 pub mod browse;
 pub mod preview;
-pub mod keybindings;
+pub mod network;
+pub mod settings;
 
 use clear_ui::layout::RenderTarget;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Page {
     Browse,
-    Keybindings,
+    Network,
+    Settings,
 }
 
 impl Page {
-    pub const ALL: [Page; 2] = [
+    pub const ALL: [Page; 3] = [
         Page::Browse,
-        Page::Keybindings,
+        Page::Network,
+        Page::Settings,
     ];
 
     pub fn label(self) -> &'static str {
         match self {
             Page::Browse => "Browse",
-            Page::Keybindings => "Keys",
+            Page::Network => "Network",
+            Page::Settings => "Settings",
         }
     }
 
     pub fn icon(self) -> &'static str {
         match self {
             Page::Browse => "📁",
-            Page::Keybindings => "⌨",
+            Page::Network => "🌐",
+            Page::Settings => "⚙",
         }
     }
 }
diff --git a/src/pages/network.rs b/src/pages/network.rs
new file mode 100644
index 0000000..e6a6195
--- /dev/null
+++ b/src/pages/network.rs
@@ -0,0 +1,229 @@
+use std::path::{Path, PathBuf};
+use crate::pages::PageContent;
+use crate::pages::browse::{BrowseState, DirEntry};
+use clear_ui::widget::{Graph, GraphNode, Widget};
+
+pub struct NetworkState {
+    pub graph: Graph,
+    pub last_dir: PathBuf,
+}
+
+impl Default for NetworkState {
+    fn default() -> Self {
+        let mut graph = Graph::new();
+        // Configure grid settings
+        graph.set_show_network_grid(true);
+        graph.set_grid_sizes(140.0, 70.0);
+        graph.set_skipped_sizes(35.0, 35.0);
+        graph.set_grid_origin(60.0, 60.0);
+        graph.set_grid_snap_enabled(true);
+        graph.set_uniform_background(false);
+        graph.set_network_opacity(0.95);
+
+        Self {
+            graph,
+            last_dir: PathBuf::new(),
+        }
+    }
+}
+
+#[allow(dead_code)]
+#[derive(Debug, Clone)]
+pub enum NetworkMessage {}
+
+impl NetworkState {
+    pub fn populate_graph(&mut self, current_dir: &Path, entries: &[DirEntry]) {
+        let mut nodes = Vec::new();
+
+        // 1. Parent directory (if any)
+        let parent_node_name = if let Some(parent) = current_dir.parent() {
+            let parent_name = parent
+                .file_name()
+                .map(|s| s.to_string_lossy().to_string())
+                .unwrap_or_else(|| "/".to_string());
+            let name = format!("📁 .. ({})", parent_name);
+            nodes.push(GraphNode {
+                name: name.clone(),
+                position: (3.0, 0.0),
+                parameters: Vec::new(),
+                geom_visible: true,
+            });
+            Some(name)
+        } else {
+            None
+        };
+
+        // 2. Current directory node
+        let current_name = current_dir
+            .file_name()
+            .map(|s| s.to_string_lossy().to_string())
+            .unwrap_or_else(|| "/".to_string());
+        let current_node_name = format!("📁 {}", current_name);
+
+        let current_params = if let Some(ref p_name) = parent_node_name {
+            vec![("input".to_string(), p_name.clone(), "string".to_string())]
+        } else {
+            Vec::new()
+        };
+
+        nodes.push(GraphNode {
+            name: current_node_name.clone(),
+            position: (3.0, 1.0),
+            parameters: current_params,
+            geom_visible: true,
+        });
+
+        // 3. Children nodes
+        for (idx, entry) in entries.iter().enumerate() {
+            let icon = if entry.is_dir { "📁" } else { "📄" };
+            let node_name = format!("{} {}", icon, entry.name);
+
+            // Spreading items in 7 columns starting at row 2
+            let col = (idx % 7) as f32;
+            let row = 2.0 + (idx / 7) as f32;
+
+            nodes.push(GraphNode {
+                name: node_name,
+                position: (col, row),
+                parameters: vec![("input".to_string(), current_node_name.clone(), "string".to_string())],
+                geom_visible: true,
+            });
+        }
+
+        self.graph.set_nodes(&nodes);
+        self.last_dir = current_dir.to_path_buf();
+    }
+}
+
+pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+
+    // 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);
+
+        // 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 };
+        if let Some(sel) = browse.selected {
+            state.graph.set_selected_node(Some(sel + offset));
+        } else {
+            state.graph.set_selected_node(None);
+        }
+    } else {
+        // Sync graph selection with browse selection when they are in sync
+        let has_parent = browse.current_dir.parent().is_some();
+        let offset = if has_parent { 2 } else { 1 };
+
+        if let Some(sel) = browse.selected {
+            let expected_node_idx = sel + offset;
+            if state.graph.selected_node() != Some(expected_node_idx) {
+                if !state.graph.is_dragging() {
+                    state.graph.set_selected_node(Some(expected_node_idx));
+                }
+            }
+        } else {
+            if let Some(graph_sel) = state.graph.selected_node() {
+                if graph_sel >= offset {
+                    state.graph.set_selected_node(None);
+                }
+            }
+        }
+    }
+
+    // Render the Graph widget into PageContent
+    clear_ui::layout::render_widget(&mut pc, &mut state.graph, cx, cy, cw, ch);
+
+    pc
+}
+
+#[allow(dead_code)]
+pub fn update(_state: &mut NetworkState, _msg: NetworkMessage) {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_populate_graph_has_parent() {
+        let mut state = NetworkState::default();
+        let current_dir = Path::new("/home/user/project");
+        let entries = vec![
+            DirEntry {
+                name: "file1.txt".to_string(),
+                path: PathBuf::from("/home/user/project/file1.txt"),
+                is_dir: false,
+                size: 100,
+                permissions: 0o644,
+                modified: String::new(),
+            },
+            DirEntry {
+                name: "subdir".to_string(),
+                path: PathBuf::from("/home/user/project/subdir"),
+                is_dir: true,
+                size: 4096,
+                permissions: 0o755,
+                modified: String::new(),
+            },
+        ];
+
+        state.populate_graph(current_dir, &entries);
+        let nodes = state.graph.get_nodes();
+
+        // 1 parent + 1 current + 2 children = 4 nodes
+        assert_eq!(nodes.len(), 4);
+
+        // Check node names
+        assert_eq!(nodes[0].name, "📁 .. (user)");
+        assert_eq!(nodes[1].name, "📁 project");
+        assert_eq!(nodes[2].name, "📄 file1.txt");
+        assert_eq!(nodes[3].name, "📁 subdir");
+
+        // Check connection parameters
+        // Current directory points to parent
+        assert_eq!(nodes[1].parameters.len(), 1);
+        assert_eq!(nodes[1].parameters[0].0, "input");
+        assert_eq!(nodes[1].parameters[0].1, "📁 .. (user)");
+
+        // Children point to current directory
+        assert_eq!(nodes[2].parameters.len(), 1);
+        assert_eq!(nodes[2].parameters[0].0, "input");
+        assert_eq!(nodes[2].parameters[0].1, "📁 project");
+
+        assert_eq!(nodes[3].parameters.len(), 1);
+        assert_eq!(nodes[3].parameters[0].0, "input");
+        assert_eq!(nodes[3].parameters[0].1, "📁 project");
+    }
+
+    #[test]
+    fn test_populate_graph_root_no_parent() {
+        let mut state = NetworkState::default();
+        let current_dir = Path::new("/");
+        let entries = vec![
+            DirEntry {
+                name: "bin".to_string(),
+                path: PathBuf::from("/bin"),
+                is_dir: true,
+                size: 4096,
+                permissions: 0o755,
+                modified: String::new(),
+            },
+        ];
+
+        state.populate_graph(current_dir, &entries);
+        let nodes = state.graph.get_nodes();
+
+        // No parent, so 1 current + 1 child = 2 nodes
+        assert_eq!(nodes.len(), 2);
+        assert_eq!(nodes[0].name, "📁 /");
+        assert_eq!(nodes[1].name, "📁 bin");
+
+        // Current has no parent parameter
+        assert!(nodes[0].parameters.is_empty());
+
+        // Child points to current
+        assert_eq!(nodes[1].parameters.len(), 1);
+        assert_eq!(nodes[1].parameters[0].0, "input");
+        assert_eq!(nodes[1].parameters[0].1, "📁 /");
+    }
+}
diff --git a/src/pages/preview.rs b/src/pages/preview.rs
index 7c0b034..877cf3e 100644
--- a/src/pages/preview.rs
+++ b/src/pages/preview.rs
@@ -17,12 +17,12 @@ pub struct PreviewState {
     pub modified: String,
     pub file_type: String,
     pub target: String, // for symlinks
+    pub content_preview: Option<String>,
 }
 
 #[derive(Debug, Clone)]
 pub enum PreviewMessage {
     SetPath { path: PathBuf },
-    NavigateTo(PathBuf),
 }
 
 // ── Helpers ─────────────────────────────────────────────────────────
@@ -90,29 +90,62 @@ fn infer_file_type(name: &str, is_dir: bool) -> String {
 
 // ── View ────────────────────────────────────────────────────────────
 
-pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
     let mut pc = PageContent::new();
     let text_fg = [0.83, 0.83, 0.83, 1.0];
     let text_dim = [0.53, 0.53, 0.60, 1.0];
     let label_fg = [0.56, 0.83, 0.56, 1.0];
-    let accent_bg = [0.36, 0.56, 0.38, 1.0];
 
     if state.path.is_none() {
         pc.text("Select a file to view details", cx + 12.0, cy + 12.0, 13.0, text_dim);
         return pc;
     }
 
+    let half_h = ch * 0.5;
+
+    // 1. Top pane: File Preview
+    pc.text("Preview", cx + 12.0, cy + 12.0, 14.0, label_fg);
+
+    let bg_color = [0.07, 0.11, 0.08, 0.5];
+    pc.rect(bg_color, cx + 12.0, cy + 32.0, cw - 24.0, half_h - 40.0);
+
+    if let Some(content) = &state.content_preview {
+        let mut text_y = cy + 44.0;
+        for line in content.lines() {
+            if text_y + 14.0 > cy + half_h - 16.0 {
+                break;
+            }
+            let line_truncated = if line.chars().count() > 45 {
+                let mut s: String = line.chars().take(42).collect();
+                s.push_str("...");
+                s
+            } else {
+                line.to_string()
+            };
+            pc.text(&line_truncated, cx + 20.0, text_y, 11.0, text_fg);
+            text_y += 15.0;
+        }
+    } else {
+        pc.text("No preview available", cx + 20.0, cy + 44.0, 11.0, text_dim);
+    }
+
+    // 2. Bottom pane: Details
+    let bottom_y = cy + half_h + 12.0;
+
+    // Pane divider
+    pc.rect([0.15, 0.20, 0.16, 1.0], cx + 12.0, bottom_y - 6.0, cw - 24.0, 1.0);
+
     let icon = if state.is_dir { "📁" } else { "📄" };
 
-    // Title row
-    pc.text(icon, cx + 12.0, cy + 12.0, 24.0, text_fg);
+    // Details header
+    pc.text(icon, cx + 12.0, bottom_y + 6.0, 20.0, text_fg);
     
     let name_truncated = if state.name.len() > 30 {
         format!("{}...", &state.name[..27])
     } else {
         state.name.clone()
     };
-    pc.text(&name_truncated, cx + 46.0, cy + 18.0, 18.0, text_fg);
+    pc.text(&name_truncated, cx + 42.0, bottom_y + 10.0, 16.0, text_fg);
 
     // Metadata details
     let details = [
@@ -123,7 +156,7 @@ pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
         ("Modified", &state.modified),
     ];
 
-    let mut y = cy + 54.0;
+    let mut y = bottom_y + 36.0;
     for (label, val) in &details {
         pc.text(label, cx + 12.0, y, 12.0, label_fg);
         
@@ -138,9 +171,6 @@ pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
 
     if !state.target.is_empty() {
         y += 8.0;
-        // Divider
-        pc.rect([0.15, 0.20, 0.16, 1.0], cx + 12.0, y, cw - 24.0, 1.0);
-        y += 12.0;
         pc.text("Target", cx + 12.0, y, 12.0, label_fg);
         
         let target_str = if state.target.len() > 40 {
@@ -149,24 +179,6 @@ pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
             state.target.clone()
         };
         pc.text(&target_str, cx + 112.0, y, 12.0, text_dim);
-        y += 20.0;
-    }
-
-    if state.is_dir {
-        if let Some(path) = &state.path {
-            y += 16.0;
-            pc.button(
-                "Open directory",
-                cx + 12.0,
-                y,
-                140.0,
-                32.0,
-                accent_bg,
-                [0.46, 0.66, 0.48, 1.0],
-                [0.10, 0.16, 0.11, 1.0],
-                crate::Message::Preview(PreviewMessage::NavigateTo(path.clone())),
-            );
-        }
     }
 
     pc
@@ -212,6 +224,43 @@ pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
 
             let path_display = path.to_string_lossy().to_string();
 
+            // Load content preview
+            let content_preview = if is_dir {
+                if let Ok(entries) = fs::read_dir(&path) {
+                    let mut names = Vec::new();
+                    for entry in entries.flatten().take(15) {
+                        let name = entry.file_name().to_string_lossy().to_string();
+                        let is_sub_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
+                        let icon = if is_sub_dir { "📁" } else { "📄" };
+                        names.push(format!("{} {}", icon, name));
+                    }
+                    if names.is_empty() {
+                        Some("[Empty directory]".to_string())
+                    } else {
+                        Some(names.join("\n"))
+                    }
+                } else {
+                    None
+                }
+            } else if let Ok(mut file) = fs::File::open(&path) {
+                use std::io::Read;
+                let mut buf = vec![0u8; 1024];
+                if let Ok(n) = file.read(&mut buf) {
+                    buf.truncate(n);
+                    if let Ok(utf8_str) = String::from_utf8(buf) {
+                        let lines: Vec<&str> = utf8_str.lines().take(15).collect();
+                        let preview_text = lines.join("\n");
+                        Some(preview_text)
+                    } else {
+                        Some("[Binary file content]".to_string())
+                    }
+                } else {
+                    None
+                }
+            } else {
+                None
+            };
+
             *state = PreviewState {
                 path: Some(path),
                 path_display,
@@ -222,8 +271,8 @@ pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
                 modified,
                 file_type,
                 target,
+                content_preview,
             };
         }
-        PreviewMessage::NavigateTo(_) => {}
     }
 }
diff --git a/src/pages/keybindings.rs b/src/pages/settings.rs
similarity index 53%
rename from src/pages/keybindings.rs
rename to src/pages/settings.rs
index 348ca4f..f2ccf9c 100644
--- a/src/pages/keybindings.rs
+++ b/src/pages/settings.rs
@@ -1,28 +1,42 @@
 use crate::pages::PageContent;
+use clear_ui::widget::{ColorSelector, Widget};
+use clear_ui::layout::Section;
 
 // ── Data ────────────────────────────────────────────────────────────
 
-#[derive(Debug, Clone, Default)]
-pub struct KeybindingsState;
+pub struct SettingsState {
+    pub color_selector: ColorSelector,
+}
 
-#[derive(Debug, Clone)]
-pub enum KeybindingsMessage {}
+impl Default for SettingsState {
+    fn default() -> Self {
+        let col = clear_ui::color::node_color();
+        let col_u8 = [
+            (col[0] * 255.0) as u8,
+            (col[1] * 255.0) as u8,
+            (col[2] * 255.0) as u8,
+        ];
+        Self {
+            color_selector: ColorSelector::new(col_u8).with_label("Node Color"),
+        }
+    }
+}
 
-// ── Static data ────────────────────────────────────────────────────
+// ── Keybindings Structs & Data ──────────────────────────────────────
 
 struct Binding {
     keys: &'static str,
     action: &'static str,
 }
 
-struct Section {
+struct SectionData {
     title: &'static str,
     icon: &'static str,
     bindings: &'static [Binding],
 }
 
-const SECTIONS: &[Section] = &[
-    Section {
+const SECTIONS: &[SectionData] = &[
+    SectionData {
         title: "Navigation",
         icon: "🧭",
         bindings: &[
@@ -34,7 +48,7 @@ const SECTIONS: &[Section] = &[
             Binding { keys: "Ctrl + F", action: "Focus search" },
         ],
     },
-    Section {
+    SectionData {
         title: "Browse",
         icon: "📁",
         bindings: &[
@@ -43,7 +57,7 @@ const SECTIONS: &[Section] = &[
             Binding { keys: "Search", action: "Filter files by name" },
         ],
     },
-    Section {
+    SectionData {
         title: "Info",
         icon: "ℹ",
         bindings: &[
@@ -51,48 +65,58 @@ const SECTIONS: &[Section] = &[
             Binding { keys: "Open dir button", action: "Navigate into selected directory" },
         ],
     },
-    Section {
+    SectionData {
         title: "General",
         icon: "⚡",
         bindings: &[
-            Binding { keys: "Ctrl + 1-2", action: "Switch page" },
+            Binding { keys: "Ctrl + 1-3", action: "Switch page" },
         ],
     },
 ];
 
 // ── View ────────────────────────────────────────────────────────────
 
-pub fn view(_state: &KeybindingsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut SettingsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
     let mut pc = PageContent::new();
     let accent = [0.36, 0.56, 0.38, 1.0];
     let text_fg = [0.83, 0.83, 0.83, 1.0];
     let text_dim = [0.53, 0.53, 0.60, 1.0];
 
-    pc.text("Keybindings", cx + 12.0, cy + 12.0, 18.0, accent);
+    pc.text("Settings", cx + 12.0, cy + 12.0, 18.0, accent);
+
+    let cs_w = (cw - 24.0).min(300.0);
+    let cs_h = 24.0;
+    let cs_y = cy + 48.0 + state.color_selector.top_room();
+
+    state.color_selector.set_row_rect(cx + 12.0, cs_w);
+    clear_ui::layout::render_widget(&mut pc, &mut state.color_selector, cx + 12.0, cs_y, cs_w, cs_h);
 
-    let mut y = cy + 42.0;
+    let y = cs_y + cs_h + 32.0;
 
-    for section in SECTIONS {
+    // Keyboard Shortcuts Section
+    let mut sec_keys = Section::new(&mut pc, cx, y, cw, "Keyboard Shortcuts");
+    sec_keys.spacing(8.0);
+
+    for (i, section) in SECTIONS.iter().enumerate() {
         // Section Header
         let header_str = format!("{} {}", section.icon, section.title);
-        pc.text(&header_str, cx + 12.0, y, 14.0, text_fg);
-        y += 22.0;
+        sec_keys.text(&mut pc, &header_str, 12.0, 0.0, 13.0, text_fg);
+        sec_keys.spacing(20.0);
 
         for b in section.bindings {
-            pc.text(b.keys, cx + 12.0, y, 12.0, accent);
-            pc.text(b.action, cx + 160.0, y, 12.0, text_dim);
-            y += 18.0;
+            sec_keys.text(&mut pc, b.keys, 12.0, 0.0, 11.0, accent);
+            sec_keys.text(&mut pc, b.action, 160.0, 0.0, 11.0, text_dim);
+            sec_keys.spacing(16.0);
         }
 
-        y += 8.0;
-        // Divider
-        pc.rect([0.15, 0.20, 0.16, 1.0], cx + 12.0, y, cw - 24.0, 1.0);
-        y += 12.0;
+        if i < SECTIONS.len() - 1 {
+            sec_keys.spacing(8.0);
+            sec_keys.separator(&mut pc);
+            sec_keys.spacing(12.0);
+        }
     }
 
+    sec_keys.finish(&mut pc);
+
     pc
 }
-
-// ── Update ──────────────────────────────────────────────────────────
-
-pub fn update(_state: &mut KeybindingsState, _msg: KeybindingsMessage) {}