Add install-man cli subcommand to install manpages on your system
Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 9m28s Details
Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Successful in 10m46s Details

If meli is installed via cargo or a package without manpages, this
command can be used to install them to the user's system.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/300/head
Manos Pitsidianakis 2023-09-09 12:24:07 +03:00
parent 747e39bf55
commit 45d4f611b1
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
7 changed files with 225 additions and 86 deletions

View File

@ -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);
}
}

View File

@ -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

View File

@ -23,31 +23,6 @@
use super::*;
#[cfg(feature = "cli-docs")]
fn parse_manpage(src: &str) -> Result<ManPages> {
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<PathBuf>,
},
#[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<Option<bool>>,
}
#[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<ManPages> {
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<PathBuf>) -> Result<PathBuf> {
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> 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)
}
}
}

View File

@ -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<FileSettings> {
let config_path = get_config_file()?;

View File

@ -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 => {}
}

View File

@ -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<String> {
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::<usize>(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<Option<bool>>) -> 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(());
}

View File

@ -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();
}
}
}
}
}