codex-ops 0.1.8

A local operations CLI for Codex auth, usage, and cycle workflows.
Documentation
use crate::error::AppError;
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, MultiSelect, Select};
use std::io::{self, ErrorKind, IsTerminal};

pub trait Prompt {
    fn select(&mut self, prompt: &str, items: &[String]) -> Result<Option<usize>, AppError>;

    fn multi_select(
        &mut self,
        prompt: &str,
        items: &[String],
    ) -> Result<Option<Vec<usize>>, AppError>;

    fn confirm(&mut self, prompt: &str, default: bool) -> Result<Option<bool>, AppError>;
}

#[derive(Default)]
pub struct DialoguerPrompt {
    theme: ColorfulTheme,
}

impl Prompt for DialoguerPrompt {
    fn select(&mut self, prompt: &str, items: &[String]) -> Result<Option<usize>, AppError> {
        Select::with_theme(&self.theme)
            .with_prompt(prompt)
            .items(items)
            .default(0)
            .interact_opt()
            .map_err(prompt_error)
    }

    fn multi_select(
        &mut self,
        prompt: &str,
        items: &[String],
    ) -> Result<Option<Vec<usize>>, AppError> {
        MultiSelect::with_theme(&self.theme)
            .with_prompt(prompt)
            .items(items)
            .interact_opt()
            .map_err(prompt_error)
    }

    fn confirm(&mut self, prompt: &str, default: bool) -> Result<Option<bool>, AppError> {
        Confirm::with_theme(&self.theme)
            .with_prompt(prompt)
            .default(default)
            .interact_opt()
            .map_err(prompt_error)
    }
}

pub fn stdin_and_stderr_are_terminals() -> bool {
    io::stdin().is_terminal() && io::stderr().is_terminal()
}

fn prompt_error(error: dialoguer::Error) -> AppError {
    match error {
        dialoguer::Error::IO(error) if error.kind() == ErrorKind::Interrupted => {
            AppError::new("Prompt cancelled.")
        }
        error => AppError::new(format!("Prompt failed: {error}")),
    }
}