restructure the config parsing so it can look for reloads
This commit is contained in:
parent
2acdda99ac
commit
371d6fb356
9 changed files with 171 additions and 28 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -784,6 +784,7 @@ dependencies = [
|
||||||
"structopt",
|
"structopt",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
|
@ -1302,6 +1303,18 @@ dependencies = [
|
||||||
"webpki",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
|
16
Cargo.toml
16
Cargo.toml
|
@ -19,18 +19,18 @@ chrono = "0.4.19"
|
||||||
crossterm = "0.19.0"
|
crossterm = "0.19.0"
|
||||||
fern = "0.6.0"
|
fern = "0.6.0"
|
||||||
futures = "0.3.12"
|
futures = "0.3.12"
|
||||||
|
imap = { path = "imap" }
|
||||||
lettre = "0.9.5"
|
lettre = "0.9.5"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
notify = "4.0.15"
|
notify = "4.0.15"
|
||||||
pin-project = "1.0.4"
|
pin-project = "1.0.4"
|
||||||
rustls-connector = "0.13.1"
|
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"] }
|
serde = { version = "1.0.123", features = ["derive"] }
|
||||||
structopt = "0.3.21"
|
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"
|
||||||
|
|
|
@ -5,11 +5,11 @@ Panorama is a terminal Personal Information Manager (PIM).
|
||||||
|
|
||||||
Goals:
|
Goals:
|
||||||
|
|
||||||
|
- Never have to actually close the application.
|
||||||
- Handles email, calendar, and address books using open standards.
|
- Handles email, calendar, and address books using open standards.
|
||||||
- Unified "feed" that any app can submit to.
|
- Unified "feed" that any app can submit to.
|
||||||
- Hot-reload on-disk config.
|
- Hot-reload on-disk config.
|
||||||
- Submit notifications to gotify-shaped notification servers.
|
- Submit notifications to gotify-shaped notification servers.
|
||||||
- Never have to actually close the application.
|
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -1,8 +1,84 @@
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
use std::fs::File;
|
||||||
pub struct Config {
|
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<Option<MailConfig>>;
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct MailConfig {
|
||||||
pub server: String,
|
pub server: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: 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<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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
35
src/mail.rs
35
src/mail.rs
|
@ -19,16 +19,42 @@ use tokio::{
|
||||||
sync::mpsc::{self, UnboundedReceiver},
|
sync::mpsc::{self, UnboundedReceiver},
|
||||||
};
|
};
|
||||||
use tokio_rustls::{rustls::ClientConfig, webpki::DNSNameRef, TlsConnector};
|
use tokio_rustls::{rustls::ClientConfig, webpki::DNSNameRef, TlsConnector};
|
||||||
|
use tokio_stream::wrappers::WatchStream;
|
||||||
use tokio_util::codec::{Decoder, LinesCodec, LinesCodecError};
|
use tokio_util::codec::{Decoder, LinesCodec, LinesCodecError};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::{MailConfig, ConfigWatcher};
|
||||||
|
|
||||||
pub enum MailCommand {
|
pub enum MailCommand {
|
||||||
Refresh,
|
Refresh,
|
||||||
Raw(Command),
|
Raw(Command),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_mail(config: Config, cmd_in: UnboundedReceiver<MailCommand>) -> Result<()> {
|
/// Main entrypoint for the mail listener.
|
||||||
|
pub async fn run_mail(
|
||||||
|
config_watcher: ConfigWatcher,
|
||||||
|
cmd_in: UnboundedReceiver<MailCommand>,
|
||||||
|
) -> 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 server = config.server.as_str();
|
||||||
let port = config.port;
|
let port = config.port;
|
||||||
|
|
||||||
|
@ -62,13 +88,16 @@ pub async fn run_mail(config: Config, cmd_in: UnboundedReceiver<MailCommand>) ->
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Action that should be taken after the connection loop quits.
|
||||||
enum LoopExit<S, S2> {
|
enum LoopExit<S, S2> {
|
||||||
|
/// Used in case the STARTTLS command is issued; perform TLS negotiation on top of the current
|
||||||
|
/// stream
|
||||||
NegotiateTls(S, S2),
|
NegotiateTls(S, S2),
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn listen_loop<S, S2>(
|
async fn listen_loop<S, S2>(
|
||||||
config: Config,
|
config: MailConfig,
|
||||||
st: &mut State,
|
st: &mut State,
|
||||||
sink: S2,
|
sink: S2,
|
||||||
mut stream: S,
|
mut stream: S,
|
||||||
|
|
36
src/main.rs
36
src/main.rs
|
@ -19,8 +19,9 @@ use anyhow::Result;
|
||||||
use futures::future::TryFutureExt;
|
use futures::future::TryFutureExt;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::{spawn_config_watcher, MailConfig};
|
||||||
|
|
||||||
type ExitSender = oneshot::Sender<()>;
|
type ExitSender = oneshot::Sender<()>;
|
||||||
|
|
||||||
|
@ -38,26 +39,35 @@ struct Opt {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
// parse command line arguments into options struct
|
||||||
let opt = Opt::from_args();
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
|
// print logs to file as directed by command line options
|
||||||
setup_logger(&opt)?;
|
setup_logger(&opt)?;
|
||||||
|
|
||||||
let config: Config = {
|
let xdg = BaseDirectories::new()?;
|
||||||
let config_path = opt
|
let config_update = spawn_config_watcher()?;
|
||||||
.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 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::<()>();
|
let (exit_tx, exit_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
|
// used to send commands to the mail service
|
||||||
let (mail_tx, mail_rx) = mpsc::unbounded_channel();
|
let (mail_tx, mail_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
tokio::spawn(mail::run_mail(config.clone(), mail_rx).unwrap_or_else(report_err));
|
tokio::spawn(mail::run_mail(config_update.clone(), mail_rx).unwrap_or_else(report_err));
|
||||||
let mut stdout = std::io::stdout();
|
let stdout = std::io::stdout();
|
||||||
tokio::spawn(ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err));
|
tokio::spawn(ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err));
|
||||||
|
|
||||||
exit_rx.await?;
|
exit_rx.await?;
|
||||||
|
|
|
@ -17,7 +17,7 @@ use crate::ExitSender;
|
||||||
|
|
||||||
use self::table::Table;
|
use self::table::Table;
|
||||||
|
|
||||||
const FRAME: Duration = Duration::from_millis(16);
|
const FRAME: Duration = Duration::from_millis(20);
|
||||||
|
|
||||||
/// X Y W H
|
/// X Y W H
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
|
@ -51,6 +51,7 @@ pub async fn run_ui(mut w: impl Write, exit: ExitSender) -> Result<()> {
|
||||||
// approx 60fps
|
// approx 60fps
|
||||||
time::sleep(FRAME).await;
|
time::sleep(FRAME).await;
|
||||||
|
|
||||||
|
// check to see if there's even an event this frame. otherwise, just keep going
|
||||||
if event::poll(FRAME)? {
|
if event::poll(FRAME)? {
|
||||||
let event = event::read()?;
|
let event = event::read()?;
|
||||||
table.update(&event);
|
table.update(&event);
|
||||||
|
|
|
@ -82,6 +82,17 @@ impl Table {
|
||||||
}
|
}
|
||||||
println!("{}", s);
|
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 {
|
} else {
|
||||||
let msg = "Nothing in this table!";
|
let msg = "Nothing in this table!";
|
||||||
let x = rect.0 + (rect.2 - msg.len() as u16) / 2;
|
let x = rect.0 + (rect.2 - msg.len() as u16) / 2;
|
||||||
|
|
3
src/ui/tabs.rs
Normal file
3
src/ui/tabs.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub struct Tabs {
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue