carryctx 0.0.3

Persistent project context for coding agents
use std::io::{self, BufRead, IsTerminal, Write};

use crate::error::CarryCtxError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalAnswer<T> {
    Value(T),
    Cancel,
    Eof,
}

pub trait Terminal {
    fn stdin_is_tty(&self) -> bool;
    fn text(&self, message: &str) -> Result<TerminalAnswer<String>, CarryCtxError>;
    fn confirm(
        &self,
        message: &str,
        initial_value: bool,
    ) -> Result<TerminalAnswer<bool>, CarryCtxError>;
}

pub struct ProcessTerminal;

impl ProcessTerminal {
    pub fn new() -> Self {
        Self
    }
}

impl Default for ProcessTerminal {
    fn default() -> Self {
        Self::new()
    }
}

impl Terminal for ProcessTerminal {
    fn stdin_is_tty(&self) -> bool {
        io::stdin().is_terminal()
    }

    fn text(&self, message: &str) -> Result<TerminalAnswer<String>, CarryCtxError> {
        let mut stdout = io::stdout();
        write!(stdout, "{}: ", message).map_err(|_| CarryCtxError::interrupted())?;
        stdout.flush().map_err(|_| CarryCtxError::interrupted())?;

        let mut input = String::new();
        match io::stdin().lock().read_line(&mut input) {
            Ok(0) => Ok(TerminalAnswer::Eof),
            Ok(_) => {
                let trimmed = input.trim().to_string();
                if trimmed.is_empty() {
                    Ok(TerminalAnswer::Cancel)
                } else {
                    Ok(TerminalAnswer::Value(trimmed))
                }
            }
            Err(_) => Ok(TerminalAnswer::Cancel),
        }
    }

    fn confirm(
        &self,
        message: &str,
        initial_value: bool,
    ) -> Result<TerminalAnswer<bool>, CarryCtxError> {
        let default_str = if initial_value { "Y/n" } else { "y/N" };
        let mut stdout = io::stdout();
        write!(stdout, "{} [{}]: ", message, default_str)
            .map_err(|_| CarryCtxError::interrupted())?;
        stdout.flush().map_err(|_| CarryCtxError::interrupted())?;

        let mut input = String::new();
        match io::stdin().lock().read_line(&mut input) {
            Ok(0) => Ok(TerminalAnswer::Eof),
            Ok(_) => {
                let trimmed = input.trim().to_lowercase();
                match trimmed.as_str() {
                    "" => Ok(TerminalAnswer::Value(initial_value)),
                    "y" | "yes" | "true" => Ok(TerminalAnswer::Value(true)),
                    "n" | "no" | "false" => Ok(TerminalAnswer::Value(false)),
                    "c" => Ok(TerminalAnswer::Cancel),
                    _ => Ok(TerminalAnswer::Value(initial_value)),
                }
            }
            Err(_) => Ok(TerminalAnswer::Cancel),
        }
    }
}

pub struct FakeTerminal {
    pub stdin_is_tty: bool,
    pub text_responses: Vec<TerminalAnswer<String>>,
    pub confirm_responses: Vec<TerminalAnswer<bool>>,
}

impl Default for FakeTerminal {
    fn default() -> Self {
        Self::new()
    }
}

impl FakeTerminal {
    pub fn new() -> Self {
        Self {
            stdin_is_tty: true,
            text_responses: vec![],
            confirm_responses: vec![],
        }
    }

    pub fn with_text(mut self, responses: Vec<TerminalAnswer<String>>) -> Self {
        self.text_responses = responses;
        self
    }

    pub fn with_confirm(mut self, responses: Vec<TerminalAnswer<bool>>) -> Self {
        self.confirm_responses = responses;
        self
    }
}

impl Terminal for FakeTerminal {
    fn stdin_is_tty(&self) -> bool {
        self.stdin_is_tty
    }

    fn text(&self, _message: &str) -> Result<TerminalAnswer<String>, CarryCtxError> {
        if let Some(response) = self.text_responses.first().cloned() {
            Ok(response)
        } else {
            Ok(TerminalAnswer::Cancel)
        }
    }

    fn confirm(
        &self,
        _message: &str,
        _initial_value: bool,
    ) -> Result<TerminalAnswer<bool>, CarryCtxError> {
        if let Some(response) = self.confirm_responses.first().cloned() {
            Ok(response)
        } else {
            Ok(TerminalAnswer::Cancel)
        }
    }
}