From 47e6d5d935a2b5124efbe847dac885b859200469 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 26 Apr 2023 13:35:29 +0300 Subject: [PATCH] meli: add edit-config CLI subcommand that opens config files on EDITOR --- src/args.rs | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ src/conf.rs | 95 ++++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 112 +++++++++++++--------------------------------------- 3 files changed, 227 insertions(+), 87 deletions(-) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 00000000..aea88e28 --- /dev/null +++ b/src/args.rs @@ -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 . + */ + +//! Command line arguments. + +use meli::*; + +#[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 { + /// use specified configuration file + #[structopt(short, long, parse(from_os_str))] + pub config: Option, + + #[structopt(subcommand)] + pub subcommand: Option, +} + +#[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, + }, + /// 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, + }, + #[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>, +} diff --git a/src/conf.rs b/src/conf.rs index 2ece4864..eaff8689 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -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 { } } +pub fn get_included_configs(conf_path: PathBuf) -> Result> { + 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::, 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 { + 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)?; diff --git a/src/main.rs b/src/main.rs index dc74d2a3..408352be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,15 +19,17 @@ * along with meli. If not, see . */ +//! 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 { - 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, - - #[structopt(subcommand)] - subcommand: Option, -} - -#[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, - }, - /// 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, - }, - #[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>, -} - 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;