changed the cli to a tui environment.
This commit is contained in:
Generated
+993
-35
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,9 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.6.1", features = ["derive"] }
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
crossterm = "0.29.0"
|
||||||
iced = { version = "0.14.0", features = ["advanced", "tokio"] }
|
iced = { version = "0.14.0", features = ["advanced", "tokio"] }
|
||||||
|
ratatui = "0.30.0"
|
||||||
rayon = "1.12.0"
|
rayon = "1.12.0"
|
||||||
rhai = { version = "1.24.0", features = ["metadata", "sync"] }
|
rhai = { version = "1.24.0", features = ["metadata", "sync"] }
|
||||||
rustc-hash = "2.1.2"
|
rustc-hash = "2.1.2"
|
||||||
|
|||||||
+192
-44
@@ -1,7 +1,20 @@
|
|||||||
use crate::*;
|
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::error::Error;
|
||||||
use std::io::Write;
|
use std::io::{Stdout, Write};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub fn get_user_input(prompt: &str) -> Result<String, Box<dyn Error>> {
|
pub fn get_user_input(prompt: &str) -> Result<String, Box<dyn Error>> {
|
||||||
println!("{}", prompt);
|
println!("{}", prompt);
|
||||||
@@ -10,76 +23,211 @@ pub fn get_user_input(prompt: &str) -> Result<String, Box<dyn Error>> {
|
|||||||
return Ok(response.trim().to_string());
|
return Ok(response.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli(mut state: AppState, main_rx: Receiver<ToolMessage>) {
|
pub fn run_tui(
|
||||||
println!("Starting tetanus CLI...");
|
mut state: AppState,
|
||||||
let (input_tx, input_rx) = channel::<String>();
|
main_rx: Receiver<ToolMessage>,
|
||||||
thread::spawn(move || {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let stdin = io::stdin();
|
enable_raw_mode()?;
|
||||||
let mut input_buffer = String::new();
|
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 {
|
loop {
|
||||||
input_buffer.clear();
|
if event::poll(Duration::from_millis(100)).unwrap_or(false) {
|
||||||
if stdin.read_line(&mut input_buffer).is_ok() {
|
match event::read() {
|
||||||
let trimmed = input_buffer.trim().to_string();
|
Ok(Event::Key(key)) => {
|
||||||
if !trimmed.is_empty() {
|
if key.kind == KeyEventKind::Press {
|
||||||
if input_tx.send(trimmed).is_err() {
|
if input_tx.send(AppEvent::Key(key)).is_err() {
|
||||||
break;
|
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());
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 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));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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" => {
|
"reload-modules" => {
|
||||||
println!("reloadling modules...");
|
|
||||||
state.initialize_modules();
|
state.initialize_modules();
|
||||||
|
state.output.push("Reloading module paths...".into());
|
||||||
}
|
}
|
||||||
"help" => {
|
"help" => {
|
||||||
println!("\nAvailable Built-In & Custom Script Modules:");
|
state.output.push("Available Modules:".into());
|
||||||
for (name, cmd) in &state.module_loader.commands {
|
for name in state.module_loader.commands.keys() {
|
||||||
println!(" - {:<15} (Outputs: {})", name, cmd.output_type);
|
state.output.push(format!(" - {}", name));
|
||||||
if !cmd.help.is_empty() {
|
|
||||||
println!(" Help: {}", cmd.help.trim());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
"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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Everything else gets dynamically routed to your Rhai engine execution matrix!
|
|
||||||
command_name => {
|
command_name => {
|
||||||
if state.module_loader.commands.contains_key(command_name) {
|
if state.module_loader.commands.contains_key(command_name) {
|
||||||
println!(
|
state.output.push(format!(
|
||||||
"[Worker] Dispatching script '{}' to Rayon engine layer...",
|
"[Worker] Executing script '{}'...",
|
||||||
command_name
|
command_name
|
||||||
);
|
));
|
||||||
if let Err(e) = state.execute_command(command_name, None) {
|
if let Err(e) = state.execute_command(command_name, None) {
|
||||||
eprintln!("[Error] Failed running script: {}", e);
|
state
|
||||||
|
.output
|
||||||
|
.push(format!("[Error] Pipeline fail: {}", e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("[Error] Command '{}' unrecognized.", command_name);
|
state.output.push(format!(
|
||||||
|
"[Error] Command '{}' unknown.",
|
||||||
|
command_name
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print!("tetanus> ");
|
state.curent_intput.clear();
|
||||||
let _ = io::stdout().flush();
|
}
|
||||||
|
}
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
cursor::Show,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub fn install() -> Result<(), Box<dyn Error>> {
|
|||||||
let upcoming_notes_path = PathBuf::from(get_user_input(
|
let upcoming_notes_path = PathBuf::from(get_user_input(
|
||||||
"enter the path to store upcoming project notes, or none if you want to be prompted everytime",
|
"enter the path to store upcoming project notes, or none if you want to be prompted everytime",
|
||||||
)?);
|
)?);
|
||||||
|
let template_box = get_user_input(
|
||||||
|
"enter the name of your template distrobox, NONE in all caps if you are not using distrobox",
|
||||||
|
)?;
|
||||||
config_path.push("client.conf");
|
config_path.push("client.conf");
|
||||||
projects_path.push("default");
|
projects_path.push("default");
|
||||||
create_dir_all(&projects_path)?;
|
create_dir_all(&projects_path)?;
|
||||||
@@ -60,6 +63,7 @@ pub fn install() -> Result<(), Box<dyn Error>> {
|
|||||||
client_config_file
|
client_config_file
|
||||||
.write(format!("upcoming_notes: {}\n", upcoming_notes_path.display()).as_bytes())?;
|
.write(format!("upcoming_notes: {}\n", upcoming_notes_path.display()).as_bytes())?;
|
||||||
client_config_file.write(format!("module_path: {}\n", module_path.display()).as_bytes())?;
|
client_config_file.write(format!("module_path: {}\n", module_path.display()).as_bytes())?;
|
||||||
|
client_config_file.write(format!("template_box: {}\n", template_box).as_bytes())?;
|
||||||
} else {
|
} else {
|
||||||
eprintln!("error finding home directory!");
|
eprintln!("error finding home directory!");
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|||||||
+94
-47
@@ -1,8 +1,9 @@
|
|||||||
|
use ratatui::crossterm::event;
|
||||||
use rhai::{AST, Dynamic, Engine, Scope};
|
use rhai::{AST, Dynamic, Engine, Scope};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::{read_dir, read_to_string};
|
use std::fs::{File, create_dir_all, read_dir, read_to_string};
|
||||||
use std::io;
|
use std::io::{self, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
use std::sync::{Arc, mpsc::Sender, mpsc::channel};
|
use std::sync::{Arc, mpsc::Sender, mpsc::channel};
|
||||||
@@ -24,6 +25,7 @@ pub struct AppState {
|
|||||||
pub selected_project: Option<Project>,
|
pub selected_project: Option<Project>,
|
||||||
pub curent_intput: String,
|
pub curent_intput: String,
|
||||||
pub module_loader: ModuleLoader,
|
pub module_loader: ModuleLoader,
|
||||||
|
pub output_scroll: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -48,6 +50,7 @@ impl AppState {
|
|||||||
selected_project: None,
|
selected_project: None,
|
||||||
curent_intput: String::new(),
|
curent_intput: String::new(),
|
||||||
module_loader: ModuleLoader::new(),
|
module_loader: ModuleLoader::new(),
|
||||||
|
output_scroll: 0,
|
||||||
},
|
},
|
||||||
main_rx,
|
main_rx,
|
||||||
)
|
)
|
||||||
@@ -114,14 +117,29 @@ impl AppState {
|
|||||||
eprintln!("Warning: 'module_path' is missing from AppState config map!");
|
eprintln!("Warning: 'module_path' is missing from AppState config map!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatches a command execution payload to the Rayon thread pool
|
|
||||||
pub fn execute_command(
|
pub fn execute_command(
|
||||||
&self,
|
&mut self,
|
||||||
command_name: &str,
|
command_name: &str,
|
||||||
raw_input_string: Option<String>,
|
command_args: Option<String>,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
// 1. Retrieve the compiled AST and Engine reference
|
let tx = self.main_tx.clone();
|
||||||
|
match command_name {
|
||||||
|
"new_project" | "np" => {
|
||||||
|
let args = command_args.unwrap();
|
||||||
|
let (org, name) = args.split_once(" ").unwrap();
|
||||||
|
if let Err(e) = self.new_project(org.to_string(), name.to_string()) {
|
||||||
|
tx.send(ToolMessage::Output(format!(
|
||||||
|
"Error making {}-{}: {e}",
|
||||||
|
org, name
|
||||||
|
)))?;
|
||||||
|
} else {
|
||||||
|
tx.send(ToolMessage::Output(format!(
|
||||||
|
"{}-{} created successfully!",
|
||||||
|
org, name
|
||||||
|
)))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
let ast = self
|
let ast = self
|
||||||
.module_loader
|
.module_loader
|
||||||
.asts
|
.asts
|
||||||
@@ -136,11 +154,7 @@ impl AppState {
|
|||||||
.get(command_name)
|
.get(command_name)
|
||||||
.ok_or_else(|| format!("Metadata not found for command: {}", command_name))?
|
.ok_or_else(|| format!("Metadata not found for command: {}", command_name))?
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
// 2. Build out the script Scope using values cloned from AppState
|
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
|
|
||||||
// Dynamically add requested args into script context based on config.conf parsing rules
|
|
||||||
for arg_requirement in &cmd_meta.args {
|
for arg_requirement in &cmd_meta.args {
|
||||||
match arg_requirement.as_str() {
|
match arg_requirement.as_str() {
|
||||||
"project" => {
|
"project" => {
|
||||||
@@ -156,7 +170,8 @@ impl AppState {
|
|||||||
scope.push("projects", arr);
|
scope.push("projects", arr);
|
||||||
}
|
}
|
||||||
"host" => {
|
"host" => {
|
||||||
if let Some(host) = self.selected_project.as_ref().and_then(|p| p.hosts.first())
|
if let Some(host) =
|
||||||
|
self.selected_project.as_ref().and_then(|p| p.hosts.first())
|
||||||
{
|
{
|
||||||
scope.push("host", host.clone());
|
scope.push("host", host.clone());
|
||||||
}
|
}
|
||||||
@@ -167,7 +182,6 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"config" => {
|
"config" => {
|
||||||
// Turn Hashmap into rhai::Map
|
|
||||||
let mut map = rhai::Map::new();
|
let mut map = rhai::Map::new();
|
||||||
for (k, v) in &self.config {
|
for (k, v) in &self.config {
|
||||||
map.insert(k.clone().into(), v.clone().into());
|
map.insert(k.clone().into(), v.clone().into());
|
||||||
@@ -175,8 +189,7 @@ impl AppState {
|
|||||||
scope.push("config", map);
|
scope.push("config", map);
|
||||||
}
|
}
|
||||||
_ if arg_requirement.starts_with("string") => {
|
_ if arg_requirement.starts_with("string") => {
|
||||||
// If it asks for an input string, supply what the user provided or the current interactive line input
|
let input_val = command_args
|
||||||
let input_val = raw_input_string
|
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| self.curent_intput.clone());
|
.unwrap_or_else(|| self.curent_intput.clone());
|
||||||
scope.push("input_string", input_val);
|
scope.push("input_string", input_val);
|
||||||
@@ -184,21 +197,16 @@ impl AppState {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Clone our main communication MPSC tx channel to ship results back out of the Rayon worker threads
|
|
||||||
let tx = self.main_tx.clone();
|
|
||||||
|
|
||||||
// 4. Delegate to Rayon
|
|
||||||
self.workers.spawn(move || {
|
self.workers.spawn(move || {
|
||||||
// This runs in a background thread inside Rayon!
|
|
||||||
match engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast) {
|
match engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast) {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
// Handle output based on the output type requested
|
|
||||||
let result_str = if result.is_array() {
|
let result_str = if result.is_array() {
|
||||||
if let Ok(arr) = result.into_array() {
|
if let Ok(arr) = result.into_array() {
|
||||||
let lines: Vec<String> = arr
|
let lines: Vec<String> = arr
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| item.into_string().unwrap_or_else(|_| "".into()))
|
.map(|item| {
|
||||||
|
item.into_string().unwrap_or_else(|_| "".into())
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -211,8 +219,14 @@ impl AppState {
|
|||||||
} else {
|
} else {
|
||||||
format!("{:?}", result)
|
format!("{:?}", result)
|
||||||
};
|
};
|
||||||
|
if result_str.contains("\n") {
|
||||||
|
result_str.lines().into_iter().for_each(|line| {
|
||||||
|
let _ = tx.send(ToolMessage::Output(line.to_string()));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
let _ = tx.send(ToolMessage::Output(result_str));
|
let _ = tx.send(ToolMessage::Output(result_str));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let _ = tx.send(ToolMessage::Output(format!(
|
let _ = tx.send(ToolMessage::Output(format!(
|
||||||
"Script Error in execution: {}",
|
"Script Error in execution: {}",
|
||||||
@@ -221,11 +235,33 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_project(&mut self) {}
|
pub fn new_project(&mut self, org: String, name: String) -> Result<(), Box<dyn Error>> {
|
||||||
|
let template_box = self.config.get("template_box").unwrap();
|
||||||
|
let project_files = PathBuf::from(self.config.get("upcoming_files").unwrap())
|
||||||
|
.join(format!("{}/{}", org, name));
|
||||||
|
let project_notes = PathBuf::from(self.config.get("upcoming_notes").unwrap())
|
||||||
|
.join(format!("{}/{}", org, name));
|
||||||
|
create_dir_all(&project_files)?;
|
||||||
|
create_dir_all(&project_notes)?;
|
||||||
|
let mut new_project = Project::new();
|
||||||
|
let mut project_conf_folder = self.config_file.clone();
|
||||||
|
project_conf_folder.pop();
|
||||||
|
project_conf_folder.push(format!("projects/{}-{}", org, name));
|
||||||
|
new_project.config_folder(project_conf_folder);
|
||||||
|
new_project.box_name(format!("{}-{}-{}", template_box, org, name));
|
||||||
|
new_project.name = name;
|
||||||
|
new_project.org_name(org);
|
||||||
|
new_project.files(project_files);
|
||||||
|
new_project.notes(project_notes);
|
||||||
|
new_project.save_config()?;
|
||||||
|
self.projects.push(new_project);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -243,6 +279,7 @@ pub struct Project {
|
|||||||
pub files: PathBuf,
|
pub files: PathBuf,
|
||||||
pub hosts: Vec<Host>,
|
pub hosts: Vec<Host>,
|
||||||
pub current: bool,
|
pub current: bool,
|
||||||
|
pub boxname: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
@@ -255,6 +292,7 @@ impl Project {
|
|||||||
files: PathBuf::new(),
|
files: PathBuf::new(),
|
||||||
hosts: Vec::new(),
|
hosts: Vec::new(),
|
||||||
current: false,
|
current: false,
|
||||||
|
boxname: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +320,10 @@ impl Project {
|
|||||||
self.config_folder = path;
|
self.config_folder = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn box_name(&mut self, boxname: String) {
|
||||||
|
self.boxname = boxname;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_config(&mut self) -> Result<(), Box<dyn Error>> {
|
pub fn load_config(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
let config_contents = read_to_string(&self.config_folder)?;
|
let config_contents = read_to_string(&self.config_folder)?;
|
||||||
for line in config_contents.lines() {
|
for line in config_contents.lines() {
|
||||||
@@ -304,6 +346,27 @@ impl Project {
|
|||||||
println!("{} | {} loaded!", self.org_name, self.name);
|
println!("{} | {} loaded!", self.org_name, self.name);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_config(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
|
create_dir_all(&self.config_folder)?;
|
||||||
|
let mut conf_file = self.config_folder.clone();
|
||||||
|
conf_file.push("project.conf");
|
||||||
|
let mut file = File::create(conf_file)?;
|
||||||
|
let mut out_string = format!(
|
||||||
|
"org_name: {}\nname: {}\nnotes: {}\nfiles: {}\n",
|
||||||
|
self.org_name,
|
||||||
|
self.name,
|
||||||
|
self.notes.display(),
|
||||||
|
self.files.display()
|
||||||
|
);
|
||||||
|
if self.current {
|
||||||
|
out_string.push_str("stage: current");
|
||||||
|
} else {
|
||||||
|
out_string.push_str("stage: upcoming");
|
||||||
|
}
|
||||||
|
file.write_all(out_string.as_bytes())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Project {
|
impl std::fmt::Display for Project {
|
||||||
@@ -440,14 +503,12 @@ pub struct ToolCommand {
|
|||||||
pub struct ModuleLoader {
|
pub struct ModuleLoader {
|
||||||
pub engine: Arc<Engine>,
|
pub engine: Arc<Engine>,
|
||||||
pub commands: HashMap<String, ToolCommand>,
|
pub commands: HashMap<String, ToolCommand>,
|
||||||
// Stored separately because AST doesn't implement Debug/Clone easily
|
|
||||||
pub asts: HashMap<String, AST>,
|
pub asts: HashMap<String, AST>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModuleLoader {
|
impl ModuleLoader {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
// 1. Register PathBuf
|
|
||||||
engine
|
engine
|
||||||
.register_type_with_name::<PathBuf>("PathBuf")
|
.register_type_with_name::<PathBuf>("PathBuf")
|
||||||
.register_get_set(
|
.register_get_set(
|
||||||
@@ -455,8 +516,6 @@ impl ModuleLoader {
|
|||||||
|p: &mut PathBuf| p.to_string_lossy().into_owned(),
|
|p: &mut PathBuf| p.to_string_lossy().into_owned(),
|
||||||
|p: &mut PathBuf, s: String| *p = PathBuf::from(s),
|
|p: &mut PathBuf, s: String| *p = PathBuf::from(s),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Register Host
|
|
||||||
engine
|
engine
|
||||||
.register_type_with_name::<Host>("Host")
|
.register_type_with_name::<Host>("Host")
|
||||||
.register_get_set(
|
.register_get_set(
|
||||||
@@ -476,8 +535,6 @@ impl ModuleLoader {
|
|||||||
)
|
)
|
||||||
.register_fn("add_port", Host::add_port)
|
.register_fn("add_port", Host::add_port)
|
||||||
.register_fn("add_finding", Host::add_finding);
|
.register_fn("add_finding", Host::add_finding);
|
||||||
|
|
||||||
// 3. Register Project
|
|
||||||
engine
|
engine
|
||||||
.register_type_with_name::<Project>("Project")
|
.register_type_with_name::<Project>("Project")
|
||||||
.register_get_set(
|
.register_get_set(
|
||||||
@@ -496,10 +553,6 @@ impl ModuleLoader {
|
|||||||
|p: &mut Project, val: bool| p.current = val,
|
|p: &mut Project, val: bool| p.current = val,
|
||||||
)
|
)
|
||||||
.register_fn("add_host", Project::add_host);
|
.register_fn("add_host", Project::add_host);
|
||||||
|
|
||||||
// 4. Register custom Vector indexers / custom array conversion helpers
|
|
||||||
// Rhai works naturally with its own rhai::Array (Vec<Dynamic>).
|
|
||||||
// To bridge Vec<Host> to Rhai scripts seamlessly, we can expose specialized utility methods.
|
|
||||||
engine.register_fn("get_host", |hosts: &mut Vec<Host>, index: i64| {
|
engine.register_fn("get_host", |hosts: &mut Vec<Host>, index: i64| {
|
||||||
hosts.get(index as usize).cloned().unwrap_or_else(Host::new)
|
hosts.get(index as usize).cloned().unwrap_or_else(Host::new)
|
||||||
});
|
});
|
||||||
@@ -511,7 +564,6 @@ impl ModuleLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_all(&mut self, base_path: &PathBuf) -> Result<(), std::io::Error> {
|
pub fn load_all(&mut self, base_path: &PathBuf) -> Result<(), std::io::Error> {
|
||||||
// Clear out existing maps in case this is called during a hot-reload
|
|
||||||
self.commands.clear();
|
self.commands.clear();
|
||||||
self.asts.clear();
|
self.asts.clear();
|
||||||
|
|
||||||
@@ -532,7 +584,6 @@ impl ModuleLoader {
|
|||||||
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
if let Some((command, ast)) = self.parse_module_directory(&path) {
|
if let Some((command, ast)) = self.parse_module_directory(&path) {
|
||||||
// Cache both side-by-side using the parsed command name as the key
|
|
||||||
self.asts.insert(command.name.clone(), ast);
|
self.asts.insert(command.name.clone(), ast);
|
||||||
self.commands.insert(command.name.clone(), command);
|
self.commands.insert(command.name.clone(), command);
|
||||||
}
|
}
|
||||||
@@ -549,13 +600,10 @@ impl ModuleLoader {
|
|||||||
if !config_path.exists() || !help_path.exists() || !script_path.exists() {
|
if !config_path.exists() || !help_path.exists() || !script_path.exists() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Parse the config.conf file line by line
|
|
||||||
let config_content = read_to_string(&config_path).ok()?;
|
let config_content = read_to_string(&config_path).ok()?;
|
||||||
let mut name = String::new();
|
let mut name = String::new();
|
||||||
let mut output_type = String::new();
|
let mut output_type = String::new();
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
|
|
||||||
for line in config_content.lines() {
|
for line in config_content.lines() {
|
||||||
if let Some((key, val)) = line.split_once(':') {
|
if let Some((key, val)) = line.split_once(':') {
|
||||||
match key.trim() {
|
match key.trim() {
|
||||||
@@ -572,15 +620,10 @@ impl ModuleLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Read the help text
|
|
||||||
let help = read_to_string(&help_path).unwrap_or_default();
|
let help = read_to_string(&help_path).unwrap_or_default();
|
||||||
|
|
||||||
// 3. Compile the Rhai script into an AST
|
|
||||||
let ast = match self.engine.compile_file(script_path.clone()) {
|
let ast = match self.engine.compile_file(script_path.clone()) {
|
||||||
Ok(compiled_ast) => compiled_ast,
|
Ok(compiled_ast) => compiled_ast,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -588,8 +631,6 @@ impl ModuleLoader {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Assemble your exact ToolCommand struct
|
|
||||||
let command = ToolCommand {
|
let command = ToolCommand {
|
||||||
name,
|
name,
|
||||||
path: script_path,
|
path: script_path,
|
||||||
@@ -603,3 +644,9 @@ impl ModuleLoader {
|
|||||||
Some((command, ast))
|
Some((command, ast))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AppEvent {
|
||||||
|
Key(event::KeyEvent),
|
||||||
|
Worker(ToolMessage),
|
||||||
|
Mouse(event::MouseEvent),
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::{env, path::PathBuf, process::exit};
|
use std::{env, path::PathBuf, process::exit};
|
||||||
use tetanus::AppState;
|
use tetanus::AppState;
|
||||||
use tetanus::funcs::cli;
|
use tetanus::funcs::*;
|
||||||
|
|
||||||
mod install;
|
mod install;
|
||||||
|
|
||||||
@@ -59,6 +59,6 @@ fn main() {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
appstate.initialize_modules();
|
appstate.initialize_modules();
|
||||||
cli(appstate, rx);
|
run_tui(appstate, rx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user