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"
+192 -44
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 _ = 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(())
} }
+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);
+94 -47
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,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
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);
} }
} }