toon_format/tui/components/
file_browser.rs1use std::fs;
4
5use ratatui::{
6 layout::{
7 Alignment,
8 Constraint,
9 Direction,
10 Layout,
11 Rect,
12 },
13 text::{
14 Line,
15 Span,
16 },
17 widgets::{
18 Block,
19 Borders,
20 List,
21 ListItem,
22 Paragraph,
23 },
24 Frame,
25};
26
27use crate::tui::{
28 state::AppState,
29 theme::Theme,
30};
31
32pub struct FileBrowser {
34 pub selected_index: usize,
35 pub scroll_offset: usize,
36}
37
38impl FileBrowser {
39 pub fn new() -> Self {
40 Self {
41 selected_index: 0,
42 scroll_offset: 0,
43 }
44 }
45
46 pub fn move_up(&mut self) {
47 if self.selected_index > 0 {
48 self.selected_index -= 1;
49 if self.selected_index < self.scroll_offset {
50 self.scroll_offset = self.selected_index;
51 }
52 }
53 }
54
55 pub fn move_down(&mut self, max: usize) {
56 if self.selected_index < max.saturating_sub(1) {
57 self.selected_index += 1;
58 }
59 }
60
61 pub fn get_selected_entry(&self, dir: &std::path::Path) -> Option<std::path::PathBuf> {
62 let entries = self.get_directory_entries(dir);
63 if self.selected_index < entries.len() {
64 let (name, _is_dir, _, _) = &entries[self.selected_index];
65 if name == ".." {
66 dir.parent().map(|p| p.to_path_buf())
67 } else {
68 Some(dir.join(name))
69 }
70 } else {
71 None
72 }
73 }
74
75 pub fn get_entry_count(&self, dir: &std::path::Path) -> usize {
76 self.get_directory_entries(dir).len()
77 }
78
79 pub fn render(&mut self, f: &mut Frame, area: Rect, app: &AppState, theme: &Theme) {
80 let block = Block::default()
81 .borders(Borders::ALL)
82 .border_style(theme.border_style(true))
83 .title(" File Browser - Press Esc to close ")
84 .title_alignment(Alignment::Center);
85
86 let inner = block.inner(area);
87 f.render_widget(block, area);
88
89 let chunks = Layout::default()
90 .direction(Direction::Vertical)
91 .constraints([
92 Constraint::Length(2),
93 Constraint::Min(10),
94 Constraint::Length(3),
95 ])
96 .split(inner);
97
98 let current_dir = Paragraph::new(Line::from(vec![
99 Span::styled("Current: ", theme.line_number_style()),
100 Span::styled(
101 app.file_state.current_dir.display().to_string(),
102 theme.info_style(),
103 ),
104 ]));
105 f.render_widget(current_dir, chunks[0]);
106
107 let entries = self.get_directory_entries(&app.file_state.current_dir);
108 let items: Vec<ListItem> = entries
109 .iter()
110 .enumerate()
111 .map(|(idx, (name, is_dir, is_json, is_toon))| {
112 let icon = if *is_dir {
113 "📁"
114 } else if *is_json {
115 "📄"
116 } else if *is_toon {
117 "📋"
118 } else {
119 "📃"
120 };
121
122 let style = if idx == self.selected_index {
123 theme.selection_style()
124 } else if *is_json || *is_toon {
125 theme.highlight_style()
126 } else {
127 theme.normal_style()
128 };
129
130 ListItem::new(Line::from(vec![
131 Span::styled(format!(" {icon} "), style),
132 Span::styled(name, style),
133 ]))
134 })
135 .collect();
136
137 let list = List::new(items);
138 f.render_widget(list, chunks[1]);
139
140 let instructions = Paragraph::new(Line::from(vec![
141 Span::styled("↑↓", theme.info_style()),
142 Span::styled(" Navigate | ", theme.line_number_style()),
143 Span::styled("Enter", theme.info_style()),
144 Span::styled(" Open | ", theme.line_number_style()),
145 Span::styled("Space", theme.info_style()),
146 Span::styled(" Select | ", theme.line_number_style()),
147 Span::styled("Esc", theme.info_style()),
148 Span::styled(" Close", theme.line_number_style()),
149 ]))
150 .alignment(Alignment::Center);
151 f.render_widget(instructions, chunks[2]);
152 }
153
154 fn get_directory_entries(&self, dir: &std::path::Path) -> Vec<(String, bool, bool, bool)> {
155 let mut entries = vec![("..".to_string(), true, false, false)];
156
157 if let Ok(read_dir) = fs::read_dir(dir) {
158 let mut files: Vec<_> = read_dir
159 .filter_map(|entry| entry.ok())
160 .filter_map(|entry| {
161 let path = entry.path();
162 let name = path.file_name()?.to_str()?.to_string();
163 let is_dir = path.is_dir();
164 let is_json =
165 !is_dir && path.extension().and_then(|e| e.to_str()) == Some("json");
166 let is_toon =
167 !is_dir && path.extension().and_then(|e| e.to_str()) == Some("toon");
168 Some((name, is_dir, is_json, is_toon))
169 })
170 .collect();
171
172 files.sort_by(|a, b| {
173 if a.1 == b.1 {
174 a.0.cmp(&b.0)
175 } else {
176 b.1.cmp(&a.1)
177 }
178 });
179
180 entries.extend(files);
181 }
182
183 entries
184 }
185}
186
187impl Default for FileBrowser {
188 fn default() -> Self {
189 Self::new()
190 }
191}