1use 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#[derive(Debug, Clone)]
19struct FileEntry {
20 name: String,
21 node_type: NodeType,
22 #[allow(dead_code)] size: usize,
24 #[allow(dead_code)] selected: bool,
26}
27
28pub struct FileManager {
30 window_id: WindowId,
32
33 surface_id: u32,
35 pool_id: u32,
37 pool_buf_id: u32,
39
40 current_path: String,
42
43 entries: Vec<FileEntry>,
45
46 selected_index: usize,
48
49 scroll_offset: usize,
51
52 width: u32,
54 height: u32,
55}
56
57impl FileManager {
58 pub fn new() -> Result<Self, KernelError> {
60 let width = 640;
61 let height = 480;
62
63 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 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 fm.refresh_directory()?;
91
92 println!("[FILE-MANAGER] Created file manager window {}", window_id);
93
94 Ok(fm)
95 }
96
97 pub fn refresh_directory(&mut self) -> Result<(), KernelError> {
99 println!("[FILE-MANAGER] Refreshing directory: {}", self.current_path);
100
101 self.entries.clear();
102
103 let vfs = get_vfs();
105
106 match vfs
108 .read()
109 .open(&self.current_path, crate::fs::file::OpenFlags::read_only())
110 {
111 Ok(dir_node) => {
112 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, 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 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 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 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 self.open_selected()?;
171 }
172 'j' | 'J' => {
173 if self.selected_index < self.entries.len().saturating_sub(1) {
175 self.selected_index += 1;
176 }
177 }
178 'k' | 'K' => {
179 if self.selected_index > 0 {
181 self.selected_index -= 1;
182 }
183 }
184 'h' | 'H' => {
185 self.navigate_parent()?;
187 }
188 'r' | 'R' => {
189 self.refresh_directory()?;
191 }
192 _ => {
193 match scancode {
195 0x80 => {
196 if self.selected_index > 0 {
198 self.selected_index -= 1;
199 }
200 }
201 0x81 => {
202 if self.selected_index < self.entries.len().saturating_sub(1) {
204 self.selected_index += 1;
205 }
206 }
207 0x82 => {
208 self.navigate_parent()?;
210 }
211 0x83 => {
212 self.open_selected()?;
214 }
215 _ => {}
216 }
217 }
218 }
219 }
220 InputEvent::MouseButton {
221 button: 0,
222 pressed: true,
223 x,
224 y,
225 } => {
226 self.handle_click(x, y)?;
228 }
229 _ => {}
230 }
231
232 Ok(())
233 }
234
235 fn handle_click(&mut self, _x: i32, y: i32) -> Result<(), KernelError> {
237 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 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 if entry.name == ".." {
263 return self.navigate_parent();
264 }
265 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 let file_path = if self.current_path == "/" {
278 format!("/{}", entry.name)
279 } else {
280 format!("{}/{}", self.current_path, entry.name)
281 };
282
283 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 let mime = crate::desktop::mime::MimeDatabase::detect_mime(
291 &entry.name,
292 header_bytes.as_deref(),
293 );
294
295 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 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 fn navigate_parent(&mut self) -> Result<(), KernelError> {
355 if self.current_path == "/" {
356 return Ok(()); }
358
359 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 pub fn render(&self, buf: &mut [u8], width: usize, _height: usize) -> Result<(), KernelError> {
378 use super::renderer::draw_char_into_buffer;
379
380 for chunk in buf.chunks_exact_mut(4) {
382 chunk[0] = 0x2A; chunk[1] = 0x2A; chunk[2] = 0x2A; chunk[3] = 0xFF; }
387
388 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 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 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 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 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 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 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 pub fn window_id(&self) -> WindowId {
458 self.window_id
459 }
460
461 pub fn surface_id(&self) -> u32 {
463 self.surface_id
464 }
465
466 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
495static FILE_MANAGER: GlobalState<RwLock<FileManager>> = GlobalState::new();
497
498pub fn init() -> Result<(), KernelError> {
500 println!("[FILE-MANAGER] File manager initialized");
501 Ok(())
502}
503
504pub 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
516pub 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}