circles/rhai_repl_cli/src/main.rs
2025-06-05 00:29:10 +03:00

190 lines
8.2 KiB
Rust

use url::Url;
use tracing_subscriber::EnvFilter;
use circle_client_ws::CircleWsClient;
use rustyline::error::ReadlineError;
// Remove direct History import, DefaultEditor handles it.
use rustyline::{DefaultEditor, Config, EditMode};
use std::fs;
use std::process::Command;
use std::env;
use tempfile::Builder as TempFileBuilder; // Use Builder for suffix
// std::io::Write is not used if we don't pre-populate temp_file
// use std::io::Write;
async fn execute_script(client: &mut CircleWsClient, script_content: String) {
if script_content.trim().is_empty() {
println!("Script is empty, not sending.");
return;
}
println!("Sending script to server:\n---\n{}\n---", script_content);
match client.play(script_content).await {
Ok(play_result) => {
println!("server: {}", play_result.output);
}
Err(e) => {
eprintln!("Error executing script: {}", e);
if matches!(e, circle_client_ws::CircleWsClientError::NotConnected | circle_client_ws::CircleWsClientError::ConnectionError(_)) {
eprintln!("Connection lost. You may need to restart the REPL and reconnect.");
// Optionally, could attempt to trigger a full exit here or set a flag
}
}
}
}
async fn run_repl(ws_url_str: String) -> Result<(), Box<dyn std::error::Error>> {
println!("Attempting to connect to {}...", ws_url_str);
let mut client = CircleWsClient::new(ws_url_str.clone());
match client.connect().await {
Ok(_) => {
println!("Connected to {}!", ws_url_str);
println!("Type Rhai scripts, '.edit' to use $EDITOR, '.run <path>' to execute a file, or 'exit'/'quit'.");
println!("Vi mode enabled for input line.");
}
Err(e) => {
return Err(format!("Failed to connect: {}", e).into());
}
}
let config = Config::builder()
.edit_mode(EditMode::Vi)
.auto_add_history(true) // Automatically add to history
.build();
let mut rl = DefaultEditor::with_config(config)?;
let history_file = ".rhai_repl_history.txt"; // Simple history file in current dir
if rl.load_history(history_file).is_err() {
// No history found or error loading, not critical
}
let prompt = format!("rhai ({})> ", ws_url_str);
loop {
let readline = rl.readline(&prompt);
match readline {
Ok(line) => {
let input = line.trim();
if input.eq_ignore_ascii_case("exit") || input.eq_ignore_ascii_case("quit") {
println!("Exiting REPL.");
break;
} else if input.eq_ignore_ascii_case(".edit") {
// Correct way to create a temp file with a suffix
let temp_file = TempFileBuilder::new()
.prefix("rhai_script_") // Optional: add a prefix
.suffix(".rhai")
.tempfile_in(".") // Create in current directory for simplicity
.map_err(|e| format!("Failed to create temp file: {}", e))?;
// You can pre-populate the temp file if needed:
// use std::io::Write; // Add this import if using write_all
// if let Err(e) = temp_file.as_file().write_all(b"// Start your Rhai script here\n") {
// eprintln!("Failed to write initial content to temp file: {}", e);
// }
let temp_path = temp_file.path().to_path_buf();
let editor_cmd_str = env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
let mut editor_parts = editor_cmd_str.split_whitespace();
let editor_executable = editor_parts.next().unwrap_or("vi"); // Default to vi if $EDITOR is empty string
let editor_args: Vec<&str> = editor_parts.collect();
println!("Launching editor: '{}' with args: {:?} for script editing. Save and exit editor to execute.", editor_executable, editor_args);
let mut command = Command::new(editor_executable);
command.args(editor_args); // Add any arguments from $EDITOR (like -w)
command.arg(&temp_path); // Add the temp file path as the last argument
let status = command.status();
match status {
Ok(exit_status) if exit_status.success() => {
match fs::read_to_string(&temp_path) {
Ok(script_content) => {
execute_script(&mut client, script_content).await;
}
Err(e) => eprintln!("Error reading temp file {:?}: {}", temp_path, e),
}
}
Ok(exit_status) => eprintln!("Editor exited with status: {}. Script not executed.", exit_status),
Err(e) => eprintln!("Failed to launch editor '{}': {}. Ensure it's in your PATH.", editor_executable, e), // Changed 'editor' to 'editor_executable'
}
// temp_file is automatically deleted when it goes out of scope
} else if input.starts_with(".run ") || input.starts_with("run ") {
let parts: Vec<&str> = input.splitn(2, ' ').collect();
if parts.len() == 2 {
let file_path = parts[1];
println!("Attempting to run script from file: {}", file_path);
match fs::read_to_string(file_path) {
Ok(script_content) => {
execute_script(&mut client, script_content).await;
}
Err(e) => eprintln!("Error reading file {}: {}", file_path, e),
}
} else {
eprintln!("Usage: .run <filepath>");
}
} else if !input.is_empty() {
execute_script(&mut client, input.to_string()).await;
}
// rl.add_history_entry(line.as_str()) is handled by auto_add_history(true)
}
Err(ReadlineError::Interrupted) => { // Ctrl-C
println!("Input interrupted. Type 'exit' or 'quit' to close.");
continue;
}
Err(ReadlineError::Eof) => { // Ctrl-D
println!("Exiting REPL (EOF).");
break;
}
Err(err) => {
eprintln!("Error reading input: {:?}", err);
break;
}
}
}
if rl.save_history(history_file).is_err() {
// Failed to save history, not critical
}
client.disconnect().await;
println!("Disconnected.");
Ok(())
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("rhai_repl_cli=info".parse().unwrap()).add_directive("circle_client_ws=info".parse().unwrap()))
.init();
let args: Vec<String> = env::args().collect();
let ws_url_str = if args.len() > 1 {
args[1].clone()
} else {
let default_url = "ws://127.0.0.1:8081/ws".to_string(); // Default to first circle
println!("No WebSocket URL provided. Defaulting to: {}", default_url);
println!("You can also provide a URL as a command line argument, e.g.: cargo run ws://127.0.0.1:8082/ws");
default_url
};
match Url::parse(&ws_url_str) {
Ok(parsed_url) => {
if parsed_url.scheme() != "ws" && parsed_url.scheme() != "wss" {
eprintln!("Invalid WebSocket URL scheme: {}. Must be 'ws' or 'wss'.", parsed_url.scheme());
return;
}
if let Err(e) = run_repl(ws_url_str).await { // Pass the original string URL
eprintln!("REPL error: {}", e);
}
}
Err(e) => {
eprintln!("Invalid WebSocket URL format '{}': {}", ws_url_str, e);
}
}
}