Skip to content

Commit

Permalink
fix wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
soywod committed Dec 3, 2023
1 parent f24a047 commit c54ada7
Show file tree
Hide file tree
Showing 29 changed files with 288 additions and 173 deletions.
19 changes: 19 additions & 0 deletions src/backend/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#[cfg(feature = "imap-backend")]
use email::imap::ImapConfig;
#[cfg(feature = "notmuch-backend")]
use email::notmuch::NotmuchConfig;
#[cfg(feature = "smtp-sender")]
use email::smtp::SmtpConfig;
use email::{maildir::MaildirConfig, sendmail::SendmailConfig};

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BackendConfig {
Maildir(MaildirConfig),
#[cfg(feature = "imap-backend")]
Imap(ImapConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(NotmuchConfig),
#[cfg(feature = "smtp-sender")]
Smtp(SmtpConfig),
Sendmail(SendmailConfig),
}
25 changes: 22 additions & 3 deletions src/backend.rs → src/backend/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
pub mod config;
pub(crate) mod wizard;

use anyhow::Result;
use async_trait::async_trait;
use std::ops::Deref;
Expand Down Expand Up @@ -47,11 +50,9 @@ use serde::{Deserialize, Serialize};

use crate::{account::TomlAccountConfig, Envelopes, IdMapper};

#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackendKind {
#[default]
None,
Maildir,
#[serde(skip_deserializing)]
MaildirForSync,
Expand All @@ -64,6 +65,24 @@ pub enum BackendKind {
Sendmail,
}

impl ToString for BackendKind {
fn to_string(&self) -> String {
let kind = match self {
Self::Maildir => "Maildir",
Self::MaildirForSync => "Maildir",
#[cfg(feature = "imap-backend")]
Self::Imap => "IMAP",
#[cfg(feature = "notmuch-backend")]
Self::Notmuch => "Notmuch",
#[cfg(feature = "smtp-sender")]
Self::Smtp => "SMTP",
Self::Sendmail => "Sendmail",
};

kind.to_string()
}
}

#[derive(Clone, Default)]
pub struct BackendContextBuilder {
maildir: Option<MaildirSessionBuilder>,
Expand Down
71 changes: 71 additions & 0 deletions src/backend/wizard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use anyhow::Result;
use dialoguer::Select;

#[cfg(feature = "imap-backend")]
use crate::imap;
#[cfg(feature = "notmuch-backend")]
use crate::notmuch;
#[cfg(feature = "smtp-sender")]
use crate::smtp;
use crate::{config::wizard::THEME, maildir, sendmail};

use super::{config::BackendConfig, BackendKind};

const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
BackendKind::Maildir,
#[cfg(feature = "imap-backend")]
BackendKind::Imap,
#[cfg(feature = "notmuch-backend")]
BackendKind::Notmuch,
];

const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
BackendKind::Sendmail,
#[cfg(feature = "smtp-sender")]
BackendKind::Smtp,
];

pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Default email backend")
.items(DEFAULT_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));

let config = match kind {
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
#[cfg(feature = "imap-backend")]
Some(kind) if kind == BackendKind::Imap => {
Some(imap::wizard::configure(account_name, email).await?)
}
#[cfg(feature = "notmuch-backend")]
Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?),
_ => None,
};

Ok(config)
}

pub(crate) async fn configure_sender(
account_name: &str,
email: &str,
) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Default email backend")
.items(SEND_MESSAGE_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone));

let config = match kind {
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
#[cfg(feature = "smtp-sender")]
Some(kind) if kind == BackendKind::Smtp => {
Some(smtp::wizard::configure(account_name, email).await?)
}
_ => None,
};

Ok(config)
}
127 changes: 88 additions & 39 deletions src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ use email::{
config::Config,
email::{EmailHooks, EmailTextPlainFormat},
};
use log::{debug, trace};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf, process::exit};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
process,
};
use toml;

