phetch 1.2.0

quick lil gopher client
Documentation
//! Helper functions and macros.
use std::{
    borrow::Cow,
    io::{Result, Write},
    process::{self, Stdio},
};

/// Debug macro that appends a line to `phetch.log`.
/// Useful for printf-style debugging - add your `log!()` calls,
/// and `tail -f phetch.log` while running phetch to peek inside.
#[allow(unused_macros)]
macro_rules! log {
    ($e:expr) => {{
        if cfg!(debug_assertions) {
            if let Ok(mut file) = std::fs::OpenOptions::new()
            .append(true)
            .create(true)
            .open("phetch.log")
        {
            use std::io::prelude::*;
            file.write($e.as_ref()).unwrap();
            file.write(b"\n").unwrap();
        }
    }
    }};
    ($e:expr, $($y:expr),*) => {
        if cfg!(debug_assertions) {
            log!(format!($e, $($y),*));
        }
    };
}

/// Creates an Other kind of io::Error.
macro_rules! error {
    ($e:expr) => {
        std::io::Error::new(std::io::ErrorKind::Other, $e)
    };
    ($e:expr, $($y:expr),*) => {
        error!(format!($e, $($y),*))
    };
}

/// Number of bytes in a human-ish readable format.
pub fn human_bytes(bytes: usize) -> String {
    let (count, tag) = if bytes < 1000 {
        (bytes, " bytes")
    } else if bytes < 1_000_000 {
        (bytes / 1000, "Kb")
    } else if bytes < 1_000_000_000 {
        (bytes / 1_000_000, "Mb")
    } else {
        (bytes / 1_000_000_000, "Gb")
    };

    format!("{}{}", count, tag)
}

/// Copies data to the system clipboard, if possible.
/// Uses `pbcopy` on macOS or `xclip -sel clip` on Linux.
pub fn copy_to_clipboard(data: &str) -> Result<()> {
    #[cfg(target_os = "macos")]
    let mut cmd = process::Command::new("pbcopy");
    #[cfg(not(target_os = "macos"))]
    let mut cmd = process::Command::new("xclip");
    #[cfg(not(target_os = "macos"))]
    let cmd = cmd.args(&["-sel", "clip"]);

    cmd.stdin(Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            let child_stdin = child.stdin.as_mut().unwrap();
            child_stdin.write_all(data.as_bytes())
        })
        .map_err(|e| error!("Clipboard error: {}", e))
}

/// Used to open non-Gopher URLs.
/// Runs `open` command on macOS or `xdg-open` on Linux.
pub fn open_external(url: &str) -> Result<()> {
    #[cfg(target_os = "macos")]
    let cmd = "open";
    #[cfg(not(target_os = "macos"))]
    let cmd = "xdg-open";

    let output = process::Command::new(cmd)
        .arg(url)
        .output()
        .map_err(|e| error!("`open` error: {}", e))?;

    if output.stderr.is_empty() {
        Ok(())
    } else {
        Err(error!(
            "`open` error: {}",
            String::from_utf8_lossy(&output.stderr).trim_end()
        ))
    }
}

/// Opens a media file with `mpv` or `--media`.
pub fn open_media(program: &str, url: &str) -> Result<()> {
    use {crate::terminal, std::io};

    // mpv only supports /9/
    let url = if program.ends_with("mpv") {
        Cow::from(url.replace("/;/", "/9/").replace("/s/", "/9/"))
    } else {
        Cow::from(url)
    };

    // support URL: selectors
    let url = if let Some(idx) = url.find("URL:") {
        url.split_at(idx).1.trim_start_matches("URL:")
    } else {
        &url
    };

    let errfn = |e| {
        terminal::enable_raw_mode().unwrap();
        error!("Media player error: {}", e)
    };

    // clear screen first
    let mut stdout = io::stdout();
    write!(stdout, "{}{}", terminal::ClearAll, terminal::Goto(1, 1))?;
    stdout.flush()?;

    terminal::disable_raw_mode()?;
    let mut cmd = process::Command::new(program)
        .arg(url)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .spawn()
        .map_err(errfn)?;
    cmd.wait().map_err(errfn)?;
    terminal::enable_raw_mode()?;

    Ok(())
}