keyfork-crossterm: add FdTerminal struct to manage non-default terminals

This commit is contained in:
Ryan Heywood 2024-01-10 22:35:31 -05:00
parent 6825ac9cea
commit f6b41fce5f
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
12 changed files with 122 additions and 34 deletions

2
Cargo.lock generated
View File

@ -1427,7 +1427,7 @@ dependencies = [
[[package]]
name = "keyfork-crossterm"
version = "0.27.0"
version = "0.27.1"
dependencies = [
"async-std",
"bitflags 2.4.1",

View File

@ -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<WindowSize>;
}
#[cfg(unix)]
pub struct FdTerminal {
fd: i32,
stored_termios: Option<libc::termios>,
}
impl<T> From<T> 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<WindowSize> {
sys::fd_window_size(self.fd)
}
}
#[doc(no_inline)]
use crate::Command;
use crate::{csi, impl_display};

View File

@ -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")]

View File

@ -34,6 +34,21 @@ impl From<winsize> for WindowSize {
}
}
pub(crate) fn fd_window_size(fd: i32) -> io::Result<WindowSize> {
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<WindowSize> {
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
@ -59,6 +74,10 @@ pub(crate) fn window_size() -> io::Result<WindowSize> {
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<Termios> {
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

View File

@ -2,11 +2,11 @@ use std::io::{stdin, stdout};
use keyfork_prompt::{
validators::{mnemonic, Validator},
PromptManager,
Terminal,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut mgr = PromptManager::new(stdin(), stdout())?;
let mut mgr = Terminal::new(stdin(), stdout())?;
let transport_validator = mnemonic::MnemonicSetValidator {
word_lengths: [9, 24],
};

View File

@ -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<R, W> {
pub struct Terminal<R, W> {
read: BufReader<R>,
write: W,
terminal: FdTerminal,
}
impl<R, W> PromptManager<R, W>
impl<R, W> Terminal<R, W>
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<Stdin, Stderr>;
pub type DefaultTerminal = Terminal<Stdin, Stderr>;
pub fn default_prompt_manager() -> Result<DefaultPromptManager> {
PromptManager::new(stdin(), stderr())
pub fn default_terminal() -> Result<DefaultTerminal> {
Terminal::new(stdin(), stderr())
}

View File

@ -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<Self> {
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();
}
}

View File

@ -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<dyn std::error::Error>> {
let mut pm = PromptManager::new(stdin(), stdout())?;
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None;

View File

@ -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],

View File

@ -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<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring {
full_certs: Vec<Cert>,
root: Option<Cert>,
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()?,
})
}

View File

@ -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<str>) -> String {
pub struct SmartcardManager {
current_card: Option<Card<Open>>,
root: Option<Cert>,
pm: DefaultPromptManager,
pm: DefaultTerminal,
pin_cache: HashMap<Fingerprint, String>,
}
@ -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::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?;
let transaction = card.transaction().map_err(Error::Transaction)?;

View File

@ -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<String> = HashSet::new();
let stdout = std::io::stdout();