mod anim;
mod archive;
mod config;
#[cfg(feature = "data")]
mod data;
mod dir;
mod docx;
mod epub;
mod fileop;
mod format;
mod git;
mod hex;
mod highlight;
mod html;
mod icons;
mod imgview;
mod ipynb;
mod keynote;
mod lineedit;
mod markdown;
mod marks;
mod media;
mod pdf;
mod pdfium;
mod plain;
mod pptx;
mod query;
mod search;
mod sheet;
mod svg;
mod text;
mod theme;
mod tui;
mod typeahead;
mod util;
mod video;
mod xlsx;
use format::Format;
use std::io::{self, IsTerminal};
use std::path::Path;
use std::process::ExitCode;
use std::{env, fs};
fn main() -> ExitCode {
let code = run();
anim::dump_stats();
code
}
fn run() -> ExitCode {
let mut plain_flag = false;
let mut path: Option<String> = None;
let mut cli_theme: Option<String> = None;
let mut cli_icons: Option<String> = None;
let mut cli_layout: Option<String> = None;
let mut cli_no_git = false;
let mut cli_no_mouse = false;
let mut cli_no_animate = false;
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--plain" | "-p" => plain_flag = true,
"--theme" => cli_theme = args.next(),
"--icons" => cli_icons = args.next(),
"--layout" => cli_layout = args.next(),
"--no-git" => cli_no_git = true,
"--no-mouse" => cli_no_mouse = true,
"--no-animate" => cli_no_animate = true,
"-h" | "--help" => {
eprintln!(
"usage: sucher [--plain] [--theme NAME] [--icons unicode|nerd|none] [--layout auto|miller|double] [--no-git] [--no-mouse] [--no-animate] [file|dir]"
);
return ExitCode::SUCCESS;
}
_ => path = Some(arg),
}
}
let cli_git = if cli_no_git { Some(false) } else { None };
let cli_mouse = if cli_no_mouse { Some(false) } else { None };
let cli_animate = if cli_no_animate { Some(false) } else { None };
let config = config::load(
cli_theme,
cli_icons,
cli_layout,
cli_git,
cli_mouse,
cli_animate,
);
theme::init(config.palette);
anim::set_enabled(config.animate);
let path = path.unwrap_or_else(|| ".".to_string());
let interactive = !plain_flag && io::stdout().is_terminal();
let title = file_title(&path);
match format::classify_path(Path::new(&path)) {
Format::Directory => {
if interactive {
if let Err(e) =
dir::run(path, config.icons, config.layout, config.git, config.mouse)
{
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&dir::dump(&path));
}
}
Format::Image => {
if interactive {
if let Err(e) = imgview::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
match crate::util::image_dimensions(std::path::Path::new(&path)) {
Ok((w, h)) => return emit(&format!("{path}: image {w}×{h}px\n")),
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
}
}
}
Format::Sheet | Format::Data => {
if interactive {
if let Err(e) = sheet::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&sheet::dump(&path));
}
}
Format::Svg => {
if interactive {
if let Err(e) = svg::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&text::dump(&path));
}
}
Format::Pdf => {
if interactive {
if let Err(e) = pdf::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&pdf::dump(&path));
}
}
Format::Video => {
if interactive {
if let Err(e) = video::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&video::dump(&path));
}
}
Format::Docx => {
let src = match docx::to_markdown(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
};
let images = if interactive {
docx::media(&path)
} else {
Vec::new()
};
return render_markdown(interactive, title, src, images, Some(path.clone()));
}
Format::Pptx => {
let src = match pptx::to_markdown(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
};
let images = if interactive {
pptx::media(&path)
} else {
Vec::new()
};
return render_markdown(interactive, title, src, images, Some(path.clone()));
}
Format::Epub => {
let src = match epub::to_markdown(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
};
let images = if interactive {
epub::media(&path)
} else {
Vec::new()
};
return render_markdown(interactive, title, src, images, Some(path.clone()));
}
Format::Ipynb => {
let src = match ipynb::to_markdown(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
};
let images = if interactive {
ipynb::media(&path)
} else {
Vec::new()
};
return render_markdown(interactive, title, src, images, Some(path.clone()));
}
Format::Keynote => {
if interactive {
if let Err(e) = keynote::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&format!("{path}: Keynote presentation\n"));
}
}
Format::Archive => {
if interactive {
if let Err(e) = archive::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&archive::dump(&path));
}
}
Format::Binary => {
if interactive {
if let Err(e) = hex::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&hex::dump(&path));
}
}
Format::Text => {
if interactive {
if let Err(e) = text::run(title, path.clone()) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&text::dump(&path));
}
}
Format::Markdown => {
let src = match fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
};
return render_markdown(interactive, title, src, Vec::new(), Some(path.clone()));
}
Format::Html => {
let src = match html::to_markdown(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("sucher: {path}: {e}");
return ExitCode::FAILURE;
}
};
return render_markdown(interactive, title, src, Vec::new(), Some(path.clone()));
}
f @ (Format::Doc | Format::Audio) => {
return unsupported(&path, f, interactive);
}
}
ExitCode::SUCCESS
}
fn unsupported(path: &str, format: Format, interactive: bool) -> ExitCode {
let name = file_title(path);
let meta = metadata_line(path);
if interactive {
eprintln!("sucher: no viewer for {} ({name})", format.label());
eprintln!(" {meta}");
ExitCode::SUCCESS
} else {
emit(&format!("{meta}\n"))
}
}
fn emit(s: &str) -> ExitCode {
use std::io::Write;
let mut out = io::stdout();
match out.write_all(s.as_bytes()).and_then(|()| out.flush()) {
Ok(()) => ExitCode::SUCCESS,
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(e) => {
eprintln!("sucher: {e}");
ExitCode::FAILURE
}
}
}
fn metadata_line(path: &str) -> String {
match fs::metadata(path) {
Ok(m) => {
let mut s = util::human_size(m.len());
if let Ok(t) = m.modified() {
s.push_str(&format!(" · {}", util::rel_time(t)));
}
s
}
Err(e) => format!("({e})"),
}
}
fn render_markdown(
interactive: bool,
title: String,
src: String,
images: Vec<std::path::PathBuf>,
open: Option<String>,
) -> ExitCode {
if interactive {
if let Err(e) = tui::run(title, src, images, open) {
eprintln!("sucher: {e}");
return ExitCode::FAILURE;
}
} else {
return emit(&plain::render(&src));
}
ExitCode::SUCCESS
}
fn file_title(path: &str) -> String {
Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string())
}
pub fn open_interactive(path: &str) {
let title = file_title(path);
let format = format::classify_path(Path::new(path));
let r = match format {
Format::Image => imgview::run(title, path.to_string()),
Format::Svg => svg::run(title, path.to_string()),
Format::Sheet | Format::Data => sheet::run(title, path.to_string()),
Format::Pdf => pdf::run(title, path.to_string()),
Format::Video => video::run(title, path.to_string()),
Format::Text => text::run(title, path.to_string()),
Format::Docx => match docx::to_markdown(path) {
Ok(src) => tui::run(title, src, docx::media(path), Some(path.to_string())),
Err(e) => {
eprintln!("sucher: {path}: {e}");
Ok(())
}
},
Format::Pptx => match pptx::to_markdown(path) {
Ok(src) => tui::run(title, src, pptx::media(path), Some(path.to_string())),
Err(e) => {
eprintln!("sucher: {path}: {e}");
Ok(())
}
},
Format::Epub => match epub::to_markdown(path) {
Ok(src) => tui::run(title, src, epub::media(path), Some(path.to_string())),
Err(e) => {
eprintln!("sucher: {path}: {e}");
Ok(())
}
},
Format::Ipynb => match ipynb::to_markdown(path) {
Ok(src) => tui::run(title, src, ipynb::media(path), Some(path.to_string())),
Err(e) => {
eprintln!("sucher: {path}: {e}");
Ok(())
}
},
Format::Keynote => keynote::run(title, path.to_string()),
Format::Archive => archive::run(title, path.to_string()),
Format::Binary => hex::run(title, path.to_string()),
Format::Markdown => match fs::read_to_string(path) {
Ok(src) => tui::run(title, src, Vec::new(), Some(path.to_string())),
Err(e) => {
eprintln!("sucher: {path}: {e}");
Ok(())
}
},
Format::Html => match html::to_markdown(path) {
Ok(src) => tui::run(title, src, Vec::new(), Some(path.to_string())),
Err(e) => {
eprintln!("sucher: {path}: {e}");
Ok(())
}
},
Format::Directory | Format::Doc | Format::Audio => {
eprintln!("sucher: no viewer for {} ({title})", format.label());
Ok(())
}
};
if let Err(e) = r {
eprintln!("sucher: {e}");
}
}