diff --git a/src/lib.rs b/src/lib.rs index a105933..2a02fcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,142 @@ -pub mod config; \ No newline at end of file +use std::io::{Error, Stdout}; +use std::net::TcpStream; +use std::time::{Duration, Instant}; +use crossterm::{event}; +use crossterm::event::{Event, KeyCode}; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::prelude::{Color, Modifier, Style}; +use ratatui::Terminal; +use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use crate::config::Config; + +pub mod config; + +const POLL_INTERVAL: Duration = Duration::from_secs(30); + +pub(crate) struct Email { + subject: String, + from: String, + date: String, +} + +pub fn main(config: &Config, mut terminal: &mut Terminal>) -> Result<(), Error>{ + let mut inbox = fetch_inbox(config); + let mut last_fetch = Instant::now(); + + // --- Main loop --- + loop { + if last_fetch.elapsed() >= POLL_INTERVAL { + inbox = fetch_inbox(&config); + last_fetch = Instant::now(); + } + + terminal.draw(|frame| { + let area = frame.area(); + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(3), Constraint::Length(1)]) + .split(area); + + match &inbox { + Ok(emails) if !emails.is_empty() => { + let items: Vec = emails + .iter() + .map(|e| { + ListItem::new(format!("{} | {} | {}", e.date, e.from, e.subject)) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .title(format!("Inbox ({} messages)", emails.len())) + .borders(Borders::ALL), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + + frame.render_widget(list, layout[0]); + } + Ok(_) => { + let p = Paragraph::new("No messages in inbox.") + .block(Block::default().title("Inbox").borders(Borders::ALL)) + .style(Style::default().fg(Color::Yellow)); + frame.render_widget(p, layout[0]); + } + Err(e) => { + let p = Paragraph::new(format!("IMAP error: {}", e)) + .block(Block::default().title("Inbox").borders(Borders::ALL)) + .style(Style::default().fg(Color::Red)); + frame.render_widget(p, layout[0]); + } + } + + let status = Paragraph::new(" 'q' quit | 'r' refresh") + .style(Style::default().fg(Color::DarkGray)); + frame.render_widget(status, layout[1]); + })?; + + // --- Input handling --- + if event::poll(Duration::from_millis(200))? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => break, + KeyCode::Char('r') => { + inbox = fetch_inbox(&config); + last_fetch = Instant::now(); + } + _ => {} + } + } + } + } + + Ok(()) +} + +pub(crate) fn fetch_inbox(config: &Config) -> Result, String> { + let imap_cfg = &config.imap; + let stream = + TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?; + let client = imap::Client::new(stream); + let mut session = client + .login(&imap_cfg.username, &imap_cfg.password) + .map_err(|(e, _)| e.to_string())?; + + session.select("INBOX").map_err(|e| e.to_string())?; + + let mut emails = Vec::new(); + + let messages = session + .fetch("1:*", "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") + .map_err(|e| e.to_string())?; + + for message in messages.iter() { + if let Some(body) = message.header() { + let header = String::from_utf8_lossy(body); + let mut subject = String::new(); + let mut from = String::new(); + let mut date = String::new(); + + for line in header.lines() { + if let Some(val) = line.strip_prefix("Subject: ") { + subject = val.to_string(); + } else if let Some(val) = line.strip_prefix("From: ") { + from = val.to_string(); + } else if let Some(val) = line.strip_prefix("Date: ") { + date = val.to_string(); + } + } + + emails.push(Email { + subject, + from, + date, + }); + } + } + + let _ = session.logout(); + Ok(emails) +} diff --git a/src/main.rs b/src/main.rs index 2921f36..7487c41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,153 +1,27 @@ -use std::{io, net::TcpStream, time::{Duration, Instant}}; +use std::io; use crossterm::{ - event::{self, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use hello_ratatui::config::Config; use ratatui::{ backend::CrosstermBackend, - layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - widgets::{Block, Borders, List, ListItem, Paragraph}, Terminal, }; -use hello_ratatui::config::Config; - -struct Email { - subject: String, - from: String, - date: String, -} - -fn fetch_inbox(config: &Config) -> Result, String> { - let imap_cfg = &config.imap; - let stream = - TcpStream::connect((&*imap_cfg.host, imap_cfg.port)).map_err(|e| e.to_string())?; - let client = imap::Client::new(stream); - let mut session = client - .login(&imap_cfg.username, &imap_cfg.password) - .map_err(|(e, _)| e.to_string())?; - - session.select("INBOX").map_err(|e| e.to_string())?; - - let mut emails = Vec::new(); - - let messages = session - .fetch("1:*", "BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)]") - .map_err(|e| e.to_string())?; - - for message in messages.iter() { - if let Some(body) = message.header() { - let header = String::from_utf8_lossy(body); - let mut subject = String::new(); - let mut from = String::new(); - let mut date = String::new(); - - for line in header.lines() { - if let Some(val) = line.strip_prefix("Subject: ") { - subject = val.to_string(); - } else if let Some(val) = line.strip_prefix("From: ") { - from = val.to_string(); - } else if let Some(val) = line.strip_prefix("Date: ") { - date = val.to_string(); - } - } - - emails.push(Email { - subject, - from, - date, - }); - } - } - - let _ = session.logout(); - Ok(emails) -} - -const POLL_INTERVAL: Duration = Duration::from_secs(30); fn main() -> io::Result<()> { let config = Config::load().unwrap(); - let mut inbox = fetch_inbox(&config); - let mut last_fetch = Instant::now(); // --- Setup terminal --- - enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; + enable_raw_mode()?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // --- Main loop --- - loop { - if last_fetch.elapsed() >= POLL_INTERVAL { - inbox = fetch_inbox(&config); - last_fetch = Instant::now(); - } - - terminal.draw(|frame| { - let area = frame.area(); - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(3), Constraint::Length(1)]) - .split(area); - - match &inbox { - Ok(emails) if !emails.is_empty() => { - let items: Vec = emails - .iter() - .map(|e| { - ListItem::new(format!("{} | {} | {}", e.date, e.from, e.subject)) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .title(format!("Inbox ({} messages)", emails.len())) - .borders(Borders::ALL), - ) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - - frame.render_widget(list, layout[0]); - } - Ok(_) => { - let p = Paragraph::new("No messages in inbox.") - .block(Block::default().title("Inbox").borders(Borders::ALL)) - .style(Style::default().fg(Color::Yellow)); - frame.render_widget(p, layout[0]); - } - Err(e) => { - let p = Paragraph::new(format!("IMAP error: {}", e)) - .block(Block::default().title("Inbox").borders(Borders::ALL)) - .style(Style::default().fg(Color::Red)); - frame.render_widget(p, layout[0]); - } - } - - let status = Paragraph::new(" 'q' quit | 'r' refresh") - .style(Style::default().fg(Color::DarkGray)); - frame.render_widget(status, layout[1]); - })?; - - // --- Input handling --- - if event::poll(Duration::from_millis(200))? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') | KeyCode::Esc => break, - KeyCode::Char('r') => { - inbox = fetch_inbox(&config); - last_fetch = Instant::now(); - } - _ => {} - } - } - } - } + hello_ratatui::main(&config, &mut terminal)?; // --- Restore terminal --- disable_raw_mode()?;