From 22f11544e0135561acb6923e1053755b4680ff26 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Fri, 26 Mar 2021 14:39:06 -0500 Subject: [PATCH] ok replaced tui's closure-style read loop with a non-closure loop --- Cargo.lock | 33 ++-- Cargo.toml | 19 ++- src/lib.rs | 2 - src/mail/store.rs | 4 +- src/main.rs | 8 +- src/ui/colon_prompt.rs | 2 +- src/ui/input.rs | 2 +- src/ui/mail_view.rs | 98 ++++++------ src/ui/mod.rs | 52 ++++--- src/ui/windows.rs | 4 +- tui/Cargo.toml | 6 + tui/src/lib.rs | 340 +++++++++++++++++++++++++++++++++++++++++ 12 files changed, 468 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 799115f..3d7343a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,9 +49,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" [[package]] name = "archery" @@ -1525,7 +1525,7 @@ dependencies = [ "futures-core", "inotify-sys", "libc", - "tokio 1.3.0", + "tokio 1.4.0", ] [[package]] @@ -2014,7 +2014,6 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "chrono-humanize", - "crossterm 0.19.0", "downcast-rs", "fern", "format-bytes", @@ -2027,6 +2026,7 @@ dependencies = [ "notify-rust", "panorama-imap", "panorama-smtp", + "panorama-tui", "parking_lot", "quoted_printable", "serde", @@ -2034,12 +2034,11 @@ dependencies = [ "shellexpand", "sqlx", "structopt", - "tokio 1.3.0", + "tokio 1.4.0", "tokio-rustls", "tokio-stream", "tokio-util", "toml", - "tui", "webpki-roots", "xdg", ] @@ -2060,7 +2059,7 @@ dependencies = [ "pest", "pest_derive", "quoted_printable", - "tokio 1.3.0", + "tokio 1.4.0", "tokio-rustls", "tokio-util", "webpki-roots", @@ -2073,6 +2072,14 @@ version = "0.1.0" [[package]] name = "panorama-tui" version = "0.1.0" +dependencies = [ + "anyhow", + "crossterm 0.19.0", + "futures 0.3.13", + "log", + "tokio 1.4.0", + "tui", +] [[package]] name = "parking" @@ -2869,7 +2876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ce2e16b6774c671cc183e1d202386fdf9cde1e8468c1894a7f2a63eb671c4f4" dependencies = [ "once_cell", - "tokio 1.3.0", + "tokio 1.4.0", "tokio-rustls", ] @@ -3143,9 +3150,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d56477f6ed99e10225f38f9f75f872f29b8b8bd8c0b946f63345bb144e9eeda" +checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722" dependencies = [ "autocfg", "bytes 1.0.1", @@ -3179,7 +3186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls", - "tokio 1.3.0", + "tokio 1.4.0", "webpki", ] @@ -3191,7 +3198,7 @@ checksum = "c535f53c0cfa1acace62995a8994fc9cc1f12d202420da96ff306ee24d576469" dependencies = [ "futures-core", "pin-project-lite 0.2.6", - "tokio 1.3.0", + "tokio 1.4.0", "tokio-util", ] @@ -3208,7 +3215,7 @@ dependencies = [ "log", "pin-project-lite 0.2.6", "slab", - "tokio 1.3.0", + "tokio 1.4.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 514d657..3c36dbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,38 +16,37 @@ members = [ ] [dependencies] +# tantivy = "0.14.0" anyhow = "1.0.39" async-trait = "0.1.48" cfg-if = "1.0.0" chrono = "0.4.19" chrono-humanize = "0.1.2" -crossterm = { version = "0.19.0", features = ["event-stream"] } +downcast-rs = "1.2.0" fern = { version = "0.6.0", features = ["colored"] } format-bytes = "0.2.2" futures = "0.3.13" gluon = "0.17.2" +hex = "0.4.3" inotify = { version = "0.9.2", features = ["stream"] } log = "0.4.14" +mailparse = "0.13.2" notify-rust = { version = "4.3.0", default-features = false, features = ["z"] } +panorama-tui = { path = "tui" } parking_lot = "0.11.1" +quoted_printable = "0.4.2" serde = { version = "1.0.124", features = ["derive"] } +sha2 = "0.9.3" +shellexpand = "2.1.0" +sqlx = { version = "0.5.1", features = ["runtime-tokio-rustls", "sqlite"] } structopt = "0.3.21" tokio = { version = "1.3.0", features = ["full"] } tokio-rustls = "0.22.0" tokio-stream = { version = "0.1.4", features = ["sync"] } tokio-util = { version = "0.6.4", features = ["full"] } toml = "0.5.8" -tui = { version = "0.14.0", default-features = false, features = ["crossterm"] } webpki-roots = "0.21.0" xdg = "2.2.0" -downcast-rs = "1.2.0" -quoted_printable = "0.4.2" -sqlx = { version = "0.5.1", features = ["runtime-tokio-rustls", "sqlite"] } -sha2 = "0.9.3" -hex = "0.4.3" -shellexpand = "2.1.0" -mailparse = "0.13.2" -# tantivy = "0.14.0" [dependencies.panorama-imap] path = "imap" diff --git a/src/lib.rs b/src/lib.rs index 9efbe9a..5881af2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,6 @@ extern crate anyhow; #[macro_use] extern crate async_trait; #[macro_use] -extern crate crossterm; -#[macro_use] extern crate format_bytes; #[macro_use] extern crate serde; diff --git a/src/mail/store.rs b/src/mail/store.rs index d9c0471..8ed9ca6 100644 --- a/src/mail/store.rs +++ b/src/mail/store.rs @@ -154,7 +154,7 @@ impl MailStore { .fetch_one(&inner.pool) .await, )?; - mem::drop(inner); + mem::drop(read); if let Some(existing) = existing { let rowid = existing.0; @@ -272,7 +272,7 @@ impl MailStore { .context("error inserting email into db")? .last_insert_rowid(); } - mem::drop(inner); + mem::drop(read); // self.email_events // .send(EmailUpdateInfo {}) diff --git a/src/main.rs b/src/main.rs index adbe28f..5ab581e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,7 +74,13 @@ async fn run(opt: Opt) -> Result<()> { if !opt.headless { let config_update2 = config_update.clone(); - run_ui(config_update2, mail_store.clone(), exit_tx, mail2ui_rx, ui2vm_tx); + run_ui( + config_update2, + mail_store.clone(), + exit_tx, + mail2ui_rx, + ui2vm_tx, + ); } exit_rx.recv().await; diff --git a/src/ui/colon_prompt.rs b/src/ui/colon_prompt.rs index 592e4e5..5da8d9e 100644 --- a/src/ui/colon_prompt.rs +++ b/src/ui/colon_prompt.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent}; +use panorama_tui::crossterm::event::{KeyCode, KeyEvent}; use super::input::{HandlesInput, InputResult}; use super::TermType; diff --git a/src/ui/input.rs b/src/ui/input.rs index 63c3f06..909a328 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -6,8 +6,8 @@ use std::sync::{ }; use anyhow::Result; -use crossterm::event::{self, Event, KeyCode, KeyEvent}; use downcast_rs::Downcast; +use panorama_tui::crossterm::event::{self, Event, KeyCode, KeyEvent}; use super::colon_prompt::ColonPrompt; use super::TermType; diff --git a/src/ui/mail_view.rs b/src/ui/mail_view.rs index dd573ca..58f366d 100644 --- a/src/ui/mail_view.rs +++ b/src/ui/mail_view.rs @@ -8,14 +8,16 @@ use std::sync::{ use anyhow::Result; use chrono::{DateTime, Datelike, Duration, Local}; use chrono_humanize::HumanTime; -use crossterm::event::{KeyCode, KeyEvent}; use panorama_imap::response::Envelope; -use tui::{ - buffer::Buffer, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::*, +use panorama_tui::{ + crossterm::event::{KeyCode, KeyEvent}, + tui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::*, + }, }; use crate::mail::EmailMetadata; @@ -54,36 +56,36 @@ impl Window for MailView { String::from("email") } - async fn draw(&self) { - // let chunks = Layout::default() - // .direction(Direction::Horizontal) - // .margin(0) - // .constraints([Constraint::Length(20), Constraint::Max(5000)]) - // .split(area); + async fn draw(&self, f: &mut FrameType<'_, '_>, area: Rect, ui: &UI) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints([Constraint::Length(20), Constraint::Max(5000)]) + .split(area); - // let accts = self.mail_store.list_accounts().await; + let accts = self.mail_store.list_accounts().await; - // // folder list - // let mut items = vec![]; - // for (acct_name, acct_ref) in accts.iter() { - // let folders = acct_ref.folders().await; + // folder list + let mut items = vec![]; + for (acct_name, acct_ref) in accts.iter() { + let folders = acct_ref.folders().await; - // items.push(ListItem::new(acct_name.to_owned())); - // for folder in folders { - // items.push(ListItem::new(format!(" {}", folder))); - // } - // } + items.push(ListItem::new(acct_name.to_owned())); + for folder in folders { + items.push(ListItem::new(format!(" {}", folder))); + } + } - // let dirlist = List::new(items) - // .block(Block::default().borders(Borders::NONE).title(Span::styled( - // "hellosu", - // Style::default().add_modifier(Modifier::BOLD), - // ))) - // .style(Style::default().fg(Color::White)) - // .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) - // .highlight_symbol(">>"); + let dirlist = List::new(items) + .block(Block::default().borders(Borders::NONE).title(Span::styled( + "hellosu", + Style::default().add_modifier(Modifier::BOLD), + ))) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + .highlight_symbol(">>"); - // let mut rows = vec![]; + let mut rows = vec![]; // for acct in accts.iter() { // // TODO: messages // let result: Option> = None; // self.mail_store.messages_of(acct); @@ -108,23 +110,23 @@ impl Window for MailView { // } // } - // let table = Table::new(rows) - // .style(Style::default().fg(Color::White)) - // .widths(&[ - // Constraint::Length(1), - // Constraint::Max(3), - // Constraint::Min(20), - // Constraint::Min(35), - // Constraint::Max(5000), - // ]) - // .header( - // Row::new(vec!["", "UID", "Date", "From", "Subject"]) - // .style(Style::default().add_modifier(Modifier::BOLD)), - // ) - // .highlight_style(Style::default().bg(Color::DarkGray)); + let table = Table::new(rows) + .style(Style::default().fg(Color::White)) + .widths(&[ + Constraint::Length(1), + Constraint::Max(3), + Constraint::Min(20), + Constraint::Min(35), + Constraint::Max(5000), + ]) + .header( + Row::new(vec!["", "UID", "Date", "From", "Subject"]) + .style(Style::default().add_modifier(Modifier::BOLD)), + ) + .highlight_style(Style::default().bg(Color::DarkGray)); - // f.render_widget(dirlist, chunks[0]); - // f.render_widget(table, chunks[1]); + f.render_widget(dirlist, chunks[0]); + f.render_widget(table, chunks[1]); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8e38bf4..d341892 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -19,23 +19,25 @@ use std::time::Duration; use anyhow::Result; use chrono::{Local, TimeZone}; -use crossterm::{ - cursor, - event::{self, Event, EventStream, KeyCode, KeyEvent}, - style, terminal, -}; use downcast_rs::Downcast; use futures::{future::FutureExt, select, stream::StreamExt}; use panorama_imap::response::{AttributeValue, Envelope}; -use tokio::{sync::mpsc, time}; -use tui::{ - backend::CrosstermBackend, - layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::*, +use panorama_tui::{ + crossterm::{ + cursor, + event::{self, Event, EventStream, KeyCode, KeyEvent}, + execute, queue, style, terminal, + }, + tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::*, + }, Frame, Terminal, }; +use tokio::{sync::mpsc, time}; use crate::config::ConfigWatcher; use crate::mail::{EmailMetadata, MailEvent, MailStore}; @@ -46,8 +48,9 @@ use self::mail_view::MailView; pub(crate) use self::messages::*; use self::windows::*; -pub(crate) type FrameType<'a, 'b, 'c> = &'c mut Frame<'a, CrosstermBackend<&'b mut Stdout>>; -pub(crate) type TermType<'a, 'b> = &'b mut Terminal>; +pub(crate) type FrameType<'a, 'b> = Frame<'a, &'b mut Stdout>; +// pub(crate) type FrameType<'a, 'b, 'c> = &'c mut Frame<'a, CrosstermBackend<&'b mut Stdout>>; +pub(crate) type TermType<'b> = &'b mut Terminal; /// Parameters for passing to the UI thread pub struct UiParams { @@ -76,8 +79,8 @@ pub async fn run_ui2(params: UiParams) -> Result<()> { execute!(stdout, cursor::Hide, terminal::EnterAlternateScreen)?; terminal::enable_raw_mode()?; - let backend = CrosstermBackend::new(&mut stdout); - let mut term = Terminal::new(backend)?; + // let backend = CrosstermBackend::new(&mut stdout); + let mut term = Terminal::new(&mut stdout)?; let mut ui_events = EventStream::new(); let should_exit = Arc::new(AtomicBool::new(false)); @@ -96,9 +99,12 @@ pub async fn run_ui2(params: UiParams) -> Result<()> { // let mut input_states: Vec> = vec![]; while !should_exit.load(Ordering::Relaxed) { - term.draw(|f| { - ui.draw(f); - })?; + term.pre_draw()?; + { + let mut frame = term.get_frame(); + ui.draw(&mut frame).await; + } + term.post_draw()?; select! { // got an event from the mail thread @@ -117,8 +123,8 @@ pub async fn run_ui2(params: UiParams) -> Result<()> { } } - mem::drop(term); mem::drop(ui); + mem::drop(term); execute!( stdout, @@ -142,7 +148,7 @@ pub struct UI { } impl UI { - fn draw(&mut self, f: FrameType) { + async fn draw(&mut self, f: &mut FrameType<'_, '_>) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(0) @@ -167,12 +173,14 @@ impl UI { .collect(); let tabs = Tabs::new(titles).style(Style::default().bg(Color::DarkGray)); f.render_widget(tabs, chunks[1]); + debug!("drew chunks"); // render all other windows let visible = self.window_layout.visible_windows(chunks[0]); for (layout_id, area) in visible.into_iter() { if let Some(window) = self.windows.get(&layout_id) { - window.draw(); + window.draw(f, area, self).await; + debug!("drew {:?} {:?}", layout_id, area); } } } diff --git a/src/ui/windows.rs b/src/ui/windows.rs index 5625095..00b0b44 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; use futures::future::Future; -use tui::layout::Rect; +use panorama_tui::tui::layout::Rect; use super::{FrameType, HandlesInput, UI}; @@ -12,7 +12,7 @@ pub trait Window: HandlesInput { fn name(&self) -> String; /// Main draw function - async fn draw(&self); + async fn draw(&self, f: &mut FrameType<'_, '_>, area: Rect, ui: &UI); // async fn draw(&self, f: FrameType, area: Rect, ui: Rc); /// Update function diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 7d965be..caa19d7 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -5,3 +5,9 @@ authors = ["Michael Zhang "] edition = "2018" [dependencies] +anyhow = "1.0.40" +crossterm = { version = "0.19.0", features = ["event-stream"] } +futures = "0.3.13" +log = "0.4.14" +tokio = { version = "1.4.0", features = ["full"] } +tui = { version = "0.14.0", default-features = false, features = ["crossterm"] } diff --git a/tui/src/lib.rs b/tui/src/lib.rs index e69de29..3858687 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -0,0 +1,340 @@ +//! messily modified version of `tui` (MIT licensed) with async support for events +//! +//! (note: still uses synchronous stdout api since crossterm doesn't have tokio support +//! and no way i'm porting crossterm to tokio) + +#[macro_use] +extern crate log; + +pub extern crate crossterm; +pub extern crate tui; + +use std::io::{Stdout, Write}; +use std::mem; + +use anyhow::Result; +use crossterm::{ + cursor::{MoveTo, Show}, + execute, queue, + style::{ + Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, + SetForegroundColor, + }, + terminal::{self, Clear, ClearType}, +}; +use futures::future::Future; +use tokio::io::{AsyncWrite, AsyncWriteExt}; +use tui::{ + buffer::{Buffer, Cell}, + layout::Rect, + style::{Color, Modifier}, + widgets::Widget, +}; + +pub struct Terminal { + stdout: W, + buffers: [Buffer; 2], + current: usize, + hidden_cursor: bool, + viewport: Viewport, +} + +impl Terminal { + pub fn new(stdout: W) -> Result { + let (width, height) = terminal::size()?; + let size = Rect::new(0, 0, width, height); + Terminal::with_options( + stdout, + TerminalOptions { + viewport: Viewport { + area: size, + resize_behavior: ResizeBehavior::Auto, + }, + }, + ) + } + + pub fn with_options(stdout: W, options: TerminalOptions) -> Result { + Ok(Terminal { + stdout, + buffers: [ + Buffer::empty(options.viewport.area), + Buffer::empty(options.viewport.area), + ], + current: 0, + hidden_cursor: false, + viewport: options.viewport, + }) + } + + pub fn pre_draw(&mut self) -> Result<()> { + self.autoresize()?; + Ok(()) + } + + pub fn post_draw(&mut self) -> Result<()> { + self.flush()?; + + self.buffers[1 - self.current].reset(); + self.current = 1 - self.current; + + self.stdout.flush()?; + Ok(()) + } + + pub async fn draw(&mut self, f: F) -> Result<()> + where + F: FnOnce(&mut Frame) -> F2, + F2: Future, + { + self.autoresize()?; + + let mut frame = self.get_frame(); + f(&mut frame).await; + self.flush()?; + + self.buffers[1 - self.current].reset(); + self.current = 1 - self.current; + + self.stdout.flush()?; + Ok(()) + } + + fn autoresize(&mut self) -> Result<()> { + if self.viewport.resize_behavior == ResizeBehavior::Auto { + let size = self.size()?; + if size != self.viewport.area { + self.resize(size)?; + } + }; + Ok(()) + } + + fn resize(&mut self, area: Rect) -> Result<()> { + self.buffers[self.current].resize(area); + self.buffers[1 - self.current].resize(area); + self.viewport.area = area; + self.clear() + } + + fn clear(&mut self) -> Result<()> { + execute!(self.stdout, Clear(ClearType::All))?; + // Reset the back buffer to make sure the next update will redraw everything. + self.buffers[1 - self.current].reset(); + Ok(()) + } + + fn flush(&mut self) -> Result<()> { + let previous_buffer = &self.buffers[1 - self.current]; + let current_buffer = &self.buffers[self.current]; + let updates = previous_buffer + .diff(current_buffer) + .into_iter() + .map(|(a, b, c)| (a, b, c.clone())) + .collect::>(); + self.draw_backend(updates.into_iter()) + } + + pub fn get_frame(&mut self) -> Frame { + Frame { + terminal: self, + cursor_position: None, + } + } + + fn draw_backend(&mut self, content: I) -> Result<()> + where + I: Iterator, + { + let mut fg = Color::Reset; + let mut bg = Color::Reset; + let mut modifier = Modifier::empty(); + let mut last_pos: Option<(u16, u16)> = None; + for (x, y, cell) in content { + // Move the cursor if the previous location was not (x - 1, y) + if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) { + queue!(self.stdout, MoveTo(x, y))?; + } + last_pos = Some((x, y)); + if cell.modifier != modifier { + let diff = ModifierDiff { + from: modifier, + to: cell.modifier, + }; + diff.queue(&mut self.stdout)?; + modifier = cell.modifier; + } + if cell.fg != fg { + let color = color_conv(cell.fg); + queue!(self.stdout, SetForegroundColor(color))?; + fg = cell.fg; + } + if cell.bg != bg { + let color = color_conv(cell.bg); + queue!(self.stdout, SetBackgroundColor(color))?; + bg = cell.bg; + } + + queue!(self.stdout, Print(&cell.symbol))?; + } + + queue!( + self.stdout, + SetForegroundColor(CColor::Reset), + SetBackgroundColor(CColor::Reset), + SetAttribute(CAttribute::Reset) + )?; + + Ok(()) + } + + pub fn current_buffer_mut(&mut self) -> &mut Buffer { + &mut self.buffers[self.current] + } + + pub fn show_cursor(&mut self) -> Result<()> { + execute!(self.stdout, Show)?; + self.hidden_cursor = false; + Ok(()) + } + + pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<()> { + execute!(self.stdout, MoveTo(x, y))?; + Ok(()) + } + + pub fn size(&self) -> Result { + let (width, height) = terminal::size()?; + Ok(Rect::new(0, 0, width, height)) + } +} + +fn color_conv(color: Color) -> CColor { + match color { + Color::Reset => CColor::Reset, + Color::Black => CColor::Black, + Color::Red => CColor::DarkRed, + Color::Green => CColor::DarkGreen, + Color::Yellow => CColor::DarkYellow, + Color::Blue => CColor::DarkBlue, + Color::Magenta => CColor::DarkMagenta, + Color::Cyan => CColor::DarkCyan, + Color::Gray => CColor::Grey, + Color::DarkGray => CColor::DarkGrey, + Color::LightRed => CColor::Red, + Color::LightGreen => CColor::Green, + Color::LightBlue => CColor::Blue, + Color::LightYellow => CColor::Yellow, + Color::LightMagenta => CColor::Magenta, + Color::LightCyan => CColor::Cyan, + Color::White => CColor::White, + Color::Indexed(i) => CColor::AnsiValue(i), + Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, + } +} + +pub struct Frame<'a, W> { + terminal: &'a mut Terminal, + cursor_position: Option<(u16, u16)>, +} + +impl<'a, W: Write> Frame<'a, W> { + pub fn set_cursor(&mut self, x: u16, y: u16) { + self.cursor_position = Some((x, y)); + } + + pub fn size(&self) -> Rect { + self.terminal.viewport.area + } + + pub fn render_widget(&mut self, widget: W2, area: Rect) + where + W2: Widget, + { + widget.render(area, self.terminal.current_buffer_mut()); + } +} + +#[derive(Debug)] +struct ModifierDiff { + pub from: Modifier, + pub to: Modifier, +} + +impl ModifierDiff { + fn queue(&self, w: &mut W) -> Result<()> { + //use crossterm::Attribute; + let removed = self.from - self.to; + if removed.contains(Modifier::REVERSED) { + queue!(w, SetAttribute(CAttribute::NoReverse))?; + } + if removed.contains(Modifier::BOLD) { + queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + if self.to.contains(Modifier::DIM) { + queue!(w, SetAttribute(CAttribute::Dim))?; + } + } + if removed.contains(Modifier::ITALIC) { + queue!(w, SetAttribute(CAttribute::NoItalic))?; + } + if removed.contains(Modifier::UNDERLINED) { + queue!(w, SetAttribute(CAttribute::NoUnderline))?; + } + if removed.contains(Modifier::DIM) { + queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + } + if removed.contains(Modifier::CROSSED_OUT) { + queue!(w, SetAttribute(CAttribute::NotCrossedOut))?; + } + if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { + queue!(w, SetAttribute(CAttribute::NoBlink))?; + } + + let added = self.to - self.from; + if added.contains(Modifier::REVERSED) { + queue!(w, SetAttribute(CAttribute::Reverse))?; + } + if added.contains(Modifier::BOLD) { + queue!(w, SetAttribute(CAttribute::Bold))?; + } + if added.contains(Modifier::ITALIC) { + queue!(w, SetAttribute(CAttribute::Italic))?; + } + if added.contains(Modifier::UNDERLINED) { + queue!(w, SetAttribute(CAttribute::Underlined))?; + } + if added.contains(Modifier::DIM) { + queue!(w, SetAttribute(CAttribute::Dim))?; + } + if added.contains(Modifier::CROSSED_OUT) { + queue!(w, SetAttribute(CAttribute::CrossedOut))?; + } + if added.contains(Modifier::SLOW_BLINK) { + queue!(w, SetAttribute(CAttribute::SlowBlink))?; + } + if added.contains(Modifier::RAPID_BLINK) { + queue!(w, SetAttribute(CAttribute::RapidBlink))?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +enum ResizeBehavior { + Fixed, + Auto, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Viewport { + area: Rect, + resize_behavior: ResizeBehavior, +} + +#[derive(Debug, Clone, PartialEq)] +/// Options to pass to [`Terminal::with_options`] +pub struct TerminalOptions { + /// Viewport used to draw to the terminal + pub viewport: Viewport, +}