From f6b41fce5f000863759f5cd1f2e70c6d68f1fa20 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 10 Jan 2024 22:35:31 -0500 Subject: [PATCH] keyfork-crossterm: add FdTerminal struct to manage non-default terminals --- Cargo.lock | 2 +- keyfork-crossterm/src/terminal.rs | 53 ++++++++++++++++++++- keyfork-crossterm/src/terminal/sys.rs | 3 +- keyfork-crossterm/src/terminal/sys/unix.rs | 33 +++++++++++++ keyfork-prompt/src/bin/test-basic-prompt.rs | 4 +- keyfork-prompt/src/lib.rs | 20 ++++---- keyfork-prompt/src/raw_mode.rs | 10 ++-- keyfork-shard/src/lib.rs | 4 +- keyfork-shard/src/openpgp.rs | 4 +- keyfork-shard/src/openpgp/keyring.rs | 6 +-- keyfork-shard/src/openpgp/smartcard.rs | 13 +++-- keyfork/src/cli/wizard.rs | 4 +- 12 files changed, 122 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4d0a7c..333660f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1427,7 +1427,7 @@ dependencies = [ [[package]] name = "keyfork-crossterm" -version = "0.27.0" +version = "0.27.1" dependencies = [ "async-std", "bitflags 2.4.1", diff --git a/keyfork-crossterm/src/terminal.rs b/keyfork-crossterm/src/terminal.rs index a367e5d..09b231d 100644 --- a/keyfork-crossterm/src/terminal.rs +++ b/keyfork-crossterm/src/terminal.rs @@ -83,7 +83,7 @@ //! //! For manual execution control check out [crossterm::queue](../macro.queue.html). -use std::{fmt, io}; +use std::{fmt, io, os}; #[cfg(windows)] use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer}; @@ -92,6 +92,57 @@ use serde::{Deserialize, Serialize}; #[cfg(windows)] use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT; +pub trait TerminalIoctl { + fn enable_raw_mode(&mut self) -> io::Result<()>; + + fn disable_raw_mode(&mut self) -> io::Result<()>; + + fn size(&self) -> io::Result<(u16, u16)>; + + fn window_size(&self) -> io::Result; +} + +#[cfg(unix)] +pub struct FdTerminal { + fd: i32, + stored_termios: Option, +} + +impl From for FdTerminal where T: os::fd::AsRawFd { + fn from(value: T) -> Self { + Self { + fd: value.as_raw_fd(), + stored_termios: None, + } + } +} + +#[cfg(unix)] +impl TerminalIoctl for FdTerminal { + fn enable_raw_mode(&mut self) -> io::Result<()> { + if self.stored_termios.is_none() { + let termios = sys::fd_enable_raw_mode(self.fd)?; + let _ = self.stored_termios.insert(termios); + } + Ok(()) + } + + fn disable_raw_mode(&mut self) -> io::Result<()> { + if let Some(termios) = self.stored_termios.take() { + sys::fd_disable_raw_mode(self.fd, termios)?; + } + Ok(()) + } + + fn size(&self) -> io::Result<(u16, u16)> { + sys::fd_size(self.fd) + } + + fn window_size(&self) -> io::Result { + sys::fd_window_size(self.fd) + } +} + #[doc(no_inline)] use crate::Command; use crate::{csi, impl_display}; diff --git a/keyfork-crossterm/src/terminal/sys.rs b/keyfork-crossterm/src/terminal/sys.rs index 9dde47d..a857382 100644 --- a/keyfork-crossterm/src/terminal/sys.rs +++ b/keyfork-crossterm/src/terminal/sys.rs @@ -5,7 +5,8 @@ pub use self::unix::supports_keyboard_enhancement; #[cfg(unix)] pub(crate) use self::unix::{ - disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size, + disable_raw_mode, enable_raw_mode, fd_disable_raw_mode, fd_enable_raw_mode, fd_size, + fd_window_size, is_raw_mode_enabled, size, window_size, }; #[cfg(windows)] #[cfg(feature = "events")] diff --git a/keyfork-crossterm/src/terminal/sys/unix.rs b/keyfork-crossterm/src/terminal/sys/unix.rs index ed545c5..da19a1f 100644 --- a/keyfork-crossterm/src/terminal/sys/unix.rs +++ b/keyfork-crossterm/src/terminal/sys/unix.rs @@ -34,6 +34,21 @@ impl From for WindowSize { } } +pub(crate) fn fd_window_size(fd: i32) -> io::Result { + let mut size = winsize { + ws_row: 0, + ws_col: 0, + ws_xpixel: 0, + ws_ypixel: 0, + }; + + if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { + return Ok(size.into()); + } + + Err(std::io::Error::last_os_error()) +} + #[allow(clippy::useless_conversion)] pub(crate) fn window_size() -> io::Result { // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc @@ -59,6 +74,10 @@ pub(crate) fn window_size() -> io::Result { Err(std::io::Error::last_os_error().into()) } +pub(crate) fn fd_size(fd: i32) -> io::Result<(u16, u16)> { + fd_window_size(fd).map(|WindowSize { rows, columns, .. }| (columns, rows)) +} + #[allow(clippy::useless_conversion)] pub(crate) fn size() -> io::Result<(u16, u16)> { if let Ok(window_size) = window_size() { @@ -68,6 +87,15 @@ pub(crate) fn size() -> io::Result<(u16, u16)> { tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) } +pub(crate) fn fd_enable_raw_mode(fd: i32) -> io::Result { + let mut ios = get_terminal_attr(fd)?; + let original_mode_ios = ios; + + raw_terminal_attr(&mut ios); + set_terminal_attr(fd, &ios)?; + Ok(original_mode_ios) +} + pub(crate) fn enable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); @@ -89,6 +117,11 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { Ok(()) } +pub(crate) fn fd_disable_raw_mode(fd: i32, termios: Termios) -> io::Result<()> { + set_terminal_attr(fd, &termios)?; + Ok(()) +} + /// Reset the raw mode. /// /// More precisely, reset the whole termios mode to what it was before the first call diff --git a/keyfork-prompt/src/bin/test-basic-prompt.rs b/keyfork-prompt/src/bin/test-basic-prompt.rs index c12cd62..5d529e8 100644 --- a/keyfork-prompt/src/bin/test-basic-prompt.rs +++ b/keyfork-prompt/src/bin/test-basic-prompt.rs @@ -2,11 +2,11 @@ use std::io::{stdin, stdout}; use keyfork_prompt::{ validators::{mnemonic, Validator}, - PromptManager, + Terminal, }; fn main() -> Result<(), Box> { - let mut mgr = PromptManager::new(stdin(), stdout())?; + let mut mgr = Terminal::new(stdin(), stdout())?; let transport_validator = mnemonic::MnemonicSetValidator { word_lengths: [9, 24], }; diff --git a/keyfork-prompt/src/lib.rs b/keyfork-prompt/src/lib.rs index 703bfb3..3577503 100644 --- a/keyfork-prompt/src/lib.rs +++ b/keyfork-prompt/src/lib.rs @@ -10,7 +10,7 @@ use keyfork_crossterm::{ cursor, event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers}, style::{Print, PrintStyledContent, Stylize}, - terminal, + terminal::{self, TerminalIoctl, FdTerminal}, tty::IsTty, QueueableCommand, }; @@ -44,12 +44,13 @@ pub enum Message { Data(String), } -pub struct PromptManager { +pub struct Terminal { read: BufReader, write: W, + terminal: FdTerminal, } -impl PromptManager +impl Terminal where R: Read + Sized, W: Write + AsRawFd + Sized, @@ -60,6 +61,7 @@ where } Ok(Self { read: BufReader::new(read_handle), + terminal: FdTerminal::from(write_handle.as_raw_fd()), write: write_handle, }) } @@ -140,7 +142,7 @@ where } terminal.flush()?; - let (mut cols, mut _rows) = terminal::size()?; + let (mut cols, mut _rows) = self.terminal.size()?; let mut input = String::new(); loop { @@ -290,7 +292,7 @@ where } terminal.flush()?; - let (mut cols, mut _rows) = terminal::size()?; + let (mut cols, mut _rows) = self.terminal.size()?; let mut passphrase = String::new(); loop { @@ -334,7 +336,7 @@ where let mut terminal = RawMode::new(&mut terminal)?; loop { - let (cols, rows) = terminal::size()?; + let (cols, rows) = self.terminal.size()?; terminal .queue(terminal::Clear(terminal::ClearType::All))? @@ -402,8 +404,8 @@ where } } -pub type DefaultPromptManager = PromptManager; +pub type DefaultTerminal = Terminal; -pub fn default_prompt_manager() -> Result { - PromptManager::new(stdin(), stderr()) +pub fn default_terminal() -> Result { + Terminal::new(stdin(), stderr()) } diff --git a/keyfork-prompt/src/raw_mode.rs b/keyfork-prompt/src/raw_mode.rs index d60eb18..a294350 100644 --- a/keyfork-prompt/src/raw_mode.rs +++ b/keyfork-prompt/src/raw_mode.rs @@ -3,7 +3,7 @@ use std::{ os::fd::AsRawFd, }; -use keyfork_crossterm::terminal; +use keyfork_crossterm::terminal::{FdTerminal, TerminalIoctl}; use crate::Result; @@ -12,16 +12,18 @@ where W: Write + AsRawFd + Sized, { write: &'a mut W, + terminal: FdTerminal, } -// TODO: fork crossterm to allow using FD from as_raw_fd() impl<'a, W> RawMode<'a, W> where W: Write + AsRawFd + Sized, { pub(crate) fn new(write_handle: &'a mut W) -> Result { - terminal::enable_raw_mode()?; + let mut terminal = FdTerminal::from(write_handle.as_raw_fd()); + terminal.enable_raw_mode()?; Ok(Self { + terminal, write: write_handle, }) } @@ -72,6 +74,6 @@ where W: Write + AsRawFd + Sized, { fn drop(&mut self) { - terminal::disable_raw_mode().unwrap(); + self.terminal.disable_raw_mode().unwrap(); } } diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs index 5de6177..3c00078 100644 --- a/keyfork-shard/src/lib.rs +++ b/keyfork-shard/src/lib.rs @@ -9,7 +9,7 @@ use keyfork_mnemonic_util::{Mnemonic, Wordlist}; use keyfork_prompt::{ qrencode, validators::{mnemonic::MnemonicSetValidator, Validator}, - Message as PromptMessage, PromptManager, + Message as PromptMessage, Terminal, }; use sha2::Sha256; use sharks::{Share, Sharks}; @@ -43,7 +43,7 @@ pub(crate) const HUNK_OFFSET: usize = 2; /// The function may panic if it is given payloads generated using a version of Keyfork that is /// incompatible with the currently running version. pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box> { - let mut pm = PromptManager::new(stdin(), stdout())?; + let mut pm = Terminal::new(stdin(), stdout())?; let wordlist = Wordlist::default(); let mut iter_count = None; diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index 1b217e8..711b4bf 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -19,7 +19,7 @@ use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationEr use keyfork_prompt::{ qrencode, validators::{mnemonic::MnemonicSetValidator, Validator}, - Error as PromptError, Message as PromptMessage, PromptManager, + Error as PromptError, Message as PromptMessage, Terminal, }; use openpgp::{ armor::{Kind, Writer}, @@ -409,7 +409,7 @@ pub fn decrypt( metadata: &EncryptedMessage, encrypted_messages: &[EncryptedMessage], ) -> Result<()> { - let mut pm = PromptManager::new(stdin(), stdout())?; + let mut pm = Terminal::new(stdin(), stdout())?; let wordlist = Wordlist::default(); let validator = MnemonicSetValidator { word_lengths: [9, 24], diff --git a/keyfork-shard/src/openpgp/keyring.rs b/keyfork-shard/src/openpgp/keyring.rs index d380a4c..e7500dd 100644 --- a/keyfork-shard/src/openpgp/keyring.rs +++ b/keyfork-shard/src/openpgp/keyring.rs @@ -1,4 +1,4 @@ -use keyfork_prompt::{Error as PromptError, DefaultPromptManager, default_prompt_manager}; +use keyfork_prompt::{Error as PromptError, DefaultTerminal, default_terminal}; use super::openpgp::{ self, @@ -25,7 +25,7 @@ pub type Result = std::result::Result; pub struct Keyring { full_certs: Vec, root: Option, - pm: DefaultPromptManager, + pm: DefaultTerminal, } impl Keyring { @@ -33,7 +33,7 @@ impl Keyring { Ok(Self { full_certs: certs.as_ref().to_vec(), root: Default::default(), - pm: default_prompt_manager()?, + pm: default_terminal()?, }) } diff --git a/keyfork-shard/src/openpgp/smartcard.rs b/keyfork-shard/src/openpgp/smartcard.rs index c9f4000..e1ead8b 100644 --- a/keyfork-shard/src/openpgp/smartcard.rs +++ b/keyfork-shard/src/openpgp/smartcard.rs @@ -1,9 +1,9 @@ use std::collections::{HashMap, HashSet}; use keyfork_prompt::{ - default_prompt_manager, + default_terminal, validators::{PinValidator, Validator}, - DefaultPromptManager, Error as PromptError, Message, + DefaultTerminal, Error as PromptError, Message, }; use super::openpgp::{ @@ -69,7 +69,7 @@ fn format_name(input: impl AsRef) -> String { pub struct SmartcardManager { current_card: Option>, root: Option, - pm: DefaultPromptManager, + pm: DefaultTerminal, pin_cache: HashMap, } @@ -78,7 +78,7 @@ impl SmartcardManager { Ok(Self { current_card: None, root: None, - pm: default_prompt_manager()?, + pm: default_terminal()?, pin_cache: Default::default(), }) } @@ -99,9 +99,8 @@ impl SmartcardManager { if let Some(c) = PcscBackend::cards(None)?.next().transpose()? { break c; } - self.pm.prompt_message(&Message::Text( - "No smart card was found".to_string(), - ))?; + self.pm + .prompt_message(&Message::Text("No smart card was found".to_string()))?; }; let mut card = Card::::new(card_backend).map_err(Error::OpenSmartCard)?; let transaction = card.transaction().map_err(Error::Transaction)?; diff --git a/keyfork/src/cli/wizard.rs b/keyfork/src/cli/wizard.rs index ca36b29..b19dda8 100644 --- a/keyfork/src/cli/wizard.rs +++ b/keyfork/src/cli/wizard.rs @@ -12,7 +12,7 @@ use keyfork_derive_util::{ }; use keyfork_prompt::{ validators::{PinValidator, Validator}, - Message, PromptManager, + Message, Terminal, }; #[derive(thiserror::Error, Debug)] @@ -102,7 +102,7 @@ fn factory_reset_current_card( fn generate_shard_secret(threshold: u8, max: u8, keys_per_shard: u8) -> Result<()> { let seed = keyfork_entropy::generate_entropy_of_size(256 / 8)?; - let mut pm = PromptManager::new(std::io::stdin(), std::io::stderr())?; + let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?; let mut certs = vec![]; let mut seen_cards: HashSet = HashSet::new(); let stdout = std::io::stdout();