mod app;
mod background;
mod cli;
mod constants;
mod display;
mod find;
mod fs_ops;
mod glob;
mod input;
mod options;
mod search;
mod strip;
mod ui;
mod utils;
mod viewer;
use app::AppState;
use color_eyre::Result;
use crossterm::{
cursor::Show,
event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use input::handle_input;
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io::{self, IsTerminal, Stdout, stdout};
use ui::render_ui;
type Tui = Terminal<CrosstermBackend<Stdout>>;
fn main() -> Result<()> {
color_eyre::install()?;
let (left, right) = match cli::parse(std::env::args_os().skip(1)) {
Ok(cli::Command::Help) => {
println!("{}", cli::help());
return Ok(());
}
Ok(cli::Command::Version) => {
println!("{}", cli::version());
return Ok(());
}
Ok(cli::Command::Run { left, right }) => (start_dir(left), start_dir(right)),
Err(message) => usage_error(&message),
};
if !stdout().is_terminal() {
eprintln!("fm84 needs a terminal.");
eprintln!();
eprintln!("Run it from one, or launch it through one, for example:");
eprintln!(" kitty -e fm84");
eprintln!();
eprintln!("In a .desktop file, set Terminal=true or make Exec start a terminal.");
std::process::exit(1);
}
install_panic_hook();
let mut terminal = init_terminal()?;
let run_result = run(&mut terminal, left, right);
let restore_result = restore_terminal();
run_result?;
restore_result?;
Ok(())
}
fn start_dir(dir: Option<std::path::PathBuf>) -> Option<std::path::PathBuf> {
let dir = dir?;
match std::path::absolute(&dir) {
Ok(absolute) if absolute.is_dir() => Some(absolute),
Ok(_) => usage_error(&format!("not a directory: {}", dir.display())),
Err(e) => usage_error(&format!("cannot use {}: {}", dir.display(), e)),
}
}
fn usage_error(message: &str) -> ! {
eprintln!("fm84: {message}");
eprintln!("Try 'fm84 --help' for more information.");
std::process::exit(2);
}
fn init_terminal() -> io::Result<Tui> {
take_terminal()?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn take_terminal() -> io::Result<()> {
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste)
}
fn restore_terminal() -> io::Result<()> {
execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, DisableBracketedPaste, Show)?;
disable_raw_mode()
}
fn install_panic_hook() {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = restore_terminal();
hook(panic_info);
}));
}
fn run(terminal: &mut Tui, left: Option<std::path::PathBuf>, right: Option<std::path::PathBuf>) -> io::Result<()> {
let mut app_state = AppState::new();
display::apply(&app_state.options);
if app_state.options.remember_dirs
&& let Some((left, right)) = options::load_session()
{
app_state.dir_left = left.dir().to_path_buf();
app_state.dir_right = right.dir().to_path_buf();
app_state.tabs_left = app::Tabs::new(left.tabs, left.active);
app_state.tabs_right = app::Tabs::new(right.tabs, right.active);
}
if let Some(left) = left {
app_state.dir_left = left;
}
if let Some(right) = right {
app_state.dir_right = right;
}
app_state.show_preview = app_state.options.preview_on_start;
app_state.reload_panel(true, None);
app_state.reload_panel(false, None);
loop {
app_state.poll_listings();
app_state.poll_watchers();
app_state.refresh_cursor_detail();
app_state.refresh_preview();
app_state.poll_transfer();
app_state.poll_find_files();
app_state.poll_dir_sizes();
app_state.image_cache.collect();
render_ui(terminal, &mut app_state);
let refitted = app_state.fit_viewer_image() | app_state.fit_viewer_wrap();
if refitted {
render_ui(terminal, &mut app_state);
}
app_state.refresh_stale_panels();
if !handle_input(&mut app_state)? {
break;
}
if let Some(path) = app_state.external_edit.take() {
run_external_editor(terminal, &mut app_state, &path)?;
}
}
if app_state.options.remember_dirs {
let panel = |(tabs, active)| options::PanelSession { tabs, active };
let _ = options::save_session(&panel(app_state.tab_dirs(true)), &panel(app_state.tab_dirs(false)));
}
Ok(())
}
fn run_external_editor(terminal: &mut Tui, app_state: &mut AppState, path: &std::path::Path) -> io::Result<()> {
let command = app_state.options.editor.clone();
let mut parts: Vec<std::ffi::OsString> = command.split_whitespace().map(|part| utils::substitute(part, path.as_os_str())).collect();
if !command.contains("{}") {
parts.push(path.as_os_str().to_owned());
}
let program = parts.remove(0);
restore_terminal()?;
let result = std::process::Command::new(&program).args(&parts).current_dir(path.parent().unwrap_or(std::path::Path::new("."))).status();
take_terminal()?;
terminal.clear()?;
match result {
Ok(status) if !status.success() => app_state.display_error(format!("{} exited with {}", program.display(), status)),
Ok(_) => {}
Err(e) => app_state.display_error(format!("Cannot start {}: {}", program.display(), e)),
}
app_state.reload_panel(true, None);
app_state.reload_panel(false, None);
Ok(())
}