use crate::*; use crossterm::{ cursor, event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::{ prelude::*, widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, }; use std::error::Error; use std::time::Duration; pub fn get_user_input(prompt: &str) -> Result> { println!("{}", prompt); let mut response = String::new(); std::io::stdin().read_line(&mut response)?; return Ok(response.trim().to_string()); } pub fn run_tui( mut state: AppState, main_rx: Receiver, ) -> Result<(), Box> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!( stdout, EnterAlternateScreen, cursor::Hide, EnableMouseCapture )?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let (event_tx, event_rx) = channel::(); let input_tx = event_tx.clone(); std::thread::spawn(move || { loop { if event::poll(Duration::from_millis(100)).unwrap_or(false) { match event::read() { Ok(Event::Key(key)) => { if key.kind == KeyEventKind::Press { if input_tx.send(AppEvent::Key(key)).is_err() { break; } } } Ok(Event::Mouse(mouse)) => { if input_tx.send(AppEvent::Mouse(mouse)).is_err() { break; } } _ => {} } } } }); let worker_tx = event_tx.clone(); std::thread::spawn(move || { while let Ok(msg) = main_rx.recv() { if worker_tx.send(AppEvent::Worker(msg)).is_err() { break; } } }); let mut project_list_state = ListState::default(); if !state.projects.is_empty() { project_list_state.select(Some(0)); } let mut history_index = state.history.len(); loop { terminal.draw(|f| { let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0), Constraint::Max(3)]) .split(f.area()); let top_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) .split(main_chunks[0]); let projects: Vec = state .projects .iter() .map(|p| ListItem::new(format!(" {} | {}", p.org_name, p.name))) .collect(); let projects_list = List::new(projects) .block( Block::default() .borders(Borders::ALL) .title(" Projects (ctrl + arrow keys to select) "), ) .highlight_style( Style::default() .bg(Color::Blue) .fg(Color::White) .add_modifier(Modifier::BOLD), ) .highlight_symbol(">> "); f.render_stateful_widget(projects_list, top_chunks[0], &mut project_list_state); let output_lines: Vec = state .output .iter() .map(|text| Line::from(Span::raw(text))) .collect(); let text_area_height = top_chunks[1].height.saturating_sub(2) as usize; if state.output_scroll == u16::MAX { if state.output.len() > text_area_height { state.output_scroll = (state.output.len() - text_area_height) as u16; } else { state.output_scroll = 0; } } let output_paragraph = Paragraph::new(output_lines) .block( Block::default() .borders(Borders::ALL) .title(" Script Engine Output "), ) .scroll((state.output_scroll, 0)) .wrap(ratatui::widgets::Wrap { trim: false }); f.render_widget(output_paragraph, top_chunks[1]); let input_paragraph = Paragraph::new(state.curent_intput.as_str()).block( Block::default() .borders(Borders::ALL) .title(" What is thy bidding, my master? "), ); f.render_widget(input_paragraph, main_chunks[1]); })?; if let Ok(event) = event_rx.recv() { match event { AppEvent::Worker(msg) => match msg { ToolMessage::Output(txt) => { state.log.push(txt.clone()); state.output.push(txt); state.output_scroll = u16::MAX; } _ => {} }, AppEvent::Key(key) => match key.code { KeyCode::Esc => break, KeyCode::Enter => { let trimmed = state.curent_intput.trim().to_string(); if !trimmed.is_empty() { state.history.push(trimmed.clone()); state.output.push("\n".to_string()); state.output.push(format!("[user input] > {}", trimmed)); state.output.push("\n".to_string()); let (command, args) = trimmed.split_once(' ').unwrap_or((&trimmed, "")); match command { "exit" | "quit" => break, "reload-modules" => { state.initialize_modules(); state.output.push("Reloading module paths...".into()); } "help" => { state.output.push("Available Modules:".into()); for name in state.module_loader.commands.keys() { state.output.push(format!(" - {}", name)); } } "new_project" | "np" => { if args.split_once(' ').is_some() { let _ = state.execute_command(command, Some(args.to_string())); } else { state.output.push("Error: USAGE -> np ".into()); } } command_name => { if state.module_loader.commands.contains_key(command_name) { state.output.push(format!( "[Worker] Executing script '{}'...", command_name )); if let Err(e) = state.execute_command(command_name, None) { state .output .push(format!("[Error] Pipeline fail: {}", e)); } } else { state.output.push(format!( "[Error] Command '{}' unknown.", command_name )); } } } state.curent_intput.clear(); } history_index = state.history.len(); } KeyCode::Char(c) => { state.curent_intput.push(c); } KeyCode::Backspace => { state.curent_intput.pop(); } KeyCode::Up if key.modifiers.is_empty() => { if !state.history.is_empty() && history_index > 0 { history_index -= 1; state.curent_intput = state.history[history_index].clone(); } } KeyCode::Down if key.modifiers.is_empty() => { if history_index < state.history.len() { history_index += 1; if history_index == state.history.len() { state.curent_intput.clear(); // Clear back to a fresh prompt } else { state.curent_intput = state.history[history_index].clone(); } } } KeyCode::Up if key .modifiers .contains(crossterm::event::KeyModifiers::CONTROL) => { if let Some(selected) = project_list_state.selected() { if selected > 0 { project_list_state.select(Some(selected - 1)); } } } KeyCode::Down if key .modifiers .contains(crossterm::event::KeyModifiers::CONTROL) => { if let Some(selected) = project_list_state.selected() { if selected + 1 < state.projects.len() { project_list_state.select(Some(selected + 1)); } } } _ => {} }, AppEvent::Mouse(mouse) => { if mouse.kind == crossterm::event::MouseEventKind::ScrollUp { state.output_scroll = state.output_scroll.saturating_sub(1); } else if mouse.kind == crossterm::event::MouseEventKind::ScrollDown { state.output_scroll = state.output_scroll.saturating_add(1); } } } } } disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, cursor::Show, DisableMouseCapture )?; Ok(()) }