Add install-man cli subcommand to install manpages on your system
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
parent
747e39bf55
commit
45d4f611b1
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
152
meli/src/args.rs
152
meli/src/args.rs
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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 => {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(());
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue