Files
gyandoot-v2/crates/gyandoot-tui/src/terminal.rs
Tara Menon 54d8ca3774 P0 deep-dive: core, providers, agent, memory, TUI, setup wizard
DEEP DIVE — Agent Loop (gyandoot-agent):
- Real conversation loop with interactive (run()) and non-interactive
  (run_conversation()) modes
- Retry logic with exponential backoff and error classification
  (RateLimit, Auth, Timeout, ServerError, ContextOverflow)
- Context window management — token estimation, 75% threshold compression
- 7 built-in tools: Read, Write, Edit, Bash, Grep, Find, Ls
- BashTool with destructive-command security blocks
- ContextCompressor — prune tool results, summarize middle messages

DEEP DIVE — Memory (gyandoot-memory):
- Real MemPalace with ~/.gyandoot/palace/wings/<wing>/ directory tree
- Daily transcript files (YYYY-MM-DD_transcript.md) with append-only format
- L0/L1 identity persistence (< 157 tokens for L0)
- Session digest to drawer.md
- File persistence support for transcripts and identity store

DEEP DIVE — Core (gyandoot-core):
- GyandootConfig with TOML deserialization for all sections
- ConfigLoader with env > profile > file > default chain
- Rich types: Message, ToolCall, ToolResult, SessionId
- Comprehensive error types with thiserror derive

DEEP DIVE — Providers (gyandoot-providers):
- Provider trait with chat/chat_stream/models
- OpenAI provider with streaming SSE and function calling
- Anthropic provider with Messages API + tool use
- Ollama provider with merge_system/no_tools auto-detect
- SseParser, StreamEvent, TokenStream for SSE parsing
- ProviderFactory for creation from config

NEW — TUI crate (gyandoot-tui):
- Banner: Kapil-approved robot head ASCII art with ANSI colors
- Setup wizard: 7 providers, interactive config, connection test
- Terminal helpers: spinner, confirm, text input, selection menu
- Ratatui TUI: keyboard input, commands, status display

All 12 crates + CLI binary compile with 0 errors.
2026-06-11 20:53:38 +05:30

