Files
tetanus/src/funcs.rs
T

260 lines
11 KiB
Rust

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<String, Box<dyn Error>> {
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<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 {
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<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 (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<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;
}
}
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 <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();
}
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(())
}