use crate::{
Expand Down Expand Up @@ -52,58 +56,103 @@ pub struct TomlConfig {
}

impl TomlConfig {
/// Tries to create a config from an optional path.
pub async fn from_maybe_path(path: Option<&str>) -> Result<Self> {
debug!("path: {:?}", path);

let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) {
let content = fs::read_to_string(path).context("cannot read config file")?;
toml::from_str(&content).context("cannot parse config file")?
} else {
wizard_warn!("Himalaya could not find an already existing configuration file.");

if !Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create one with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default()
{
exit(0);
}
/// Read and parse the TOML configuration at the given path.
///
/// Returns an error if the configuration file cannot be read or
/// if its content cannot be parsed.
fn from_path(path: &Path) -> Result<Self> {
let content =
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
}

wizard::configure().await?
};
/// Create and save a TOML configuration using the wizard.
///
/// If the user accepts the confirmation, the wizard starts and
/// help him to create his configuration file. Otherwise the
/// program stops.
///
/// NOTE: the wizard can only be used with interactive shells.
async fn from_wizard(path: PathBuf) -> Result<Self> {
wizard_warn!("Cannot find existing configuration at {path:?}.");

let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create it with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default();

if !confirm {
process::exit(0);
}

wizard::configure().await
}

if config.accounts.is_empty() {
return Err(anyhow!("config file must contain at least one account"));
/// Read and parse the TOML configuration from default paths.
pub async fn from_default_paths() -> Result<Self> {
match Self::first_valid_default_path() {
Some(path) => Self::from_path(&path),
None => Self::from_wizard(Self::default_path()?).await,
}
}

trace!("config: {:#?}", config);
Ok(config)
/// Read and parse the TOML configuration at the optional given
/// path.
///
/// If the given path exists, then read and parse the TOML
/// configuration from it.
///
/// If the given path does not exist, then create it using the
/// wizard.
///
/// If no path is given, then either read and parse the TOML
/// configuration at the first valid default path, otherwise
/// create it using the wizard. wizard.
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
match path.map(Into::into) {
Some(ref path) if path.exists() => Self::from_path(path),
Some(path) => Self::from_wizard(path).await,
None => match Self::first_valid_default_path() {
Some(path) => Self::from_path(&path),
None => Self::from_wizard(Self::default_path()?).await,
},
}
}

/// Tries to return a config path from a few default settings.
/// Get the default configuration path.
///
/// Tries paths in this order:
/// Returns an error if the XDG configuration directory cannot be
/// found.
pub fn default_path() -> Result<PathBuf> {
Ok(config_dir()
.ok_or(anyhow!("cannot get XDG config directory"))?
.join("himalaya")
.join("config.toml"))
}

/// Get the first default configuration path that points to a
/// valid file.
///
/// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
/// OSes.)
/// - `"$HOME/.config/himalaya/config.toml"`
/// - `"$HOME/.himalayarc"`
/// Tries paths in this order:
///
/// Returns `Some(path)` if the path exists, otherwise `None`.
pub fn path() -> Option<PathBuf> {
config_dir()
.map(|p| p.join("himalaya").join("config.toml"))
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
/// `$XDG_CONFIG_DIR` in other OSes.)
/// - `$HOME/.config/himalaya/config.toml`
/// - `$HOME/.himalayarc`
pub fn first_valid_default_path() -> Option<PathBuf> {
Self::default_path()
.ok()
.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())
}

/// Build account configurations from a given account name.
pub fn into_account_configs(
self,
account_name: Option<&str>,
Expand Down Expand Up @@ -232,7 +281,7 @@ mod tests {
async fn make_config(config: &str) -> Result<TomlConfig> {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", config).unwrap();
TomlConfig::from_maybe_path(file.into_temp_path().to_str()).await
TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await
}

#[tokio::test]
Expand Down
5 changes: 4 additions & 1 deletion src/config/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ pub(crate) async fn configure() -> Result<TomlConfig> {
// accounts are setup, decide which will be the default. If no
// accounts are setup, exit the process.
let default_account = match config.accounts.len() {
0 => process::exit(0),
0 => {
wizard_warn!("No account configured, exiting.");
process::exit(0);
}
1 => Some(config.accounts.values_mut().next().unwrap()),
_ => {
let accounts = config.accounts.clone();
Expand Down
53 changes: 48 additions & 5 deletions src/domain/account/wizard.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use anyhow::{anyhow, Result};
use anyhow::{bail, Result};
use dialoguer::Input;
use email_address::EmailAddress;

use crate::config::wizard::THEME;
use crate::{
backend::{self, config::BackendConfig, BackendKind},
config::wizard::THEME,
message::config::{MessageConfig, MessageSendConfig},
};

use super::TomlAccountConfig;

Expand All @@ -20,7 +24,7 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
if EmailAddress::is_valid(email) {
Ok(())
} else {
Err(anyhow!("Invalid email address: {email}"))
bail!("Invalid email address: {email}")
}
})
.interact()?;
Expand All @@ -31,9 +35,48 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
.interact()?,
);

// config.backend = backend::wizard::configure(&account_name, &config.email).await?;
match backend::wizard::configure(&account_name, &config.email).await? {
Some(BackendConfig::Maildir(mdir_config)) => {
config.maildir = Some(mdir_config);
config.backend = Some(BackendKind::Maildir);
}
#[cfg(feature = "imap-backend")]
Some(BackendConfig::Imap(imap_config)) => {
config.imap = Some(imap_config);
config.backend = Some(BackendKind::Imap);
}
#[cfg(feature = "notmuch-backend")]
Some(BackendConfig::Notmuch(notmuch_config)) => {
config.notmuch = Some(notmuch_config);
config.backend = Some(BackendKind::Notmuch);
}
_ => (),
};

// config.sender = sender::wizard::configure(&account_name, &config.email).await?;
match backend::wizard::configure_sender(&account_name, &config.email).await? {
Some(BackendConfig::Sendmail(sendmail_config)) => {
config.sendmail = Some(sendmail_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Sendmail),
..Default::default()
}),
..Default::default()
});
}
#[cfg(feature = "smtp-sender")]
Some(BackendConfig::Smtp(smtp_config)) => {
config.smtp = Some(smtp_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Smtp),
..Default::default()
}),
..Default::default()
});
}
_ => (),
};

Ok(Some((account_name, config)))
}
3 changes: 0 additions & 3 deletions src/domain/backend/imap/mod.rs

This file was deleted.

1 change: 0 additions & 1 deletion src/domain/backend/maildir/mod.rs

This file was deleted.

Loading

0 comments on commit c54ada7

Please sign in to comment.