use crate::command::chat::oneshot::display::{box_width, extract_bash_command, make_args_preview};
use crate::command::chat::tools::classification::ToolCategory;
use colored::Colorize;
use crossterm::event::{self, Event, KeyCode};
use crossterm::{cursor, execute, terminal};
use std::io::{self, Write};
pub(crate) fn draw_top_border(stdout: &mut io::Stdout, bw: usize, title: &str) -> io::Result<()> {
let title_text = format!(" {} ", title);
let inner_w = bw.saturating_sub(2);
let dash_fill = inner_w.saturating_sub(title_text.chars().count() + 2);
let dash_tail = "─".repeat(dash_fill);
writeln!(
stdout,
" {}{}{}{}{}\r",
"┌".yellow().bold(),
"─".yellow(),
title_text.white().bold(),
"─".yellow(),
format!("{}{}", dash_tail, "┐").yellow().bold(),
)
}
pub(crate) fn draw_content_line(stdout: &mut io::Stdout, content: &str) -> io::Result<()> {
writeln!(stdout, " {} {}\r", "│".yellow(), content.white())
}
pub(crate) fn draw_empty_line(stdout: &mut io::Stdout) -> io::Result<()> {
writeln!(stdout, " {}\r", "│".yellow())
}
pub(crate) fn draw_hint_line(stdout: &mut io::Stdout, hint: &str) -> io::Result<()> {
writeln!(stdout, " {} {}\r", "│".yellow(), hint.dimmed())
}
pub(crate) fn draw_bottom_border(stdout: &mut io::Stdout, bw: usize) -> io::Result<()> {
writeln!(
stdout,
" {}{}{}\r",
"└".yellow().bold(),
"─".repeat(bw.saturating_sub(3)).yellow(),
"┘".yellow().bold(),
)?;
stdout.flush()
}
pub(crate) fn draw_selected_option(stdout: &mut io::Stdout, label: &str) -> io::Result<()> {
writeln!(
stdout,
" {} {} {}\r",
"│".yellow(),
"❯".cyan().bold(),
label.white().bold()
)
}
pub(crate) fn draw_unselected_option(stdout: &mut io::Stdout, label: &str) -> io::Result<()> {
writeln!(stdout, " {} {}\r", "│".yellow(), label.dimmed())
}
pub(crate) fn draw_option_description(stdout: &mut io::Stdout, desc: &str) -> io::Result<()> {
writeln!(stdout, " {} {}\r", "│".yellow(), desc.dimmed())
}
pub(crate) fn draw_multi_selected_option(
stdout: &mut io::Stdout,
checked: bool,
label: &str,
) -> io::Result<()> {
let check = if checked { "◉" } else { "○" };
writeln!(
stdout,
" {} {} {} {}\r",
"│".yellow(),
"❯".cyan().bold(),
check.cyan().bold(),
label.cyan().bold()
)
}
pub(crate) fn draw_multi_unselected_option(
stdout: &mut io::Stdout,
checked: bool,
label: &str,
) -> io::Result<()> {
let check = if checked { "◉" } else { "○" };
writeln!(
stdout,
" {} {} {}\r",
"│".yellow(),
check.white(),
label.white()
)
}
pub(crate) fn clear_drawn_lines(stdout: &mut io::Stdout, lines: u16) -> io::Result<()> {
if lines > 0 {
let _ = execute!(stdout, cursor::MoveUp(lines));
}
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
Ok(())
}
pub(crate) fn interactive_confirm(
tool_name: &str,
arguments: &str,
options: &[&str],
initial: usize,
) -> Option<usize> {
let bw = box_width();
let category = ToolCategory::from_name(tool_name);
let icon = category.icon();
let mut stdout = io::stdout();
let mut cursor_pos = initial;
let desc = if tool_name == "Bash" || tool_name == "Shell" {
if let Some(cmd) = extract_bash_command(arguments) {
format!("$ {}", cmd)
} else {
make_args_preview(arguments)
}
} else {
make_args_preview(arguments)
};
let desc_display = if desc.len() > bw.saturating_sub(8) {
format!("{}...", &desc[..bw.saturating_sub(11)])
} else {
desc.clone()
};
let total_lines = (options.len() + 6) as u16;
let draw = |stdout: &mut io::Stdout, cursor_pos: usize, first: bool| -> io::Result<()> {
if !first {
clear_drawn_lines(stdout, total_lines)?;
}
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
let title = format!("{} {} 需要确认", icon, tool_name);
draw_top_border(stdout, bw, &title)?;
draw_content_line(stdout, &desc_display)?;
draw_empty_line(stdout)?;
for (i, opt) in options.iter().enumerate() {
if cursor_pos == i {
draw_selected_option(stdout, opt)?;
} else {
draw_unselected_option(stdout, opt)?;
}
}
draw_empty_line(stdout)?;
draw_hint_line(stdout, "• ↑↓ 移动 Enter 确认")?;
draw_bottom_border(stdout, bw)?;
stdout.flush()?;
Ok(())
};
if terminal::enable_raw_mode().is_err() {
return None;
}
let _ = draw(&mut stdout, cursor_pos, true);
let result = loop {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
cursor_pos = cursor_pos.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
if cursor_pos + 1 < options.len() {
cursor_pos += 1;
}
}
KeyCode::Enter => break Some(cursor_pos),
KeyCode::Esc | KeyCode::Char('q') => break None,
_ => continue,
}
let _ = draw(&mut stdout, cursor_pos, false);
}
};
let _ = terminal::disable_raw_mode();
{
let _ = clear_drawn_lines(&mut stdout, total_lines);
}
result
}