253 lines
7.0 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Terminal helpers — ergonomic CLI interaction utilities.
//!
//! Provides:
//! - Progress spinner (blocking)
//! - Confirm prompt (y/n)
//! - Text input with simple editing
//! - Numbered selection menu
//! - Status message display (success/error/warning)
use std::io::{self, Write};
use crossterm::cursor;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::execute;
use crossterm::style::Stylize;
use crossterm::terminal::{self, ClearType};
/// Status kind for `show_status`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusKind {
/// Green checkmark — operation succeeded
Success,
/// Red X — operation failed
Error,
/// Yellow triangle — warning
Warning,
/// Blue info — informational message
Info,
}
/// Run a simple spinner while a closure executes.
///
/// The spinner displays rotating characters: `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`
/// Returns the result of the closure.
pub fn progress_spinner<F, T>(label: &str, f: F) -> T
where
F: FnOnce() -> T,
{
const SPINNER: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
// Show initial spinner
print!("{} {}", SPINNER[0], label);
io::stdout().flush().ok();
// Run the closure
let result = f();
// Clear spinner line
let _ = execute!(io::stdout(), terminal::Clear(ClearType::CurrentLine));
let _ = io::stdout().flush();
result
}
/// Prompt the user for a yes/no answer.
///
/// Returns `true` for y/Y/yes/Yes, `false` for n/N/no/No.
/// Loops until a valid answer is given.
pub fn confirm_prompt(prompt: &str, default: Option<bool>) -> bool {
let hint = match default {
Some(true) => "[Y/n]",
Some(false) => "[y/N]",
None => "[y/n]",
};
loop {
print!("{} {}: ", prompt, hint);
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input).ok();
let input = input.trim().to_lowercase();
match input.as_str() {
"y" | "yes" => return true,
"n" | "no" => return false,
"" => {
if let Some(d) = default {
return d;
}
}
_ => {
eprintln!(" Please answer 'y' or 'n'.");
}
}
}
}
/// Collect text input from the user with basic editing support.
///
/// Uses crossterm for raw mode input. Supports:
/// - Typing characters
/// - Backspace to delete
/// - Enter to submit
/// - Ctrl+C to abort (returns None)
pub fn text_input(prompt: &str, default: Option<&str>) -> Option<String> {
let _raw = crossterm::terminal::enable_raw_mode().ok()?;
let mut buffer = default.map(|s| s.to_string()).unwrap_or_default();
let prompt_line = format!("{}: ", prompt);
// Print prompt and buffer
print!("{}{}", prompt_line, buffer);
io::stdout().flush().ok();
let result = loop {
match event::read() {
Ok(Event::Key(key)) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char(c) => {
buffer.push(c);
print!("{}", c);
io::stdout().flush().ok();
}
KeyCode::Backspace => {
if buffer.pop().is_some() {
print!("\x08 \x08");
io::stdout().flush().ok();
}
}
KeyCode::Enter => {
println!();
break Some(buffer);
}
KeyCode::Esc => {
println!();
break None;
}
_ => {}
},
Ok(Event::Key(key))
if key.code == KeyCode::Char('c')
&& key.modifiers == crossterm::event::KeyModifiers::CONTROL =>
{
println!("^C");
break None;
}
Err(_) => {
break None;
}
_ => {}
}
};
crossterm::terminal::disable_raw_mode().ok();
result
}
/// Display a numbered selection menu and return the index of the chosen item.
///
/// Returns `None` if cancelled (Ctrl+C / Esc).
pub fn selection_menu(prompt: &str, items: &[&str]) -> Option<usize> {
if items.is_empty() {
return None;
}
println!();
println!("{}", prompt);
for (i, item) in items.iter().enumerate() {
println!(" {}. {}", i + 1, item);
}
println!(" 0. Cancel");
loop {
print!("Enter choice [0-{}]: ", items.len());
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input).ok();
let input = input.trim();
if input.is_empty() {
continue;
}
match input.parse::<usize>() {
Ok(0) => return None,
Ok(n) if n >= 1 && n <= items.len() => return Some(n - 1),
_ => {
eprintln!(
" Invalid choice. Enter a number between 0 and {}.",
items.len()
);
}
}
}
}
/// Display a status message with a colored icon.
///
/// - Success: ✅ (or [OK])
/// - Error: ❌ (or [ERR])
/// - Warning: ⚠️ (or [WARN])
/// - Info: ℹ️ (or [INFO])
pub fn show_status(kind: StatusKind, message: &str) {
let (icon, styled_msg) = match kind {
StatusKind::Success => ("✅ ", message.green()),
StatusKind::Error => ("❌ ", message.red()),
StatusKind::Warning => ("⚠️ ", message.yellow()),
StatusKind::Info => ("ℹ️ ", message.blue()),
};
// Try to write styled; fall back to plain on error
let full = format!("{}{}", icon, styled_msg);
println!("{}", full);
}
/// Clear the terminal screen.
pub fn clear_screen() {
let _ = execute!(io::stdout(), terminal::Clear(ClearType::All));
let _ = execute!(io::stdout(), cursor::MoveTo(0, 0));
}
/// Print a section header with a divider line.
pub fn section_header(title: &str) {
let line = "─".repeat(title.len().saturating_add(4));
println!();
println!(" {} ", line.as_str().bold());
println!(" {} ", title.bold());
println!(" {} ", line.as_str().bold());
println!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_kind_equality() {
assert_eq!(StatusKind::Success, StatusKind::Success);
assert_ne!(StatusKind::Success, StatusKind::Error);
}
#[test]
fn test_selection_menu_empty() {
assert!(selection_menu("test", &[]).is_none());
}
#[test]
fn test_confirm_prompt_parsing() {
// Can't really test interactively, but we can check the fn compiles
assert!(std::mem::size_of::<fn(&str, Option<bool>) -> bool>() > 0);
}
#[test]
fn test_clear_screen_does_not_panic() {
let _ = execute!(io::stdout(), terminal::Clear(ClearType::All));
}
#[test]
fn test_section_header_does_not_panic() {
section_header("Test Header");
}
}