1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
//! Module for setting up config files and watchers.
//!
//! One of the primary goals of panorama is to be able to always hot-reload configuration files.

use std::fs::File;
use std::sync::mpsc::{self, Receiver};
use std::time::Duration;
use std::io::Read;
use std::path::Path;

use anyhow::{Result, Context};
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::watch;
use xdg::BaseDirectories;

/// Alias for a MailConfig receiver.
pub type ConfigWatcher = watch::Receiver<Option<MailConfig>>;

/// Configuration
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct MailConfig {
    /// Host of the IMAP server (needs to be hostname for TLS)
    pub server: String,

    /// Port of the IMAP server
    pub port: u16,

    /// Username for authenticating to IMAP
    pub username: String,

    /// Password for authenticating to IMAP
    pub password: String,
}

/// Spawns a notify::RecommendedWatcher to watch the XDG config directory. Whenever the config file
/// is updated, the config file is parsed and sent to the receiver.
fn start_watcher() -> Result<(
    RecommendedWatcher,
    Receiver<DebouncedEvent>,
)> {
    let (tx, rx) = mpsc::channel();
    let mut watcher = RecommendedWatcher::new(tx, Duration::from_secs(5))?;

    let xdg = BaseDirectories::new()?;
    let config_home = xdg.get_config_home();
    debug!("config_home: {:?}", config_home);
    watcher.watch(config_home.join("panorama"), RecursiveMode::Recursive).context("could not watch config_home")?;

    Ok((watcher, rx))
}

async fn read_config(path: impl AsRef<Path>) -> Result<MailConfig> {
    let mut file = File::open(path.as_ref())?;
    let mut contents = Vec::new();
    file.read_to_end(&mut contents)?;

    let config = toml::from_slice(&contents)?;
    Ok(config)
}

/// The inner loop of the watcher, which is responsible for taking events received by the watcher
/// and trying to parse and return the config.
///
/// This exists so all errors are able to be caught in one go.
async fn watcher_loop(
    fs_events: Receiver<DebouncedEvent>,
    config_tx: watch::Sender<Option<MailConfig>>,
) -> Result<()> {
    // first try opening the config file directly on load
    // (so the config isn't blank until the user touches the config file)
    let xdg = BaseDirectories::new()?;
    if let Some(config_path) = xdg.find_config_file("panorama/panorama.toml") {
        debug!("found config at {:?}", config_path);
        let config = read_config(config_path).await?;
        config_tx.send(Some(config))?;
    }

    for event in fs_events {
        debug!("new event: {:?}", event);
        // config_tx.send(Some(config))?;
    }

    Ok(())
}

/// Start the entire config watcher system, and return a [ConfigWatcher][self::ConfigWatcher],
/// which is a cloneable receiver of config update events.
pub fn spawn_config_watcher() -> Result<ConfigWatcher> {
    let (_watcher, config_rx) = start_watcher()?;
    let (config_tx, config_update) = watch::channel(None);

    tokio::spawn(async move {
        match watcher_loop(config_rx, config_tx).await {
            Ok(_) => {}
            Err(err) => {
                debug!("config watcher died: {:?}", err);
            }
        }
    });

    Ok(config_update)
}