phetch 1.2.0

quick lil gopher client
Documentation
use phetch::{
    args,
    config::{Config, SharedConfig},
    gopher, menu, terminal, theme,
    ui::{Mode, UI},
};
use std::{
    env,
    error::Error,
    io::{self, stdout, Write},
    panic, process, str,
};

fn main() {
    if let Err(e) = run() {
        eprintln!("{}", e);
        process::exit(1);
    }
}

/// Start the app. Returns UNIX exit code.
fn run() -> Result<(), Box<dyn Error>> {
    let str_args = env::args().skip(1).collect::<Vec<String>>();
    let mut cfg = args::parse(&str_args)?;

    // check for simple modes
    match cfg.mode {
        Mode::Raw => return print_raw(&cfg.start, cfg.tls, cfg.tor),
        Mode::Version => return print_version(),
        Mode::Help => return print_usage(),
        Mode::PrintTheme => return print_theme(cfg),
        Mode::NoTTY => return print_plain(&cfg.start, cfg.tls, cfg.tor),
        Mode::Print => cfg.wide = true,
        Mode::Run => {}
    }

    // load url
    let start = cfg.start.clone();
    let mode = cfg.mode;
    let mut ui = UI::new(cfg);
    ui.open(&start, &start)?;

    // print rendered version
    if mode == Mode::Print {
        println!("{}", ui.render()?);
        return Ok(());
    }

    // run app
    setup_terminal();
    ui.run()?;
    cleanup_terminal();

    Ok(())
}

/// --version
fn print_version() -> Result<(), Box<dyn Error>> {
    println!(
        "phetch v{version} ({built})",
        built = phetch::BUILD_DATE,
        version = phetch::VERSION
    );
    Ok(())
}

/// --help
fn print_usage() -> Result<(), Box<dyn Error>> {
    print_version()?;
    println!(
        "
Usage:

    phetch [options]       Launch phetch in interactive mode
    phetch [options] url   Open Gopher URL in interactive mode

Options:

    -s, --tls              Try to open Gopher URLs securely w/ TLS
    -o, --tor              Use local Tor proxy to open all pages
    -S, -O                 Disable TLS or Tor

    -w, --wrap COLUMN      Wrap long lines in \"text\" views at COLUMN.
    -m, --media PROGRAM    Use to open media files. Default: mpv
    -M, --no-media         Just download media files, don't download

    -a, --autoplay         Autoplay media without prompting.
    -A, --no-autoplay      Prompt before playing media.

    -r, --raw              Print raw Gopher response only
    -p, --print            Print rendered Gopher response only
    -l, --local            Connect to 127.0.0.1:7070
    -e, --encoding         Render text documents in CP437 or UTF8.

    -c, --config FILE      Use instead of ~/.config/phetch/phetch.conf
    -C, --no-config        Don't use any config file
    -t, --theme FILE       Use FILE for color theme or print current theme.
    --print-theme          Print current theme.

    -h, --help             Show this screen
    -v, --version          Show phetch version

Command line options always override options set in phetch.conf.

Once you've launched phetch, use `ctrl-h` to view the on-line help."
    );
    Ok(())
}

/// Print just the raw Gopher response.
fn print_raw(url: &str, tls: bool, tor: bool) -> Result<(), Box<dyn Error>> {
    let (_, out) = gopher::fetch_url(url, tls, tor)?;
    println!("{}", gopher::response_to_string(&out));
    Ok(())
}

/// Print a colorless, plain version of the response for a non-tty
/// (like a pipe).
fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box<dyn Error>> {
    let mut out = String::new();
    let typ = gopher::type_for_url(url);
    let (_, response) = gopher::fetch_url(url, tls, tor)?;
    let response = gopher::response_to_string(&response);
    match typ {
        gopher::Type::Menu => {
            let menu = menu::parse(url, response, SharedConfig::default());
            for line in menu.lines() {
                out.push_str(line.text());
                out.push('\n');
            }
        }
        gopher::Type::Text => println!("{}", response.trim_end_matches(".\r\n")),
        _ => {
            return Err(Box::new(io::Error::new(
                io::ErrorKind::Other,
                format!("can't print gopher type: {:?}", typ),
            )));
        }
    };
    print!("{}", out);
    Ok(())
}

/// Print current theme as plaintext
fn print_theme(cfg: Config) -> Result<(), Box<dyn Error>> {
    println!("{}", cfg.theme);
    Ok(())
}

/// Put the terminal into raw mode, enter the alternate screen, and
/// setup the panic handler.
fn setup_terminal() {
    let old_handler = panic::take_hook();
    panic::set_hook(Box::new(move |info| {
        cleanup_terminal();
        old_handler(info);
    }));

    terminal::enable_raw_mode().expect("Fatal Error entering Raw Mode.");
    write!(stdout(), "{}", terminal::ToAlternateScreen)
        .expect("Fatal Error entering Alternate Mode.");
}

/// Leave raw mode. Need to always do this, even on panic.
fn cleanup_terminal() {
    let mut stdout = stdout();
    write!(
        stdout,
        "{}{}{}{}{}",
        theme::color::Reset,
        terminal::ClearAll,
        terminal::Goto(1, 1),
        terminal::ShowCursor,
        terminal::ToMainScreen
    )
    .expect("Fatal Error cleaning up terminal.");
    stdout.flush().expect("Fatal Error cleaning up terminal.");
    terminal::disable_raw_mode().expect("Fatal Error leaving Raw Mode.");
}