Skip to main content
Glama
8b-is
by 8b-is
mod.rs18.5 kB
//! Smart Tree Terminal Interface (STTI) - Your Coding Companion 🌳 //! //! Like a construction helper who hands you tools before you ask! //! This module provides context-aware terminal assistance. use anyhow::Result; use crossterm::{ event::{self, Event, KeyCode, KeyEvent}, terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span, Text}, widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, Terminal, }; use std::{ io::{self, Stdout}, path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant}, }; use tokio::sync::mpsc; use crate::smart::ProjectType; /// Terminal UI state #[derive(Debug, Clone)] pub struct TerminalState { /// Current working directory pub cwd: PathBuf, /// Active file being edited pub active_file: Option<PathBuf>, /// Recent file changes pub recent_changes: Vec<FileChange>, /// Current suggestions pub suggestions: Vec<Suggestion>, /// Command history pub command_history: Vec<String>, /// Current input buffer pub input: String, /// Cursor position in input pub cursor_pos: usize, /// Project context pub project_type: Option<ProjectType>, /// Status message pub status_message: Option<StatusMessage>, } /// File change event #[derive(Debug, Clone)] pub struct FileChange { pub path: PathBuf, pub change_type: ChangeType, pub timestamp: Instant, } #[derive(Debug, Clone)] pub enum ChangeType { Created, Modified, Deleted, Renamed { from: PathBuf }, } /// Suggestion from the AI assistant #[derive(Debug, Clone)] pub struct Suggestion { pub icon: &'static str, pub title: String, pub description: String, pub action: SuggestionAction, pub confidence: f32, } #[derive(Debug, Clone)] pub enum SuggestionAction { InsertText(String), RunCommand(String), OpenFile(PathBuf), CreateFile { path: PathBuf, content: String }, RefactorCode { file: PathBuf, operation: String }, } /// Status message with severity #[derive(Debug, Clone)] pub struct StatusMessage { pub text: String, pub severity: MessageSeverity, pub timestamp: Instant, } #[derive(Debug, Clone, Copy)] pub enum MessageSeverity { Info, Success, Warning, Error, } /// Main terminal interface pub struct SmartTreeTerminal { /// Terminal handle terminal: Terminal<CrosstermBackend<Stdout>>, /// Current state state: Arc<Mutex<TerminalState>>, /// Context watcher context_watcher: ContextWatcher, /// Pattern analyzer pattern_analyzer: PatternAnalyzer, /// Suggestion receiver suggestion_rx: mpsc::Receiver<Suggestion>, /// Suggestion sender (for background tasks) _suggestion_tx: mpsc::Sender<Suggestion>, } impl SmartTreeTerminal { /// Create new terminal interface pub fn new() -> Result<Self> { // Setup terminal terminal::enable_raw_mode()?; let mut stdout = io::stdout(); stdout.execute(EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let terminal = Terminal::new(backend)?; // Create channels let (suggestion_tx, suggestion_rx) = mpsc::channel(100); // Initial state let state = Arc::new(Mutex::new(TerminalState { cwd: std::env::current_dir()?, active_file: None, recent_changes: Vec::new(), suggestions: Vec::new(), command_history: Vec::new(), input: String::new(), cursor_pos: 0, project_type: None, status_message: None, })); Ok(Self { terminal, state: state.clone(), context_watcher: ContextWatcher::new(state.clone(), suggestion_tx.clone()), pattern_analyzer: PatternAnalyzer::new(state.clone(), suggestion_tx.clone()), suggestion_rx, _suggestion_tx: suggestion_tx, }) } /// Run the terminal interface pub async fn run(&mut self) -> Result<()> { // Start background tasks self.context_watcher.start().await?; self.pattern_analyzer.start().await?; loop { // Draw UI self.draw()?; // Handle events if event::poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { if self.handle_key(key).await? { break; } } } // Process suggestions while let Ok(suggestion) = self.suggestion_rx.try_recv() { let mut state = self.state.lock().unwrap(); state.suggestions.push(suggestion); // Keep only the 5 most recent suggestions if state.suggestions.len() > 5 { state.suggestions.remove(0); } } } // Cleanup terminal::disable_raw_mode()?; self.terminal.backend_mut().execute(LeaveAlternateScreen)?; Ok(()) } /// Draw the UI fn draw(&mut self) -> Result<()> { let state = self.state.lock().unwrap().clone(); self.terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // Header Constraint::Length(3), // Context bar Constraint::Min(10), // Main area Constraint::Length(3), // Input Constraint::Length(1), // Status bar ]) .split(f.size()); // Header Self::draw_header(f, chunks[0], &state); // Context bar Self::draw_context(f, chunks[1], &state); // Main area (split into suggestions and history) let main_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(60), // History/Output Constraint::Percentage(40), // Suggestions ]) .split(chunks[2]); Self::draw_history(f, main_chunks[0], &state); Self::draw_suggestions(f, main_chunks[1], &state); // Input area Self::draw_input(f, chunks[3], &state); // Status bar Self::draw_status(f, chunks[4], &state); })?; Ok(()) } /// Draw header fn draw_header(f: &mut Frame, area: Rect, _state: &TerminalState) { let show_banner = std::env::var("ST_BANNER").is_ok_and(|v| v == "1" || v.to_lowercase() == "true"); let mut lines: Vec<Line> = Vec::new(); if show_banner { lines.push(Line::from(vec![ Span::styled("⚡ ", Style::default().fg(Color::Yellow)), Span::styled( "SMART TREE TERMINAL", Style::default() .fg(Color::Green) .add_modifier(Modifier::BOLD), ), Span::styled(" 🌊 ", Style::default().fg(Color::Cyan)), Span::styled("rocking your repo", Style::default().fg(Color::Magenta)), Span::raw(" 🎸"), ])); } lines.push(Line::from(vec![ Span::styled( "Smart Tree Terminal", Style::default() .fg(Color::Green) .add_modifier(Modifier::BOLD), ), Span::raw(" v5.5 - "), Span::styled("Your Coding Companion ", Style::default().fg(Color::Cyan)), Span::raw("🌳"), ])); let header = Paragraph::new(Text::from(lines)) .block(Block::default().borders(Borders::ALL)) .alignment(ratatui::layout::Alignment::Center); f.render_widget(header, area); } /// Draw context information fn draw_context(f: &mut Frame, area: Rect, state: &TerminalState) { let mut context_items = vec![Span::styled("Context: ", Style::default().fg(Color::Gray))]; if let Some(file) = &state.active_file { context_items.push(Span::styled( format!("Editing: {} ", file.display()), Style::default().fg(Color::Yellow), )); } if let Some(project) = &state.project_type { context_items.push(Span::styled( format!("| Project: {:?} ", project), Style::default().fg(Color::Blue), )); } if std::env::var("HOT_TUB").is_ok_and(|v| v == "1" || v.to_lowercase() == "true") { context_items.push(Span::styled( "| 🛁 Hot Tub Mode ", Style::default() .fg(Color::Magenta) .add_modifier(Modifier::BOLD), )); } let context = Paragraph::new(Line::from(context_items)) .block(Block::default().borders(Borders::LEFT | Borders::RIGHT)); f.render_widget(context, area); } /// Draw command history fn draw_history(f: &mut Frame, area: Rect, state: &TerminalState) { let history_items: Vec<ListItem> = state .command_history .iter() .rev() .take(area.height as usize - 2) .map(|cmd| ListItem::new(cmd.as_str())) .collect(); let history = List::new(history_items).block(Block::default().title("History").borders(Borders::ALL)); f.render_widget(history, area); } /// Draw suggestions panel fn draw_suggestions(f: &mut Frame, area: Rect, state: &TerminalState) { let suggestion_items: Vec<ListItem> = state .suggestions .iter() .map(|s| { ListItem::new(vec![ Line::from(vec![ Span::raw(s.icon), Span::raw(" "), Span::styled(&s.title, Style::default().add_modifier(Modifier::BOLD)), ]), Line::from(Span::styled( &s.description, Style::default().fg(Color::Gray), )), ]) }) .collect(); let suggestions = List::new(suggestion_items).block( Block::default() .title("💡 Suggestions") .borders(Borders::ALL), ); f.render_widget(suggestions, area); } /// Draw input area fn draw_input(f: &mut Frame, area: Rect, state: &TerminalState) { let input = Paragraph::new(state.input.as_str()).block( Block::default() .title(format!("{}$ ", state.cwd.display())) .borders(Borders::ALL), ); f.render_widget(input, area); // Set cursor position f.set_cursor(area.x + state.cursor_pos as u16 + 1, area.y + 1); } /// Draw status bar fn draw_status(f: &mut Frame, area: Rect, state: &TerminalState) { let status_text = if let Some(msg) = &state.status_message { let color = match msg.severity { MessageSeverity::Info => Color::Blue, MessageSeverity::Success => Color::Green, MessageSeverity::Warning => Color::Yellow, MessageSeverity::Error => Color::Red, }; Span::styled(&msg.text, Style::default().fg(color)) } else { Span::raw("Ready") }; let status = Paragraph::new(Line::from(vec![ status_text, Span::raw(" | "), Span::raw("Press Ctrl+C to exit"), ])); f.render_widget(status, area); } /// Handle keyboard input async fn handle_key(&mut self, key: KeyEvent) -> Result<bool> { match key.code { KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => { return Ok(true); // Exit } KeyCode::Char(c) => { let _cursor_pos = { let mut state = self.state.lock().unwrap(); let cursor_pos_local = state.cursor_pos; state.input.insert(cursor_pos_local, c); state.cursor_pos += 1; cursor_pos_local }; // lock dropped here before await // Trigger pattern analysis on input change self.pattern_analyzer.analyze_input().await?; } KeyCode::Backspace => { let mut state = self.state.lock().unwrap(); if state.cursor_pos > 0 { let cursor_pos = state.cursor_pos; state.input.remove(cursor_pos - 1); state.cursor_pos -= 1; } } KeyCode::Enter => { let command = { let mut state = self.state.lock().unwrap(); let command = state.input.clone(); state.command_history.push(command.clone()); state.input.clear(); state.cursor_pos = 0; command }; // lock dropped // Process command self.process_command(&command).await?; } KeyCode::Tab => { // Accept top suggestion let maybe_action = { let state = self.state.lock().unwrap(); state.suggestions.first().map(|s| s.action.clone()) }; // drop lock before await if let Some(action) = maybe_action { self.apply_suggestion(action).await?; } } _ => {} } Ok(false) } /// Process a command async fn process_command(&mut self, command: &str) -> Result<()> { // This is where we'd integrate with the shell // For now, just update status { let mut state = self.state.lock().unwrap(); state.status_message = Some(StatusMessage { text: format!("Executed: {}", command), severity: MessageSeverity::Info, timestamp: Instant::now(), }); } Ok(()) } /// Apply a suggestion async fn apply_suggestion(&mut self, action: SuggestionAction) -> Result<()> { match action { SuggestionAction::InsertText(text) => { let mut state = self.state.lock().unwrap(); let cursor_pos = state.cursor_pos; state.input.insert_str(cursor_pos, &text); state.cursor_pos += text.len(); } SuggestionAction::RunCommand(cmd) => { self.process_command(&cmd).await?; } _ => { // TODO: Implement other actions } } Ok(()) } } /// Context watcher - monitors file system and project state pub struct ContextWatcher { state: Arc<Mutex<TerminalState>>, _suggestion_tx: mpsc::Sender<Suggestion>, } impl ContextWatcher { fn new(state: Arc<Mutex<TerminalState>>, _suggestion_tx: mpsc::Sender<Suggestion>) -> Self { Self { state, _suggestion_tx, } } async fn start(&self) -> Result<()> { // TODO: Implement file watching // For now, detect project type let cwd = { let state = self.state.lock().unwrap(); state.cwd.clone() }; if cwd.join("Cargo.toml").exists() { { let mut state = self.state.lock().unwrap(); state.project_type = Some(ProjectType::Rust); } // Send a suggestion let _ = self ._suggestion_tx .send(Suggestion { icon: "🦀", title: "Rust Project Detected".to_string(), description: "Run 'cargo build' to compile".to_string(), action: SuggestionAction::RunCommand("cargo build".to_string()), confidence: 0.9, }) .await; } Ok(()) } } /// Pattern analyzer - analyzes coding patterns and suggests actions pub struct PatternAnalyzer { state: Arc<Mutex<TerminalState>>, _suggestion_tx: mpsc::Sender<Suggestion>, } impl PatternAnalyzer { fn new(state: Arc<Mutex<TerminalState>>, _suggestion_tx: mpsc::Sender<Suggestion>) -> Self { Self { state, _suggestion_tx, } } async fn start(&self) -> Result<()> { // TODO: Implement pattern learning Ok(()) } async fn analyze_input(&self) -> Result<()> { let input = { let state = self.state.lock().unwrap(); state.input.clone() }; // Simple pattern matching for demo if input.starts_with("git com") { let _ = self ._suggestion_tx .send(Suggestion { icon: "📝", title: "Git Commit".to_string(), description: "Commit recent changes".to_string(), action: SuggestionAction::InsertText("mit -m \"".to_string()), confidence: 0.8, }) .await; } else if input.contains("import") { let _ = self ._suggestion_tx .send(Suggestion { icon: "📦", title: "Import Suggestion".to_string(), description: "Add commonly used imports".to_string(), action: SuggestionAction::InsertText(" { useState } from 'react'".to_string()), confidence: 0.7, }) .await; } Ok(()) } } // Trisha says: "This terminal is like having a personal assistant who knows // exactly which receipt you need before you even open the filing cabinet!" 📁

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server