Skip to main content

07_file_browser/
07_file_browser.rs

1//! Example 07: File Browser
2//!
3//! Demonstrates real filesystem navigation with a list view.
4//!
5//! Run with: cargo run -p telex-tui --example 07_file_browser
6
7use crossterm::event::KeyCode;
8use crossterm::style::Color;
9use std::fs;
10use std::path::PathBuf;
11use telex::prelude::*;
12
13telex::require_api!(0, 2);
14
15fn main() {
16    telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
17}
18
19struct App;
20
21fn list_directory(path: &PathBuf) -> Vec<String> {
22    let mut entries = Vec::new();
23
24    // Add parent directory option if not at root
25    if path.parent().is_some() {
26        entries.push("..".to_string());
27    }
28
29    if let Ok(read_dir) = fs::read_dir(path) {
30        let mut items: Vec<_> = read_dir
31            .filter_map(|e| e.ok())
32            .map(|e| {
33                let name = e.file_name().to_string_lossy().to_string();
34                if e.path().is_dir() {
35                    format!("{}/", name)
36                } else {
37                    name
38                }
39            })
40            .collect();
41        items.sort();
42        entries.extend(items);
43    }
44
45    entries
46}
47
48impl Component for App {
49    fn render(&self, cx: Scope) -> View {
50        let current_path =
51            state!(cx, || std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
52        let selected = state!(cx, || 0usize);
53        let show_file_info = state!(cx, || false);
54        let selected_file_path = state!(cx, String::new);
55        let show_help = state!(cx, || false);
56        let entries = list_directory(&current_path.get());
57
58        // F1 toggles help
59        cx.use_command(
60            KeyBinding::key(KeyCode::F(1)),
61            with!(show_help => move || show_help.update(|v| *v = !*v)),
62        );
63
64        // Track selection (just updates index, doesn't navigate)
65        let on_select = with!(selected => move |idx: usize| {
66            selected.set(idx);
67        });
68
69        // Dismiss modal
70        let on_dismiss = with!(show_file_info => move || show_file_info.set(false));
71
72        // Open directory or show file info on Enter
73        let entries_for_cmd = entries.clone();
74        cx.use_command(
75            KeyBinding::key(KeyCode::Enter),
76            with!(current_path, selected, show_file_info, selected_file_path => move || {
77                let idx = selected.get();
78                if idx < entries_for_cmd.len() {
79                    let entry = &entries_for_cmd[idx];
80                    let path = current_path.get();
81
82                    if entry == ".." {
83                        if let Some(parent) = path.parent() {
84                            current_path.set(parent.to_path_buf());
85                            selected.set(0);
86                        }
87                    } else if entry.ends_with('/') {
88                        let dir_name = entry.trim_end_matches('/');
89                        let new_path = path.join(dir_name);
90                        current_path.set(new_path);
91                        selected.set(0);
92                    } else {
93                        // It's a file - show info modal
94                        let full_path = path.join(entry);
95                        selected_file_path.set(full_path.to_string_lossy().to_string());
96                        show_file_info.set(true);
97                    }
98                }
99            }),
100        );
101
102        let path_display = current_path.get().to_string_lossy().to_string();
103
104        View::vstack()
105            .child(
106                View::styled_text("File Browser")
107                    .color(Color::Cyan)
108                    .bold()
109                    .build(),
110            )
111            .child(View::gap(1))
112            .child(
113                View::styled_text(&path_display)
114                    .color(Color::Yellow)
115                    .build(),
116            )
117            .child(View::gap(1))
118            .child(
119                View::list()
120                    .items(entries)
121                    .selected(selected.get())
122                    .on_select(on_select)
123                    .build(),
124            )
125            .child(View::gap(1))
126            .child(
127                View::styled_text("↑/↓ navigate • Enter open • F1 help • Ctrl+Q quit")
128                    .dim()
129                    .build(),
130            )
131            .child(
132                View::modal()
133                    .visible(show_file_info.get())
134                    .title("File")
135                    .width(60)
136                    .height(20)
137                    .on_dismiss(on_dismiss)
138                    .child(
139                        View::vstack()
140                            .child(View::text(selected_file_path.get()))
141                            .child(View::gap(1))
142                            .child(View::styled_text("Press Escape to close").dim().build())
143                            .build(),
144                    )
145                    .build(),
146            )
147            .child(
148                View::modal()
149                    .visible(show_help.get())
150                    .title("Example 07: File Browser")
151                    .on_dismiss(with!(show_help => move || show_help.set(false)))
152                    .child(
153                        View::vstack()
154                            .child(View::styled_text("What you're seeing").bold().build())
155                            .child(View::text("• Real filesystem navigation"))
156                            .child(View::text("• cx.use_command() for keyboard shortcuts"))
157                            .child(View::text("• Modal for file details"))
158                            .child(View::gap(1))
159                            .child(View::styled_text("Key concepts").bold().build())
160                            .child(View::text("• std::fs::read_dir for directory listing"))
161                            .child(View::text(
162                                "• KeyBinding::key(KeyCode::Enter) for Enter handling",
163                            ))
164                            .child(View::text("• Directories shown with trailing /"))
165                            .child(View::gap(1))
166                            .child(View::styled_text("Try this").bold().build())
167                            .child(View::text("• Navigate into directories with Enter"))
168                            .child(View::text("• Go up with '..' entry"))
169                            .child(View::text("• Select a file to see its path"))
170                            .child(View::gap(1))
171                            .child(View::styled_text("Next up").bold().build())
172                            .child(View::text(
173                                "→ 08_system_monitor: multiple concurrent streams",
174                            ))
175                            .child(View::gap(1))
176                            .child(View::styled_text("Press Escape to close").dim().build())
177                            .build(),
178                    )
179                    .build(),
180            )
181            .build()
182    }
183}