keyfork-prompt: move Terminal into a module

This commit is contained in:
Ryan Heywood 2024-01-10 23:32:26 -05:00
parent b5320cabf3
commit dd3ffe74b3
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
2 changed files with 500 additions and 493 deletions

View File

@ -1,21 +1,11 @@
use std::{ use std::borrow::Borrow;
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
os::fd::AsRawFd, borrow::Borrow,
};
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
use keyfork_mnemonic_util::Wordlist; use keyfork_mnemonic_util::Wordlist;
use keyfork_crossterm::{ pub mod terminal;
cursor,
event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers},
style::{Print, PrintStyledContent, Stylize},
terminal::{self, EnterAlternateScreen, FdTerminal, LeaveAlternateScreen, TerminalIoctl},
tty::IsTty,
ExecutableCommand, QueueableCommand,
};
pub mod validators; pub mod validators;
pub use terminal::{Terminal, DefaultTerminal, default_terminal};
#[cfg(feature = "qrencode")] #[cfg(feature = "qrencode")]
pub mod qrencode; pub mod qrencode;
@ -70,483 +60,3 @@ pub trait PromptHandler {
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>; fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>;
} }
struct TerminalGuard<'a, R, W>
where
W: Write + AsRawFd,
{
read: &'a mut BufReader<R>,
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<R>, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self {
Self {
read,
write,
terminal,
}
}
fn alternate_screen(mut self) -> std::io::Result<Self> {
self.execute(EnterAlternateScreen)?;
Ok(self)
}
fn raw_mode(self) -> std::io::Result<Self> {
self.terminal.enable_raw_mode()?;
Ok(self)
}
fn bracketed_paste(mut self) -> std::io::Result<Self> {
self.execute(EnableBracketedPaste)?;
Ok(self)
}
}
impl<R, W> 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<terminal::WindowSize> {
self.terminal.window_size()
}
}
impl<R, W> Read for TerminalGuard<'_, R, W>
where
R: Read,
W: Write + AsRawFd,
{
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read.read(buf)
}
}
impl<R, W> 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<R, W> Write for TerminalGuard<'_, R, W>
where
W: Write + AsRawFd,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.write.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.write.flush()
}
}
impl<R, W> 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<R, W> {
read: BufReader<R>,
write: W,
terminal: FdTerminal,
}
impl<R, W> Terminal<R, W>
where
R: Read + Sized,
W: Write + AsRawFd + Sized,
{
pub fn new(read_handle: R, write_handle: W) -> Result<Self> {
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<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
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<V, F, E>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
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<String> {
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<V, F, E>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
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<String>
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
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<Message>) -> 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<Stdin, Stderr>;
pub fn default_terminal() -> Result<DefaultTerminal> {
Terminal::new(stdin(), stderr())
}

View File

@ -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<T, E = Error> = std::result::Result<T, E>;
struct TerminalGuard<'a, R, W>
where
W: Write + AsRawFd,
{
read: &'a mut BufReader<R>,
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<R>, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self {
Self {
read,
write,
terminal,
}
}
fn alternate_screen(mut self) -> std::io::Result<Self> {
self.execute(EnterAlternateScreen)?;
Ok(self)
}
fn raw_mode(self) -> std::io::Result<Self> {
self.terminal.enable_raw_mode()?;
Ok(self)
}
fn bracketed_paste(mut self) -> std::io::Result<Self> {
self.execute(EnableBracketedPaste)?;
Ok(self)
}
}
impl<R, W> 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<terminal::WindowSize> {
self.terminal.window_size()
}
}
impl<R, W> Read for TerminalGuard<'_, R, W>
where
R: Read,
W: Write + AsRawFd,
{
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read.read(buf)
}
}
impl<R, W> 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<R, W> Write for TerminalGuard<'_, R, W>
where
W: Write + AsRawFd,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.write.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.write.flush()
}
}
impl<R, W> 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<R, W> {
read: BufReader<R>,
write: W,
terminal: FdTerminal,
}
impl<R, W> Terminal<R, W>
where
R: Read + Sized,
W: Write + AsRawFd + Sized,
{
pub fn new(read_handle: R, write_handle: W) -> Result<Self> {
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<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
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<V, F, E>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
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<String> {
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<V, F, E>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
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<String>
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
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<Message>) -> 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<Stdin, Stderr>;
pub fn default_terminal() -> Result<DefaultTerminal> {
Terminal::new(stdin(), stderr())
}