diff --git a/Justfile b/Justfile index 5698236..8498573 100644 --- a/Justfile +++ b/Justfile @@ -6,3 +6,9 @@ doc-open: watch: cargo watch -x 'clippy --all --all-features' + +run: + cargo run -- --log-file output.log + +tail: + tail -f output.log diff --git a/imap/src/client/auth.rs b/imap/src/client/auth.rs index 7390902..adb5b59 100644 --- a/imap/src/client/auth.rs +++ b/imap/src/client/auth.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use futures::stream::StreamExt; use crate::command::Command; use crate::response::{Response, ResponseDone, Status}; @@ -11,6 +12,8 @@ pub trait Auth { // TODO: return the unauthed client if failed? async fn perform_auth(self, client: ClientUnauthenticated) -> Result; + /// Converts the wrappers around the client once the authentication has happened. Should only + /// be called by the `perform_auth` function. fn convert_client(client: ClientUnauthenticated) -> ClientAuthenticated { match client { ClientUnauthenticated::Encrypted(e) => ClientAuthenticated::Encrypted(e), @@ -32,19 +35,26 @@ impl Auth for Plain { password: self.password, }; - let (result, _) = client.execute(command).await?; - let result = result.await?; + let result = client.execute(command).await?; + let done = result.done().await?; - if !matches!( - result, - Response::Done(ResponseDone { - status: Status::Ok, - .. - }) - ) { - bail!("unable to login: {:?}", result); + assert!(done.is_some()); + let done = done.unwrap(); + + if done.status != Status::Ok { + bail!("unable to login: {:?}", done); } + // if !matches!( + // result, + // Response::Done(ResponseDone { + // status: Status::Ok, + // .. + // }) + // ) { + // bail!("unable to login: {:?}", result); + // } + Ok(::convert_client(client)) } } diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index 3d44831..588cf30 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -1,19 +1,21 @@ -use std::collections::{HashSet, VecDeque}; use std::pin::Pin; use std::sync::Arc; -use std::task::{Context, Poll, Waker}; +use std::task::{Context, Poll}; -use anyhow::{Context as AnyhowContext, Error, Result}; +use anyhow::Result; use futures::{ - future::{self, Either, Future, FutureExt, TryFutureExt}, - stream::StreamExt, + future::{self, Either, FutureExt, TryFutureExt}, + stream::{Peekable, Stream, StreamExt}, }; -use parking_lot::RwLock; +use parking_lot::Mutex; use tokio::{ io::{ self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf, }, - sync::{mpsc, oneshot}, + sync::{ + mpsc, + oneshot::{self, error::TryRecvError}, + }, task::JoinHandle, }; use tokio_rustls::{ @@ -23,169 +25,89 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use crate::command::Command; use crate::parser::{parse_capability, parse_response}; -use crate::response::{Capability, Response, ResponseCode, ResponseData, ResponseDone, Status}; +use crate::response::{Response, ResponseDone}; use super::ClientConfig; -pub type CapsLock = Arc>>>; -pub type ResponseFuture = Box> + Send + Unpin>; -pub type ResponseSender = mpsc::UnboundedSender; -pub type ResponseStream = mpsc::UnboundedReceiver; -type ResultQueue = Arc>>; -pub type GreetingState = Arc, Option)>>; pub const TAG_PREFIX: &str = "ptag"; +type Command2 = (Command, mpsc::UnboundedSender); -#[derive(Debug)] -struct HandlerResult { - id: usize, - end: Option>, - sender: ResponseSender, - waker: Option, -} - -/// The lower-level Client struct, that is shared by all of the exported structs in the state machine. pub struct Client { + ctr: usize, config: ClientConfig, - - /// write half of the connection conn: WriteHalf, - - /// counter for monotonically incrementing unique ids - id: usize, - - results: ResultQueue, - - /// cached set of capabilities - caps: CapsLock, - - /// join handle for the listener thread + cmd_tx: mpsc::UnboundedSender, + greeting_rx: Option>, + exit_tx: oneshot::Sender<()>, listener_handle: JoinHandle>>, - - /// used for telling the listener thread to stop and return the read half - exit_tx: mpsc::Sender<()>, - - /// used for receiving the greeting - greeting: GreetingState, } impl Client where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { - /// Creates a new client that wraps a connection pub fn new(conn: C, config: ClientConfig) -> Self { let (read_half, write_half) = io::split(conn); - let results = Arc::new(RwLock::new(VecDeque::new())); - let (exit_tx, exit_rx) = mpsc::channel(1); - let greeting = Arc::new(RwLock::new((None, None))); - let caps: CapsLock = Arc::new(RwLock::new(None)); - - let listener_handle = tokio::spawn( - listen( - read_half, - caps.clone(), - results.clone(), - exit_rx, - greeting.clone(), - ) - .map_err(|err| { + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); + let (greeting_tx, greeting_rx) = oneshot::channel(); + let (exit_tx, exit_rx) = oneshot::channel(); + let handle = tokio::spawn( + listen(read_half, cmd_rx, greeting_tx, exit_rx).map_err(|err| { error!("Help, the listener loop died: {}", err); err }), ); Client { - config, + ctr: 0, conn: write_half, - id: 0, - results, - listener_handle, - caps, + config, + cmd_tx, + greeting_rx: Some(greeting_rx), exit_tx, - greeting, + listener_handle: handle, } } - /// Returns a future that doesn't resolve until we receive a greeting from the server. - pub fn wait_for_greeting(&self) -> GreetingWaiter { - debug!("waiting for greeting"); - GreetingWaiter(self.greeting.clone()) + pub async fn wait_for_greeting(&mut self) -> Result<()> { + if let Some(greeting_rx) = self.greeting_rx.take() { + greeting_rx.await?; + } + Ok(()) } - /// Sends a command to the server and returns a handle to retrieve the result - pub async fn execute(&mut self, cmd: Command) -> Result<(ResponseFuture, ResponseStream)> { - // debug!("executing command {:?}", cmd); - let id = self.id; - self.id += 1; + pub async fn execute(&mut self, cmd: Command) -> Result { + let id = self.ctr; + self.ctr += 1; - // create a channel for sending the final response - let (end_tx, end_rx) = oneshot::channel(); - - // create a channel for sending responses for this particular client call - // this should queue up responses - let (tx, rx) = mpsc::unbounded_channel(); - - debug!("EX[{}]: adding handler result to the handlers queue", id); - { - let mut handlers = self.results.write(); - handlers.push_back(HandlerResult { - id, - end: Some(end_tx), - sender: tx, - waker: None, - }); - } - - debug!("EX[{}]: send the command to the server", id); let cmd_str = format!("{}{} {}\r\n", TAG_PREFIX, id, cmd); self.conn.write_all(cmd_str.as_bytes()).await?; self.conn.flush().await?; - debug!("EX[{}]: hellosu", id); - let q = self.results.clone(); - // let end = Box::new(end_rx.map_err(|err| Error::from).map(move |resp| resp)); - let end = Box::new(end_rx.map_err(Error::from).map(move |resp| { - debug!("EX[{}]: -end result- {:?}", id, resp); - // pop the first entry from the list - let mut results = q.write(); - results.pop_front(); - resp - })); + let (tx, rx) = mpsc::unbounded_channel(); + self.cmd_tx.send((cmd, tx))?; - Ok((end, rx)) + Ok(ResponseStream { inner: rx }) } - /// Executes the CAPABILITY command - pub async fn capabilities(&mut self, force: bool) -> Result<()> { - { - let caps = self.caps.read(); - if caps.is_some() && !force { - return Ok(()); + pub async fn has_capability(&mut self, cap: impl AsRef) -> Result { + // TODO: cache capabilities if needed? + let cap = cap.as_ref(); + let cap = parse_capability(cap)?; + + let resp = self.execute(Command::Capability).await?; + let (_, data) = resp.wait().await?; + + for resp in data { + if let Response::Capabilities(caps) = resp { + return Ok(caps.contains(&cap)); } + // debug!("cap: {:?}", resp); } - let cmd = Command::Capability; - // debug!("sending: {:?} {:?}", cmd, cmd.to_string()); - let (result, intermediate) = self - .execute(cmd) - .await - .context("error executing CAPABILITY command")?; - let _ = result.await?; - - if let Some(Response::Capabilities(new_caps)) = UnboundedReceiverStream::new(intermediate) - .filter(|resp| future::ready(matches!(resp, Response::Capabilities(_)))) - .next() - .await - { - debug!("FOUND NEW CAPABILITIES: {:?}", new_caps); - let mut caps = self.caps.write(); - *caps = Some(new_caps.iter().cloned().collect()); - } - - Ok(()) + Ok(false) } - /// Attempts to upgrade this connection using STARTTLS pub async fn upgrade(mut self) -> Result>> { // TODO: make sure STARTTLS is in the capability list if !self.has_capability("STARTTLS").await? { @@ -193,17 +115,17 @@ where } // first, send the STARTTLS command - let (resp, _) = self.execute(Command::Starttls).await?; - let resp = resp.await?; + let mut resp = self.execute(Command::Starttls).await?; + let resp = resp.next().await.unwrap(); debug!("server response to starttls: {:?}", resp); debug!("sending exit for upgrade"); - self.exit_tx.send(()).await?; + // TODO: check that the channel is still open? + self.exit_tx.send(()).unwrap(); let reader = self.listener_handle.await??; let writer = self.conn; let conn = reader.unsplit(writer); - let server_name = &self.config.hostname; let mut tls_config = RustlsConfig::new(); @@ -217,147 +139,106 @@ where Ok(Client::new(stream, self.config)) } +} - /// Check if this client has a particular capability - pub async fn has_capability(&mut self, cap: impl AsRef) -> Result { - let cap = cap.as_ref().to_owned(); - debug!("checking for the capability: {:?}", cap); - let cap = parse_capability(cap)?; +pub struct ResponseStream { + inner: mpsc::UnboundedReceiver, +} - self.capabilities(false).await?; - let caps = self.caps.read(); - // TODO: refresh caps +impl ResponseStream { + /// Retrieves just the DONE item in the stream, discarding the rest + pub async fn done(mut self) -> Result> { + while let Some(resp) = self.inner.recv().await { + if let Response::Done(done) = resp { + return Ok(Some(done)); + } + } + Ok(None) + } - let caps = caps.as_ref().unwrap(); - let result = caps.contains(&cap); - debug!("cap result: {:?}", result); - Ok(result) + /// Waits for the entire stream to finish, returning the DONE status and the stream + pub async fn wait(mut self) -> Result<(Option, Vec)> { + let mut done = None; + let mut vec = Vec::new(); + while let Some(resp) = self.inner.recv().await { + if let Response::Done(d) = resp { + done = Some(d); + break; + } else { + vec.push(resp); + } + } + Ok((done, vec)) } } -pub struct GreetingWaiter(GreetingState); - -impl Future for GreetingWaiter { - type Output = Response; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let (state, waker) = &mut *self.0.write(); - debug!("g {:?}", state); - if waker.is_none() { - *waker = Some(cx.waker().clone()); - } - - match state.take() { - Some(v) => Poll::Ready(v), - None => Poll::Pending, - } +impl Stream for ResponseStream { + type Item = Response; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.inner.poll_recv(cx) } } -/// Main listen loop for the application +#[allow(unreachable_code)] async fn listen( - conn: C, - caps: CapsLock, - results: ResultQueue, - mut exit: mpsc::Receiver<()>, - greeting: GreetingState, -) -> Result + conn: ReadHalf, + mut cmd_rx: mpsc::UnboundedReceiver, + greeting_tx: oneshot::Sender<()>, + mut exit_rx: oneshot::Receiver<()>, +) -> Result> where C: AsyncRead + Unpin, { - // debug!("amogus"); let mut reader = BufReader::new(conn); - let mut greeting = Some(greeting); + let mut greeting_tx = Some(greeting_tx); + let mut curr_cmd: Option = None; + let mut exit_rx = exit_rx.map_err(|_| ()).shared(); + // let mut exit_fut = Some(exit_rx.fuse()); + // let mut fut1 = None; loop { let mut next_line = String::new(); - let fut = reader.read_line(&mut next_line).fuse(); - pin_mut!(fut); - let fut2 = exit.recv().fuse(); - pin_mut!(fut2); + let read_fut = reader.read_line(&mut next_line).fuse(); + pin_mut!(read_fut); - match future::select(fut, fut2).await { - Either::Left((res, _)) => { - let bytes = res.context("read failed")?; - if bytes == 0 { - bail!("connection probably died"); - } + // only listen for a new command if there isn't one already + let mut cmd_fut = if let Some(_) = curr_cmd { + // if there is one, just make a future that never resolves so it'll always pick the + // other options in the select. + future::pending().boxed().fuse() + } else { + cmd_rx.recv().boxed().fuse() + }; - debug!("[LISTEN] got a new line {:?}", next_line); - let resp = parse_response(next_line)?; - debug!("[LISTEN] parsed as {:?}", resp); + select! { + _ = exit_rx => { + debug!("exiting the loop"); + break; + } - // if this is the very first message, treat it as a greeting - if let Some(greeting) = greeting.take() { - let (greeting, waker) = &mut *greeting.write(); - debug!("[LISTEN] received greeting!"); - *greeting = Some(resp.clone()); - if let Some(waker) = waker.take() { - waker.wake(); - } - } - - // update capabilities list - // TODO: probably not really necessary here (done somewhere else)? - if let Response::Capabilities(new_caps) - | Response::Data(ResponseData { - status: Status::Ok, - code: Some(ResponseCode::Capabilities(new_caps)), - .. - }) = &resp - { - let caps = &mut *caps.write(); - *caps = Some(new_caps.iter().cloned().collect()); - debug!("new caps: {:?}", caps); - } - - match &resp { - // bye - Response::Data(ResponseData { - status: Status::Bye, - .. - }) => { - bail!("disconnected"); - } - - Response::Done(ResponseDone { tag, .. }) => { - if tag.starts_with(TAG_PREFIX) { - // let id = tag.trim_start_matches(TAG_PREFIX).parse::()?; - debug!("[LISTEN] Done: {:?}", tag); - let mut results = results.write(); - if let Some(HandlerResult { end, waker, .. }) = - results.iter_mut().next() - { - if let Some(end) = end.take() { - end.send(resp).unwrap(); - } - // *opt = Some(resp); - if let Some(waker) = waker.take() { - waker.wake(); - } - } - } - } - - _ => { - debug!("[LISTEN] RESPONSE: {:?}", resp); - let mut results = results.write(); - if let Some(HandlerResult { id, sender, .. }) = results.iter_mut().next() { - // we don't really care if it fails to send - // this just means that the other side has dropped the channel - // - // which is fine since that just means they don't care about - // intermediate messages - let _ = sender.send(resp); - debug!("[LISTEN] pushed to intermediate for id {}", id); - debug!("[LISTEN] res: {:?}", results); - } - } // _ => {} + cmd = cmd_fut => { + if curr_cmd.is_none() { + curr_cmd = cmd; } } - Either::Right((_, _)) => { - debug!("exiting read loop"); - break; + len = read_fut => { + // res should not be None here + let resp = parse_response(next_line)?; + + // if this is the very first response, then it's a greeting + if let Some(greeting_tx) = greeting_tx.take() { + greeting_tx.send(()); + } + + if let Response::Done(_) = resp { + // since this is the DONE message, clear curr_cmd so another one can be sent + if let Some((_, cmd_tx)) = curr_cmd.take() { + cmd_tx.send(resp)?; + } + } else if let Some((ref cmd, ref mut cmd_tx)) = curr_cmd { + cmd_tx.send(resp)?; + } } } } diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs index 0998c54..afeff53 100644 --- a/imap/src/client/mod.rs +++ b/imap/src/client/mod.rs @@ -50,9 +50,9 @@ use tokio_rustls::{ use tokio_stream::wrappers::UnboundedReceiverStream; use crate::command::Command; -use crate::response::{Response, ResponseData, ResponseDone}; +use crate::response::{MailboxData, Response, ResponseData, ResponseDone}; -pub use self::inner::{Client, ResponseFuture, ResponseStream}; +pub use self::inner::{Client, ResponseStream}; /// Struct used to start building the config for a client. /// @@ -93,12 +93,12 @@ impl ClientConfig { let dnsname = DNSNameRef::try_from_ascii_str(hostname).unwrap(); let conn = tls_config.connect(dnsname, conn).await?; - let inner = Client::new(conn, self); - inner.wait_for_greeting().await; + let mut inner = Client::new(conn, self); + inner.wait_for_greeting().await?; return Ok(ClientUnauthenticated::Encrypted(inner)); } else { - let inner = Client::new(conn, self); - inner.wait_for_greeting().await; + let mut inner = Client::new(conn, self); + inner.wait_for_greeting().await?; return Ok(ClientUnauthenticated::Unencrypted(inner)); } } @@ -121,7 +121,7 @@ impl ClientUnauthenticated { } /// Exposing low-level execute - async fn execute(&mut self, cmd: Command) -> Result<(ResponseFuture, ResponseStream)> { + async fn execute(&mut self, cmd: Command) -> Result { match self { ClientUnauthenticated::Encrypted(e) => e.execute(cmd).await, ClientUnauthenticated::Unencrypted(e) => e.execute(cmd).await, @@ -137,12 +137,6 @@ impl ClientUnauthenticated { } } -#[derive(Debug)] -pub struct ResponseCombined { - pub data: Vec, - pub done: ResponseDone, -} - pub enum ClientAuthenticated { Encrypted(Client>), Unencrypted(Client), @@ -150,45 +144,13 @@ pub enum ClientAuthenticated { impl ClientAuthenticated { /// Exposing low-level execute - async fn execute(&mut self, cmd: Command) -> Result<(ResponseFuture, ResponseStream)> { + async fn execute(&mut self, cmd: Command) -> Result { match self { ClientAuthenticated::Encrypted(e) => e.execute(cmd).await, ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await, } } - /// A wrapper around `execute` that waits for the response and returns a combined data - /// structure containing the intermediate results as well as the final status - async fn execute_combined(&mut self, cmd: Command) -> Result { - let (resp, mut stream) = self.execute(cmd).await?; - let mut resp = resp.into_stream(); // turn into stream to avoid mess with returning futures from select - - let mut data = Vec::new(); - debug!("[COMBI] loop"); - let done = loop { - let fut1 = resp.next().fuse(); - let fut2 = stream.recv().fuse(); - pin_mut!(fut1); - pin_mut!(fut2); - - match future::select(fut1, fut2).await { - Either::Left((Some(Ok(Response::Done(done))), _)) => { - debug!("[COMBI] left: {:?}", done); - break done; - } - Either::Left(_) => unreachable!("got non-Response::Done from listen!"), - - Either::Right((Some(resp), _)) => { - debug!("[COMBI] right: {:?}", resp); - data.push(resp); - } - Either::Right(_) => unreachable!(), - } - }; - - Ok(ResponseCombined { data, done }) - } - /// Runs the LIST command pub async fn list(&mut self) -> Result> { let cmd = Command::List { @@ -196,29 +158,17 @@ impl ClientAuthenticated { mailbox: "*".to_owned(), }; - let res = self.execute_combined(cmd).await?; - debug!("res: {:?}", res); - todo!() + let res = self.execute(cmd).await?; + let (_, data) = res.wait().await?; - // let mut folders = Vec::new(); - // loop { - // let st_next = st.recv(); - // pin_mut!(st_next); + let mut folders = Vec::new(); + for resp in data { + if let Response::MailboxData(MailboxData::List { name, .. }) = resp { + folders.push(name.to_owned()); + } + } - // match future::select(resp, st_next).await { - // Either::Left((v, _)) => { - // break; - // } - - // Either::Right((v, _) ) => { - // debug!("RESP: {:?}", v); - // // folders.push(v); - // } - // } - // } - - // let resp = resp.await?; - // debug!("list response: {:?}", resp); + Ok(folders) } /// Runs the SELECT command @@ -226,11 +176,12 @@ impl ClientAuthenticated { let cmd = Command::Select { mailbox: mailbox.as_ref().to_owned(), }; - let (resp, mut st) = self.execute(cmd).await?; + let mut stream = self.execute(cmd).await?; + // let (resp, mut st) = self.execute(cmd).await?; debug!("execute called returned..."); - debug!("ST: {:?}", st.recv().await); - let resp = resp.await?; - debug!("select response: {:?}", resp); + debug!("ST: {:?}", stream.next().await); + // let resp = resp.await?; + // debug!("select response: {:?}", resp); // nuke the capabilities cache self.nuke_capabilities(); @@ -240,10 +191,10 @@ impl ClientAuthenticated { /// Runs the IDLE command #[cfg(feature = "rfc2177-idle")] - pub async fn idle(&mut self) -> Result> { + pub async fn idle(&mut self) -> Result { let cmd = Command::Idle; - let (_, stream) = self.execute(cmd).await?; - Ok(UnboundedReceiverStream::new(stream)) + let stream = self.execute(cmd).await?; + Ok(stream) } fn nuke_capabilities(&mut self) { diff --git a/imap/src/command/mod.rs b/imap/src/command/mod.rs index 8874c6d..043a74d 100644 --- a/imap/src/command/mod.rs +++ b/imap/src/command/mod.rs @@ -1,7 +1,7 @@ use std::fmt; /// Commands, without the tag part. -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum Command { Capability, Starttls, @@ -21,6 +21,22 @@ pub enum Command { Idle, } +impl fmt::Debug for Command { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Command::*; + match self { + Capability => write!(f, "CAPABILITY"), + Starttls => write!(f, "STARTTLS"), + Login { .. } => write!(f, "LOGIN"), + Select { mailbox } => write!(f, "SELECT {}", mailbox), + List { reference, mailbox } => write!(f, "LIST {:?} {:?}", reference, mailbox), + + #[cfg(feature = "rfc2177-idle")] + Idle => write!(f, "IDLE"), + } + } +} + impl fmt::Display for Command { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Command::*; diff --git a/src/mail/mod.rs b/src/mail/mod.rs index 33ebf6b..69fe5bc 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -21,6 +21,7 @@ use tokio_stream::wrappers::WatchStream; use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; /// Command sent to the mail thread by something else (i.e. UI) +#[derive(Debug)] pub enum MailCommand { /// Refresh the list Refresh, @@ -30,6 +31,7 @@ pub enum MailCommand { } /// Possible events returned from the server that should be sent to the UI +#[derive(Debug)] pub enum MailEvent { /// Got the list of folders FolderList(Vec), diff --git a/src/main.rs b/src/main.rs index cf21323..0c556e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,11 @@ use std::thread; use anyhow::Result; use fern::colors::{Color, ColoredLevelConfig}; use futures::future::TryFutureExt; -use panorama::{config::spawn_config_watcher_system, mail, report_err, ui}; +use panorama::{ + config::spawn_config_watcher_system, + mail::{self, MailEvent}, + report_err, ui, +}; use structopt::StructOpt; use tokio::{ runtime::{Builder as RuntimeBuilder, Runtime}, @@ -63,7 +67,7 @@ async fn run(opt: Opt) -> Result<()> { }); if !opt.headless { - run_ui(exit_tx); + run_ui(exit_tx, mail2ui_rx); } exit_rx.recv().await; @@ -76,7 +80,7 @@ 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<()>) { +fn run_ui(exit_tx: mpsc::Sender<()>, mail2ui_rx: mpsc::UnboundedReceiver) { let stdout = std::io::stdout(); let rt = RuntimeBuilder::new_current_thread() @@ -88,7 +92,9 @@ fn run_ui(exit_tx: mpsc::Sender<()>) { let localset = LocalSet::new(); localset.spawn_local(async { - ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err).await; + ui::run_ui(stdout, exit_tx, mail2ui_rx) + .unwrap_or_else(report_err) + .await; }); rt.block_on(localset); diff --git a/src/ui/mail_tab.rs b/src/ui/mail_tab.rs index c81df2e..e3e1c4f 100644 --- a/src/ui/mail_tab.rs +++ b/src/ui/mail_tab.rs @@ -1,21 +1,29 @@ use tui::{ buffer::Buffer, - layout::Rect, - widgets::{StatefulWidget, Widget}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + widgets::*, }; -pub struct MailTabState {} +use super::FrameType; -impl MailTabState { - pub fn new() -> Self { - MailTabState {} - } -} - -pub struct MailTab; - -impl StatefulWidget for MailTab { - type State = MailTabState; - - fn render(self, rect: Rect, buffer: &mut Buffer, state: &mut Self::State) {} +pub fn render_mail_tab(f: &mut FrameType, area: Rect, folders: &[String]) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints([Constraint::Length(20), Constraint::Max(5000)]) + .split(area); + + let items = folders + .iter() + .map(|s| ListItem::new(s.to_owned())) + .collect::>(); + + let dirlist = List::new(items) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + .highlight_symbol(">>"); + + f.render_widget(dirlist, chunks[0]); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d8dec6a..09befa8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -12,6 +12,7 @@ use crossterm::{ event::{self, Event, KeyCode, KeyEvent}, style, terminal, }; +use futures::{future::FutureExt, select, stream::StreamExt}; use tokio::{sync::mpsc, time}; use tui::{ backend::CrosstermBackend, @@ -22,70 +23,42 @@ use tui::{ Frame, Terminal, }; -use self::mail_tab::{MailTab, MailTabState}; +use crate::mail::MailEvent; -// pub(crate) type FrameType<'a> = Frame<'a, CrosstermBackend>; +use self::mail_tab::render_mail_tab; + +pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>; const FRAME_DURATION: Duration = Duration::from_millis(17); /// Main entrypoint for the UI -pub async fn run_ui(mut stdout: Stdout, exit_tx: mpsc::Sender<()>) -> Result<()> { +pub async fn run_ui( + mut stdout: Stdout, + exit_tx: mpsc::Sender<()>, + mut mail2ui_rx: mpsc::UnboundedReceiver, +) -> Result<()> { execute!(stdout, cursor::Hide, terminal::EnterAlternateScreen)?; terminal::enable_raw_mode()?; let backend = CrosstermBackend::new(&mut stdout); let mut term = Terminal::new(backend)?; - let mut mail_state = MailTabState::new(); + let mut folders = Vec::::new(); loop { term.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .margin(0) - .constraints([ - Constraint::Length(1), - Constraint::Max(5000), - // Constraint::Percentage(10), - // Constraint::Percentage(80), - // Constraint::Percentage(10), - ]) + .constraints([Constraint::Length(1), Constraint::Max(5000)]) .split(f.size()); - // let chunks2 = Layout::default() - // .direction(Direction::Horizontal) - // .margin(0) - // .constraints([ - // Constraint::Length(20), - // Constraint::Max(5000), - // // - // ]) - // .split(chunks[1]); - // this is the title bar let titles = vec!["hellosu"].into_iter().map(Spans::from).collect(); let tabs = Tabs::new(titles); f.render_widget(tabs, chunks[0]); - let mail_tab = MailTab; - f.render_stateful_widget(mail_tab, chunks[1], &mut mail_state); - // TODO: check active tab - // let items = [ - // ListItem::new("Osu"), - // ListItem::new("Game").style(Style::default().add_modifier(Modifier::BOLD)), - // ]; - // let dirlist = List::new(items) - // .block(Block::default().title("List").borders(Borders::ALL)) - // .style(Style::default().fg(Color::White)) - // .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) - // .highlight_symbol(">>"); - // f.render_widget(dirlist, chunks2[0]); - - // let block = Block::default().title("Block").borders(Borders::ALL); - // f.render_widget(block, chunks2[1]); - - // let block = Block::default().title("Block 2").borders(Borders::ALL); - // f.render_widget(block, chunks[1]); + render_mail_tab(f, chunks[1], &folders); })?; let event = if event::poll(FRAME_DURATION)? { @@ -105,17 +78,22 @@ pub async fn run_ui(mut stdout: Stdout, exit_tx: mpsc::Sender<()>) -> Result<()> None }; - // approx 60fps - time::sleep(FRAME_DURATION).await; + select! { + mail_evt = mail2ui_rx.recv().fuse() => { + debug!("received mail event: {:?}", mail_evt); + // TODO: handle case that channel is closed later + let mail_evt = mail_evt.unwrap(); - // if let Event::Input(input) = events.next()? { - // match input { - // Key::Char('q') => { - // break; - // } - // _ => {} - // } - // } + match mail_evt { + MailEvent::FolderList(new_folders) => { + folders = new_folders; + } + } + } + + // approx 60fps + _ = time::sleep(FRAME_DURATION).fuse() => {} + } } mem::drop(term);