Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration wizard #432

Merged
merged 19 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ tempfile = "3.3"
[dependencies]
anyhow = "1.0"
atty = "0.2"
chrono = "0.4"
clap = "4.0"
clap_complete = "4.0"
clap_mangen = "0.2"
console = "0.15.2"
dirs = "4.0.0"
dialoguer = "0.10.2"
email_address = "0.2.4"
env_logger = "0.8"
erased-serde = "0.3"
himalaya-lib = { git = "https://git.sr.ht/~soywod/himalaya-lib", branch = "develop" }
log = "0.4"
once_cell = "1.16.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shellexpand = "2.1"
Expand Down
86 changes: 35 additions & 51 deletions src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
//! user configuration file.

use anyhow::{anyhow, Context, Result};
use dirs::{config_dir, home_dir};
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
use log::{debug, trace};
use serde::Deserialize;
use std::{collections::HashMap, env, fs, path::PathBuf};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf};
use toml;

use crate::{account::DeserializedAccountConfig, config::prelude::*};
use crate::{
account::DeserializedAccountConfig,
config::{prelude::*, wizard::wizard},
};

/// Represents the user config file.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig {
#[serde(alias = "name")]
Expand All @@ -27,14 +31,14 @@ pub struct DeserializedConfig {

pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(default, with = "email_text_plain_format")]
#[serde(default, with = "EmailTextPlainFormatOptionDef", skip_serializing_if = "Option::is_none")]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_reading_verify_cmd: Option<String>,
pub email_reading_decrypt_cmd: Option<String>,
pub email_writing_headers: Option<Vec<String>>,
pub email_writing_sign_cmd: Option<String>,
pub email_writing_encrypt_cmd: Option<String>,
#[serde(default, with = "email_hooks")]
#[serde(default, with = "EmailHooksOptionDef", skip_serializing_if = "Option::is_none")]
pub email_hooks: Option<EmailHooks>,

#[serde(flatten)]
Expand All @@ -47,9 +51,13 @@ impl DeserializedConfig {
trace!(">> parse config from path");
debug!("path: {:?}", path);

let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config: Self = toml::from_str(&content).context("cannot parse config file")?;
let config: Self = match path.map(|s| s.into()).or_else(Self::path) {
Some(path) => {
let content = fs::read_to_string(path).context("cannot read config file")?;
toml::from_str(&content).context("cannot parse config file")?
}
None => wizard()?,
};

if config.accounts.is_empty() {
return Err(anyhow!("config file must contain at least one account"));
Expand All @@ -60,48 +68,24 @@ impl DeserializedConfig {
Ok(config)
}

/// Tries to get the XDG config file path from XDG_CONFIG_HOME
/// environment variable.
fn path_from_xdg() -> Result<PathBuf> {
let path = env::var("XDG_CONFIG_HOME").context("cannot read env var XDG_CONFIG_HOME")?;
let path = PathBuf::from(path).join("himalaya").join("config.toml");
Ok(path)
}

/// Tries to get the XDG config file path from HOME environment
/// variable.
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
let path = PathBuf::from(path)
.join(".config")
.join("himalaya")
.join("config.toml");
Ok(path)
}

/// Tries to get the .himalayarc config file path from HOME
/// environment variable.
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
let path = PathBuf::from(path).join(".himalayarc");
Ok(path)
}

/// Tries to get the config file path.
pub fn path() -> Result<PathBuf> {
Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
/// Tries to return a config path from a few default settings.
///
/// Tries paths in this order:
///
/// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
/// OSes.)
/// - `"$HOME/.config/himalaya/config.toml"`
/// - `"$HOME/.himalayarc"`
///
/// Returns `Some(path)` if the path exists, otherwise `None`.
pub fn path() -> Option<PathBuf> {
config_dir()
.map(|p| p.join("himalaya").join("config.toml"))
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
.filter(|p| p.exists())
}

pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> {
Expand Down
1 change: 1 addition & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod args;
pub mod config;
pub mod prelude;
mod wizard;

pub use config::*;
76 changes: 29 additions & 47 deletions src/config/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[cfg(feature = "imap-backend")]
Expand All @@ -11,7 +11,7 @@ use himalaya_lib::MaildirConfig;
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig;

#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "SmtpConfig")]
struct SmtpConfigDef {
#[serde(rename = "smtp-host")]
Expand All @@ -31,7 +31,7 @@ struct SmtpConfigDef {
}

#[cfg(feature = "imap-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "ImapConfig")]
pub struct ImapConfigDef {
#[serde(rename = "imap-host")]
Expand All @@ -57,48 +57,39 @@ pub struct ImapConfigDef {
}

#[cfg(feature = "maildir-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "MaildirConfig")]
pub struct MaildirConfigDef {
#[serde(rename = "maildir-root-dir")]
pub root_dir: PathBuf,
}

#[cfg(feature = "notmuch-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "NotmuchConfig")]
pub struct NotmuchConfigDef {
#[serde(rename = "notmuch-db-path")]
pub db_path: PathBuf,
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "Option<EmailTextPlainFormat>")]
pub enum EmailTextPlainFormatOptionDef {
#[serde(with = "EmailTextPlainFormatDef")]
Some(EmailTextPlainFormat),
#[default]
None,
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
enum EmailTextPlainFormatDef {
pub enum EmailTextPlainFormatDef {
Auto,
Flowed,
Fixed(usize),
}

pub mod email_text_plain_format {
use himalaya_lib::EmailTextPlainFormat;
use serde::{Deserialize, Deserializer};

use super::EmailTextPlainFormatDef;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailTextPlainFormat>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper(#[serde(with = "EmailTextPlainFormatDef")] EmailTextPlainFormat);

let helper = Option::deserialize(deserializer)?;
Ok(helper.map(|Helper(external)| external))
}
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")]
pub enum EmailSenderDef {
None,
Expand All @@ -108,36 +99,27 @@ pub enum EmailSenderDef {
Sendmail(SendmailConfig),
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "SendmailConfig")]
pub struct SendmailConfigDef {
#[serde(rename = "sendmail-cmd")]
cmd: String,
}

#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "Option<EmailHooks>")]
pub enum EmailHooksOptionDef {
#[serde(with = "EmailHooksDef")]
Some(EmailHooks),
#[default]
None,
}

/// Represents the email hooks. Useful for doing extra email
/// processing before or after sending it.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailHooks")]
struct EmailHooksDef {
pub struct EmailHooksDef {
/// Represents the hook called just before sending an email.
pub pre_send: Option<String>,
}

pub mod email_hooks {
use himalaya_lib::EmailHooks;
use serde::{Deserialize, Deserializer};

use super::EmailHooksDef;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailHooks>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper(#[serde(with = "EmailHooksDef")] EmailHooks);

let helper = Option::deserialize(deserializer)?;
Ok(helper.map(|Helper(external)| external))
}
}
Loading