diff --git a/meli/build.rs b/meli/build.rs index fb65c71f..02b8d43f 100644 --- a/meli/build.rs +++ b/meli/build.rs @@ -45,32 +45,42 @@ fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let mut out_dir_path = Path::new(&out_dir).to_path_buf(); - let mut cl = |filepath: &str, output: &str| { + let mut cl = |filepath: &str, output: &str, source: bool| { out_dir_path.push(output); - let output = Command::new("mandoc") - .args(MANDOC_OPTS) - .arg(filepath) - .output() - .or_else(|_| Command::new("man").arg("-l").arg(filepath).output()) - .expect( - "could not execute `mandoc` or `man`. If the binaries are not available in \ - the PATH, disable `cli-docs` feature to be able to continue compilation.", - ); + let output = if source { + std::fs::read_to_string(filepath).unwrap().into_bytes() + } else { + let output = Command::new("mandoc") + .args(MANDOC_OPTS) + .arg(filepath) + .output() + .or_else(|_| Command::new("man").arg("-l").arg(filepath).output()) + .expect( + "could not execute `mandoc` or `man`. If the binaries are not available \ + in the PATH, disable `cli-docs` feature to be able to continue \ + compilation.", + ); + output.stdout + }; let file = File::create(&out_dir_path).unwrap_or_else(|err| { panic!("Could not create file {}: {}", out_dir_path.display(), err) }); let mut gz = GzBuilder::new() - .comment(output.stdout.len().to_string().into_bytes()) + .comment(output.len().to_string().into_bytes()) .write(file, Compression::default()); - gz.write_all(&output.stdout).unwrap(); + gz.write_all(&output).unwrap(); gz.finish().unwrap(); out_dir_path.pop(); }; - cl("docs/meli.1", "meli.txt.gz"); - cl("docs/meli.conf.5", "meli.conf.txt.gz"); - cl("docs/meli-themes.5", "meli-themes.txt.gz"); - cl("docs/meli.7", "meli.7.txt.gz"); + cl("docs/meli.1", "meli.txt.gz", false); + cl("docs/meli.conf.5", "meli.conf.txt.gz", false); + cl("docs/meli-themes.5", "meli-themes.txt.gz", false); + cl("docs/meli.7", "meli.7.txt.gz", false); + cl("docs/meli.1", "meli.mdoc.gz", true); + cl("docs/meli.conf.5", "meli.conf.mdoc.gz", true); + cl("docs/meli-themes.5", "meli-themes.mdoc.gz", true); + cl("docs/meli.7", "meli.7.mdoc.gz", true); } } diff --git a/meli/docs/meli.1 b/meli/docs/meli.1 index 9bf1c2a9..7afd2477 100644 --- a/meli/docs/meli.1 +++ b/meli/docs/meli.1 @@ -75,12 +75,18 @@ or Test a configuration file for syntax issues or missing options. .It Cm man Op Ar page Print documentation page and exit (Piping to a pager is recommended). +.It Cm install-man Op Ar path +Install manual pages to the first location provided by +.Ar MANPATH +or +.Xr manpath 1 , +unless you specify the directory as an argument. .It Cm print-default-theme Print default theme keys and values in TOML syntax, to be used as a blueprint. .It Cm print-loaded-themes Print all loaded themes in TOML syntax. .It Cm print-used-paths -Print all paths that meli creates/uses. +Print all paths that are created and used. .It Cm compiled-with Print compile time feature flags of this binary. .It Cm view diff --git a/meli/src/args.rs b/meli/src/args.rs index f9bb14d6..9db2dc6d 100644 --- a/meli/src/args.rs +++ b/meli/src/args.rs @@ -23,31 +23,6 @@ use super::*; -#[cfg(feature = "cli-docs")] -fn parse_manpage(src: &str) -> Result { - match src { - "" | "meli" | "meli.1" | "main" => Ok(ManPages::Main), - "meli.7" | "guide" => Ok(ManPages::Guide), - "meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf), - "meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => Ok(ManPages::Themes), - _ => Err(Error::new(format!("Invalid documentation page: {}", src))), - } -} - -#[cfg(feature = "cli-docs")] -#[derive(Copy, Clone, Debug)] -/// Choose manpage -pub enum ManPages { - /// meli(1) - Main = 0, - /// meli.conf(5) - Conf = 1, - /// meli-themes(5) - Themes = 2, - /// meli(7) - Guide = 3, -} - #[derive(Debug, StructOpt)] #[structopt(name = "meli", about = "terminal mail client", version_short = "v")] pub struct Opt { @@ -89,9 +64,15 @@ pub enum SubCommand { Man(ManOpt), #[structopt(display_order = 4)] + /// Install manual pages to the first location provided by $MANPATH / + /// manpath(1), unless you specify the directory as an argument. + InstallMan { + #[structopt(value_name = "DESTINATION_PATH", parse(from_os_str))] + destination_path: Option, + }, + #[structopt(display_order = 5)] /// print compile time feature flags of this binary CompiledWith, - /// View mail from input file. View { #[structopt(value_name = "INPUT", parse(from_os_str))] @@ -101,11 +82,126 @@ pub enum SubCommand { #[derive(Debug, StructOpt)] pub struct ManOpt { - #[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = parse_manpage))] + #[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = manpages::parse_manpage))] #[cfg(feature = "cli-docs")] - pub page: ManPages, + pub page: manpages::ManPages, /// If true, output text in stdout instead of spawning $PAGER. #[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")] #[cfg(feature = "cli-docs")] pub no_raw: Option>, } + +#[cfg(feature = "cli-docs")] +pub mod manpages { + use std::{ + env, fs, + path::{Path, PathBuf}, + sync::Arc, + }; + + use melib::log; + + use crate::{Error, Result}; + + pub fn parse_manpage(src: &str) -> Result { + match src { + "" | "meli" | "meli.1" | "main" => Ok(ManPages::Main), + "meli.7" | "guide" => Ok(ManPages::Guide), + "meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf), + "meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => { + Ok(ManPages::Themes) + } + _ => Err(Error::new(format!("Invalid documentation page: {src}",))), + } + } + + #[derive(Copy, Clone, Debug)] + /// Choose manpage + pub enum ManPages { + /// meli(1) + Main = 0, + /// meli.conf(5) + Conf = 1, + /// meli-themes(5) + Themes = 2, + /// meli(7) + Guide = 3, + } + + impl std::fmt::Display for ManPages { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + fmt, + "{}", + match self { + Self::Main => "meli.1", + Self::Conf => "meli.conf.5", + Self::Themes => "meli-themes.5", + Self::Guide => "meli.7", + } + ) + } + } + + impl ManPages { + pub fn install(destination: Option) -> Result { + fn path_valid(p: &Path, tries: &mut Vec) -> bool { + tries.push(p.into()); + p.exists() + && p.is_dir() + && fs::metadata(p) + .ok() + .map(|m| !m.permissions().readonly()) + .unwrap_or(false) + } + + let mut tries = vec![]; + let Some(mut path) = destination + .filter(|p| path_valid(p, &mut tries)) + .or_else(|| { + if let Some(paths) = env::var_os("MANPATH") { + if let Some(path) = + env::split_paths(&paths).find(|p| path_valid(p, &mut tries)) + { + return Some(path); + } + } + None + }) + .or_else(|| { + #[allow(deprecated)] + env::home_dir() + .map(|p| p.join("local").join("share")) + .filter(|p| path_valid(p, &mut tries)) + }) + else { + return Err(format!("Could not write to any of these paths: {:?}", tries).into()); + }; + + for (p, dir) in [ + (ManPages::Main, "man1"), + (ManPages::Conf, "man5"), + (ManPages::Themes, "man5"), + (ManPages::Guide, "man7"), + ] { + let text = crate::subcommands::man(p, true)?; + path.push(dir); + std::fs::create_dir_all(&path).map_err(|err| { + Error::new(format!("Could not create {} directory.", path.display())) + .set_source(Some(Arc::new(err))) + })?; + path.push(&p.to_string()); + + fs::write(&path, text.as_bytes()).map_err(|err| { + Error::new(format!("Could not write to {}", path.display())) + .set_source(Some(Arc::new(err))) + })?; + log::trace!("Installed {} to {}", p, path.display()); + path.pop(); + path.pop(); + } + + Ok(path) + } + } +} diff --git a/meli/src/conf.rs b/meli/src/conf.rs index 4e472c02..21dc4af1 100644 --- a/meli/src/conf.rs +++ b/meli/src/conf.rs @@ -33,7 +33,10 @@ use std::{ use melib::{backends::TagHash, search::Query, SortField, SortOrder, StderrLogger}; -use crate::{conf::deserializers::non_empty_opt_string, terminal::Color}; +use crate::{ + conf::deserializers::non_empty_opt_string, + terminal::{Ask, Color}, +}; #[rustfmt::skip] mod overrides; @@ -403,40 +406,6 @@ define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl Ok(String::from_utf8_lossy(&stdout).to_string()) } -struct Ask { - message: String, -} - -impl Ask { - fn run(self) -> bool { - let mut buffer = String::new(); - let stdin = io::stdin(); - let mut handle = stdin.lock(); - - print!("{} [Y/n] ", &self.message); - let _ = io::stdout().flush(); - loop { - buffer.clear(); - handle - .read_line(&mut buffer) - .expect("Could not read from stdin."); - - match buffer.trim() { - "" | "Y" | "y" | "yes" | "YES" | "Yes" => { - return true; - } - "n" | "N" | "no" | "No" | "NO" => { - return false; - } - _ => { - print!("\n{} [Y/n] ", &self.message); - let _ = io::stdout().flush(); - } - } - } - } -} - impl FileSettings { pub fn new() -> Result { let config_path = get_config_file()?; diff --git a/meli/src/main.rs b/meli/src/main.rs index cd5b4c10..47e7d1d4 100644 --- a/meli/src/main.rs +++ b/meli/src/main.rs @@ -101,8 +101,8 @@ fn run_app(opt: Opt) -> Result<()> { Some(SubCommand::EditConfig) => { return subcommands::edit_config(); } - Some(SubCommand::Man(manopt)) => { - return subcommands::man(manopt); + Some(SubCommand::Man(ManOpt { page, no_raw })) => { + return subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw)); } Some(SubCommand::CompiledWith) => { return subcommands::compiled_with(); @@ -133,6 +133,13 @@ fn run_app(opt: Opt) -> Result<()> { println!("{}", temp_dir.display()); return Ok(()); } + Some(SubCommand::InstallMan { destination_path }) => { + match args::manpages::ManPages::install(destination_path) { + Ok(p) => println!("Installed at {}.", p.display()), + Err(err) => return Err(err), + } + return Ok(()); + } None => {} } diff --git a/meli/src/subcommands.rs b/meli/src/subcommands.rs index 87a5dcc8..7c0f516d 100644 --- a/meli/src/subcommands.rs +++ b/meli/src/subcommands.rs @@ -73,15 +73,25 @@ pub fn edit_config() -> Result<()> { } #[cfg(feature = "cli-docs")] -pub fn man(ManOpt { page, no_raw }: ManOpt) -> Result<()> { +pub fn man(page: manpages::ManPages, source: bool) -> Result { const MANPAGES: [&[u8]; 4] = [ include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")), include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")), include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")), include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")), ]; + const MANPAGES_MDOC: [&[u8]; 4] = [ + include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")), + include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")), + ]; - let mut gz = GzDecoder::new(MANPAGES[page as usize]); + let mut gz = GzDecoder::new(if source { + MANPAGES_MDOC[page as usize] + } else { + MANPAGES[page as usize] + }); let mut v = String::with_capacity( str::parse::(unsafe { std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap()) @@ -90,17 +100,22 @@ pub fn man(ManOpt { page, no_raw }: ManOpt) -> Result<()> { ); gz.read_to_string(&mut v)?; + Ok(v) +} + +#[cfg(feature = "cli-docs")] +pub fn pager(v: String, no_raw: Option>) -> Result<()> { if let Some(no_raw) = no_raw { match no_raw { Some(true) => {} None if (unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }) => {} Some(false) | None => { - println!("{}", &v); + println!("{v}"); return Ok(()); } } } else if unsafe { libc::isatty(libc::STDOUT_FILENO) != 1 } { - println!("{}", &v); + println!("{v}"); return Ok(()); } diff --git a/meli/src/terminal.rs b/meli/src/terminal.rs index e50e4ea8..7ab57dab 100644 --- a/meli/src/terminal.rs +++ b/meli/src/terminal.rs @@ -36,6 +36,8 @@ pub mod keys; pub mod embed; pub mod text_editing; +use std::io::{BufRead, Write}; + pub use braille::BraillePixelIter; pub use screen::{Screen, StateStdout}; @@ -123,3 +125,37 @@ derive_csi_sequence!( #[doc = "Empty struct with a Display implementation that returns the byte sequence to end [Bracketed Paste Mode](http://www.xfree86.org/current/ctlseqs.html#Bracketed%20Paste%20Mode)"] (BracketModeEnd, "?2004l") ); + +pub struct Ask { + pub message: String, +} + +impl Ask { + pub fn run(self) -> bool { + let mut buffer = String::new(); + let stdin = std::io::stdin(); + let mut handle = stdin.lock(); + + print!("{} [Y/n] ", &self.message); + let _ = std::io::stdout().flush(); + loop { + buffer.clear(); + handle + .read_line(&mut buffer) + .expect("Could not read from stdin."); + + match buffer.trim() { + "" | "Y" | "y" | "yes" | "YES" | "Yes" => { + return true; + } + "n" | "N" | "no" | "No" | "NO" => { + return false; + } + _ => { + print!("\n{} [Y/n] ", &self.message); + let _ = std::io::stdout().flush(); + } + } + } + } +}