diff --git a/keyfork-prompt/src/lib.rs b/keyfork-prompt/src/lib.rs index 69fa38a..102eae4 100644 --- a/keyfork-prompt/src/lib.rs +++ b/keyfork-prompt/src/lib.rs @@ -1,21 +1,11 @@ -use std::{ - io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, - os::fd::AsRawFd, borrow::Borrow, -}; +use std::borrow::Borrow; #[cfg(feature = "mnemonic")] use keyfork_mnemonic_util::Wordlist; -use keyfork_crossterm::{ - cursor, - event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers}, - style::{Print, PrintStyledContent, Stylize}, - terminal::{self, EnterAlternateScreen, FdTerminal, LeaveAlternateScreen, TerminalIoctl}, - tty::IsTty, - ExecutableCommand, QueueableCommand, -}; - +pub mod terminal; pub mod validators; +pub use terminal::{Terminal, DefaultTerminal, default_terminal}; #[cfg(feature = "qrencode")] pub mod qrencode; @@ -70,483 +60,3 @@ pub trait PromptHandler { fn prompt_message(&mut self, prompt: impl Borrow) -> Result<()>; } - -struct TerminalGuard<'a, R, W> -where - W: Write + AsRawFd, -{ - read: &'a mut BufReader, - write: &'a mut W, - terminal: &'a mut FdTerminal, -} - -impl<'a, R, W> TerminalGuard<'a, R, W> -where - W: Write + AsRawFd, - R: Read, -{ - fn new(read: &'a mut BufReader, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self { - Self { - read, - write, - terminal, - } - } - - fn alternate_screen(mut self) -> std::io::Result { - self.execute(EnterAlternateScreen)?; - Ok(self) - } - - fn raw_mode(self) -> std::io::Result { - self.terminal.enable_raw_mode()?; - Ok(self) - } - - fn bracketed_paste(mut self) -> std::io::Result { - self.execute(EnableBracketedPaste)?; - Ok(self) - } -} - -impl TerminalIoctl for TerminalGuard<'_, R, W> -where - R: Read, - W: Write + AsRawFd, -{ - fn enable_raw_mode(&mut self) -> std::io::Result<()> { - self.terminal.enable_raw_mode() - } - - fn disable_raw_mode(&mut self) -> std::io::Result<()> { - self.terminal.disable_raw_mode() - } - - fn size(&self) -> std::io::Result<(u16, u16)> { - self.terminal.size() - } - - fn window_size(&self) -> std::io::Result { - self.terminal.window_size() - } -} - -impl Read for TerminalGuard<'_, R, W> -where - R: Read, - W: Write + AsRawFd, -{ - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - self.read.read(buf) - } -} - -impl BufRead for TerminalGuard<'_, R, W> -where - R: Read, - W: Write + AsRawFd, -{ - fn fill_buf(&mut self) -> std::io::Result<&[u8]> { - self.read.fill_buf() - } - - fn consume(&mut self, amt: usize) { - self.read.consume(amt) - } -} - -impl Write for TerminalGuard<'_, R, W> -where - W: Write + AsRawFd, -{ - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.write.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.write.flush() - } -} - -impl Drop for TerminalGuard<'_, R, W> -where - W: Write + AsRawFd, -{ - fn drop(&mut self) { - self.write.execute(DisableBracketedPaste).unwrap(); - self.write.execute(LeaveAlternateScreen).unwrap(); - self.terminal.disable_raw_mode().unwrap(); - } -} - -pub struct Terminal { - read: BufReader, - write: W, - terminal: FdTerminal, -} - -impl Terminal -where - R: Read + Sized, - W: Write + AsRawFd + Sized, -{ - pub fn new(read_handle: R, write_handle: W) -> Result { - if !write_handle.is_tty() { - return Err(Error::NotATTY); - } - Ok(Self { - read: BufReader::new(read_handle), - terminal: FdTerminal::from(write_handle.as_raw_fd()), - write: write_handle, - }) - } - - fn lock(&mut self) -> TerminalGuard<'_, R, W> { - TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal) - } - -} - -impl PromptHandler for Terminal where R: Read + Sized, W: Write + AsRawFd + Sized { - - fn prompt_input(&mut self, prompt: &str) -> Result { - let mut terminal = self.lock().alternate_screen()?; - terminal - .queue(terminal::Clear(terminal::ClearType::All))? - .queue(cursor::MoveTo(0, 0))?; - let mut lines = prompt.lines().peekable(); - while let Some(line) = lines.next() { - terminal.queue(Print(line))?; - if lines.peek().is_some() { - terminal - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - } - } - terminal.flush()?; - - let mut line = String::new(); - terminal.read.read_line(&mut line)?; - Ok(line) - } - - #[cfg(feature = "mnemonic")] - fn prompt_validated_wordlist( - &mut self, - prompt: &str, - wordlist: &Wordlist, - retries: u8, - validator_fn: F, - ) -> Result - where - F: Fn(String) -> Result, - E: std::error::Error, - { - let mut last_error = None; - for _ in 0..retries { - let s = self.prompt_wordlist(prompt, wordlist)?; - match validator_fn(s) { - Ok(v) => return Ok(v), - Err(e) => { - self.prompt_message(&Message::Text(format!("Error validating wordlist: {e}")))?; - let _ = last_error.insert(e); - } - } - } - Err(Error::Validation( - retries, - last_error - .map(|e| e.to_string()) - .unwrap_or_else(|| "Unknown".to_string()), - )) - } - - #[cfg(feature = "mnemonic")] - #[allow(clippy::too_many_lines)] - fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result { - let mut terminal = self - .lock() - .alternate_screen()? - .raw_mode()? - .bracketed_paste()?; - - terminal - .queue(terminal::Clear(terminal::ClearType::All))? - .queue(cursor::MoveTo(0, 0))?; - let mut lines = prompt.lines().peekable(); - let mut prefix_length = 0; - while let Some(line) = lines.next() { - prefix_length = line.len(); - terminal.queue(Print(line))?; - if lines.peek().is_some() { - terminal - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - } - } - terminal.flush()?; - - let (mut cols, mut _rows) = terminal.size()?; - let mut input = String::new(); - - loop { - match read()? { - Event::Resize(new_cols, new_rows) => { - cols = new_cols; - _rows = new_rows; - } - Event::Paste(mut p) => { - p.retain(|c| c != '\n'); - input.push_str(&p); - } - Event::Key(k) => match k.code { - KeyCode::Enter => { - break; - } - KeyCode::Backspace => { - input.pop(); - } - KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => { - let mut has_deleted_text = true; - while input.pop().is_some_and(char::is_whitespace) { - has_deleted_text = false; - } - while input.pop().is_some_and(|c| !c.is_whitespace()) { - has_deleted_text = true; - } - if !input.is_empty() && has_deleted_text { - input.push(' '); - } - } - KeyCode::Char(' ') => { - if !input.chars().next_back().is_some_and(char::is_whitespace) { - input.push(' '); - } - } - KeyCode::Char(c) => { - input.push(c); - } - _ => (), - }, - _ => (), - } - - let usable_space = cols as usize - prefix_length - 1; - - #[allow(clippy::cast_possible_truncation)] - terminal - .queue(cursor::MoveToColumn( - std::cmp::min(u16::MAX as usize, prefix_length) as u16, - ))? - .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? - .flush()?; - - let printable_input_start = if input.len() > usable_space { - let start_index = input.len() - usable_space; - // Find a word boundary, otherwise slice the current word in half - if let Some((index, _)) = input - .chars() - .enumerate() - .skip(start_index) - .find(|(_, ch)| ch.is_whitespace()) - { - index - } else { - start_index - } - } else { - 0 - }; - - let printable_input = &input[printable_input_start..]; - let mut iter = printable_input.split_whitespace().peekable(); - - while let Some(word) = iter.next() { - if wordlist.contains(word) { - terminal.queue(PrintStyledContent(word.green()))?; - } else { - terminal.queue(PrintStyledContent(word.red()))?; - } - if iter.peek().is_some() - || printable_input - .chars() - .next_back() - .is_some_and(char::is_whitespace) - { - terminal.queue(Print(" "))?; - } - } - - terminal.flush()?; - } - - Ok(input) - } - - fn prompt_validated_passphrase( - &mut self, - prompt: &str, - retries: u8, - validator_fn: F, - ) -> Result - where - F: Fn(String) -> Result, - E: std::error::Error, - { - let mut last_error = None; - for _ in 0..retries { - let s = self.prompt_passphrase(prompt)?; - match validator_fn(s) { - Ok(v) => return Ok(v), - Err(e) => { - self.prompt_message(&Message::Text(format!( - "Error validating passphrase: {e}" - )))?; - let _ = last_error.insert(e); - } - } - } - Err(Error::Validation( - retries, - last_error - .map(|e| e.to_string()) - .unwrap_or_else(|| "Unknown".to_string()), - )) - } - - // TODO: return secrecy::Secret - fn prompt_passphrase(&mut self, prompt: &str) -> Result { - let mut terminal = self.lock().alternate_screen()?.raw_mode()?; - - terminal - .queue(terminal::Clear(terminal::ClearType::All))? - .queue(cursor::MoveTo(0, 0))?; - let mut lines = prompt.lines().peekable(); - let mut prefix_length = 0; - while let Some(line) = lines.next() { - prefix_length = line.len(); - terminal.queue(Print(line))?; - if lines.peek().is_some() { - terminal - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - } - } - terminal.flush()?; - - let (mut cols, mut _rows) = terminal.size()?; - - let mut passphrase = String::new(); - loop { - match read()? { - Event::Resize(new_cols, new_rows) => { - cols = new_cols; - _rows = new_rows; - } - Event::Key(k) => match k.code { - KeyCode::Enter => { - break; - } - KeyCode::Backspace => { - let passphrase_len = passphrase.len(); - if passphrase.pop().is_some() - && prefix_length + passphrase_len < cols as usize - { - terminal - .queue(cursor::MoveLeft(1))? - .queue(Print(" "))? - .queue(cursor::MoveLeft(1))? - .flush()?; - } - } - KeyCode::Char(c) => { - if prefix_length + passphrase.len() < (cols - 1) as usize { - terminal.queue(Print("*"))?.flush()?; - } - passphrase.push(c); - } - _ => (), - }, - _ => (), - } - } - Ok(passphrase) - } - - fn prompt_message(&mut self, prompt: impl Borrow) -> Result<()> { - let mut terminal = self.lock().alternate_screen()?.raw_mode()?; - - loop { - let (cols, rows) = terminal.size()?; - - terminal - .queue(terminal::Clear(terminal::ClearType::All))? - .queue(cursor::MoveTo(0, 0))?; - - match prompt.borrow() { - Message::Text(text) => { - for line in text.lines() { - let mut written_chars = 0; - for word in line.split_whitespace() { - #[allow(clippy::cast_possible_truncation)] - let len = std::cmp::min(u16::MAX as usize, word.len()) as u16; - written_chars += len + 1; - if written_chars > cols { - terminal - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - written_chars = len + 1; - } - terminal.queue(Print(word))?.queue(Print(" "))?; - } - terminal - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - } - } - Message::Data(data) => { - let count = data.lines().count(); - // NOTE: GE to allow a MoveDown(1) - if count >= rows as usize { - let msg = format!( - "{} {count} {} {rows} {}", - "Could not print data", "lines long (screen is", "lines long)" - ); - terminal - .queue(Print(msg))? - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - } else { - for line in data.lines() { - terminal - .queue(Print(line))? - .queue(cursor::MoveDown(1))? - .queue(cursor::MoveToColumn(0))?; - } - } - } - } - terminal - .queue(cursor::DisableBlinking)? - .queue(PrintStyledContent(" OK ".negative()))? - .flush()?; - - #[allow(clippy::single_match)] - match read()? { - Event::Key(k) => match k.code { - KeyCode::Enter | KeyCode::Char(' ' | 'q') => break, - _ => (), - }, - _ => (), - } - } - terminal.queue(cursor::EnableBlinking)?.flush()?; - Ok(()) - } -} - -pub type DefaultTerminal = Terminal; - -pub fn default_terminal() -> Result { - Terminal::new(stdin(), stderr()) -} diff --git a/keyfork-prompt/src/terminal.rs b/keyfork-prompt/src/terminal.rs new file mode 100644 index 0000000..6c49b60 --- /dev/null +++ b/keyfork-prompt/src/terminal.rs @@ -0,0 +1,497 @@ +use std::{ + io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, + os::fd::AsRawFd, borrow::Borrow, +}; + +use keyfork_crossterm::{ + cursor, + event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers}, + style::{Print, PrintStyledContent, Stylize}, + terminal::{self, EnterAlternateScreen, FdTerminal, LeaveAlternateScreen, TerminalIoctl}, + tty::IsTty, + ExecutableCommand, QueueableCommand, +}; + +use crate::{PromptHandler, Message, Wordlist, Error}; + +pub type Result = std::result::Result; + +struct TerminalGuard<'a, R, W> +where + W: Write + AsRawFd, +{ + read: &'a mut BufReader, + write: &'a mut W, + terminal: &'a mut FdTerminal, +} + +impl<'a, R, W> TerminalGuard<'a, R, W> +where + W: Write + AsRawFd, + R: Read, +{ + fn new(read: &'a mut BufReader, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self { + Self { + read, + write, + terminal, + } + } + + fn alternate_screen(mut self) -> std::io::Result { + self.execute(EnterAlternateScreen)?; + Ok(self) + } + + fn raw_mode(self) -> std::io::Result { + self.terminal.enable_raw_mode()?; + Ok(self) + } + + fn bracketed_paste(mut self) -> std::io::Result { + self.execute(EnableBracketedPaste)?; + Ok(self) + } +} + +impl TerminalIoctl for TerminalGuard<'_, R, W> +where + R: Read, + W: Write + AsRawFd, +{ + fn enable_raw_mode(&mut self) -> std::io::Result<()> { + self.terminal.enable_raw_mode() + } + + fn disable_raw_mode(&mut self) -> std::io::Result<()> { + self.terminal.disable_raw_mode() + } + + fn size(&self) -> std::io::Result<(u16, u16)> { + self.terminal.size() + } + + fn window_size(&self) -> std::io::Result { + self.terminal.window_size() + } +} + +impl Read for TerminalGuard<'_, R, W> +where + R: Read, + W: Write + AsRawFd, +{ + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.read.read(buf) + } +} + +impl BufRead for TerminalGuard<'_, R, W> +where + R: Read, + W: Write + AsRawFd, +{ + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { + self.read.fill_buf() + } + + fn consume(&mut self, amt: usize) { + self.read.consume(amt) + } +} + +impl Write for TerminalGuard<'_, R, W> +where + W: Write + AsRawFd, +{ + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.write.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.write.flush() + } +} + +impl Drop for TerminalGuard<'_, R, W> +where + W: Write + AsRawFd, +{ + fn drop(&mut self) { + self.write.execute(DisableBracketedPaste).unwrap(); + self.write.execute(LeaveAlternateScreen).unwrap(); + self.terminal.disable_raw_mode().unwrap(); + } +} + +pub struct Terminal { + read: BufReader, + write: W, + terminal: FdTerminal, +} + +impl Terminal +where + R: Read + Sized, + W: Write + AsRawFd + Sized, +{ + pub fn new(read_handle: R, write_handle: W) -> Result { + if !write_handle.is_tty() { + return Err(Error::NotATTY); + } + Ok(Self { + read: BufReader::new(read_handle), + terminal: FdTerminal::from(write_handle.as_raw_fd()), + write: write_handle, + }) + } + + fn lock(&mut self) -> TerminalGuard<'_, R, W> { + TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal) + } + +} + +impl PromptHandler for Terminal where R: Read + Sized, W: Write + AsRawFd + Sized { + + fn prompt_input(&mut self, prompt: &str) -> Result { + let mut terminal = self.lock().alternate_screen()?; + terminal + .queue(terminal::Clear(terminal::ClearType::All))? + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + while let Some(line) = lines.next() { + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal.flush()?; + + let mut line = String::new(); + terminal.read.read_line(&mut line)?; + Ok(line) + } + + #[cfg(feature = "mnemonic")] + fn prompt_validated_wordlist( + &mut self, + prompt: &str, + wordlist: &Wordlist, + retries: u8, + validator_fn: F, + ) -> Result + where + F: Fn(String) -> Result, + E: std::error::Error, + { + let mut last_error = None; + for _ in 0..retries { + let s = self.prompt_wordlist(prompt, wordlist)?; + match validator_fn(s) { + Ok(v) => return Ok(v), + Err(e) => { + self.prompt_message(&Message::Text(format!("Error validating wordlist: {e}")))?; + let _ = last_error.insert(e); + } + } + } + Err(Error::Validation( + retries, + last_error + .map(|e| e.to_string()) + .unwrap_or_else(|| "Unknown".to_string()), + )) + } + + #[cfg(feature = "mnemonic")] + #[allow(clippy::too_many_lines)] + fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result { + let mut terminal = self + .lock() + .alternate_screen()? + .raw_mode()? + .bracketed_paste()?; + + terminal + .queue(terminal::Clear(terminal::ClearType::All))? + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + let mut prefix_length = 0; + while let Some(line) = lines.next() { + prefix_length = line.len(); + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal.flush()?; + + let (mut cols, mut _rows) = terminal.size()?; + let mut input = String::new(); + + loop { + match read()? { + Event::Resize(new_cols, new_rows) => { + cols = new_cols; + _rows = new_rows; + } + Event::Paste(mut p) => { + p.retain(|c| c != '\n'); + input.push_str(&p); + } + Event::Key(k) => match k.code { + KeyCode::Enter => { + break; + } + KeyCode::Backspace => { + input.pop(); + } + KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => { + let mut has_deleted_text = true; + while input.pop().is_some_and(char::is_whitespace) { + has_deleted_text = false; + } + while input.pop().is_some_and(|c| !c.is_whitespace()) { + has_deleted_text = true; + } + if !input.is_empty() && has_deleted_text { + input.push(' '); + } + } + KeyCode::Char(' ') => { + if !input.chars().next_back().is_some_and(char::is_whitespace) { + input.push(' '); + } + } + KeyCode::Char(c) => { + input.push(c); + } + _ => (), + }, + _ => (), + } + + let usable_space = cols as usize - prefix_length - 1; + + #[allow(clippy::cast_possible_truncation)] + terminal + .queue(cursor::MoveToColumn( + std::cmp::min(u16::MAX as usize, prefix_length) as u16, + ))? + .queue(terminal::Clear(terminal::ClearType::UntilNewLine))? + .flush()?; + + let printable_input_start = if input.len() > usable_space { + let start_index = input.len() - usable_space; + // Find a word boundary, otherwise slice the current word in half + if let Some((index, _)) = input + .chars() + .enumerate() + .skip(start_index) + .find(|(_, ch)| ch.is_whitespace()) + { + index + } else { + start_index + } + } else { + 0 + }; + + let printable_input = &input[printable_input_start..]; + let mut iter = printable_input.split_whitespace().peekable(); + + while let Some(word) = iter.next() { + if wordlist.contains(word) { + terminal.queue(PrintStyledContent(word.green()))?; + } else { + terminal.queue(PrintStyledContent(word.red()))?; + } + if iter.peek().is_some() + || printable_input + .chars() + .next_back() + .is_some_and(char::is_whitespace) + { + terminal.queue(Print(" "))?; + } + } + + terminal.flush()?; + } + + Ok(input) + } + + fn prompt_validated_passphrase( + &mut self, + prompt: &str, + retries: u8, + validator_fn: F, + ) -> Result + where + F: Fn(String) -> Result, + E: std::error::Error, + { + let mut last_error = None; + for _ in 0..retries { + let s = self.prompt_passphrase(prompt)?; + match validator_fn(s) { + Ok(v) => return Ok(v), + Err(e) => { + self.prompt_message(&Message::Text(format!( + "Error validating passphrase: {e}" + )))?; + let _ = last_error.insert(e); + } + } + } + Err(Error::Validation( + retries, + last_error + .map(|e| e.to_string()) + .unwrap_or_else(|| "Unknown".to_string()), + )) + } + + // TODO: return secrecy::Secret + fn prompt_passphrase(&mut self, prompt: &str) -> Result { + let mut terminal = self.lock().alternate_screen()?.raw_mode()?; + + terminal + .queue(terminal::Clear(terminal::ClearType::All))? + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + let mut prefix_length = 0; + while let Some(line) = lines.next() { + prefix_length = line.len(); + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal.flush()?; + + let (mut cols, mut _rows) = terminal.size()?; + + let mut passphrase = String::new(); + loop { + match read()? { + Event::Resize(new_cols, new_rows) => { + cols = new_cols; + _rows = new_rows; + } + Event::Key(k) => match k.code { + KeyCode::Enter => { + break; + } + KeyCode::Backspace => { + let passphrase_len = passphrase.len(); + if passphrase.pop().is_some() + && prefix_length + passphrase_len < cols as usize + { + terminal + .queue(cursor::MoveLeft(1))? + .queue(Print(" "))? + .queue(cursor::MoveLeft(1))? + .flush()?; + } + } + KeyCode::Char(c) => { + if prefix_length + passphrase.len() < (cols - 1) as usize { + terminal.queue(Print("*"))?.flush()?; + } + passphrase.push(c); + } + _ => (), + }, + _ => (), + } + } + Ok(passphrase) + } + + fn prompt_message(&mut self, prompt: impl Borrow) -> Result<()> { + let mut terminal = self.lock().alternate_screen()?.raw_mode()?; + + loop { + let (cols, rows) = terminal.size()?; + + terminal + .queue(terminal::Clear(terminal::ClearType::All))? + .queue(cursor::MoveTo(0, 0))?; + + match prompt.borrow() { + Message::Text(text) => { + for line in text.lines() { + let mut written_chars = 0; + for word in line.split_whitespace() { + #[allow(clippy::cast_possible_truncation)] + let len = std::cmp::min(u16::MAX as usize, word.len()) as u16; + written_chars += len + 1; + if written_chars > cols { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + written_chars = len + 1; + } + terminal.queue(Print(word))?.queue(Print(" "))?; + } + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + Message::Data(data) => { + let count = data.lines().count(); + // NOTE: GE to allow a MoveDown(1) + if count >= rows as usize { + let msg = format!( + "{} {count} {} {rows} {}", + "Could not print data", "lines long (screen is", "lines long)" + ); + terminal + .queue(Print(msg))? + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } else { + for line in data.lines() { + terminal + .queue(Print(line))? + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + } + } + terminal + .queue(cursor::DisableBlinking)? + .queue(PrintStyledContent(" OK ".negative()))? + .flush()?; + + #[allow(clippy::single_match)] + match read()? { + Event::Key(k) => match k.code { + KeyCode::Enter | KeyCode::Char(' ' | 'q') => break, + _ => (), + }, + _ => (), + } + } + terminal.queue(cursor::EnableBlinking)?.flush()?; + Ok(()) + } +} + +pub type DefaultTerminal = Terminal; + +pub fn default_terminal() -> Result { + Terminal::new(stdin(), stderr()) +}