2c78f26dd2
project.
260 lines
11 KiB
Rust
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(())
|
|
}
|