what the FUCK
This commit is contained in:
commit
16545a4dd1
12 changed files with 1596 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1315
Cargo.lock
generated
Normal file
1315
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "panorama"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Michael Zhang <mail@mzhang.io>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
async-trait = "0.1.42"
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
crossterm = "0.19.0"
|
||||||
|
fern = "0.6.0"
|
||||||
|
futures = "0.3.12"
|
||||||
|
lettre = "0.9.5"
|
||||||
|
log = "0.4.14"
|
||||||
|
notify = "4.0.15"
|
||||||
|
pin-project = "1.0.4"
|
||||||
|
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"
|
12
README.md
Normal file
12
README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
panorama
|
||||||
|
========
|
||||||
|
|
||||||
|
Panorama is a terminal Personal Information Manager (PIM).
|
||||||
|
|
||||||
|
Goals:
|
||||||
|
|
||||||
|
- 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.
|
7
output.log
Normal file
7
output.log
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[2021-02-12][01:42:31][panorama][INFO] poggers
|
||||||
|
[2021-02-12][01:42:36][panorama][INFO] poggers
|
||||||
|
[2021-02-12][01:56:24][panorama][INFO] poggers
|
||||||
|
[2021-02-12][01:56:50][panorama][INFO] poggers
|
||||||
|
[2021-02-12][01:56:50][panorama::panorama][DEBUG] starting all apps...
|
||||||
|
[2021-02-12][02:04:53][panorama][INFO] poggers
|
||||||
|
[2021-02-12][02:04:53][panorama::panorama][DEBUG] starting all apps...
|
29
src/app.rs
Normal file
29
src/app.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use crate::config::ConfigWatcher;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AnyApp {
|
||||||
|
fn say_hello(&self) {
|
||||||
|
debug!("hello from app");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An App is usually associated with a particular separate service or piece of functionality.
|
||||||
|
pub struct App<A: AppI> {
|
||||||
|
inner: A,
|
||||||
|
// config_watcher: ConfigWatcher<A::Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: AppI> App<A> {
|
||||||
|
pub fn new(app: A) -> Self {
|
||||||
|
App { inner: app }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The interface that anything that wants to become an App must implement
|
||||||
|
pub trait AppI: Sized {
|
||||||
|
type Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: AppI> AnyApp for App<A> {}
|
25
src/config.rs
Normal file
25
src/config.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use notify::{DebouncedEvent, RecursiveMode, Watcher};
|
||||||
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
|
pub struct ConfigWatcher<C> {
|
||||||
|
_ty: PhantomData<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch_config() -> Result<()> {
|
||||||
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
|
let xdg = BaseDirectories::new()?;
|
||||||
|
let config_home = xdg.get_config_home();
|
||||||
|
let mut watcher = notify::watcher(tx, Duration::from_secs(5))?;
|
||||||
|
watcher.watch(config_home, RecursiveMode::Recursive)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let evt = rx.recv()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
1
src/event.rs
Normal file
1
src/event.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub enum Event {}
|
21
src/mailapp.rs
Normal file
21
src/mailapp.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use lettre::SmtpClient;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::app::AppI;
|
||||||
|
|
||||||
|
pub struct MailApp {
|
||||||
|
client: SmtpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppI for MailApp {
|
||||||
|
type Config = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailApp {
|
||||||
|
pub fn new(domain: impl AsRef<str>) -> Result<Self> {
|
||||||
|
let client = SmtpClient::new_simple(domain.as_ref())?;
|
||||||
|
Ok(MailApp { client })
|
||||||
|
}
|
||||||
|
}
|
65
src/main.rs
Normal file
65
src/main.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate async_trait;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate cfg_if;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate crossterm;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate pin_project;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod config;
|
||||||
|
mod event;
|
||||||
|
mod mailapp;
|
||||||
|
mod panorama;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
use crate::panorama::Panorama;
|
||||||
|
use crate::config::watch_config;
|
||||||
|
use crate::ui::Ui;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(|out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"{}[{}][{}] {}",
|
||||||
|
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||||
|
record.target(),
|
||||||
|
record.level(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(log::LevelFilter::Debug)
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
|
||||||
|
let runtime = Runtime::new()?;
|
||||||
|
thread::spawn(move || {
|
||||||
|
let panorama = Panorama::new().unwrap();
|
||||||
|
runtime.block_on(panorama.run());
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let (evts_tx, evts_rx) = channel();
|
||||||
|
|
||||||
|
// spawn a thread for listening to configuration changes
|
||||||
|
thread::spawn(move || {
|
||||||
|
watch_config();
|
||||||
|
});
|
||||||
|
info!("poggers");
|
||||||
|
|
||||||
|
// run the ui on the main thread
|
||||||
|
let ui = Ui::init(stdout, evts_rx)?;
|
||||||
|
ui.run()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
37
src/panorama.rs
Normal file
37
src/panorama.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::mailapp::MailApp;
|
||||||
|
use crate::app::{AnyApp, App};
|
||||||
|
|
||||||
|
pub struct Panorama {
|
||||||
|
apps: Vec<Box<dyn AnyApp>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panorama {
|
||||||
|
pub fn new() -> Result<Panorama> {
|
||||||
|
let mut apps = Vec::new();
|
||||||
|
|
||||||
|
let mail = MailApp::new("mzhang.io")?;
|
||||||
|
let mail = Box::new(App::new(mail)) as Box<dyn AnyApp>;
|
||||||
|
apps.push(mail);
|
||||||
|
|
||||||
|
Ok(Panorama {
|
||||||
|
apps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> Result<()> {
|
||||||
|
debug!("starting all apps...");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
self.apps.iter().map(|app| {
|
||||||
|
app.say_hello();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
60
src/ui.rs
Normal file
60
src/ui.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::mpsc::Receiver;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use crossterm::{
|
||||||
|
event,
|
||||||
|
terminal::{self, EnterAlternateScreen},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::event::Event;
|
||||||
|
|
||||||
|
pub struct Ui<S: Write> {
|
||||||
|
screen: S,
|
||||||
|
evts: Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Write> Ui<S> {
|
||||||
|
pub fn init(mut screen: S, evts: Receiver<Event>) -> Result<Self> {
|
||||||
|
execute!(screen, EnterAlternateScreen)?;
|
||||||
|
terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
|
Ok(Ui { screen, evts })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self) -> Result<()> {
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// check for new events
|
||||||
|
use std::sync::mpsc::TryRecvError;
|
||||||
|
match self.evts.try_recv() {
|
||||||
|
Ok(evt) => {}
|
||||||
|
Err(TryRecvError::Empty) => {} // skip
|
||||||
|
Err(TryRecvError::Disconnected) => todo!("impossible?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// read events from the terminal
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('q'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Write> Drop for Ui<S> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
use crossterm::{cursor::Show, style::ResetColor, terminal::LeaveAlternateScreen};
|
||||||
|
|
||||||
|
execute!(self.screen, ResetColor, Show, LeaveAlternateScreen,).unwrap();
|
||||||
|
terminal::disable_raw_mode().unwrap();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue