scanpw 1.0.0

Read a password from standard input
Documentation
//! Actual implementation

use std::io::{self, stdout};

use crossterm::{
    cursor::{position, MoveLeft, MoveToNextLine},
    event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
    execute,
    style::Print,
    terminal,
};

/// Error setting up, reading, or shutting down
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error {
    /// The actual error type
    inner: io::Error,
}

impl Error {
    /// Convert an [`io::Error`] into `Self`
    fn from_io(other: io::Error) -> Self {
        Self {
            inner: other,
        }
    }
}

/// Attempts to read a password from standard input
///
/// `echo` controls whether a replacement character should be printed each time
/// the user enters a character, and if so, which character. The result is
/// either a [`String`] or a [`crossterm::ErrorKind`]. Input begins wherever the
/// cursor was before calling this function, which is likely to be on its own
/// empty line.
///
/// # Errors
///
/// This function can fail if the password cannot be read from standard input.
pub fn try_scanpw(echo: Option<char>) -> Result<String, Error> {
    // Enter raw mode so we can control character echoing
    terminal::enable_raw_mode().map_err(Error::from_io)?;

    // In case anything was printed prior to the beginning of the input on the
    // same line, store the column the cursor started at
    let (max_left, _height) = position().map_err(Error::from_io)?;

    // The password
    let mut pw = String::new();

    loop {
        if let Event::Key(k) = event::read().map_err(Error::from_io)? {
            match k {
                // Password input completed
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Enter,
                    ..
                } => {
                    execute!(stdout(), MoveToNextLine(1))
                        .map_err(Error::from_io)?;
                    break;
                }

                // Handle backspace
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Backspace,
                    ..
                } => {
                    // If echo characters are enabled and any exist, remove the
                    // rightmost one
                    if echo.is_some() {
                        let (cur_left, _height) =
                            position().map_err(Error::from_io)?;

                        // True if the next position isn't past the left of the
                        // column where the cursor started
                        let not_too_far = cur_left
                            .checked_sub(1)
                            .map_or(false, |np| np >= max_left);

                        if not_too_far {
                            execute!(
                                stdout(),
                                MoveLeft(1),
                                Print(" "),
                                MoveLeft(1)
                            )
                            .map_err(Error::from_io)?;
                        }
                    }

                    // Delete the character from the password
                    pw.pop();
                }

                // Pass Ctrl+C through as a signal like normal
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Char('c'),
                    modifiers,
                    ..
                } if modifiers == KeyModifiers::CONTROL => {
                    // Reset the terminal back to normal and exit
                    terminal::disable_raw_mode().map_err(Error::from_io)?;

                    ctrl_c_self();
                }

                // Normal character input
                KeyEvent {
                    kind: KeyEventKind::Press | KeyEventKind::Repeat,
                    code: KeyCode::Char(c),
                    ..
                } => {
                    if let Some(c) = echo {
                        execute!(stdout(), Print(c)).map_err(Error::from_io)?;
                    }

                    // Add the character to the password
                    pw.push(c);
                }

                // Ignore other cases
                _ => (),
            }
        }
    }

    // Reset the terminal back to normal
    terminal::disable_raw_mode().map_err(Error::from_io)?;

    Ok(pw)
}

/// Deduplicates the documentation for `ctrl_c_self`
macro_rules! ctrl_c_self_doc {
    () => {
        "Send the signal generated by `C-c` to the current process"
    };
}

#[cfg(unix)]
#[doc = ctrl_c_self_doc!()]
fn ctrl_c_self() {
    use nix::sys::signal::{raise, Signal::SIGINT};

    assert!(
        raise(SIGINT).is_ok(),
        "failed to send SIGINT to the current process"
    );
}

#[cfg(windows)]
#[doc = ctrl_c_self_doc!()]
fn ctrl_c_self() {
    use windows::Win32::System::{
        Console::{GenerateConsoleCtrlEvent, CTRL_C_EVENT},
        Threading::GetCurrentProcessId,
    };

    let res = unsafe {
        GenerateConsoleCtrlEvent(CTRL_C_EVENT, GetCurrentProcessId())
    };

    assert!(
        res.ok().is_ok(),
        "failed to send CTRL_C_EVENT to the current process"
    );
}

#[cfg(not(any(unix, windows)))]
#[doc = ctrl_c_self_doc!()]
fn ctrl_c_self() {
    std::process::exit(1);
}