From ba5d07db9169eb067b4a4e022721614874340c73 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Thu, 11 Mar 2021 04:57:47 -0600 Subject: [PATCH] some UI updates --- imap/src/client/mod.rs | 12 ++--- imap/src/command.rs | 5 ++ imap/src/parser/mod.rs | 117 ++++++++++++++++++++++------------------- notes.md | 2 + src/main.rs | 11 +++- src/script.rs | 2 + src/ui/messages.rs | 9 ++++ src/ui/mod.rs | 20 ++++++- src/ui/windows.rs | 78 +++++++++++++++++++++++++++ 9 files changed, 192 insertions(+), 64 deletions(-) create mode 100644 src/ui/messages.rs create mode 100644 src/ui/windows.rs diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs index 5717b16..afc400c 100644 --- a/imap/src/client/mod.rs +++ b/imap/src/client/mod.rs @@ -199,13 +199,13 @@ impl ClientAuthenticated { mailbox: mailbox.as_ref().to_owned(), }; let stream = self.execute(cmd).await?; - let (done, data) = stream.wait().await?; + let (_, data) = stream.wait().await?; for resp in data { debug!("execute called returned: {:?}", resp); } // nuke the capabilities cache - self.nuke_capabilities(); + // self.nuke_capabilities(); Ok(()) } @@ -272,12 +272,12 @@ impl ClientAuthenticated { let sender = self.sender(); Ok(IdleToken { stream, sender }) } - - fn nuke_capabilities(&mut self) { - // TODO: do something here - } } +/// A token that represents an idling connection. +/// +/// Dropping this token indicates that the idling should be completed, and the DONE command will be +/// sent to the server as a result. #[cfg(feature = "rfc2177-idle")] #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))] pub struct IdleToken { diff --git a/imap/src/command.rs b/imap/src/command.rs index 619645c..aa86ee5 100644 --- a/imap/src/command.rs +++ b/imap/src/command.rs @@ -109,8 +109,12 @@ pub enum FetchItems { All, Fast, Full, + Items(Vec), } +#[derive(Clone, Debug)] +pub enum FetchAttr {} + impl fmt::Display for FetchItems { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use FetchItems::*; @@ -118,6 +122,7 @@ impl fmt::Display for FetchItems { All => write!(f, "ALL"), Fast => write!(f, "FAST"), Full => write!(f, "FULL"), + FetchAttr => write!(f, ""), } } } diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 9a29748..96e941f 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -45,76 +45,83 @@ pub fn parse_response(s: impl AsRef) -> ParseResult { fn build_response(pair: Pair) -> Response { assert!(matches!(pair.as_rule(), Rule::response)); + let pair = unwrap1(pair); + match pair.as_rule() { + Rule::response_done => build_response_done(pair), + Rule::response_data => build_response_data(pair), + Rule::continue_req => build_continue_req(pair), + _ => unreachable!("{:#?}", pair), + } +} +fn build_response_done(pair: Pair) -> Response { + assert!(matches!(pair.as_rule(), Rule::response_done)); let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); match pair.as_rule() { - Rule::response_done => { + Rule::response_tagged => { let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); - match pair.as_rule() { - Rule::response_tagged => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - let tag = pair.as_str().to_owned(); + let tag = pair.as_str().to_owned(); - let pair = pairs.next().unwrap(); - let (status, code, information) = build_resp_cond_state(pair); - Response::Done(ResponseDone { - tag, - status, - code, - information, - }) - } - _ => unreachable!("{:#?}", pair), - } - } - Rule::response_data => { - let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); - match pair.as_rule() { - Rule::resp_cond_state => { - let (status, code, information) = build_resp_cond_state(pair); - Response::Data(ResponseData { - status, - code, - information, - }) - } - Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)), - Rule::capability_data => Response::Capabilities(build_capabilities(pair)), - Rule::message_data => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - let seq: u32 = build_number(pair); - - let pair = pairs.next().unwrap(); - match pair.as_rule() { - Rule::message_data_expunge => Response::Expunge(seq), - Rule::message_data_fetch => { - let mut pairs = pair.into_inner(); - let msg_att = pairs.next().unwrap(); - let attrs = msg_att.into_inner().map(build_msg_att).collect(); - Response::Fetch(seq, attrs) - } - _ => unreachable!("{:#?}", pair), - } - } - _ => unreachable!("{:#?}", pair), - } - } - Rule::continue_req => { - let (code, s) = build_resp_text(unwrap1(pair)); - Response::Continue { + let (status, code, information) = build_resp_cond_state(pair); + Response::Done(ResponseDone { + tag, + status, code, - information: Some(s), + information, + }) + } + _ => unreachable!("{:#?}", pair), + } +} + +fn build_response_data(pair: Pair) -> Response { + assert!(matches!(pair.as_rule(), Rule::response_data)); + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::resp_cond_state => { + let (status, code, information) = build_resp_cond_state(pair); + Response::Data(ResponseData { + status, + code, + information, + }) + } + Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)), + Rule::capability_data => Response::Capabilities(build_capabilities(pair)), + Rule::message_data => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let seq: u32 = build_number(pair); + + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::message_data_expunge => Response::Expunge(seq), + Rule::message_data_fetch => { + let mut pairs = pair.into_inner(); + let msg_att = pairs.next().unwrap(); + let attrs = msg_att.into_inner().map(build_msg_att).collect(); + Response::Fetch(seq, attrs) + } + _ => unreachable!("{:#?}", pair), } } _ => unreachable!("{:#?}", pair), } } +fn build_continue_req(pair: Pair) -> Response { + assert!(matches!(pair.as_rule(), Rule::continue_req)); + let (code, s) = build_resp_text(unwrap1(pair)); + Response::Continue { + code, + information: Some(s), + } +} + fn build_resp_text(pair: Pair) -> (Option, String) { assert!(matches!(pair.as_rule(), Rule::resp_text)); let mut pairs = pair.into_inner(); diff --git a/notes.md b/notes.md index 640040f..f9d4f5c 100644 --- a/notes.md +++ b/notes.md @@ -18,6 +18,8 @@ design ideas - for ex: v1 has `{ x: Int }`, v2 has `{ [deprecated] x: Int, x2: Float }` and v3 has `{ x2: Float }` this means v1 -> v2 upgrade can be done automatically but because there are _any_ pending deprecated values being used it's not allowed to automatically upgrade to v3 +- imap repl? or more realistically gluon repl that has an imap interface + - basically lets me debug imap commands on-the-spot, with the current connection imap routine --- diff --git a/src/main.rs b/src/main.rs index 340f1ee..47a9511 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,9 @@ async fn run(opt: Opt) -> Result<()> { // send messages from the mail thread to the UI thread let (mail2ui_tx, mail2ui_rx) = mpsc::unbounded_channel(); + // send messages from the UI thread to the vm thread + let (ui2vm_tx, ui2vm_rx) = mpsc::unbounded_channel(); + tokio::spawn(async move { let config_update = config_update.clone(); mail::run_mail(config_update, ui2mail_rx, mail2ui_tx) @@ -67,7 +70,7 @@ async fn run(opt: Opt) -> Result<()> { }); if !opt.headless { - run_ui(exit_tx, mail2ui_rx); + run_ui(exit_tx, mail2ui_rx, ui2vm_tx); } exit_rx.recv().await; @@ -80,7 +83,11 @@ async fn run(opt: Opt) -> Result<()> { } // Spawns the entire UI in a different thread, since it must be thread-local -fn run_ui(exit_tx: mpsc::Sender<()>, mail2ui_rx: mpsc::UnboundedReceiver) { +fn run_ui( + exit_tx: mpsc::Sender<()>, + mail2ui_rx: mpsc::UnboundedReceiver, + ui2vm_tx: mpsc::UnboundedSender<()>, +) { let stdout = std::io::stdout(); let rt = RuntimeBuilder::new_current_thread() diff --git a/src/script.rs b/src/script.rs index e8b4e0c..dac2537 100644 --- a/src/script.rs +++ b/src/script.rs @@ -3,6 +3,8 @@ use anyhow::Result; use gluon::{import::add_extern_module, ThreadExt}; +use crate::ui::{UiCommand, UiUpdate}; + /// Creates a VM for running scripts pub async fn create_script_vm() -> Result<()> { let vm = gluon::new_vm_async().await; diff --git a/src/ui/messages.rs b/src/ui/messages.rs new file mode 100644 index 0000000..0477eb1 --- /dev/null +++ b/src/ui/messages.rs @@ -0,0 +1,9 @@ +/// Commands that are sent from the UI to the scripting VM +pub enum UiCommand { + HSplit, + VSplit, + OpenMessage, +} + +/// Updates that are sent from the scripting VM to the UI +pub enum UiUpdate {} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e8e16ff..dc542ae 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,8 +4,11 @@ mod colon_prompt; mod input; mod keybinds; mod mail_tab; +mod messages; +mod windows; use std::any::Any; +use std::collections::HashMap; use std::io::Stdout; use std::mem; use std::sync::{ @@ -39,6 +42,8 @@ use crate::mail::{EmailMetadata, MailEvent}; use self::colon_prompt::ColonPrompt; use self::input::{BaseInputHandler, HandlesInput, InputResult}; use self::mail_tab::MailTabState; +pub(crate) use self::messages::*; +use self::windows::*; pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>; pub(crate) type TermType<'a, 'b> = &'b mut Terminal>; @@ -65,6 +70,13 @@ pub async fn run_ui( mail_tab.change.clone(), ))]; + let mut window_layout = WindowLayout::default(); + let mut page_names = HashMap::new(); + + // TODO: have this be configured thru the settings? + let (mail_id, mail_page) = window_layout.new_page(); + page_names.insert(mail_page, "Email"); + while !should_exit.load(Ordering::Relaxed) { term.draw(|f| { let chunks = Layout::default() @@ -78,7 +90,13 @@ pub async fn run_ui( .split(f.size()); // this is the title bar - let titles = vec!["email"].into_iter().map(Spans::from).collect(); + // let titles = vec!["email"].into_iter().map(Spans::from).collect(); + let titles = window_layout + .list_pages() + .iter() + .filter_map(|id| page_names.get(id)) + .map(|s| Spans::from(*s)) + .collect(); let tabs = Tabs::new(titles); f.render_widget(tabs, chunks[0]); diff --git a/src/ui/windows.rs b/src/ui/windows.rs new file mode 100644 index 0000000..83de86e --- /dev/null +++ b/src/ui/windows.rs @@ -0,0 +1,78 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +use tui::layout::Rect; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct LayoutId(usize); + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct PageId(usize); + +#[derive(Default)] +pub struct WindowLayout { + ctr: usize, + currently_active: Option, + + ids: HashMap, + page_order: Vec, + pages: HashMap, + + layout_cache: HashMap>, +} + +impl WindowLayout { + /// Adds a new page to the list + pub fn new_page(&mut self) -> (LayoutId, PageId) { + let id = LayoutId(self.ctr); + self.ctr += 1; + + let pg = PageGraph::new(id); + let pid = PageId(self.ctr); + self.ctr += 1; + self.pages.insert(pid, pg); + self.page_order.push(pid); + (id, pid) + } + + pub fn list_pages(&self) -> &[PageId] { + &self.page_order + } + + /// Get a set of all windows visible on the current page + pub fn visible_windows(&self) -> HashMap { + let mut map = HashMap::new(); + if let Some(page) = self + .currently_active + .as_ref() + .and_then(|id| self.ids.get(id)) + .and_then(|pid| self.pages.get(pid)) + { + let mut q = VecDeque::new(); + q.push_back(page.root); + + while !q.is_empty() { + let front = q.pop_front().expect("not empty"); + } + } + map + } +} + +struct PageGraph { + root: LayoutId, + adj: HashMap>, +} + +enum Dir { + H, + V, +} + +impl PageGraph { + pub fn new(id: LayoutId) -> Self { + PageGraph { + root: id, + adj: HashMap::new(), + } + } +}