forked from meli/meli
1
Fork 0

meli: add edit-config CLI subcommand that opens config files on EDITOR

duesee/experiment/use_imap_codec
Manos Pitsidianakis 2023-04-26 13:35:29 +03:00
parent 39d9c2af3b
commit 47e6d5d935
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
3 changed files with 227 additions and 87 deletions

107
src/args.rs 100644
View File

@ -0,0 +1,107 @@
/*
* meli - args.rs
*
* Copyright 2017-2023 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! Command line arguments.
use meli::*;
#[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 {
/// use specified configuration file
#[structopt(short, long, parse(from_os_str))]
pub config: Option<PathBuf>,
#[structopt(subcommand)]
pub subcommand: Option<SubCommand>,
}
#[derive(Debug, StructOpt)]
pub enum SubCommand {
/// print default theme in full to stdout and exit.
PrintDefaultTheme,
/// print loaded themes in full to stdout and exit.
PrintLoadedThemes,
/// edit configuration files in `$EDITOR`/`$VISUAL`.
EditConfig,
/// create a sample configuration file with available configuration options. If PATH is not specified, meli will try to create it in $XDG_CONFIG_HOME/meli/config.toml
#[structopt(display_order = 1)]
CreateConfig {
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
},
/// test a configuration file for syntax issues or missing options.
#[structopt(display_order = 2)]
TestConfig {
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
},
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
#[structopt(display_order = 3)]
/// print documentation page and exit (Piping to a pager is recommended.).
Man(ManOpt),
#[structopt(display_order = 4)]
/// print compile time feature flags of this binary
CompiledWith,
/// View mail from input file.
View {
#[structopt(value_name = "INPUT", parse(from_os_str))]
path: PathBuf,
},
}
#[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))]
#[cfg(feature = "cli-docs")]
pub page: 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>>,
}

View File

@ -31,6 +31,9 @@ use crate::terminal::Color;
use melib::backends::TagHash;
use melib::search::Query;
use std::collections::HashSet;
use std::io::Read;
use std::process::{Command, Stdio};
mod overrides;
pub use overrides::*;
pub mod composing;
@ -304,6 +307,93 @@ pub fn get_config_file() -> Result<PathBuf> {
}
}
pub fn get_included_configs(conf_path: PathBuf) -> Result<Vec<PathBuf>> {
const M4_PREAMBLE: &str = r#"divert(-1)dnl
define(`include', `divert(0)$1
divert(-1)
')dnl
changequote(`"', `"')dnl
"#;
let mut ret = vec![];
let prefix = conf_path.parent().unwrap().to_path_buf();
let mut stack = vec![(None::<PathBuf>, conf_path)];
let mut contents = String::new();
while let Some((parent, p)) = stack.pop() {
if !p.exists() || p.is_dir() {
return Err(format!(
"Path {}{included}{in_parent} {msg}.",
p.display(),
included = if parent.is_some() {
" which is included in "
} else {
""
},
in_parent = if let Some(parent) = parent {
std::borrow::Cow::Owned(parent.display().to_string())
} else {
std::borrow::Cow::Borrowed("")
},
msg = if !p.exists() {
"does not exist"
} else {
"is a directory, not a text file"
}
)
.into());
}
contents.clear();
let mut file = std::fs::File::open(&p)?;
file.read_to_string(&mut contents)?;
let mut handle = Command::new("m4")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = handle.stdin.take().unwrap();
stdin.write_all(M4_PREAMBLE.as_bytes())?;
stdin.write_all(contents.as_bytes())?;
drop(stdin);
let stdout = handle.wait_with_output()?.stdout.clone();
for subpath in stdout.lines() {
let subpath = subpath?;
let path = &Path::new(&subpath);
if path.is_absolute() {
stack.push((Some(p.clone()), path.to_path_buf()));
} else {
stack.push((Some(p.clone()), prefix.join(path)));
}
}
ret.push(p);
}
Ok(ret)
}
pub fn expand_config(conf_path: PathBuf) -> Result<String> {
let _paths = get_included_configs(conf_path.clone())?;
const M4_PREAMBLE: &str = r#"define(`builtin_include', defn(`include'))dnl
define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
"#;
let mut contents = String::new();
contents.clear();
let mut file = std::fs::File::open(&conf_path)?;
file.read_to_string(&mut contents)?;
let mut handle = Command::new("m4")
.current_dir(conf_path.parent().unwrap_or(&Path::new("/")))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = handle.stdin.take().unwrap();
stdin.write_all(M4_PREAMBLE.as_bytes())?;
stdin.write_all(contents.as_bytes())?;
drop(stdin);
let stdout = handle.wait_with_output()?.stdout.clone();
Ok(String::from_utf8_lossy(&stdout).to_string())
}
struct Ask {
message: String,
}
@ -866,7 +956,7 @@ mod pp {
for (i, l) in contents.lines().enumerate() {
if let (_, Some(sub_path)) = include_directive().parse(l).map_err(|l| {
Error::new(format!(
"Malformed include directive in line {} of file {}: {}\nConfiguration uses the standard m4 macro include(`filename`).",
"Malformed include directive in line {} of file {}: {}\nConfiguration uses the standard m4 macro include(\"filename\").",
i,
path.display(),
l
@ -898,8 +988,7 @@ mod pp {
path.as_ref().expand()
};
let mut ret = pp_helper(&p_buf, 0)?;
drop(p_buf);
let mut ret = super::expand_config(p_buf)?;
if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("meli") {
for theme_mailbox in xdg_dirs.find_config_files("themes") {
let read_dir = std::fs::read_dir(theme_mailbox)?;

View File

@ -19,15 +19,17 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! Command line client binary.
//!
//! This crate contains the frontend stuff of the application. The application entry way on
//! `src/bin.rs` creates an event loop and passes input to a thread.
//! This crate contains the frontend stuff of the application. The application entry way on
//! `src/bin.rs` creates an event loop and passes input to a thread.
//!
//! The mail handling stuff is done in the `melib` crate which includes all backend needs. The
//! split is done to theoretically be able to create different frontends with the same innards.
//!
use meli::*;
mod args;
use args::*;
use std::os::raw::c_int;
fn notify(
@ -71,87 +73,6 @@ fn notify(
Ok(r)
}
#[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
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")]
struct Opt {
/// use specified configuration file
#[structopt(short, long, parse(from_os_str))]
config: Option<PathBuf>,
#[structopt(subcommand)]
subcommand: Option<SubCommand>,
}
#[derive(Debug, StructOpt)]
enum SubCommand {
/// print default theme in full to stdout and exit.
PrintDefaultTheme,
/// print loaded themes in full to stdout and exit.
PrintLoadedThemes,
/// create a sample configuration file with available configuration options. If PATH is not specified, meli will try to create it in $XDG_CONFIG_HOME/meli/config.toml
#[structopt(display_order = 1)]
CreateConfig {
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
},
/// test a configuration file for syntax issues or missing options.
#[structopt(display_order = 2)]
TestConfig {
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
},
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
#[structopt(display_order = 3)]
/// print documentation page and exit (Piping to a pager is recommended.).
Man(ManOpt),
#[structopt(display_order = 4)]
/// print compile time feature flags of this binary
CompiledWith,
/// View mail from input file.
View {
#[structopt(value_name = "INPUT", parse(from_os_str))]
path: PathBuf,
},
}
#[derive(Debug, StructOpt)]
struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = parse_manpage))]
#[cfg(feature = "cli-docs")]
page: 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")]
no_raw: Option<Option<bool>>,
}
fn main() {
let opt = Opt::from_args();
::std::process::exit(match run_app(opt) {
@ -193,6 +114,29 @@ fn run_app(opt: Opt) -> Result<()> {
conf::create_config_file(&config_path)?;
return Ok(());
}
Some(SubCommand::EditConfig) => {
use std::process::{Command, Stdio};
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.map_err(|err| {
format!("Could not find any value in environment variables EDITOR and VISUAL. {err}")
})?;
let config_path = crate::conf::get_config_file()?;
let mut cmd = Command::new(&editor);
let mut handle = &mut cmd;
for c in crate::conf::get_included_configs(config_path)? {
handle = handle.arg(&c);
}
let mut handle = handle
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
handle.wait()?;
return Ok(());
}
#[cfg(feature = "cli-docs")]
Some(SubCommand::Man(manopt)) => {
let ManOpt { page, no_raw } = manopt;