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;
#[derive(Debug, Clone)]
pub struct TerminalState {
pub cwd: PathBuf,
pub active_file: Option<PathBuf>,
pub recent_changes: Vec<FileChange>,
pub suggestions: Vec<Suggestion>,
pub command_history: Vec<String>,
pub input: String,
pub cursor_pos: usize,
pub project_type: Option<ProjectType>,
pub status_message: Option<StatusMessage>,
}
#[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 },
}
#[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 },
}
#[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,
}
pub struct SmartTreeTerminal {
terminal: Terminal<CrosstermBackend<Stdout>>,
state: Arc<Mutex<TerminalState>>,
context_watcher: ContextWatcher,
pattern_analyzer: PatternAnalyzer,
suggestion_rx: mpsc::Receiver<Suggestion>,
_suggestion_tx: mpsc::Sender<Suggestion>,
}
impl SmartTreeTerminal {
pub fn new() -> Result<Self> {
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
stdout.execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
let (suggestion_tx, suggestion_rx) = mpsc::channel(100);
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,
})
}
pub async fn run(&mut self) -> Result<()> {
self.context_watcher.start().await?;
self.pattern_analyzer.start().await?;
loop {
self.draw()?;
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if self.handle_key(key).await? {
break;
}
}
}
while let Ok(suggestion) = self.suggestion_rx.try_recv() {
let mut state = self.state.lock().unwrap();
state.suggestions.push(suggestion);
if state.suggestions.len() > 5 {
state.suggestions.remove(0);
}
}
}
terminal::disable_raw_mode()?;
self.terminal.backend_mut().execute(LeaveAlternateScreen)?;
Ok(())
}
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), Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), Constraint::Length(1), ])
.split(f.size());
Self::draw_header(f, chunks[0], &state);
Self::draw_context(f, chunks[1], &state);
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(60), Constraint::Percentage(40), ])
.split(chunks[2]);
Self::draw_history(f, main_chunks[0], &state);
Self::draw_suggestions(f, main_chunks[1], &state);
Self::draw_input(f, chunks[3], &state);
Self::draw_status(f, chunks[4], &state);
})?;
Ok(())
}
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);
}
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);
}
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);
}
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);
}
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);
f.set_cursor(area.x + state.cursor_pos as u16 + 1, area.y + 1);
}
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);
}
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); }
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
};
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
};
self.process_command(&command).await?;
}
KeyCode::Tab => {
let maybe_action = {
let state = self.state.lock().unwrap();
state.suggestions.first().map(|s| s.action.clone())
}; if let Some(action) = maybe_action {
self.apply_suggestion(action).await?;
}
}
_ => {}
}
Ok(false)
}
async fn process_command(&mut self, command: &str) -> Result<()> {
{
let mut state = self.state.lock().unwrap();
state.status_message = Some(StatusMessage {
text: format!("Executed: {}", command),
severity: MessageSeverity::Info,
timestamp: Instant::now(),
});
}
Ok(())
}
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?;
}
_ => {
}
}
Ok(())
}
}
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<()> {
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);
}
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(())
}
}
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<()> {
Ok(())
}
async fn analyze_input(&self) -> Result<()> {
let input = {
let state = self.state.lock().unwrap();
state.input.clone()
};
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(())
}
}