1use anyhow::Result;
7use crossterm::{
8 event::{self, Event, KeyCode, KeyEvent},
9 terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
10 ExecutableCommand,
11};
12use ratatui::{
13 backend::CrosstermBackend,
14 layout::{Constraint, Direction, Layout, Rect},
15 style::{Color, Modifier, Style},
16 text::{Line, Span, Text},
17 widgets::{Block, Borders, List, ListItem, Paragraph},
18 Frame, Terminal,
19};
20use std::{
21 io::{self, Stdout},
22 path::PathBuf,
23 sync::{Arc, Mutex},
24 time::{Duration, Instant},
25};
26use tokio::sync::mpsc;
27
28use crate::smart::ProjectType;
29
30#[derive(Debug, Clone)]
32pub struct TerminalState {
33 pub cwd: PathBuf,
35
36 pub active_file: Option<PathBuf>,
38
39 pub recent_changes: Vec<FileChange>,
41
42 pub suggestions: Vec<Suggestion>,
44
45 pub command_history: Vec<String>,
47
48 pub input: String,
50
51 pub cursor_pos: usize,
53
54 pub project_type: Option<ProjectType>,
56
57 pub status_message: Option<StatusMessage>,
59}
60
61#[derive(Debug, Clone)]
63pub struct FileChange {
64 pub path: PathBuf,
65 pub change_type: ChangeType,
66 pub timestamp: Instant,
67}
68
69#[derive(Debug, Clone)]
70pub enum ChangeType {
71 Created,
72 Modified,
73 Deleted,
74 Renamed { from: PathBuf },
75}
76
77#[derive(Debug, Clone)]
79pub struct Suggestion {
80 pub icon: &'static str,
81 pub title: String,
82 pub description: String,
83 pub action: SuggestionAction,
84 pub confidence: f32,
85}
86
87#[derive(Debug, Clone)]
88pub enum SuggestionAction {
89 InsertText(String),
90 RunCommand(String),
91 OpenFile(PathBuf),
92 CreateFile { path: PathBuf, content: String },
93 RefactorCode { file: PathBuf, operation: String },
94}
95
96#[derive(Debug, Clone)]
98pub struct StatusMessage {
99 pub text: String,
100 pub severity: MessageSeverity,
101 pub timestamp: Instant,
102}
103
104#[derive(Debug, Clone, Copy)]
105pub enum MessageSeverity {
106 Info,
107 Success,
108 Warning,
109 Error,
110}
111
112pub struct SmartTreeTerminal {
114 terminal: Terminal<CrosstermBackend<Stdout>>,
116
117 state: Arc<Mutex<TerminalState>>,
119
120 context_watcher: ContextWatcher,
122
123 pattern_analyzer: PatternAnalyzer,
125
126 suggestion_rx: mpsc::Receiver<Suggestion>,
128
129 _suggestion_tx: mpsc::Sender<Suggestion>,
131}
132
133impl SmartTreeTerminal {
134 pub fn new() -> Result<Self> {
136 terminal::enable_raw_mode()?;
138 let mut stdout = io::stdout();
139 stdout.execute(EnterAlternateScreen)?;
140
141 let backend = CrosstermBackend::new(stdout);
142 let terminal = Terminal::new(backend)?;
143
144 let (suggestion_tx, suggestion_rx) = mpsc::channel(100);
146
147 let state = Arc::new(Mutex::new(TerminalState {
149 cwd: std::env::current_dir()?,
150 active_file: None,
151 recent_changes: Vec::new(),
152 suggestions: Vec::new(),
153 command_history: Vec::new(),
154 input: String::new(),
155 cursor_pos: 0,
156 project_type: None,
157 status_message: None,
158 }));
159
160 Ok(Self {
161 terminal,
162 state: state.clone(),
163 context_watcher: ContextWatcher::new(state.clone(), suggestion_tx.clone()),
164 pattern_analyzer: PatternAnalyzer::new(state.clone(), suggestion_tx.clone()),
165 suggestion_rx,
166 _suggestion_tx: suggestion_tx,
167 })
168 }
169
170 pub async fn run(&mut self) -> Result<()> {
172 self.context_watcher.start().await?;
174 self.pattern_analyzer.start().await?;
175
176 loop {
177 self.draw()?;
179
180 if event::poll(Duration::from_millis(100))? {
182 if let Event::Key(key) = event::read()? {
183 if self.handle_key(key).await? {
184 break;
185 }
186 }
187 }
188
189 while let Ok(suggestion) = self.suggestion_rx.try_recv() {
191 let mut state = self.state.lock().unwrap();
192 state.suggestions.push(suggestion);
193 if state.suggestions.len() > 5 {
195 state.suggestions.remove(0);
196 }
197 }
198 }
199
200 terminal::disable_raw_mode()?;
202 self.terminal.backend_mut().execute(LeaveAlternateScreen)?;
203
204 Ok(())
205 }
206
207 fn draw(&mut self) -> Result<()> {
209 let state = self.state.lock().unwrap().clone();
210
211 self.terminal.draw(|f| {
212 let chunks = Layout::default()
213 .direction(Direction::Vertical)
214 .constraints([
215 Constraint::Length(3), Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), Constraint::Length(1), ])
221 .split(f.size());
222
223 Self::draw_header(f, chunks[0], &state);
225
226 Self::draw_context(f, chunks[1], &state);
228
229 let main_chunks = Layout::default()
231 .direction(Direction::Horizontal)
232 .constraints([
233 Constraint::Percentage(60), Constraint::Percentage(40), ])
236 .split(chunks[2]);
237
238 Self::draw_history(f, main_chunks[0], &state);
239 Self::draw_suggestions(f, main_chunks[1], &state);
240
241 Self::draw_input(f, chunks[3], &state);
243
244 Self::draw_status(f, chunks[4], &state);
246 })?;
247
248 Ok(())
249 }
250
251 fn draw_header(f: &mut Frame, area: Rect, _state: &TerminalState) {
253 let show_banner =
254 std::env::var("ST_BANNER").is_ok_and(|v| v == "1" || v.to_lowercase() == "true");
255
256 let mut lines: Vec<Line> = Vec::new();
257 if show_banner {
258 lines.push(Line::from(vec![
259 Span::styled("⚡ ", Style::default().fg(Color::Yellow)),
260 Span::styled(
261 "SMART TREE TERMINAL",
262 Style::default()
263 .fg(Color::Green)
264 .add_modifier(Modifier::BOLD),
265 ),
266 Span::styled(" 🌊 ", Style::default().fg(Color::Cyan)),
267 Span::styled("rocking your repo", Style::default().fg(Color::Magenta)),
268 Span::raw(" 🎸"),
269 ]));
270 }
271
272 lines.push(Line::from(vec![
273 Span::styled(
274 "Smart Tree Terminal",
275 Style::default()
276 .fg(Color::Green)
277 .add_modifier(Modifier::BOLD),
278 ),
279 Span::raw(" v5.5 - "),
280 Span::styled("Your Coding Companion ", Style::default().fg(Color::Cyan)),
281 Span::raw("🌳"),
282 ]));
283
284 let header = Paragraph::new(Text::from(lines))
285 .block(Block::default().borders(Borders::ALL))
286 .alignment(ratatui::layout::Alignment::Center);
287
288 f.render_widget(header, area);
289 }
290
291 fn draw_context(f: &mut Frame, area: Rect, state: &TerminalState) {
293 let mut context_items = vec![Span::styled("Context: ", Style::default().fg(Color::Gray))];
294
295 if let Some(file) = &state.active_file {
296 context_items.push(Span::styled(
297 format!("Editing: {} ", file.display()),
298 Style::default().fg(Color::Yellow),
299 ));
300 }
301
302 if let Some(project) = &state.project_type {
303 context_items.push(Span::styled(
304 format!("| Project: {:?} ", project),
305 Style::default().fg(Color::Blue),
306 ));
307 }
308
309 if std::env::var("HOT_TUB").is_ok_and(|v| v == "1" || v.to_lowercase() == "true") {
310 context_items.push(Span::styled(
311 "| 🛁 Hot Tub Mode ",
312 Style::default()
313 .fg(Color::Magenta)
314 .add_modifier(Modifier::BOLD),
315 ));
316 }
317
318 let context = Paragraph::new(Line::from(context_items))
319 .block(Block::default().borders(Borders::LEFT | Borders::RIGHT));
320
321 f.render_widget(context, area);
322 }
323
324 fn draw_history(f: &mut Frame, area: Rect, state: &TerminalState) {
326 let history_items: Vec<ListItem> = state
327 .command_history
328 .iter()
329 .rev()
330 .take(area.height as usize - 2)
331 .map(|cmd| ListItem::new(cmd.as_str()))
332 .collect();
333
334 let history =
335 List::new(history_items).block(Block::default().title("History").borders(Borders::ALL));
336
337 f.render_widget(history, area);
338 }
339
340 fn draw_suggestions(f: &mut Frame, area: Rect, state: &TerminalState) {
342 let suggestion_items: Vec<ListItem> = state
343 .suggestions
344 .iter()
345 .map(|s| {
346 ListItem::new(vec![
347 Line::from(vec![
348 Span::raw(s.icon),
349 Span::raw(" "),
350 Span::styled(&s.title, Style::default().add_modifier(Modifier::BOLD)),
351 ]),
352 Line::from(Span::styled(
353 &s.description,
354 Style::default().fg(Color::Gray),
355 )),
356 ])
357 })
358 .collect();
359
360 let suggestions = List::new(suggestion_items).block(
361 Block::default()
362 .title("💡 Suggestions")
363 .borders(Borders::ALL),
364 );
365
366 f.render_widget(suggestions, area);
367 }
368
369 fn draw_input(f: &mut Frame, area: Rect, state: &TerminalState) {
371 let input = Paragraph::new(state.input.as_str()).block(
372 Block::default()
373 .title(format!("{}$ ", state.cwd.display()))
374 .borders(Borders::ALL),
375 );
376
377 f.render_widget(input, area);
378
379 f.set_cursor(area.x + state.cursor_pos as u16 + 1, area.y + 1);
381 }
382
383 fn draw_status(f: &mut Frame, area: Rect, state: &TerminalState) {
385 let status_text = if let Some(msg) = &state.status_message {
386 let color = match msg.severity {
387 MessageSeverity::Info => Color::Blue,
388 MessageSeverity::Success => Color::Green,
389 MessageSeverity::Warning => Color::Yellow,
390 MessageSeverity::Error => Color::Red,
391 };
392
393 Span::styled(&msg.text, Style::default().fg(color))
394 } else {
395 Span::raw("Ready")
396 };
397
398 let status = Paragraph::new(Line::from(vec![
399 status_text,
400 Span::raw(" | "),
401 Span::raw("Press Ctrl+C to exit"),
402 ]));
403
404 f.render_widget(status, area);
405 }
406
407 async fn handle_key(&mut self, key: KeyEvent) -> Result<bool> {
409 match key.code {
410 KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
411 return Ok(true); }
413 KeyCode::Char(c) => {
414 let _cursor_pos = {
415 let mut state = self.state.lock().unwrap();
416 let cursor_pos_local = state.cursor_pos;
417 state.input.insert(cursor_pos_local, c);
418 state.cursor_pos += 1;
419 cursor_pos_local
420 }; self.pattern_analyzer.analyze_input().await?;
424 }
425 KeyCode::Backspace => {
426 let mut state = self.state.lock().unwrap();
427 if state.cursor_pos > 0 {
428 let cursor_pos = state.cursor_pos;
429 state.input.remove(cursor_pos - 1);
430 state.cursor_pos -= 1;
431 }
432 }
433 KeyCode::Enter => {
434 let command = {
435 let mut state = self.state.lock().unwrap();
436 let command = state.input.clone();
437 state.command_history.push(command.clone());
438 state.input.clear();
439 state.cursor_pos = 0;
440 command
441 }; self.process_command(&command).await?;
445 }
446 KeyCode::Tab => {
447 let maybe_action = {
449 let state = self.state.lock().unwrap();
450 state.suggestions.first().map(|s| s.action.clone())
451 }; if let Some(action) = maybe_action {
453 self.apply_suggestion(action).await?;
454 }
455 }
456 _ => {}
457 }
458
459 Ok(false)
460 }
461
462 async fn process_command(&mut self, command: &str) -> Result<()> {
464 {
467 let mut state = self.state.lock().unwrap();
468 state.status_message = Some(StatusMessage {
469 text: format!("Executed: {}", command),
470 severity: MessageSeverity::Info,
471 timestamp: Instant::now(),
472 });
473 }
474
475 Ok(())
476 }
477
478 async fn apply_suggestion(&mut self, action: SuggestionAction) -> Result<()> {
480 match action {
481 SuggestionAction::InsertText(text) => {
482 let mut state = self.state.lock().unwrap();
483 let cursor_pos = state.cursor_pos;
484 state.input.insert_str(cursor_pos, &text);
485 state.cursor_pos += text.len();
486 }
487 SuggestionAction::RunCommand(cmd) => {
488 self.process_command(&cmd).await?;
489 }
490 _ => {
491 }
493 }
494
495 Ok(())
496 }
497}
498
499pub struct ContextWatcher {
501 state: Arc<Mutex<TerminalState>>,
502 _suggestion_tx: mpsc::Sender<Suggestion>,
503}
504
505impl ContextWatcher {
506 fn new(state: Arc<Mutex<TerminalState>>, _suggestion_tx: mpsc::Sender<Suggestion>) -> Self {
507 Self {
508 state,
509 _suggestion_tx,
510 }
511 }
512
513 async fn start(&self) -> Result<()> {
514 let cwd = {
517 let state = self.state.lock().unwrap();
518 state.cwd.clone()
519 };
520
521 if cwd.join("Cargo.toml").exists() {
522 {
523 let mut state = self.state.lock().unwrap();
524 state.project_type = Some(ProjectType::Rust);
525 }
526
527 let _ = self
529 ._suggestion_tx
530 .send(Suggestion {
531 icon: "🦀",
532 title: "Rust Project Detected".to_string(),
533 description: "Run 'cargo build' to compile".to_string(),
534 action: SuggestionAction::RunCommand("cargo build".to_string()),
535 confidence: 0.9,
536 })
537 .await;
538 }
539
540 Ok(())
541 }
542}
543
544pub struct PatternAnalyzer {
546 state: Arc<Mutex<TerminalState>>,
547 _suggestion_tx: mpsc::Sender<Suggestion>,
548}
549
550impl PatternAnalyzer {
551 fn new(state: Arc<Mutex<TerminalState>>, _suggestion_tx: mpsc::Sender<Suggestion>) -> Self {
552 Self {
553 state,
554 _suggestion_tx,
555 }
556 }
557
558 async fn start(&self) -> Result<()> {
559 Ok(())
561 }
562
563 async fn analyze_input(&self) -> Result<()> {
564 let input = {
565 let state = self.state.lock().unwrap();
566 state.input.clone()
567 };
568
569 if input.starts_with("git com") {
571 let _ = self
572 ._suggestion_tx
573 .send(Suggestion {
574 icon: "📝",
575 title: "Git Commit".to_string(),
576 description: "Commit recent changes".to_string(),
577 action: SuggestionAction::InsertText("mit -m \"".to_string()),
578 confidence: 0.8,
579 })
580 .await;
581 } else if input.contains("import") {
582 let _ = self
583 ._suggestion_tx
584 .send(Suggestion {
585 icon: "📦",
586 title: "Import Suggestion".to_string(),
587 description: "Add commonly used imports".to_string(),
588 action: SuggestionAction::InsertText(" { useState } from 'react'".to_string()),
589 confidence: 0.7,
590 })
591 .await;
592 }
593
594 Ok(())
595 }
596}
597
598