lk_inside/ui/components/
file_browser.rs

1//! Provides a file browsing component for navigating the file system within the TUI.
2
3use std::{
4    fs,
5    path::PathBuf,
6};
7
8use ratatui::{
9    widgets::ListState,
10};
11
12use crossterm::event::KeyCode;
13
14// NEW: Define FileBrowserEvent enum
15pub enum FileBrowserEvent {
16    FileSelected(PathBuf),
17    DirectoryEntered,
18    NavigationUp,
19    NoChange,
20}
21
22/// A TUI component for browsing the file system.
23///
24/// Allows navigation through directories, selection of files, and displaying directory contents.
25pub struct FileBrowser {
26    /// The current directory being displayed.
27    pub current_path: PathBuf,
28    /// A list of files and directories in the `current_path`.
29    pub entries: Vec<PathBuf>,
30    /// The state of the `List` widget used to render the entries.
31    pub state: ListState,
32    /// A message to display the status of the file browser (e.g., "Empty directory").
33    status_message: String,
34}
35
36impl FileBrowser {
37    /// Creates a new `FileBrowser` instance, initialized to the current working directory.
38    ///
39    /// It automatically lists the entries in the initial directory.
40    pub fn new() -> Self {
41        let mut browser = FileBrowser {
42            current_path: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
43            entries: Vec::new(),
44            state: ListState::default(),
45            status_message: String::new(),
46        };
47        browser.list_entries();
48        browser
49    }
50
51    /// Lists the contents of the `current_path`, populating `self.entries`.
52    ///
53    /// Directories are listed before files, and both are sorted alphabetically.
54    /// A ".." entry is added if navigation upwards is possible.
55    pub fn list_entries(&mut self) {
56        self.entries.clear();
57        self.status_message.clear();
58
59        if let Ok(read_dir) = fs::read_dir(&self.current_path) {
60            let mut dirs = Vec::new();
61            let mut files = Vec::new();
62
63            for entry in read_dir {
64                if let Ok(entry) = entry {
65                    let path = entry.path();
66                    if path.is_dir() {
67                        dirs.push(path);
68                    } else {
69                        files.push(path);
70                    }
71                }
72            }
73
74            dirs.sort();
75            files.sort();
76
77            if self.current_path.parent().is_some() {
78                self.entries.push(PathBuf::from(".."));
79            }
80
81            self.entries.extend(dirs);
82            self.entries.extend(files);
83
84            if !self.entries.is_empty() {
85                self.state.select(Some(0));
86            } else {
87                self.status_message = "Current directory is empty.".to_string();
88                self.state.select(None);
89            }
90        } else {
91            self.status_message = format!("Could not read directory: {}", self.current_path.display());
92            self.state.select(None);
93        }
94    }
95
96    /// Navigates the browser one level up in the directory hierarchy.
97    ///
98    /// # Returns
99    ///
100    /// `FileBrowserEvent` indicating if navigation occurred or not.
101    pub fn navigate_up(&mut self) -> FileBrowserEvent { // Changed return type
102        if let Some(parent) = self.current_path.parent() {
103            self.current_path = parent.to_path_buf();
104            self.list_entries();
105            FileBrowserEvent::NavigationUp // Return new event type
106        } else {
107            FileBrowserEvent::NoChange
108        }
109    }
110
111    /// Handles the selection of the currently highlighted entry.
112    ///
113    /// If a directory is selected (including ".."), it navigates into it.
114    /// If a file is selected, it returns the `PathBuf` of that file.
115    ///
116    /// # Returns
117    ///
118    /// `FileBrowserEvent` indicating the outcome of the selection.
119    pub fn select_entry(&mut self) -> FileBrowserEvent { // Changed return type
120        if let Some(selected) = self.state.selected() {
121            if selected < self.entries.len() {
122                let path = self.entries[selected].clone();
123                if path.file_name().map_or(false, |name| name == "..") {
124                    let _ = self.navigate_up(); // Call navigate_up, ignore its event as we're explicitly returning one
125                    FileBrowserEvent::NavigationUp // Explicitly return event
126                } else if path.is_dir() {
127                    self.current_path = path;
128                    self.list_entries();
129                    FileBrowserEvent::DirectoryEntered // Return event
130                } else {
131                    FileBrowserEvent::FileSelected(path) // Return event
132                }
133            } else {
134                FileBrowserEvent::NoChange
135            }
136        } else {
137            FileBrowserEvent::NoChange
138        }
139    }
140
141    /// Returns the `PathBuf` of the currently selected entry without performing any action.
142    ///
143    /// # Returns
144    ///
145    /// `Some(PathBuf)` of the selected entry, or `None` if no entry is selected.
146    pub fn selected_path(&self) -> Option<PathBuf> {
147        self.state.selected().and_then(|i| self.entries.get(i).cloned())
148    }
149
150    /// Handles key events for navigating and interacting with the file browser.
151    ///
152    /// # Arguments
153    ///
154    /// * `key` - The `KeyCode` representing the pressed key.
155    ///
156    /// # Returns
157    ///
158    /// `FileBrowserEvent` indicating the outcome of the key press.
159    pub fn handle_key(&mut self, key: KeyCode) -> FileBrowserEvent { // Changed return type
160        let event = match key { // Store the event to return it
161            KeyCode::Up => {
162                if let Some(selected) = self.state.selected() {
163                    if selected > 0 {
164                        self.state.select(Some(selected - 1));
165                    } else {
166                        self.state.select(Some(self.entries.len() - 1));
167                    }
168                }
169                FileBrowserEvent::NoChange // Movement doesn't trigger a specific event beyond redraw
170            }
171            KeyCode::Down => {
172                if let Some(selected) = self.state.selected() {
173                    if selected < self.entries.len() - 1 {
174                        self.state.select(Some(selected + 1));
175                    } else {
176                        self.state.select(Some(0));
177                    }
178                } else if !self.entries.is_empty() {
179                    self.state.select(Some(0));
180                }
181                FileBrowserEvent::NoChange // Movement doesn't trigger a specific event beyond redraw
182            }
183            KeyCode::Left => {
184                self.navigate_up() // This will now return an event
185            }
186            KeyCode::Right => {
187                self.select_entry() // This will now return an event
188            }
189            _ => FileBrowserEvent::NoChange,
190        };
191        event // Return the event
192    }
193
194    pub fn get_status_message(&self) -> String { // ADDED
195        self.status_message.clone()
196    }
197}