1#![allow(dead_code)]
8
9extern crate alloc;
10use alloc::boxed::Box;
11use alloc::string::String;
12use alloc::string::ToString;
13use alloc::vec;
14use alloc::vec::Vec;
15
16use crate::apps::browser::WebBrowser;
17use crate::kernel::config;
18use crate::kernel::draw::{Color, Screen};
19use crate::kernel::fs;
20use crate::kernel::shortcut_manager::{
21 InputAction, Modifiers, Shortcut, ShortcutLayer, ShortcutManager,
22};
23use crate::kernel::window_mgr::{App, Window};
24
25static ICON_FOLDER: &[u8] = include_bytes!("../../../icons/ic_fluent_folder_20_regular.svg");
26static ICON_FILE: &[u8] = include_bytes!("../../../icons/ic_fluent_document_20_regular.svg");
27
28const CMD_NAV_UP: u32 = 1;
30const CMD_NAV_DOWN: u32 = 2;
31const CMD_NAV_LEFT: u32 = 3;
32const CMD_NAV_RIGHT: u32 = 4;
33const CMD_FOCUS_UP: u32 = 5;
34const CMD_FOCUS_DOWN: u32 = 6;
35const CMD_FOCUS_LEFT: u32 = 7;
36const CMD_FOCUS_RIGHT: u32 = 8;
37const CMD_TAB_PREV: u32 = 9;
38const CMD_TAB_NEXT: u32 = 10;
39const CMD_FILE_COPY: u32 = 11;
40const CMD_FILE_MOVE: u32 = 12;
41const CMD_ENTER: u32 = 13;
42const CMD_DELETE: u32 = 14;
43
44#[derive(Clone)]
45pub struct FilerState {
46 pub current_path: String,
47 pub folders: Vec<String>,
48 pub files: Vec<fs::FileMetadata>,
49 pub selected_folder: usize,
50 pub selected_file: usize,
51 pub scroll_folder: usize,
52 pub scroll_file: usize,
53 pub active_pane: usize, }
55
56impl FilerState {
57 pub fn new(initial_path: &str) -> Self {
58 let mut state = Self {
59 current_path: String::from(initial_path),
60 folders: Vec::new(),
61 files: Vec::new(),
62 selected_folder: 0,
63 selected_file: 0,
64 scroll_folder: 0,
65 scroll_file: 0,
66 active_pane: 0,
67 };
68 state.refresh();
69 state
70 }
71
72 pub fn refresh(&mut self) {
73 self.folders.clear();
74 self.files.clear();
75
76 let all_files = fs::get_fs().list_files();
77 let mut folder_set = alloc::collections::BTreeSet::new();
78
79 let search_path = if self.current_path.ends_with('/') {
80 self.current_path.clone()
81 } else {
82 alloc::format!("{}/", self.current_path)
83 };
84
85 for meta in all_files {
86 let filename = meta.get_filename();
87 if filename.starts_with(&search_path) {
88 let remainder = filename.get(search_path.len()..).unwrap_or("");
89 if let Some(slash_idx) = remainder.find('/') {
90 let folder_name = remainder.get(..slash_idx).unwrap_or("");
92 if !folder_name.is_empty() {
93 folder_set.insert(String::from(folder_name));
94 }
95 } else if !remainder.is_empty() {
96 self.files.push(meta);
98 }
99 } else if search_path == "/" && !filename.is_empty() && !filename.starts_with('/') {
100 if let Some(slash_idx) = filename.find('/') {
102 folder_set.insert(String::from(filename.get(..slash_idx).unwrap_or("")));
103 } else {
104 self.files.push(meta);
105 }
106 }
107 }
108
109 if self.current_path == "/" {
111 folder_set.insert(String::from("apps"));
112 folder_set.insert(String::from("user"));
113 folder_set.insert(String::from("sys"));
114 }
115
116 for f in folder_set {
117 self.folders.push(f);
118 }
119
120 if self.selected_folder >= self.folders.len() {
121 self.selected_folder = self.folders.len().saturating_sub(1);
122 }
123 if self.selected_file >= self.files.len() {
124 self.selected_file = self.files.len().saturating_sub(1);
125 }
126 }
127}
128
129#[derive(Clone)]
130pub struct TabState {
131 pub left: FilerState,
132 pub right: FilerState,
133 pub active_panel: usize,
134}
135
136pub struct FilesApp {
137 tabs: Vec<TabState>,
138 active_tab: usize,
139 status: String,
140 last_win_w: u32,
141 last_win_h: u32,
142 alt_held: bool,
143 ctrl_held: bool,
144 shortcut_mgr: ShortcutManager,
145}
146
147impl Default for FilesApp {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl FilesApp {
154 pub fn new() -> Self {
155 let tabs = vec![TabState {
156 left: FilerState::new("/"),
157 right: FilerState::new("/apps"),
158 active_panel: 0,
159 }];
160
161 let mut shortcut_mgr = ShortcutManager::new();
162
163 shortcut_mgr.register(Shortcut::new(
165 0x48,
166 Modifiers::new(true, true, false),
167 ShortcutLayer::App,
168 InputAction::AppCommand(CMD_FOCUS_UP),
169 )); shortcut_mgr.register(Shortcut::new(
171 0x50,
172 Modifiers::new(true, true, false),
173 ShortcutLayer::App,
174 InputAction::AppCommand(CMD_FOCUS_DOWN),
175 )); shortcut_mgr.register(Shortcut::new(
177 0x4B,
178 Modifiers::new(true, true, false),
179 ShortcutLayer::App,
180 InputAction::AppCommand(CMD_FOCUS_LEFT),
181 )); shortcut_mgr.register(Shortcut::new(
183 0x4D,
184 Modifiers::new(true, true, false),
185 ShortcutLayer::App,
186 InputAction::AppCommand(CMD_FOCUS_RIGHT),
187 )); shortcut_mgr.register(Shortcut::new(
191 0x19,
192 Modifiers::new(true, false, false),
193 ShortcutLayer::App,
194 InputAction::AppCommand(CMD_NAV_UP),
195 )); shortcut_mgr.register(Shortcut::new(
197 0x31,
198 Modifiers::new(true, false, false),
199 ShortcutLayer::App,
200 InputAction::AppCommand(CMD_NAV_DOWN),
201 )); shortcut_mgr.register(Shortcut::new(
203 0x21,
204 Modifiers::new(true, false, false),
205 ShortcutLayer::App,
206 InputAction::AppCommand(CMD_NAV_RIGHT),
207 )); shortcut_mgr.register(Shortcut::new(
209 0x30,
210 Modifiers::new(true, false, false),
211 ShortcutLayer::App,
212 InputAction::AppCommand(CMD_NAV_LEFT),
213 )); shortcut_mgr.register(Shortcut::new(
216 0x48,
217 Modifiers::new(false, true, false),
218 ShortcutLayer::App,
219 InputAction::AppCommand(CMD_TAB_PREV),
220 )); shortcut_mgr.register(Shortcut::new(
222 0x50,
223 Modifiers::new(false, true, false),
224 ShortcutLayer::App,
225 InputAction::AppCommand(CMD_TAB_NEXT),
226 )); shortcut_mgr.register(Shortcut::new(
228 0x2E,
229 Modifiers::new(false, true, false),
230 ShortcutLayer::App,
231 InputAction::AppCommand(CMD_FILE_COPY),
232 )); shortcut_mgr.register(Shortcut::new(
234 0x32,
235 Modifiers::new(false, true, false),
236 ShortcutLayer::App,
237 InputAction::AppCommand(CMD_FILE_MOVE),
238 )); shortcut_mgr.register(Shortcut::new(
242 0x48,
243 Modifiers::new(false, false, false),
244 ShortcutLayer::App,
245 InputAction::AppCommand(CMD_NAV_UP),
246 )); shortcut_mgr.register(Shortcut::new(
248 0x50,
249 Modifiers::new(false, false, false),
250 ShortcutLayer::App,
251 InputAction::AppCommand(CMD_NAV_DOWN),
252 )); shortcut_mgr.register(Shortcut::new(
254 0x4D,
255 Modifiers::new(false, false, false),
256 ShortcutLayer::App,
257 InputAction::AppCommand(CMD_NAV_RIGHT),
258 )); shortcut_mgr.register(Shortcut::new(
260 0x4B,
261 Modifiers::new(false, false, false),
262 ShortcutLayer::App,
263 InputAction::AppCommand(CMD_NAV_LEFT),
264 )); shortcut_mgr.register(Shortcut::new(
266 0x1C,
267 Modifiers::new(false, false, false),
268 ShortcutLayer::App,
269 InputAction::AppCommand(CMD_ENTER),
270 )); shortcut_mgr.register(Shortcut::new(
272 0x53,
273 Modifiers::new(false, false, false),
274 ShortcutLayer::App,
275 InputAction::AppCommand(CMD_DELETE),
276 )); Self {
279 tabs,
280 active_tab: 0,
281 status: String::from("Ready"),
282 last_win_w: 1000,
283 last_win_h: 600,
284 alt_held: false,
285 ctrl_held: false,
286 shortcut_mgr,
287 }
288 }
289
290 fn active_panel(&self) -> usize {
291 self.tabs[self.active_tab].active_panel
292 }
293
294 fn set_active_panel(&mut self, val: usize) {
295 self.tabs[self.active_tab].active_panel = val;
296 }
297
298 fn active_state(&mut self) -> &mut FilerState {
299 let active_tab = self.active_tab;
300 let tab = &mut self.tabs[active_tab];
301 if tab.active_panel == 0 {
302 &mut tab.left
303 } else {
304 &mut tab.right
305 }
306 }
307
308 fn inactive_state(&mut self) -> &mut FilerState {
309 let active_tab = self.active_tab;
310 let tab = &mut self.tabs[active_tab];
311 if tab.active_panel == 1 {
312 &mut tab.left
313 } else {
314 &mut tab.right
315 }
316 }
317
318 fn refresh_all(&mut self) {
319 for tab in self.tabs.iter_mut() {
320 tab.left.refresh();
321 tab.right.refresh();
322 }
323 }
324
325 fn get_active_file_path(&mut self) -> Option<String> {
326 let state = self.active_state();
327 if state.active_pane == 1 && !state.files.is_empty() {
328 Some(String::from(
329 state.files[state.selected_file].get_filename(),
330 ))
331 } else {
332 None
333 }
334 }
335
336 fn open_selected(&mut self) {
337 if let Some(filename) = self.get_active_file_path() {
338 if filename.starts_with("/apps/") {
340 let parts: Vec<&str> = filename.split('/').collect();
341 if parts.len() > 2 {
342 let app_name = parts[2];
343 if app_name == "note" {
344 let mut app = crate::apps::note::NoteApp::new();
345 if let Some((_meta, content)) =
346 fs::get_fs().read_file("/apps/note/index.html")
347 {
348 if let Ok(html_str) = core::str::from_utf8(&content) {
349 app.engine.load_html(html_str, "/apps/note/index.html", 0);
350 }
351 }
352 let win = Window::new(200, 150, 600, 450, "Note - Untitled", Box::new(app));
353 crate::kernel::window_mgr::get_instance().add_window(win);
354 self.status = "Launched Note App".to_string();
355 return;
356 } else if app_name == "settings" {
357 let app = crate::apps::settings::SettingsApp::new();
358 let win = Window::new(150, 100, 480, 560, "OS Settings", Box::new(app));
359 crate::kernel::window_mgr::get_instance().add_window(win);
360 self.status = "Launched Settings App".to_string();
361 return;
362 } else if app_name == "music" {
363 let app = crate::apps::music::MusicApp::new();
364 let win = Window::new(180, 120, 500, 420, "Music Player", Box::new(app));
365 crate::kernel::window_mgr::get_instance().add_window(win);
366 self.status = "Launched Music Player".to_string();
367 return;
368 } else if app_name == "files" {
369 let app = crate::apps::files::FilesApp::new();
370 let win = Window::new(140, 90, 1180, 760, "Files", Box::new(app));
371 crate::kernel::window_mgr::get_instance().add_window(win);
372 self.status = "Launched Files App".to_string();
373 return;
374 } else if app_name == "browser" {
375 let app = WebBrowser::new();
377 let win = Window::new(100, 100, 800, 600, "Browser - AtmOS", Box::new(app));
378 crate::kernel::window_mgr::get_instance().add_window(win);
379 self.status = "Launched Browser App".to_string();
380 return;
381 }
382 }
383 }
384
385 if filename.ends_with(".html") || filename.ends_with(".htm") {
386 let mut app = WebBrowser::new();
387 if let Some((_meta, content)) = fs::get_fs().read_file(&filename) {
388 if let Ok(html_str) = core::str::from_utf8(&content) {
389 let top = app.top_offset();
390 app.engine.load_html(html_str, &filename, top);
391 }
392 }
393 let win = Window::new(
394 100,
395 100,
396 800,
397 600,
398 &alloc::format!("Browser - {}", filename),
399 Box::new(app),
400 );
401 crate::kernel::window_mgr::get_instance().add_window(win);
402 self.status = alloc::format!("Opened HTML file '{}'", filename);
403 } else {
404 let content = fs::get_fs()
405 .read_file(&filename)
406 .and_then(|(_meta, bytes)| {
407 core::str::from_utf8(&bytes)
408 .ok()
409 .map(alloc::string::String::from)
410 })
411 .unwrap_or_default();
412 let app = crate::apps::note::NoteApp::open_file(&filename, &content);
413 let win = Window::new(
414 200,
415 150,
416 600,
417 450,
418 &alloc::format!("Note - {}", filename),
419 Box::new(app),
420 );
421 crate::kernel::window_mgr::get_instance().add_window(win);
422 self.status = alloc::format!("Opened '{}' in Note", filename);
423 }
424 }
425 }
426
427 fn copy_to_inactive_panel(&mut self) {
428 if let Some(src_path) = self.get_active_file_path() {
429 let dest_path_base = self.inactive_state().current_path.clone();
430
431 let dest_dir = if dest_path_base.ends_with('/') {
432 dest_path_base
433 } else {
434 alloc::format!("{}/", dest_path_base)
435 };
436
437 let filename_only = match src_path.rfind('/') {
438 Some(idx) => src_path.get(idx + 1..).unwrap_or(""),
439 None => &src_path,
440 };
441
442 let dest_path = alloc::format!("{}{}", dest_dir, filename_only);
443
444 if let Some((meta, content)) = fs::get_fs().read_file(&src_path) {
445 if fs::get_fs()
446 .save_file(&dest_path, &content, meta.get_labels())
447 .is_ok()
448 {
449 self.status = alloc::format!("Copied to {}", dest_path);
450 self.refresh_all();
451 return;
452 }
453 }
454 self.status = alloc::format!("Failed to copy {}", src_path);
455 }
456 }
457
458 fn move_to_inactive_panel(&mut self) {
459 if let Some(src_path) = self.get_active_file_path() {
460 let dest_path_base = self.inactive_state().current_path.clone();
461
462 let dest_dir = if dest_path_base.ends_with('/') {
463 dest_path_base
464 } else {
465 alloc::format!("{}/", dest_path_base)
466 };
467 let filename_only = match src_path.rfind('/') {
468 Some(idx) => src_path.get(idx + 1..).unwrap_or(""),
469 None => &src_path,
470 };
471 let dest_path = alloc::format!("{}{}", dest_dir, filename_only);
472
473 let fs_ref = fs::get_fs();
474 if let Some((meta, content)) = fs_ref.read_file(&src_path) {
475 if fs_ref
476 .save_file(&dest_path, &content, meta.get_labels())
477 .is_ok()
478 {
479 let _ = fs_ref.delete_file(&src_path);
480 self.status = alloc::format!("Moved to {}", dest_path);
481 self.refresh_all();
482 return;
483 }
484 }
485 self.status = alloc::format!("Failed to move {}", src_path);
486 }
487 }
488
489 fn delete_selected(&mut self) {
490 if let Some(filename) = self.get_active_file_path() {
491 if fs::get_fs().delete_file(&filename).is_ok() {
492 self.status = alloc::format!("Deleted '{}'", filename);
493 crate::kernel::audio::play_system_se(crate::kernel::audio::SeType::Delete);
494 self.refresh_all();
495 } else {
496 self.status = alloc::format!("ERROR: Failed to delete '{}'", filename);
497 }
498 }
499 }
500
501 fn get_folders_at(path: &str) -> Vec<String> {
502 let mut folder_set = alloc::collections::BTreeSet::new();
503 let all_files = fs::get_fs().list_files();
504
505 let search_path = if path.ends_with('/') {
506 path.to_string()
507 } else {
508 alloc::format!("{}/", path)
509 };
510
511 for meta in all_files {
512 let filename = meta.get_filename();
513 if filename.starts_with(&search_path) {
514 let remainder = filename.get(search_path.len()..).unwrap_or("");
515 if let Some(slash_idx) = remainder.find('/') {
516 let folder_name = remainder.get(..slash_idx).unwrap_or("");
517 if !folder_name.is_empty() {
518 folder_set.insert(String::from(folder_name));
519 }
520 }
521 }
522 }
523
524 if path == "/" {
525 folder_set.insert(String::from("apps"));
526 folder_set.insert(String::from("user"));
527 folder_set.insert(String::from("sys"));
528 }
529
530 folder_set.into_iter().collect()
531 }
532
533 fn draw_panel(
534 &self,
535 screen: &Screen,
536 is_right: bool,
537 px: u32,
538 py: u32,
539 pw: u32,
540 ph: u32,
541 active_panel: bool,
542 ) {
543 let tab = &self.tabs[self.active_tab];
544 let state = if is_right {
545 &tab.right
546 } else {
547 &tab.left
548 };
549
550 let config = config::get_config();
551 let bg = Color(config.theme.desktop_bg);
552 let list_bg = Color(config.theme.terminal_bg);
553 let fg = Color(config.theme.terminal_fg);
554 let active_color = Color(config.theme.active_window_border);
555 let inactive_color = Color(config.theme.non_active_window_border);
556 let preview_color = Color(config.theme.non_active_window_border);
557
558 let border_color = if active_panel {
560 active_color
561 } else {
562 inactive_color
563 };
564 screen.boxfill(px, py, px + pw, py + ph, border_color);
565 screen.boxfill(px + 2, py + 2, px + pw - 2, py + ph - 2, bg);
566
567 let half_h = (ph - 4) / 2;
568
569 let upper_py = py + 2;
571 let upper_ph = half_h;
572 let upper_active = active_panel && state.active_pane == 0;
573
574 screen.boxfill(
575 px + 4,
576 upper_py + 4,
577 px + pw - 4,
578 upper_py + upper_ph - 4,
579 list_bg,
580 );
581
582 screen.draw_string_vector(
584 px + 8,
585 upper_py + 8,
586 &alloc::format!("Path: {}", state.current_path),
587 Color(config.theme.focus_highlight),
588 14,
589 );
590
591 let col_w = (pw - 20) / 3;
593 let col_h = upper_ph - 38;
594 let col_y = upper_py + 36;
595
596 let header_y = upper_py + 22;
598 screen.draw_string_vector(px + 10, header_y, "Parent", Color(config.theme.non_active_window_border), 10);
599 screen.draw_string_vector(px + 10 + col_w, header_y, "Current", Color(config.theme.active_window_border), 10);
600 screen.draw_string_vector(
601 px + 10 + col_w * 2,
602 header_y,
603 "Child Preview",
604 Color(config.theme.non_active_window_border),
605 10,
606 );
607
608 let parent_path = if state.current_path == "/" {
610 String::from("/")
611 } else {
612 let mut p = state.current_path.trim_end_matches('/').to_string();
613 if let Some(idx) = p.rfind('/') {
614 p.truncate(idx + 1);
615 if p.is_empty() {
616 String::from("/")
617 } else {
618 p
619 }
620 } else {
621 String::from("/")
622 }
623 };
624
625 let parent_folders = if state.current_path == "/" {
626 Vec::new()
627 } else {
628 Self::get_folders_at(&parent_path)
629 };
630
631 screen.boxfill(
633 px + 4 + col_w,
634 col_y,
635 px + 4 + col_w + 1,
636 col_y + col_h,
637 Color(config.theme.non_active_window_border),
638 );
639
640 let trimmed_cur = state.current_path.trim_end_matches('/');
642 let current_basename = match trimmed_cur.rfind('/') {
643 Some(idx) => trimmed_cur.get(idx + 1..).unwrap_or(""),
644 None => state.current_path.trim_start_matches('/'),
645 };
646
647 let line_h = 20;
648 let visible_lines = (col_h / line_h) as usize;
649
650 for i in 0..visible_lines {
651 if i >= parent_folders.len() {
652 break;
653 }
654 let y = col_y + i as u32 * line_h;
655 let is_matched = parent_folders[i] == current_basename;
656 let col_x0 = px + 6;
657
658 if is_matched {
659 screen.boxfill(
661 col_x0,
662 y,
663 px + 4 + col_w - 2,
664 y + line_h - 2,
665 Color(config.theme.button_bg),
666 );
667 }
668
669 screen.draw_svg_icon(col_x0 + 4, y + 3, ICON_FOLDER, 12, preview_color);
670 screen.draw_string_vector(
671 col_x0 + 20,
672 y + 2,
673 &alloc::format!("{} <", parent_folders[i]),
674 preview_color,
675 12,
676 );
677 }
678
679 screen.boxfill(
682 px + 6 + col_w * 2,
683 col_y,
684 px + 6 + col_w * 2 + 1,
685 col_y + col_h,
686 Color(config.theme.non_active_window_border),
687 );
688
689 for i in 0..visible_lines {
690 let idx = state.scroll_folder + i;
691 if idx >= state.folders.len() {
692 break;
693 }
694
695 let y = col_y + i as u32 * line_h;
696 let selected = idx == state.selected_folder;
697 let col_x0 = px + 6 + col_w;
698 let col_x1 = px + 6 + col_w * 2 - 2;
699
700 if selected {
701 let sel_bg = if upper_active {
702 active_color
703 } else {
704 inactive_color
705 };
706 screen.boxfill(col_x0, y, col_x1, y + line_h - 2, sel_bg);
707 if upper_active {
709 let bord = Color(config.theme.focus_highlight);
710 screen.boxfill(col_x0, y, col_x1, y + 1, bord);
711 screen.boxfill(col_x0, y + line_h - 3, col_x1, y + line_h - 2, bord);
712 screen.boxfill(col_x0, y, col_x0 + 1, y + line_h - 2, bord);
713 screen.boxfill(col_x1 - 1, y, col_x1, y + line_h - 2, bord);
714 }
715 }
716
717 let fg_color = if selected && upper_active {
718 Color(config.theme.button_active_fg)
719 } else {
720 fg
721 };
722 let icon_color = if selected && upper_active {
723 Color(config.theme.active_window_border)
724 } else {
725 fg_color
726 };
727 screen.draw_svg_icon(col_x0 + 4, y + 3, ICON_FOLDER, 12, icon_color);
728 screen.draw_string_vector(
729 col_x0 + 20,
730 y + 2,
731 &alloc::format!("{} >", state.folders[idx]),
732 fg_color,
733 12,
734 );
735 }
736
737 let child_folders =
739 if !state.folders.is_empty() && state.selected_folder < state.folders.len() {
740 let mut child_path = state.current_path.clone();
741 if !child_path.ends_with('/') {
742 child_path.push('/');
743 }
744 child_path.push_str(&state.folders[state.selected_folder]);
745 Self::get_folders_at(&child_path)
746 } else {
747 Vec::new()
748 };
749
750 for i in 0..visible_lines {
751 if i >= child_folders.len() {
752 break;
753 }
754 let y = col_y + i as u32 * line_h;
755 let col_x0 = px + 8 + col_w * 2;
756
757 screen.draw_svg_icon(col_x0 + 4, y + 3, ICON_FOLDER, 12, preview_color);
758 screen.draw_string_vector(col_x0 + 20, y + 2, &child_folders[i], preview_color, 12);
759 }
760
761 let lower_py = py + 2 + half_h;
763 let lower_ph = half_h;
764 let lower_active = active_panel && state.active_pane == 1;
765
766 screen.boxfill(
767 px + 4,
768 lower_py + 4,
769 px + pw - 4,
770 lower_py + lower_ph - 4,
771 list_bg,
772 );
773
774 let visible_files = ((lower_ph - 12) / line_h) as usize;
775
776 for i in 0..visible_files {
777 let idx = state.scroll_file + i;
778 if idx >= state.files.len() {
779 break;
780 }
781
782 let y = lower_py + 8 + i as u32 * line_h;
783 let selected = idx == state.selected_file;
784
785 if selected {
786 let sel_bg = if lower_active {
787 active_color
788 } else {
789 inactive_color
790 };
791 let x0 = px + 6;
792 let x1 = px + pw - 6;
793 screen.boxfill(x0, y, x1, y + line_h - 2, sel_bg);
794 if lower_active {
796 let bord = Color(config.theme.focus_highlight);
797 screen.boxfill(x0, y, x1, y + 1, bord);
798 screen.boxfill(x0, y + line_h - 3, x1, y + line_h - 2, bord);
799 screen.boxfill(x0, y, x0 + 1, y + line_h - 2, bord);
800 screen.boxfill(x1 - 1, y, x1, y + line_h - 2, bord);
801 }
802 }
803
804 let fg_color = if selected && lower_active {
805 Color(config.theme.button_active_fg)
806 } else {
807 fg
808 };
809 let filename = state.files[idx].get_filename();
810
811 let basename = match filename.rfind('/') {
813 Some(slash_idx) => filename.get(slash_idx + 1..).unwrap_or(""),
814 None => filename,
815 };
816
817 let icon_color = if selected && lower_active {
818 Color(config.theme.active_window_border)
819 } else {
820 fg_color
821 };
822 screen.draw_svg_icon(px + 12, y + 3, ICON_FILE, 12, icon_color);
823 screen.draw_string_vector(px + 30, y + 2, basename, fg_color, 14);
824 }
825 }
826}
827
828impl App for FilesApp {
829 fn name(&self) -> &str {
830 "files"
831 }
832
833 fn draw(&mut self, screen: &Screen, win_x: u32, win_y: u32, win_w: u32, win_h: u32) {
834 self.last_win_w = win_w;
835 self.last_win_h = win_h;
836
837 let config = config::get_config();
838 let bg_color = Color(config.theme.desktop_bg);
839 let header_bg = Color(config.theme.window_title_bar);
840
841 screen.boxfill(win_x, win_y, win_x + win_w, win_y + win_h, bg_color);
843
844 screen.boxfill(win_x, win_y, win_x + win_w, win_y + 26, header_bg);
846 screen.draw_string_vector(
847 win_x + 10,
848 win_y + 5,
849 "2x2 Grid Filer",
850 Color(config.theme.window_title_fg),
851 14,
852 );
853
854 let panel_w = (win_w - 20) / 2;
856 let panel_h = win_h - 60; self.draw_panel(
860 screen,
861 false,
862 win_x + 6,
863 win_y + 30,
864 panel_w,
865 panel_h,
866 self.active_panel() == 0,
867 );
868
869 self.draw_panel(
871 screen,
872 true,
873 win_x + 10 + panel_w,
874 win_y + 30,
875 panel_w,
876 panel_h,
877 self.active_panel() == 1,
878 );
879
880 let footer_y = win_y + win_h - 24;
882 screen.boxfill(win_x, footer_y, win_x + win_w, win_y + win_h, header_bg);
883 screen.draw_string_vector(win_x + 8, footer_y + 4, &self.status, Color(config.theme.window_title_fg), 14);
884 }
885
886 fn on_mouse(
887 &mut self,
888 _local_x: i32,
889 _local_y: i32,
890 _btn_left: bool,
891 _btn_right: bool,
892 _wheel: i32,
893 ) {
894 }
896
897 fn on_key(&mut self, keycode: u8, pressed: bool, _ascii: Option<char>) {
898 if keycode == 0x38 {
899 self.alt_held = pressed;
901 return;
902 }
903
904 if keycode == 0x1D {
905 self.ctrl_held = pressed;
907 return;
908 }
909
910 if !pressed {
911 return;
912 }
913
914 let mods = Modifiers::new(self.ctrl_held, self.alt_held, false);
916 let action = self.shortcut_mgr.evaluate(keycode, mods);
917
918 match action {
919 InputAction::AppCommand(cmd) => {
920 match cmd {
921 CMD_FOCUS_UP => {
922 self.active_state().active_pane = 0;
923 }
924 CMD_FOCUS_DOWN => {
925 self.active_state().active_pane = 1;
926 }
927 CMD_FOCUS_LEFT => {
928 self.set_active_panel(0);
929 }
930 CMD_FOCUS_RIGHT => {
931 self.set_active_panel(1);
932 }
933
934 CMD_TAB_PREV => {
935 self.active_tab = self.active_tab.saturating_sub(1);
936 }
937 CMD_TAB_NEXT => {
938 self.active_tab = (self.active_tab + 1).min(self.tabs.len().saturating_sub(1));
939 }
940 CMD_FILE_COPY => {
941 self.copy_to_inactive_panel();
942 }
943 CMD_FILE_MOVE => {
944 self.move_to_inactive_panel();
945 }
946
947 CMD_NAV_UP | CMD_NAV_DOWN | CMD_NAV_LEFT | CMD_NAV_RIGHT | CMD_ENTER
948 | CMD_DELETE => {
949 let state = self.active_state();
950 let is_up = cmd == CMD_NAV_UP;
951 let is_down = cmd == CMD_NAV_DOWN;
952 let is_left = cmd == CMD_NAV_LEFT;
953 let is_right = cmd == CMD_NAV_RIGHT;
954
955 if state.active_pane == 0 {
956 if is_up && state.selected_folder > 0 {
958 state.selected_folder -= 1;
959 if state.selected_folder < state.scroll_folder {
960 state.scroll_folder = state.selected_folder;
961 }
962 } else if is_down && state.selected_folder + 1 < state.folders.len() {
963 state.selected_folder += 1;
964 if state.selected_folder >= state.scroll_folder + 10 {
965 state.scroll_folder += 1;
966 }
967 } else if is_right && !state.folders.is_empty() {
968 let folder_name = &state.folders[state.selected_folder];
970 let mut new_path = state.current_path.clone();
971 if !new_path.ends_with('/') {
972 new_path.push('/');
973 }
974 new_path.push_str(folder_name);
975 state.current_path = new_path;
976 state.refresh();
977 } else if is_left && state.current_path != "/" {
978 let mut p = state.current_path.trim_end_matches('/').to_string();
980 if let Some(idx) = p.rfind('/') {
981 p.truncate(idx + 1);
982 if p.is_empty() {
983 p = String::from("/");
984 }
985 } else {
986 p = String::from("/");
987 }
988 state.current_path = p;
989 state.refresh();
990 }
991 } else {
992 if is_up && state.selected_file > 0 {
994 state.selected_file -= 1;
995 if state.selected_file < state.scroll_file {
996 state.scroll_file = state.selected_file;
997 }
998 } else if is_down && state.selected_file + 1 < state.files.len() {
999 state.selected_file += 1;
1000 if state.selected_file >= state.scroll_file + 10 {
1001 state.scroll_file += 1;
1002 }
1003 } else if cmd == CMD_ENTER {
1004 } else if cmd == CMD_DELETE {
1006 }
1008 }
1009 }
1010 _ => {}
1011 }
1012 }
1013 _ => {
1014 }
1016 }
1017
1018 if let InputAction::AppCommand(cmd) = action {
1021 if cmd == CMD_ENTER && self.active_state().active_pane == 1 {
1022 self.open_selected();
1023 } else if cmd == CMD_DELETE && self.active_state().active_pane == 1 {
1024 self.delete_selected();
1025 }
1026 }
1027 }
1028
1029 fn tabs(&self) -> Vec<String> {
1030 let mut titles = Vec::new();
1031 for (i, tab) in self.tabs.iter().enumerate() {
1032 let left_name = tab.left.current_path.trim_end_matches('/').split('/').next_back().unwrap_or("");
1033 let right_name = tab.right.current_path.trim_end_matches('/').split('/').next_back().unwrap_or("");
1034 let l_label = if left_name.is_empty() { "Root" } else { left_name };
1035 let r_label = if right_name.is_empty() { "Root" } else { right_name };
1036 titles.push(alloc::format!("{}: {} | {}", i + 1, l_label, r_label));
1037 }
1038 titles
1039 }
1040
1041 fn active_tab(&self) -> usize {
1042 self.active_tab
1043 }
1044
1045 fn close_tab(&mut self, idx: usize) -> bool {
1046 if self.tabs.len() > 1 && idx < self.tabs.len() {
1047 self.tabs.remove(idx);
1048 if self.active_tab >= self.tabs.len() {
1049 self.active_tab = self.tabs.len() - 1;
1050 }
1051 false
1052 } else {
1053 true
1054 }
1055 }
1056
1057 fn switch_tab(&mut self, idx: usize) {
1058 if idx < self.tabs.len() {
1059 self.active_tab = idx;
1060 }
1061 }
1062
1063 fn add_tab(&mut self) {
1064 self.tabs.push(TabState {
1065 left: FilerState::new("/"),
1066 right: FilerState::new("/apps"),
1067 active_panel: 0,
1068 });
1069 self.active_tab = self.tabs.len() - 1;
1070 }
1071}