//! 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(()) }