mod app;
mod fs;
mod persistence;
mod ui;
use std::{
io::{self, stdout},
path::PathBuf,
process,
};
use clap::Parser;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use tui_file_explorer::Theme;
use app::App;
use fs::resolve_output_path;
use ui::draw;
#[derive(Parser)]
#[command(
name = "tfe",
version,
about = "Keyboard-driven two-pane terminal file explorer",
after_help = "\
SHELL INTEGRATION:\n\
Open selected file in $EDITOR: tfe | xargs -r $EDITOR\n\
cd into directory of selection: cd \"$(tfe --print-dir)\"\n\
NUL-delimited output: tfe -0 | xargs -0 wc -l"
)]
struct Cli {
#[arg(value_name = "PATH")]
path: Option<PathBuf>,
#[arg(short, long = "ext", value_name = "EXT", action = clap::ArgAction::Append)]
extensions: Vec<String>,
#[arg(short = 'H', long)]
hidden: bool,
#[arg(short, long, value_name = "THEME")]
theme: Option<String>,
#[arg(long)]
list_themes: bool,
#[arg(long)]
show_themes: bool,
#[arg(long)]
single_pane: bool,
#[arg(long)]
print_dir: bool,
#[arg(short = '0', long)]
null: bool,
}
fn main() {
if let Err(e) = run() {
eprintln!("tfe: {e}");
process::exit(2);
}
}
fn run() -> io::Result<()> {
let cli = Cli::parse();
let themes = Theme::all_presets();
if cli.list_themes {
let max = themes.iter().map(|(n, _, _)| n.len()).max().unwrap_or(0);
println!("{:<width$} DESCRIPTION", "THEME", width = max);
println!("{}", "\u{2500}".repeat(max + 52));
for (name, desc, _) in &themes {
println!("{name:<width$} {desc}", width = max);
}
return Ok(());
}
let saved = persistence::load_state();
let theme_name = cli
.theme
.or_else(|| saved.theme.clone())
.unwrap_or_else(|| "default".to_string());
let theme_idx = persistence::resolve_theme_idx(&theme_name, &themes);
let start_dir = match cli.path {
Some(ref p) => {
let c = p.canonicalize().unwrap_or_else(|_| p.clone());
if c.is_dir() {
c
} else {
eprintln!("tfe: {:?} is not a directory", p);
process::exit(2);
}
}
None => saved
.last_dir
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))),
};
let right_start_dir = saved
.last_dir_right
.clone()
.unwrap_or_else(|| start_dir.clone());
let show_hidden = if cli.hidden {
true
} else {
saved.show_hidden.unwrap_or(false)
};
let single_pane = if cli.single_pane {
true
} else {
saved.single_pane.unwrap_or(false)
};
let sort_mode = saved.sort_mode.unwrap_or_default();
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(app::AppOptions {
left_dir: start_dir,
right_dir: right_start_dir,
extensions: cli.extensions,
show_hidden,
theme_idx,
show_theme_panel: cli.show_themes,
single_pane,
sort_mode,
});
let result = run_loop(&mut terminal, &mut app);
let _ = disable_raw_mode();
let _ = execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
);
let _ = terminal.show_cursor();
result?;
persistence::save_state(&persistence::AppState {
theme: Some(app.theme_name().to_string()),
last_dir: Some(app.left.current_dir.clone()),
last_dir_right: Some(app.right.current_dir.clone()),
sort_mode: Some(app.left.sort_mode),
show_hidden: Some(app.left.show_hidden),
single_pane: Some(app.single_pane),
});
match app.selected {
Some(path) => {
let output = resolve_output_path(path, cli.print_dir);
fs::emit_path(&output, cli.null)?;
}
None => process::exit(1),
}
Ok(())
}
fn run_loop(
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
app: &mut App,
) -> io::Result<()> {
loop {
terminal.draw(|frame| draw(app, frame))?;
if app.handle_event()? {
break;
}
}
Ok(())
}