meli: add edit-config CLI subcommand that opens config files on EDITOR
parent
39d9c2af3b
commit
47e6d5d935
|
@ -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>>,
|
||||
}
|
95
src/conf.rs
95
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<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)?;
|
||||
|
|
112
src/main.rs
112
src/main.rs
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue