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
Generated
+993 -35
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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"
+204 -56
View File
@@ -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 worker_tx = event_tx.clone();
let _ = io::stdout().flush(); std::thread::spawn(move || {
loop { while let Ok(msg) = main_rx.recv() {
while let Ok(msg) = main_rx.try_recv() { if worker_tx.send(AppEvent::Worker(msg)).is_err() {
match msg { break;
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(); let mut project_list_state = ListState::default();
state.history.push(user_input.clone()); if !state.projects.is_empty() {
match user_input.as_str() { project_list_state.select(Some(0));
"exit" | "quit" => { }
println!("shutting down..."); loop {
return; 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..."); let output_paragraph = Paragraph::new(output_lines)
state.initialize_modules(); .block(
} Block::default()
"help" => { .borders(Borders::ALL)
println!("\nAvailable Built-In & Custom Script Modules:"); .title(" Script Engine Output "),
for (name, cmd) in &state.module_loader.commands { )
println!(" - {:<15} (Outputs: {})", name, cmd.output_type); .scroll((state.output_scroll, 0))
if !cmd.help.is_empty() { .wrap(ratatui::widgets::Wrap { trim: false });
println!(" Help: {}", cmd.help.trim());
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!(); KeyCode::Char(c) => {
} state.curent_intput.push(c);
// Everything else gets dynamically routed to your Rhai engine execution matrix! }
command_name => { KeyCode::Backspace => {
if state.module_loader.commands.contains_key(command_name) { state.curent_intput.pop();
println!( }
"[Worker] Dispatching script '{}' to Rayon engine layer...", KeyCode::Up => {
command_name if let Some(selected) = project_list_state.selected() {
); if selected > 0 {
if let Err(e) = state.execute_command(command_name, None) { project_list_state.select(Some(selected - 1));
eprintln!("[Error] Failed running script: {}", e); }
} }
} 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(())
} }
+4
View File
@@ -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);
+171 -124
View File
@@ -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,118 +117,151 @@ 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();
let ast = self match command_name {
.module_loader "new_project" | "np" => {
.asts let args = command_args.unwrap();
.get(command_name) let (org, name) = args.split_once(" ").unwrap();
.ok_or_else(|| format!("AST not found for command: {}", command_name))? if let Err(e) = self.new_project(org.to_string(), name.to_string()) {
.clone(); tx.send(ToolMessage::Output(format!(
"Error making {}-{}: {e}",
org, name
)))?;
} else {
tx.send(ToolMessage::Output(format!(
"{}-{} created successfully!",
org, name
)))?;
}
}
_ => {
let ast = self
.module_loader
.asts
.get(command_name)
.ok_or_else(|| format!("AST not found for command: {}", command_name))?
.clone();
let engine = Arc::clone(&self.module_loader.engine); let engine = Arc::clone(&self.module_loader.engine);
let cmd_meta = self let cmd_meta = self
.module_loader .module_loader
.commands .commands
.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();
let mut scope = Scope::new();
// 2. Build out the script Scope using values cloned from AppState for arg_requirement in &cmd_meta.args {
let mut scope = Scope::new(); match arg_requirement.as_str() {
"project" => {
// Dynamically add requested args into script context based on config.conf parsing rules if let Some(ref proj) = self.selected_project {
for arg_requirement in &cmd_meta.args { scope.push("project", proj.clone());
match arg_requirement.as_str() { }
"project" => { }
if let Some(ref proj) = self.selected_project { "projects" => {
scope.push("project", proj.clone()); let mut arr = rhai::Array::new();
for proj in &self.projects {
arr.push(rhai::Dynamic::from(proj.clone()));
}
scope.push("projects", arr);
}
"host" => {
if let Some(host) =
self.selected_project.as_ref().and_then(|p| p.hosts.first())
{
scope.push("host", host.clone());
}
}
"hosts" => {
if let Some(ref proj) = self.selected_project {
scope.push("hosts", proj.hosts.clone());
}
}
"config" => {
let mut map = rhai::Map::new();
for (k, v) in &self.config {
map.insert(k.clone().into(), v.clone().into());
}
scope.push("config", map);
}
_ if arg_requirement.starts_with("string") => {
let input_val = command_args
.clone()
.unwrap_or_else(|| self.curent_intput.clone());
scope.push("input_string", input_val);
}
_ => {}
} }
} }
"projects" => { self.workers.spawn(move || {
let mut arr = rhai::Array::new(); match engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast) {
for proj in &self.projects { Ok(result) => {
arr.push(rhai::Dynamic::from(proj.clone())); let result_str = if result.is_array() {
if let Ok(arr) = result.into_array() {
let lines: Vec<String> = arr
.into_iter()
.map(|item| {
item.into_string().unwrap_or_else(|_| "".into())
})
.collect();
lines.join("\n")
} else {
"failed to process array output".into()
}
} else if result.is_string() {
result
.into_string()
.unwrap_or_else(|_| "failed to parse string".into())
} else {
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));
}
}
Err(err) => {
let _ = tx.send(ToolMessage::Output(format!(
"Script Error in execution: {}",
err
)));
}
} }
scope.push("projects", arr); });
}
"host" => {
if let Some(host) = self.selected_project.as_ref().and_then(|p| p.hosts.first())
{
scope.push("host", host.clone());
}
}
"hosts" => {
if let Some(ref proj) = self.selected_project {
scope.push("hosts", proj.hosts.clone());
}
}
"config" => {
// Turn Hashmap into rhai::Map
let mut map = rhai::Map::new();
for (k, v) in &self.config {
map.insert(k.clone().into(), v.clone().into());
}
scope.push("config", map);
}
_ 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 = raw_input_string
.clone()
.unwrap_or_else(|| self.curent_intput.clone());
scope.push("input_string", input_val);
}
_ => {}
} }
} }
// 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 || {
// This runs in a background thread inside Rayon!
match engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast) {
Ok(result) => {
// Handle output based on the output type requested
let result_str = if result.is_array() {
if let Ok(arr) = result.into_array() {
let lines: Vec<String> = arr
.into_iter()
.map(|item| item.into_string().unwrap_or_else(|_| "".into()))
.collect();
lines.join("\n")
} else {
"failed to process array output".into()
}
} else if result.is_string() {
result
.into_string()
.unwrap_or_else(|_| "failed to parse string".into())
} else {
format!("{:?}", result)
};
let _ = tx.send(ToolMessage::Output(result_str));
}
Err(err) => {
let _ = tx.send(ToolMessage::Output(format!(
"Script Error in execution: {}",
err
)));
}
}
});
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
View File
@@ -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);
} }
} }