⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/
file_manager.rs

1//! GUI File Manager Application
2//!
3//! Provides graphical file browsing and management using the window manager and
4//! VFS.
5
6use alloc::{format, string::String, vec, vec::Vec};
7
8use spin::RwLock;
9
10use crate::{
11    desktop::window_manager::{with_window_manager, InputEvent, WindowId},
12    error::KernelError,
13    fs::{get_vfs, NodeType},
14    sync::once_lock::GlobalState,
15};
16
17/// File entry in the browser
18#[derive(Debug, Clone)]
19struct FileEntry {
20    name: String,
21    node_type: NodeType,
22    #[allow(dead_code)] // Set during directory scan; displayed when file details view is added
23    size: usize,
24    #[allow(dead_code)] // Set via UI interaction; used in multi-select operations (future)
25    selected: bool,
26}
27
28/// File manager state
29pub struct FileManager {
30    /// Window ID
31    window_id: WindowId,
32
33    /// Compositor surface ID
34    surface_id: u32,
35    /// SHM pool ID
36    pool_id: u32,
37    /// Pool buffer ID
38    pool_buf_id: u32,
39
40    /// Current directory path
41    current_path: String,
42
43    /// File entries in current directory
44    entries: Vec<FileEntry>,
45
46    /// Selected entry index
47    selected_index: usize,
48
49    /// Scroll offset
50    scroll_offset: usize,
51
52    /// Window dimensions
53    width: u32,
54    height: u32,
55}
56
57impl FileManager {
58    /// Create a new file manager
59    pub fn new() -> Result<Self, KernelError> {
60        let width = 640;
61        let height = 480;
62
63        // WM window includes title bar (28px) above content area
64        let title_bar_h = 28u32;
65        let window_id =
66            with_window_manager(|wm| wm.create_window(200, 100, width, height + title_bar_h, 0))
67                .ok_or(KernelError::InvalidState {
68                expected: "initialized",
69                actual: "uninitialized",
70            })??;
71
72        // Compositor surface covers the full window area (title bar + content)
73        let (surface_id, pool_id, pool_buf_id) =
74            super::renderer::create_app_surface(200, 100, width, height + title_bar_h);
75
76        let mut fm = Self {
77            window_id,
78            surface_id,
79            pool_id,
80            pool_buf_id,
81            current_path: String::from("/"),
82            entries: Vec::new(),
83            selected_index: 0,
84            scroll_offset: 0,
85            width,
86            height,
87        };
88
89        // Load initial directory
90        fm.refresh_directory()?;
91
92        println!("[FILE-MANAGER] Created file manager window {}", window_id);
93
94        Ok(fm)
95    }
96
97    /// Refresh directory listing
98    pub fn refresh_directory(&mut self) -> Result<(), KernelError> {
99        println!("[FILE-MANAGER] Refreshing directory: {}", self.current_path);
100
101        self.entries.clear();
102
103        // Get VFS
104        let vfs = get_vfs();
105
106        // Open directory
107        match vfs
108            .read()
109            .open(&self.current_path, crate::fs::file::OpenFlags::read_only())
110        {
111            Ok(dir_node) => {
112                // List directory contents
113                match dir_node.readdir() {
114                    Ok(entries) => {
115                        for entry in entries {
116                            self.entries.push(FileEntry {
117                                name: entry.name,
118                                node_type: entry.node_type,
119                                size: 0, // Size not available in DirEntry
120                                selected: false,
121                            });
122                        }
123                    }
124                    Err(_) => {
125                        println!("[FILE-MANAGER] Failed to read directory");
126                    }
127                }
128            }
129            Err(_) => {
130                println!("[FILE-MANAGER] Failed to open directory");
131            }
132        }
133
134        // Sort entries: directories first, then files
135        self.entries
136            .sort_by(|a, b| match (a.node_type, b.node_type) {
137                (NodeType::Directory, NodeType::File) => core::cmp::Ordering::Less,
138                (NodeType::File, NodeType::Directory) => core::cmp::Ordering::Greater,
139                _ => a.name.cmp(&b.name),
140            });
141
142        // Insert ".." parent directory entry at the top for non-root dirs
143        if self.current_path != "/" {
144            self.entries.insert(
145                0,
146                FileEntry {
147                    name: String::from(".."),
148                    node_type: NodeType::Directory,
149                    size: 0,
150                    selected: false,
151                },
152            );
153        }
154
155        println!("[FILE-MANAGER] Loaded {} entries", self.entries.len());
156
157        Ok(())
158    }
159
160    /// Process input event
161    pub fn process_input(&mut self, event: InputEvent) -> Result<(), KernelError> {
162        match event {
163            InputEvent::KeyPress {
164                character,
165                scancode,
166            } => {
167                match character {
168                    '\n' | '\r' => {
169                        // Enter - open selected entry
170                        self.open_selected()?;
171                    }
172                    'j' | 'J' => {
173                        // Down
174                        if self.selected_index < self.entries.len().saturating_sub(1) {
175                            self.selected_index += 1;
176                        }
177                    }
178                    'k' | 'K' => {
179                        // Up
180                        if self.selected_index > 0 {
181                            self.selected_index -= 1;
182                        }
183                    }
184                    'h' | 'H' => {
185                        // Back / parent directory
186                        self.navigate_parent()?;
187                    }
188                    'r' | 'R' => {
189                        // Refresh
190                        self.refresh_directory()?;
191                    }
192                    _ => {
193                        // Arrow keys in GUI mode (single-byte 0x80+ codes)
194                        match scancode {
195                            0x80 => {
196                                // KEY_UP
197                                if self.selected_index > 0 {
198                                    self.selected_index -= 1;
199                                }
200                            }
201                            0x81 => {
202                                // KEY_DOWN
203                                if self.selected_index < self.entries.len().saturating_sub(1) {
204                                    self.selected_index += 1;
205                                }
206                            }
207                            0x82 => {
208                                // KEY_LEFT - parent directory
209                                self.navigate_parent()?;
210                            }
211                            0x83 => {
212                                // KEY_RIGHT - open selected
213                                self.open_selected()?;
214                            }
215                            _ => {}
216                        }
217                    }
218                }
219            }
220            InputEvent::MouseButton {
221                button: 0,
222                pressed: true,
223                x,
224                y,
225            } => {
226                // Left click - select item
227                self.handle_click(x, y)?;
228            }
229            _ => {}
230        }
231
232        Ok(())
233    }
234
235    /// Handle mouse click
236    fn handle_click(&mut self, _x: i32, y: i32) -> Result<(), KernelError> {
237        // Calculate which entry was clicked
238        let line_height = 20;
239        let header_height = 40;
240
241        if y > header_height {
242            let entry_index = ((y - header_height) / line_height) as usize + self.scroll_offset;
243            if entry_index < self.entries.len() {
244                self.selected_index = entry_index;
245            }
246        }
247
248        Ok(())
249    }
250
251    /// Open selected entry
252    fn open_selected(&mut self) -> Result<(), KernelError> {
253        if self.selected_index >= self.entries.len() {
254            return Ok(());
255        }
256
257        let entry = &self.entries[self.selected_index];
258
259        match entry.node_type {
260            NodeType::Directory => {
261                // Handle ".." parent directory
262                if entry.name == ".." {
263                    return self.navigate_parent();
264                }
265                // Navigate into directory
266                if self.current_path == "/" {
267                    self.current_path = format!("/{}", entry.name);
268                } else {
269                    self.current_path = format!("{}/{}", self.current_path, entry.name);
270                }
271                self.selected_index = 0;
272                self.scroll_offset = 0;
273                self.refresh_directory()?;
274            }
275            NodeType::File => {
276                // Build full file path
277                let file_path = if self.current_path == "/" {
278                    format!("/{}", entry.name)
279                } else {
280                    format!("{}/{}", self.current_path, entry.name)
281                };
282
283                // Read file header bytes for magic-based MIME detection
284                let header_bytes = crate::fs::read_file(&file_path).ok().map(|data| {
285                    let len = core::cmp::min(data.len(), 512);
286                    data[..len].to_vec()
287                });
288
289                // Detect MIME type via extension + magic bytes
290                let mime = crate::desktop::mime::MimeDatabase::detect_mime(
291                    &entry.name,
292                    header_bytes.as_deref(),
293                );
294
295                // Look up the associated application
296                let db = crate::desktop::mime::MimeDatabase::new();
297                if let Some(assoc) = db.open_with(&mime) {
298                    let mime_str = crate::desktop::mime::MimeDatabase::mime_to_str(&mime);
299                    println!(
300                        "[FILE-MANAGER] Opening '{}' ({}) with {} ({})",
301                        entry.name, mime_str, assoc.app_name, assoc.app_exec
302                    );
303
304                    // Attempt to launch the associated application with the
305                    // file path as argument. This uses the same load+exec
306                    // infrastructure as the shell's external command execution.
307                    //
308                    // Check if the executable exists first (read guard is
309                    // dropped after resolve_path returns).
310                    let app_exists = crate::fs::get_vfs()
311                        .read()
312                        .resolve_path(&assoc.app_exec)
313                        .is_ok();
314
315                    if app_exists {
316                        match crate::userspace::load_user_program(
317                            &assoc.app_exec,
318                            &[&assoc.app_exec, &file_path],
319                            &[],
320                        ) {
321                            Ok(pid) => {
322                                println!(
323                                    "[FILE-MANAGER] Launched {} (PID {}) for '{}'",
324                                    assoc.app_name, pid.0, entry.name
325                                );
326                            }
327                            Err(e) => {
328                                println!(
329                                    "[FILE-MANAGER] Failed to launch {}: {:?}",
330                                    assoc.app_exec, e
331                                );
332                            }
333                        }
334                    } else {
335                        println!(
336                            "[FILE-MANAGER] Application '{}' not found at '{}'",
337                            assoc.app_name, assoc.app_exec
338                        );
339                    }
340                } else {
341                    println!(
342                        "[FILE-MANAGER] No application associated with '{}'",
343                        entry.name
344                    );
345                }
346            }
347            _ => {}
348        }
349
350        Ok(())
351    }
352
353    /// Navigate to parent directory
354    fn navigate_parent(&mut self) -> Result<(), KernelError> {
355        if self.current_path == "/" {
356            return Ok(()); // Already at root
357        }
358
359        // Find last '/' and truncate
360        if let Some(pos) = self.current_path.rfind('/') {
361            if pos == 0 {
362                self.current_path = String::from("/");
363            } else {
364                self.current_path.truncate(pos);
365            }
366            self.selected_index = 0;
367            self.scroll_offset = 0;
368            self.refresh_directory()?;
369        }
370
371        Ok(())
372    }
373
374    /// Render file manager to a BGRA pixel buffer.
375    ///
376    /// `buf` is width*height*4 bytes in BGRA format.
377    pub fn render(&self, buf: &mut [u8], width: usize, _height: usize) -> Result<(), KernelError> {
378        use super::renderer::draw_char_into_buffer;
379
380        // Clear to dark gray background (BGRA: 0x2A2A2A)
381        for chunk in buf.chunks_exact_mut(4) {
382            chunk[0] = 0x2A; // B
383            chunk[1] = 0x2A; // G
384            chunk[2] = 0x2A; // R
385            chunk[3] = 0xFF; // A
386        }
387
388        // Draw header: current path
389        let header = self.current_path.as_bytes();
390        let prefix = b"Path: ";
391        for (i, &ch) in prefix.iter().chain(header.iter()).enumerate() {
392            draw_char_into_buffer(buf, width, ch, 8 + i * 8, 6, 0xDDDDDD);
393        }
394
395        // Draw separator line at y=24
396        for x in 0..width {
397            let offset = (24 * width + x) * 4;
398            if offset + 3 < buf.len() {
399                buf[offset] = 0x55;
400                buf[offset + 1] = 0x55;
401                buf[offset + 2] = 0x55;
402                buf[offset + 3] = 0xFF;
403            }
404        }
405
406        // Draw entries
407        let line_height = 18;
408        let start_y = 28;
409
410        for (i, entry) in self.entries.iter().enumerate().skip(self.scroll_offset) {
411            let row = i - self.scroll_offset;
412            let y = start_y + row * line_height;
413
414            // Highlight selected row
415            if i == self.selected_index {
416                for dy in 0..line_height {
417                    for x in 0..width {
418                        let offset = ((y + dy) * width + x) * 4;
419                        if offset + 3 < buf.len() {
420                            buf[offset] = 0x50;
421                            buf[offset + 1] = 0x40;
422                            buf[offset + 2] = 0x30;
423                            buf[offset + 3] = 0xFF;
424                        }
425                    }
426                }
427            }
428
429            // Draw prefix [DIR] or [FILE]
430            let prefix: &[u8] = match entry.node_type {
431                NodeType::Directory => b"[DIR]  ",
432                NodeType::File => b"[FILE] ",
433                _ => b"[?]    ",
434            };
435
436            let (text_color, prefix_color) = match entry.node_type {
437                NodeType::Directory => (0x55AAFF_u32, 0x55AAFF_u32),
438                _ => (0xCCCCCC, 0x888888),
439            };
440
441            // Draw prefix
442            for (j, &ch) in prefix.iter().enumerate() {
443                draw_char_into_buffer(buf, width, ch, 8 + j * 8, y + 1, prefix_color);
444            }
445
446            // Draw entry name
447            let name_x = 8 + prefix.len() * 8;
448            for (j, &ch) in entry.name.as_bytes().iter().enumerate() {
449                draw_char_into_buffer(buf, width, ch, name_x + j * 8, y + 1, text_color);
450            }
451        }
452
453        Ok(())
454    }
455
456    /// Get window ID
457    pub fn window_id(&self) -> WindowId {
458        self.window_id
459    }
460
461    /// Get compositor surface ID
462    pub fn surface_id(&self) -> u32 {
463        self.surface_id
464    }
465
466    /// Render file manager contents to its compositor surface.
467    ///
468    /// The surface includes a 28px title bar; content is at y-offset 28.
469    pub fn render_to_surface(&self) {
470        let w = self.width as usize;
471        let content_h = self.height as usize;
472        let title_bar_h: usize = 28;
473        let total_h = content_h + title_bar_h;
474        let mut pixels = vec![0u8; w * total_h * 4];
475
476        let mut content = vec![0u8; w * content_h * 4];
477        let _ = self.render(&mut content, w, content_h);
478        for y in 0..content_h {
479            let src_off = y * w * 4;
480            let dst_off = (y + title_bar_h) * w * 4;
481            pixels[dst_off..dst_off + w * 4].copy_from_slice(&content[src_off..src_off + w * 4]);
482        }
483
484        super::renderer::draw_title_bar_into_surface(&mut pixels, w, total_h, self.window_id);
485
486        super::renderer::update_surface_pixels(
487            self.surface_id,
488            self.pool_id,
489            self.pool_buf_id,
490            &pixels,
491        );
492    }
493}
494
495/// Global file manager (can support multiple instances)
496static FILE_MANAGER: GlobalState<RwLock<FileManager>> = GlobalState::new();
497
498/// Initialize file manager
499pub fn init() -> Result<(), KernelError> {
500    println!("[FILE-MANAGER] File manager initialized");
501    Ok(())
502}
503
504/// Create a new file manager instance
505pub fn create_file_manager() -> Result<(), KernelError> {
506    let fm = FileManager::new()?;
507    FILE_MANAGER
508        .init(RwLock::new(fm))
509        .map_err(|_| KernelError::InvalidState {
510            expected: "uninitialized",
511            actual: "initialized",
512        })?;
513    Ok(())
514}
515
516/// Execute a function with the file manager
517pub fn with_file_manager<R, F: FnOnce(&RwLock<FileManager>) -> R>(f: F) -> Option<R> {
518    FILE_MANAGER.with(f)
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_file_entry_creation() {
527        let entry = FileEntry {
528            name: String::from("test.txt"),
529            node_type: NodeType::File,
530            size: 1024,
531            selected: false,
532        };
533
534        assert_eq!(entry.name, "test.txt");
535        assert_eq!(entry.size, 1024);
536    }
537}