Skip to main content

axonml_tui/views/
files.rs

1//! Files View - File Browser for Model and Data Files
2//!
3//! Provides a tree-like file browser to navigate directories and
4//! open model/data files for viewing and training.
5//!
6//! @version 0.1.0
7//! @author AutomataNexus Development Team
8
9use std::path::PathBuf;
10
11use ratatui::{
12    layout::{Constraint, Direction, Layout, Rect},
13    style::Style,
14    text::{Line, Span},
15    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
16    Frame,
17};
18
19use crate::theme::AxonmlTheme;
20
21// =============================================================================
22// Types
23// =============================================================================
24
25/// File entry type
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum FileType {
28    Directory,
29    Model,      // .axonml, .onnx, .pt, .safetensors
30    Dataset,    // .npz, .csv, .parquet
31    Config,     // .toml, .yaml, .json
32    Other,
33}
34
35impl FileType {
36    /// Get icon for file type
37    #[allow(dead_code)]
38    pub fn icon(&self) -> &'static str {
39        match self {
40            FileType::Directory => "\u{1F4C1}",  // Folder
41            FileType::Model => "\u{1F9E0}",      // Brain (model)
42            FileType::Dataset => "\u{1F4CA}",    // Chart (data)
43            FileType::Config => "\u{2699}",      // Gear (config)
44            FileType::Other => "\u{1F4C4}",      // Document
45        }
46    }
47
48    /// Determine file type from extension
49    #[allow(dead_code)]
50    pub fn from_extension(ext: &str) -> Self {
51        match ext.to_lowercase().as_str() {
52            "axonml" | "onnx" | "pt" | "pth" | "safetensors" | "h5" | "keras" => FileType::Model,
53            "npz" | "npy" | "csv" | "parquet" | "arrow" | "tfrecord" => FileType::Dataset,
54            "toml" | "yaml" | "yml" | "json" => FileType::Config,
55            _ => FileType::Other,
56        }
57    }
58}
59
60/// File entry in the browser
61#[derive(Debug, Clone)]
62pub struct FileEntry {
63    pub name: String,
64    pub path: PathBuf,
65    pub file_type: FileType,
66    pub size: Option<u64>,
67    pub is_expanded: bool,
68    pub depth: usize,
69}
70
71impl FileEntry {
72    /// Create a new file entry
73    #[allow(dead_code)]
74    pub fn new(name: String, path: PathBuf, file_type: FileType, depth: usize) -> Self {
75        Self {
76            name,
77            path,
78            file_type,
79            size: None,
80            is_expanded: false,
81            depth,
82        }
83    }
84}
85
86// =============================================================================
87// Files View
88// =============================================================================
89
90/// File browser view state
91pub struct FilesView {
92    /// Current directory path
93    pub current_dir: PathBuf,
94
95    /// List of file entries
96    pub entries: Vec<FileEntry>,
97
98    /// Selected entry index
99    pub selected: usize,
100
101    /// List state for navigation
102    pub list_state: ListState,
103
104    /// Show hidden files
105    pub show_hidden: bool,
106
107    /// Filter pattern
108    pub filter: Option<String>,
109
110    /// Currently previewed file info
111    pub preview: Option<String>,
112}
113
114impl FilesView {
115    /// Create a new files view with demo data
116    pub fn new() -> Self {
117        let mut view = Self {
118            current_dir: PathBuf::from("/home/user/ml-projects"),
119            entries: Vec::new(),
120            selected: 0,
121            list_state: ListState::default(),
122            show_hidden: false,
123            filter: None,
124            preview: None,
125        };
126
127        view.load_demo_entries();
128        view.list_state.select(Some(0));
129        view
130    }
131
132    /// Load demo file entries for visualization
133    pub fn load_demo_entries(&mut self) {
134        self.entries = vec![
135            FileEntry {
136                name: "..".to_string(),
137                path: PathBuf::from("/home/user"),
138                file_type: FileType::Directory,
139                size: None,
140                is_expanded: false,
141                depth: 0,
142            },
143            FileEntry {
144                name: "models".to_string(),
145                path: PathBuf::from("/home/user/ml-projects/models"),
146                file_type: FileType::Directory,
147                size: None,
148                is_expanded: true,
149                depth: 0,
150            },
151            FileEntry {
152                name: "mnist_classifier.axonml".to_string(),
153                path: PathBuf::from("/home/user/ml-projects/models/mnist_classifier.axonml"),
154                file_type: FileType::Model,
155                size: Some(940_584),
156                is_expanded: false,
157                depth: 1,
158            },
159            FileEntry {
160                name: "resnet50.onnx".to_string(),
161                path: PathBuf::from("/home/user/ml-projects/models/resnet50.onnx"),
162                file_type: FileType::Model,
163                size: Some(97_800_000),
164                is_expanded: false,
165                depth: 1,
166            },
167            FileEntry {
168                name: "bert_base.safetensors".to_string(),
169                path: PathBuf::from("/home/user/ml-projects/models/bert_base.safetensors"),
170                file_type: FileType::Model,
171                size: Some(438_000_000),
172                is_expanded: false,
173                depth: 1,
174            },
175            FileEntry {
176                name: "datasets".to_string(),
177                path: PathBuf::from("/home/user/ml-projects/datasets"),
178                file_type: FileType::Directory,
179                size: None,
180                is_expanded: true,
181                depth: 0,
182            },
183            FileEntry {
184                name: "mnist.npz".to_string(),
185                path: PathBuf::from("/home/user/ml-projects/datasets/mnist.npz"),
186                file_type: FileType::Dataset,
187                size: Some(11_490_434),
188                is_expanded: false,
189                depth: 1,
190            },
191            FileEntry {
192                name: "cifar10.npz".to_string(),
193                path: PathBuf::from("/home/user/ml-projects/datasets/cifar10.npz"),
194                file_type: FileType::Dataset,
195                size: Some(170_498_071),
196                is_expanded: false,
197                depth: 1,
198            },
199            FileEntry {
200                name: "imagenet_labels.csv".to_string(),
201                path: PathBuf::from("/home/user/ml-projects/datasets/imagenet_labels.csv"),
202                file_type: FileType::Dataset,
203                size: Some(21_384),
204                is_expanded: false,
205                depth: 1,
206            },
207            FileEntry {
208                name: "configs".to_string(),
209                path: PathBuf::from("/home/user/ml-projects/configs"),
210                file_type: FileType::Directory,
211                size: None,
212                is_expanded: false,
213                depth: 0,
214            },
215            FileEntry {
216                name: "train_config.toml".to_string(),
217                path: PathBuf::from("/home/user/ml-projects/train_config.toml"),
218                file_type: FileType::Config,
219                size: Some(1_024),
220                is_expanded: false,
221                depth: 0,
222            },
223            FileEntry {
224                name: "README.md".to_string(),
225                path: PathBuf::from("/home/user/ml-projects/README.md"),
226                file_type: FileType::Other,
227                size: Some(2_048),
228                is_expanded: false,
229                depth: 0,
230            },
231        ];
232    }
233
234    /// Move selection up
235    pub fn select_prev(&mut self) {
236        if self.selected > 0 {
237            self.selected -= 1;
238            self.list_state.select(Some(self.selected));
239            self.update_preview();
240        }
241    }
242
243    /// Move selection down
244    pub fn select_next(&mut self) {
245        if self.selected < self.entries.len() - 1 {
246            self.selected += 1;
247            self.list_state.select(Some(self.selected));
248            self.update_preview();
249        }
250    }
251
252    /// Toggle directory expansion or open file
253    pub fn toggle_or_open(&mut self) {
254        if let Some(entry) = self.entries.get_mut(self.selected) {
255            if entry.file_type == FileType::Directory {
256                entry.is_expanded = !entry.is_expanded;
257                // In real implementation, would reload children
258            }
259            // In real implementation, would open file for viewing
260        }
261    }
262
263    /// Go to parent directory
264    pub fn go_parent(&mut self) {
265        if let Some(parent) = self.current_dir.parent() {
266            self.current_dir = parent.to_path_buf();
267            // In real implementation, would reload entries
268        }
269    }
270
271    /// Go up to parent directory (alias for go_parent)
272    pub fn go_up(&mut self) {
273        self.go_parent();
274    }
275
276    /// Go to home directory
277    pub fn go_home(&mut self) {
278        if let Some(home) = dirs::home_dir() {
279            self.current_dir = home;
280            // In real implementation, would reload entries
281        }
282    }
283
284    /// Open selected file and return its path if it's a file (not directory)
285    pub fn open_selected(&mut self) -> Option<PathBuf> {
286        if let Some(entry) = self.entries.get(self.selected) {
287            if entry.file_type == FileType::Directory {
288                // Toggle expansion for directories
289                self.toggle_or_open();
290            } else {
291                return Some(entry.path.clone());
292            }
293        }
294        None
295    }
296
297    /// Toggle hidden files
298    pub fn toggle_hidden(&mut self) {
299        self.show_hidden = !self.show_hidden;
300        // In real implementation, would reload entries
301    }
302
303    /// Update preview for selected file
304    fn update_preview(&mut self) {
305        if let Some(entry) = self.entries.get(self.selected) {
306            self.preview = Some(format!(
307                "Path: {}\nType: {:?}\nSize: {}",
308                entry.path.display(),
309                entry.file_type,
310                entry.size.map(format_size).unwrap_or_else(|| "-".to_string())
311            ));
312        }
313    }
314
315    /// Render the files view
316    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
317        let chunks = Layout::default()
318            .direction(Direction::Horizontal)
319            .constraints([
320                Constraint::Percentage(60),  // File list
321                Constraint::Percentage(40),  // Preview
322            ])
323            .split(area);
324
325        self.render_file_list(frame, chunks[0]);
326        self.render_preview(frame, chunks[1]);
327    }
328
329    fn render_file_list(&mut self, frame: &mut Frame, area: Rect) {
330        let chunks = Layout::default()
331            .direction(Direction::Vertical)
332            .constraints([
333                Constraint::Length(3),  // Path bar
334                Constraint::Min(10),    // File list
335            ])
336            .split(area);
337
338        // Path bar
339        let path_text = Line::from(vec![
340            Span::styled("Path: ", AxonmlTheme::muted()),
341            Span::styled(self.current_dir.display().to_string(), AxonmlTheme::accent()),
342        ]);
343
344        let path_bar = Paragraph::new(path_text)
345            .block(
346                Block::default()
347                    .borders(Borders::ALL)
348                    .border_style(AxonmlTheme::border())
349                    .title(Span::styled(" Location ", AxonmlTheme::header())),
350            );
351
352        frame.render_widget(path_bar, chunks[0]);
353
354        // File list
355        let items: Vec<ListItem> = self
356            .entries
357            .iter()
358            .enumerate()
359            .map(|(i, entry)| {
360                let style = if i == self.selected {
361                    AxonmlTheme::selected()
362                } else {
363                    Style::default()
364                };
365
366                // Indentation for tree structure
367                let indent = "  ".repeat(entry.depth);
368
369                // Expansion indicator for directories
370                let expand_char = if entry.file_type == FileType::Directory {
371                    if entry.is_expanded { "\u{25BC} " } else { "\u{25B6} " }
372                } else {
373                    "  "
374                };
375
376                // File type styling
377                let name_style = match entry.file_type {
378                    FileType::Directory => AxonmlTheme::layer_type(),
379                    FileType::Model => AxonmlTheme::success(),
380                    FileType::Dataset => AxonmlTheme::info(),
381                    FileType::Config => AxonmlTheme::warning(),
382                    FileType::Other => AxonmlTheme::muted(),
383                };
384
385                let size_str = entry
386                    .size
387                    .map(|s| format!("{:>10}", format_size(s)))
388                    .unwrap_or_else(|| "         -".to_string());
389
390                let content = Line::from(vec![
391                    Span::raw(indent),
392                    Span::styled(expand_char, AxonmlTheme::muted()),
393                    Span::styled(&entry.name, name_style),
394                    Span::styled(format!("  {}", size_str), AxonmlTheme::muted()),
395                ]);
396
397                ListItem::new(content).style(style)
398            })
399            .collect();
400
401        let list = List::new(items)
402            .block(
403                Block::default()
404                    .borders(Borders::ALL)
405                    .border_style(AxonmlTheme::border_focused())
406                    .title(Span::styled(" Files ", AxonmlTheme::header())),
407            )
408            .highlight_style(AxonmlTheme::selected());
409
410        frame.render_stateful_widget(list, chunks[1], &mut self.list_state);
411    }
412
413    fn render_preview(&self, frame: &mut Frame, area: Rect) {
414        let selected_entry = self.entries.get(self.selected);
415
416        let preview_text = if let Some(entry) = selected_entry {
417            let type_info = match entry.file_type {
418                FileType::Directory => "Directory - Press Enter to expand/collapse",
419                FileType::Model => "Neural Network Model\nSupported: Axonml, ONNX, SafeTensors\nPress Enter to load in Model view",
420                FileType::Dataset => "Dataset File\nSupported: NPZ, CSV, Parquet\nPress Enter to load in Data view",
421                FileType::Config => "Configuration File\nPress Enter to edit",
422                FileType::Other => "Unknown file type",
423            };
424
425            vec![
426                Line::from(vec![
427                    Span::styled("Name: ", AxonmlTheme::muted()),
428                    Span::styled(&entry.name, AxonmlTheme::title()),
429                ]),
430                Line::from(""),
431                Line::from(vec![
432                    Span::styled("Type: ", AxonmlTheme::muted()),
433                    Span::styled(format!("{:?}", entry.file_type), AxonmlTheme::accent()),
434                ]),
435                Line::from(vec![
436                    Span::styled("Size: ", AxonmlTheme::muted()),
437                    Span::styled(
438                        entry.size.map(format_size).unwrap_or_else(|| "-".to_string()),
439                        AxonmlTheme::metric_value(),
440                    ),
441                ]),
442                Line::from(vec![
443                    Span::styled("Path: ", AxonmlTheme::muted()),
444                    Span::styled(entry.path.display().to_string(), AxonmlTheme::layer_shape()),
445                ]),
446                Line::from(""),
447                Line::from(Span::styled(type_info, AxonmlTheme::info())),
448            ]
449        } else {
450            vec![Line::from(Span::styled("No file selected", AxonmlTheme::muted()))]
451        };
452
453        let preview = Paragraph::new(preview_text)
454            .block(
455                Block::default()
456                    .borders(Borders::ALL)
457                    .border_style(AxonmlTheme::border())
458                    .title(Span::styled(" Preview ", AxonmlTheme::header())),
459            )
460            .wrap(ratatui::widgets::Wrap { trim: true });
461
462        frame.render_widget(preview, area);
463    }
464}
465
466impl Default for FilesView {
467    fn default() -> Self {
468        Self::new()
469    }
470}
471
472// =============================================================================
473// Helpers
474// =============================================================================
475
476fn format_size(bytes: u64) -> String {
477    const KB: u64 = 1024;
478    const MB: u64 = KB * 1024;
479    const GB: u64 = MB * 1024;
480
481    if bytes >= GB {
482        format!("{:.1} GB", bytes as f64 / GB as f64)
483    } else if bytes >= MB {
484        format!("{:.1} MB", bytes as f64 / MB as f64)
485    } else if bytes >= KB {
486        format!("{:.1} KB", bytes as f64 / KB as f64)
487    } else {
488        format!("{} B", bytes)
489    }
490}