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}