use std::{
io::{self, stdout},
path::PathBuf,
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph},
Frame, Terminal,
};
use tui_file_explorer::{render_themed, ExplorerOutcome, FileExplorer, Theme};
struct App {
explorer: FileExplorer,
selected: Option<PathBuf>,
message: String,
}
impl App {
fn new(extension_filter: Vec<String>) -> Self {
let explorer =
FileExplorer::builder(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")))
.extension_filter(extension_filter)
.show_hidden(false)
.build();
Self {
explorer,
selected: None,
message: String::new(),
}
}
fn handle_event(&mut self) -> io::Result<bool> {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
return Ok(true); }
match self.explorer.handle_key(key) {
ExplorerOutcome::Selected(path) => {
self.message = format!("✓ Selected: {}", path.display());
self.selected = Some(path);
}
ExplorerOutcome::Dismissed => {
return Ok(true); }
ExplorerOutcome::Pending => {}
ExplorerOutcome::Unhandled => {}
}
}
Ok(false)
}
}
fn draw(app: &mut App, frame: &mut Frame, theme: &Theme) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(5)])
.split(frame.area());
render_themed(&mut app.explorer, frame, chunks[0], theme);
let selected_line = match &app.selected {
Some(p) => Span::styled(
format!(" {}", p.display()),
Style::default()
.fg(Color::Rgb(80, 220, 120))
.add_modifier(Modifier::BOLD),
),
None => Span::styled(
" (nothing selected yet)",
Style::default().fg(Color::Rgb(120, 120, 130)),
),
};
let hint_line = Span::styled(
" ↑/↓ navigate Enter confirm Backspace ascend . toggle hidden Esc/q quit",
Style::default().fg(Color::Rgb(120, 120, 130)),
);
let msg_line = if app.message.is_empty() {
Line::from(vec![])
} else {
Line::from(vec![Span::styled(
format!(" {}", &app.message),
Style::default().fg(Color::Rgb(80, 200, 255)),
)])
};
let output = Paragraph::new(vec![
Line::from(vec![selected_line]),
msg_line,
Line::from(vec![hint_line]),
])
.block(
Block::default()
.title(Span::styled(
" Output ",
Style::default()
.fg(theme.brand)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent)),
)
.alignment(Alignment::Left);
frame.render_widget(output, chunks[1]);
}
fn main() -> io::Result<()> {
let extension_filter: Vec<String> = std::env::args().skip(1).collect();
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let theme = Theme::default()
.brand(Color::Rgb(200, 120, 255)) .accent(Color::Rgb(130, 180, 255)) .dir(Color::Rgb(255, 200, 80)) .sel_bg(Color::Rgb(50, 40, 80)) .success(Color::Rgb(100, 230, 140)) .match_file(Color::Rgb(100, 230, 140));
let mut app = App::new(extension_filter);
loop {
terminal.draw(|frame| draw(&mut app, frame, &theme))?;
if app.handle_event()? {
break;
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Some(path) = &app.selected {
println!("Selected: {}", path.display());
} else {
println!("No file selected.");
}
Ok(())
}