changed the cli to a tui environment.

This commit is contained in:
2026-05-20 13:33:53 -05:00
parent 3eeecb0010
commit a907de8ff3
6 changed files with 1376 additions and 217 deletions
+204 -56
View File
@@ -1,7 +1,20 @@
use crate::*;
use crossterm::{
cursor,
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
},
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::io::Write;
use std::io::{Stdout, Write};
use std::thread;
use std::time::Duration;
pub fn get_user_input(prompt: &str) -> Result<String, Box<dyn Error>> {
println!("{}", prompt);
@@ -10,76 +23,211 @@ pub fn get_user_input(prompt: &str) -> Result<String, Box<dyn Error>> {
return Ok(response.trim().to_string());
}
pub fn cli(mut state: AppState, main_rx: Receiver<ToolMessage>) {
println!("Starting tetanus CLI...");
let (input_tx, input_rx) = channel::<String>();
thread::spawn(move || {
let stdin = io::stdin();
let mut input_buffer = String::new();
pub fn run_tui(
mut state: AppState,
main_rx: Receiver<ToolMessage>,
) -> Result<(), Box<dyn std::error::Error>> {
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::<AppEvent>();
let input_tx = event_tx.clone();
std::thread::spawn(move || {
loop {
input_buffer.clear();
if stdin.read_line(&mut input_buffer).is_ok() {
let trimmed = input_buffer.trim().to_string();
if !trimmed.is_empty() {
if input_tx.send(trimmed).is_err() {
break;
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;
}
}
_ => {}
}
}
}
});
print!("tetanus> ");
let _ = io::stdout().flush();
loop {
while let Ok(msg) = main_rx.try_recv() {
match msg {
ToolMessage::Output(txt) => {
println!("\n[result] {}", txt);
state.log.push(txt.clone());
state.output.push(txt.clone());
}
_ => {}
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;
}
}
while let Ok(user_input) = input_rx.try_recv() {
state.curent_intput = user_input.clone();
state.history.push(user_input.clone());
match user_input.as_str() {
"exit" | "quit" => {
println!("shutting down...");
return;
});
let mut project_list_state = ListState::default();
if !state.projects.is_empty() {
project_list_state.select(Some(0));
}
loop {
terminal.draw(|f| {
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Max(3)])
.split(f.size());
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(main_chunks[0]);
let projects: Vec<ListItem> = 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 "))
.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<Line> = 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;
}
"reload-modules" => {
println!("reloadling modules...");
state.initialize_modules();
}
"help" => {
println!("\nAvailable Built-In & Custom Script Modules:");
for (name, cmd) in &state.module_loader.commands {
println!(" - {:<15} (Outputs: {})", name, cmd.output_type);
if !cmd.help.is_empty() {
println!(" Help: {}", cmd.help.trim());
}
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(" Execute Command (Press Enter) "),
);
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 <org> <name>".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();
}
}
println!();
}
// Everything else gets dynamically routed to your Rhai engine execution matrix!
command_name => {
if state.module_loader.commands.contains_key(command_name) {
println!(
"[Worker] Dispatching script '{}' to Rayon engine layer...",
command_name
);
if let Err(e) = state.execute_command(command_name, None) {
eprintln!("[Error] Failed running script: {}", e);
KeyCode::Char(c) => {
state.curent_intput.push(c);
}
KeyCode::Backspace => {
state.curent_intput.pop();
}
KeyCode::Up => {
if let Some(selected) = project_list_state.selected() {
if selected > 0 {
project_list_state.select(Some(selected - 1));
}
}
} else {
println!("[Error] Command '{}' unrecognized.", command_name);
}
KeyCode::Down => {
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);
}
}
}
print!("tetanus> ");
let _ = io::stdout().flush();
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
cursor::Show,
DisableMouseCapture
)?;
Ok(())
}