dot-ai 0.1.0

A minimal AI agent that lives in your terminal
Documentation
use anyhow::{Result, anyhow};
use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
    execute,
    style::{Color, Print, ResetColor, SetForegroundColor},
    terminal::{self, ClearType},
};
use std::io::{self, Write};

pub(super) fn clear_and_header(stdout: &mut impl Write) -> Result<()> {
    execute!(
        stdout,
        terminal::Clear(ClearType::All),
        cursor::MoveTo(0, 0),
        SetForegroundColor(Color::Cyan),
        Print(" ● dot — minimal ai agent\r\n\r\n"),
        ResetColor,
    )?;
    Ok(())
}

pub(super) fn select_from_menu(prompt: &str, items: &[&str]) -> Result<Option<usize>> {
    let mut stdout = io::stdout();
    let mut selected = 0usize;

    terminal::enable_raw_mode()?;
    execute!(stdout, cursor::Hide)?;

    let result: Result<Option<usize>> = (|| loop {
        clear_and_header(&mut stdout)?;

        execute!(
            stdout,
            SetForegroundColor(Color::White),
            Print(format!(" {}\r\n\r\n", prompt)),
            ResetColor,
        )?;

        for (i, item) in items.iter().enumerate() {
            if i == selected {
                execute!(
                    stdout,
                    SetForegroundColor(Color::Green),
                    Print(format!("{}\r\n", item)),
                    ResetColor,
                )?;
            } else {
                execute!(
                    stdout,
                    SetForegroundColor(Color::DarkGrey),
                    Print(format!("    {}\r\n", item)),
                    ResetColor,
                )?;
            }
        }

        execute!(
            stdout,
            Print("\r\n"),
            SetForegroundColor(Color::DarkGrey),
            Print("  ↑↓ navigate   enter select   q quit"),
            ResetColor,
        )?;
        stdout.flush()?;

        if let Event::Key(KeyEvent {
            code,
            kind: KeyEventKind::Press,
            ..
        }) = event::read()?
        {
            match code {
                KeyCode::Up => {
                    selected = selected.saturating_sub(1);
                }
                KeyCode::Down => {
                    if selected < items.len() - 1 {
                        selected += 1;
                    }
                }
                KeyCode::Enter => return Ok(Some(selected)),
                KeyCode::Char('q') | KeyCode::Esc => return Ok(None),
                _ => {}
            }
        }
    })();

    let _ = terminal::disable_raw_mode();
    execute!(stdout, cursor::Show, Print("\r\n"))?;
    result
}

pub(super) fn read_input_raw(prompt: &str, mask: bool) -> Result<String> {
    let mut stdout = io::stdout();

    execute!(
        stdout,
        SetForegroundColor(Color::Cyan),
        Print(format!("  {} ", prompt)),
        SetForegroundColor(Color::DarkGrey),
        Print(""),
        ResetColor,
    )?;
    stdout.flush()?;

    terminal::enable_raw_mode()?;
    execute!(stdout, cursor::Show)?;

    let mut input = String::new();

    let result: Result<String> = (|| loop {
        if let Event::Key(KeyEvent {
            code,
            kind: KeyEventKind::Press,
            ..
        }) = event::read()?
        {
            match code {
                KeyCode::Enter => {
                    execute!(stdout, Print("\r\n"))?;
                    return Ok(input.clone());
                }
                KeyCode::Backspace => {
                    if input.pop().is_some() {
                        execute!(stdout, Print("\x08 \x08"))?;
                    }
                }
                KeyCode::Esc => {
                    execute!(stdout, Print("\r\n"))?;
                    return Err(anyhow!("Cancelled"));
                }
                KeyCode::Char(c) => {
                    input.push(c);
                    if mask {
                        execute!(stdout, Print("*"))?;
                    } else {
                        execute!(stdout, Print(c))?;
                    }
                }
                _ => {}
            }
        }
        stdout.flush()?;
    })();

    let _ = terminal::disable_raw_mode();
    result
}

pub(super) fn read_api_key(prompt: &str) -> Result<String> {
    let mut stdout = io::stdout();
    let _ = terminal::disable_raw_mode();
    execute!(stdout, cursor::Show)?;

    clear_and_header(&mut stdout)?;

    execute!(
        stdout,
        SetForegroundColor(Color::White),
        Print(format!(" {}\r\n\r\n", prompt)),
        ResetColor,
    )?;

    let key = read_input_raw("API key", true)?;
    let trimmed = key.trim().to_string();
    if trimmed.is_empty() {
        return Err(anyhow!("API key cannot be empty"));
    }
    Ok(trimmed)
}