From 371d6fb356509afe29731ad328b8f1bd1df1a844 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Sun, 14 Feb 2021 06:11:17 -0600 Subject: [PATCH] restructure the config parsing so it can look for reloads --- Cargo.lock | 13 ++++++++ Cargo.toml | 16 +++++----- README.md | 2 +- src/config.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++++-- src/mail.rs | 35 ++++++++++++++++++++-- src/main.rs | 36 ++++++++++++++-------- src/ui/mod.rs | 3 +- src/ui/table.rs | 11 +++++++ src/ui/tabs.rs | 3 ++ 9 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 src/ui/tabs.rs diff --git a/Cargo.lock b/Cargo.lock index 00ceda4..7992455 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,6 +784,7 @@ dependencies = [ "structopt", "tokio", "tokio-rustls", + "tokio-stream", "tokio-util", "toml", "webpki-roots", @@ -1302,6 +1303,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-stream" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1981ad97df782ab506a1f43bf82c967326960d278acf3bf8279809648c3ff3ea" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 9a4116e..9274b04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,18 +19,18 @@ chrono = "0.4.19" crossterm = "0.19.0" fern = "0.6.0" futures = "0.3.12" +imap = { path = "imap" } lettre = "0.9.5" log = "0.4.14" notify = "4.0.15" pin-project = "1.0.4" rustls-connector = "0.13.1" -tokio = { version = "1.1.1", features = ["full"] } -tokio-rustls = "0.22.0" -tokio-util = { version = "0.6.3", features = ["full"] } -webpki-roots = "0.21.0" -xdg = "2.2.0" - -imap = { path = "imap" } -toml = "0.5.8" serde = { version = "1.0.123", features = ["derive"] } structopt = "0.3.21" +tokio = { version = "1.1.1", features = ["full"] } +tokio-rustls = "0.22.0" +tokio-stream = { version = "0.1.3", features = ["sync"] } +tokio-util = { version = "0.6.3", features = ["full"] } +toml = "0.5.8" +webpki-roots = "0.21.0" +xdg = "2.2.0" diff --git a/README.md b/README.md index c58c47a..7607732 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ Panorama is a terminal Personal Information Manager (PIM). Goals: +- Never have to actually close the application. - Handles email, calendar, and address books using open standards. - Unified "feed" that any app can submit to. - Hot-reload on-disk config. - Submit notifications to gotify-shaped notification servers. -- Never have to actually close the application. Credits ------- diff --git a/src/config.rs b/src/config.rs index f79dccf..41164a6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,84 @@ -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Config { +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; + +pub type ConfigWatcher = watch::Receiver>; + +#[derive(Default, Serialize, Deserialize, Clone, Debug)] +pub struct MailConfig { pub server: String, pub port: u16, pub username: String, 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, +)> { + 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) -> Result { + 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) +} + +async fn watcher_loop( + fs_events: Receiver, + config_tx: watch::Sender>, +) -> 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(()) +} + +pub fn spawn_config_watcher() -> Result { + 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) +} diff --git a/src/mail.rs b/src/mail.rs index 5cf8c50..502bb81 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -19,16 +19,42 @@ use tokio::{ sync::mpsc::{self, UnboundedReceiver}, }; use tokio_rustls::{rustls::ClientConfig, webpki::DNSNameRef, TlsConnector}; +use tokio_stream::wrappers::WatchStream; use tokio_util::codec::{Decoder, LinesCodec, LinesCodecError}; -use crate::config::Config; +use crate::config::{MailConfig, ConfigWatcher}; pub enum MailCommand { Refresh, Raw(Command), } -pub async fn run_mail(config: Config, cmd_in: UnboundedReceiver) -> Result<()> { +/// Main entrypoint for the mail listener. +pub async fn run_mail( + config_watcher: ConfigWatcher, + cmd_in: UnboundedReceiver, +) -> Result<()> { + let mut curr_conn = None; + + let mut config_watcher = WatchStream::new(config_watcher); + loop { + let config: MailConfig = match config_watcher.next().await { + Some(Some(v)) => v, + _ => break, + }; + + let handle = tokio::spawn(open_imap_connection(config)); + curr_conn = Some(handle); + } + + Ok(()) +} + +async fn open_imap_connection(config: MailConfig) -> Result<()> { + debug!( + "Opening imap connection to {}:{}", + config.server, config.port + ); let server = config.server.as_str(); let port = config.port; @@ -62,13 +88,16 @@ pub async fn run_mail(config: Config, cmd_in: UnboundedReceiver) -> Ok(()) } +/// Action that should be taken after the connection loop quits. enum LoopExit { + /// Used in case the STARTTLS command is issued; perform TLS negotiation on top of the current + /// stream NegotiateTls(S, S2), Closed, } async fn listen_loop( - config: Config, + config: MailConfig, st: &mut State, sink: S2, mut stream: S, diff --git a/src/main.rs b/src/main.rs index 18aca46..0013768 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,8 +19,9 @@ use anyhow::Result; use futures::future::TryFutureExt; use structopt::StructOpt; use tokio::sync::{mpsc, oneshot}; +use xdg::BaseDirectories; -use crate::config::Config; +use crate::config::{spawn_config_watcher, MailConfig}; type ExitSender = oneshot::Sender<()>; @@ -38,26 +39,35 @@ struct Opt { #[tokio::main] async fn main() -> Result<()> { + // parse command line arguments into options struct let opt = Opt::from_args(); + // print logs to file as directed by command line options setup_logger(&opt)?; - let config: Config = { - let config_path = opt - .config_path - .clone() - .unwrap_or_else(|| "config.toml".into()); - let mut config_file = File::open(config_path)?; - let mut contents = Vec::new(); - config_file.read_to_end(&mut contents)?; - toml::from_slice(&contents)? - }; + let xdg = BaseDirectories::new()?; + let config_update = spawn_config_watcher()?; + let config = MailConfig::default(); + // let config: MailConfig = { + // let config_path = opt + // .config_path + // .clone() + // .unwrap_or_else(|| "config.toml".into()); + // let mut config_file = File::open(config_path)?; + // let mut contents = Vec::new(); + // config_file.read_to_end(&mut contents)?; + // toml::from_slice(&contents)? + // }; + + // used to notify the runtime that the process should exit let (exit_tx, exit_rx) = oneshot::channel::<()>(); + + // used to send commands to the mail service let (mail_tx, mail_rx) = mpsc::unbounded_channel(); - tokio::spawn(mail::run_mail(config.clone(), mail_rx).unwrap_or_else(report_err)); - let mut stdout = std::io::stdout(); + tokio::spawn(mail::run_mail(config_update.clone(), mail_rx).unwrap_or_else(report_err)); + let stdout = std::io::stdout(); tokio::spawn(ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err)); exit_rx.await?; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d31de07..32e5cfa 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -17,7 +17,7 @@ use crate::ExitSender; use self::table::Table; -const FRAME: Duration = Duration::from_millis(16); +const FRAME: Duration = Duration::from_millis(20); /// X Y W H #[derive(Copy, Clone)] @@ -51,6 +51,7 @@ pub async fn run_ui(mut w: impl Write, exit: ExitSender) -> Result<()> { // approx 60fps time::sleep(FRAME).await; + // check to see if there's even an event this frame. otherwise, just keep going if event::poll(FRAME)? { let event = event::read()?; table.update(&event); diff --git a/src/ui/table.rs b/src/ui/table.rs index cf06048..537de73 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -82,6 +82,17 @@ impl Table { } println!("{}", s); } + + let d = "\u{b7}".repeat(rect.2 as usize); + queue!( + w, + style::SetBackgroundColor(Color::Black), + style::SetForegroundColor(Color::White) + )?; + for j in self.rows.len() as u16..rect.3 { + queue!(w, cursor::MoveTo(rect.0, rect.1 + j))?; + println!("{}", d); + } } else { let msg = "Nothing in this table!"; let x = rect.0 + (rect.2 - msg.len() as u16) / 2; diff --git a/src/ui/tabs.rs b/src/ui/tabs.rs new file mode 100644 index 0000000..0cf9414 --- /dev/null +++ b/src/ui/tabs.rs @@ -0,0 +1,3 @@ +pub struct Tabs { + +}