use std::io::{self, IsTerminal, Write};
use anyhow::{Context, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use super::common::SetupInterrupted;
pub(super) fn read_secret_line(prompt: &str) -> Result<Option<String>> {
if io::stdin().is_terminal() {
read_secret_line_tty(prompt)
} else {
read_secret_line_fallback(prompt)
}
}
pub(super) fn mask_key(key: &str) -> String {
let chars: Vec<char> = key.chars().collect();
let len = chars.len();
if len <= 12 {
return "••••••••".to_string();
}
let head: String = chars[..4].iter().collect();
let tail: String = chars[len - 4..].iter().collect();
format!("{head}…{tail}")
}
fn read_secret_line_fallback(prompt: &str) -> Result<Option<String>> {
eprintln!("Warning: stdin is not a terminal — the pasted key will be visible.");
print!("{prompt}");
io::stdout().flush().context("Failed to flush secret prompt")?;
let mut input = String::new();
io::stdin().read_line(&mut input).context("Failed to read API key input")?;
let trimmed = input.trim().to_string();
Ok(if trimmed.is_empty() { None } else { Some(trimmed) })
}
fn read_secret_line_tty(prompt: &str) -> Result<Option<String>> {
let _guard = RawModeGuard::enable()?;
{
let mut stdout = io::stdout();
write!(stdout, "{prompt}").ok();
stdout.flush().ok();
}
let mut buffer = String::new();
loop {
let event = event::read().with_context(|| "Failed to read keypress while entering API key")?;
match handle_key(event, &mut buffer)? {
KeyAction::Continue => continue,
KeyAction::Submit => {
let mut stdout = io::stdout();
write!(stdout, "\r\n").ok();
stdout.flush().ok();
let trimmed = buffer.trim().to_string();
return Ok(if trimmed.is_empty() { None } else { Some(trimmed) });
}
KeyAction::Abort => return Err(SetupInterrupted.into()),
}
}
}
enum KeyAction {
Continue,
Submit,
Abort,
}
fn handle_key(event: Event, buffer: &mut String) -> Result<KeyAction> {
let Event::Key(key) = event else {
return Ok(KeyAction::Continue);
};
if key.kind != KeyEventKind::Press {
return Ok(KeyAction::Continue);
}
let mut stdout = io::stdout();
match key.code {
KeyCode::Enter => Ok(KeyAction::Submit),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => Ok(KeyAction::Abort),
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => Ok(KeyAction::Submit),
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
for _ in 0..buffer.chars().count() {
write!(stdout, "\u{8} \u{8}").ok();
}
stdout.flush().ok();
buffer.clear();
Ok(KeyAction::Continue)
}
KeyCode::Backspace => {
if !buffer.is_empty() {
buffer.pop();
write!(stdout, "\u{8} \u{8}").ok();
stdout.flush().ok();
}
Ok(KeyAction::Continue)
}
KeyCode::Char(c) if !c.is_control() => {
buffer.push(c);
write!(stdout, "*").ok();
stdout.flush().ok();
Ok(KeyAction::Continue)
}
_ => Ok(KeyAction::Continue),
}
}
struct RawModeGuard;
impl RawModeGuard {
fn enable() -> Result<Self> {
enable_raw_mode().context("Failed to enable raw mode for secret input")?;
Ok(RawModeGuard)
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
while let Ok(true) = event::poll(std::time::Duration::from_millis(0)) {
let _ = event::read();
}
let _ = disable_raw_mode();
let _ = io::stdout().flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mask_key_short_keys_fully_masked() {
assert_eq!(mask_key("short"), "••••••••");
assert_eq!(mask_key("exactly12c"), "••••••••"); assert_eq!(mask_key("exactly12ch"), "••••••••"); }
#[test]
fn mask_key_boundary_12_chars_fully_masked() {
assert_eq!(mask_key("abcdefghijkl"), "••••••••"); }
#[test]
fn mask_key_long_keys_show_head_and_tail() {
let masked = mask_key("sk-or-v1-abcdefgh1234567890");
assert!(masked.starts_with("sk-o"), "head should be first 4 chars: {masked}");
assert!(masked.ends_with("7890"), "tail should be last 4 chars: {masked}");
assert!(masked.contains('…'), "middle should be ellipsis: {masked}");
}
#[test]
fn mask_key_never_leaks_middle() {
let key = "ABCDEFGHIJKLMNOP"; let masked = mask_key(key);
assert!(!masked.contains("EFGHIJ"), "middle of the key must not appear: {masked}");
assert!(!masked.contains("JKLM"), "middle of the key must not appear: {masked}");
}
#[test]
fn mask_key_handles_unicode_without_panicking() {
let masked = mask_key("🔑🔑🔑🔑🔑🔑🔑🔑🔑🔑🔑🔑🔑"); assert!(masked.contains('…'));
}
}