use clap::Parser;
use std::net::SocketAddr;
use tracing::debug;
use crate::error::{CliError, CliResult};
use crate::CommandContext;
#[derive(Debug, Parser)]
pub struct DynamicCompletionArgs {
#[arg(required = true)]
pub line: String,
#[arg(required = true)]
pub current: String,
#[arg(short, long, default_value = "127.0.0.1:9000")]
pub address: SocketAddr,
}
pub async fn execute_async(args: &DynamicCompletionArgs, ctx: &CommandContext) -> CliResult<()> {
debug!("Generating dynamic completion for: '{}'", args.line);
debug!("Current word: '{}'", args.current);
let completions = generate_dynamic_completions(args, ctx).await?;
for completion in completions {
println!("{}", completion);
}
Ok(())
}
async fn generate_dynamic_completions(
args: &DynamicCompletionArgs,
_ctx: &CommandContext,
) -> CliResult<Vec<String>> {
let words: Vec<&str> = args.line.split_whitespace().collect();
match words.as_slice() {
["theater"] => Ok(get_command_completions(&args.current)),
["theater", "start"] => get_manifest_completions(&args.current).await,
["theater", "stop"] => get_actor_id_completions(&args.current, args.address).await,
["theater", "state"] => get_actor_id_completions(&args.current, args.address).await,
["theater", "inspect"] => get_actor_id_completions(&args.current, args.address).await,
["theater", "message"] => get_actor_id_completions(&args.current, args.address).await,
["theater", "events"] => get_actor_id_completions(&args.current, args.address).await,
["theater", "channel", "open"] => {
get_actor_id_completions(&args.current, args.address).await
}
["theater", "create"] => Ok(get_template_completions(&args.current)),
["theater", "completion"] => Ok(get_shell_completions(&args.current)),
_ => Ok(vec![]),
}
}
fn get_command_completions(current: &str) -> Vec<String> {
let commands = vec![
"build",
"channel",
"completion",
"create",
"events",
"inspect",
"list",
"list-stored",
"message",
"start",
"state",
"stop",
"subscribe",
];
commands
.into_iter()
.filter(|cmd| cmd.starts_with(current))
.map(|s| s.to_string())
.collect()
}
fn get_template_completions(current: &str) -> Vec<String> {
let templates = vec!["basic", "http"];
templates
.into_iter()
.filter(|tmpl| tmpl.starts_with(current))
.map(|s| s.to_string())
.collect()
}
fn get_shell_completions(current: &str) -> Vec<String> {
let shells = vec!["bash", "zsh", "fish", "powershell", "elvish"];
shells
.into_iter()
.filter(|shell| shell.starts_with(current))
.map(|s| s.to_string())
.collect()
}
async fn get_manifest_completions(current: &str) -> CliResult<Vec<String>> {
let mut completions = Vec::new();
if let Ok(entries) = std::fs::read_dir(".") {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name == "manifest.toml" || name.ends_with(".toml") {
if name.starts_with(current) {
completions.push(name.to_string());
}
}
}
}
}
if let Ok(stored_actors) = get_stored_actor_ids().await {
for actor_id in stored_actors {
if actor_id.starts_with(current) {
completions.push(actor_id);
}
}
}
Ok(completions)
}
async fn get_actor_id_completions(current: &str, address: SocketAddr) -> CliResult<Vec<String>> {
debug!("Getting actor completions from server at: {}", address);
match get_running_actor_ids(address).await {
Ok(actor_ids) => {
let completions = actor_ids
.into_iter()
.filter(|id| id.starts_with(current))
.collect();
Ok(completions)
}
Err(e) => {
debug!("Failed to get running actors: {}", e);
get_stored_actor_ids().await.map(|ids| {
ids.into_iter()
.filter(|id| id.starts_with(current))
.collect()
})
}
}
}
async fn get_running_actor_ids(address: SocketAddr) -> CliResult<Vec<String>> {
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use tokio::net::TcpStream;
use tokio_util::codec::{Framed, LengthDelimitedCodec};
let socket = TcpStream::connect(address)
.await
.map_err(|e| CliError::connection_failed(address, e))?;
let mut framed = Framed::new(socket, LengthDelimitedCodec::new());
let request = serde_json::json!({
"type": "list_actors"
});
let request_bytes = Bytes::from(serde_json::to_vec(&request).unwrap());
framed
.send(request_bytes)
.await
.map_err(|e| CliError::NetworkError {
operation: "send list request".to_string(),
source: Box::new(e),
})?;
if let Some(response_frame) = framed.next().await {
let response_bytes = response_frame.map_err(|e| CliError::NetworkError {
operation: "receive list response".to_string(),
source: Box::new(e),
})?;
let response: serde_json::Value =
serde_json::from_slice(&response_bytes).map_err(|e| CliError::InvalidResponse {
message: "Failed to parse actor list response".to_string(),
source: Some(Box::new(e)),
})?;
if let Some(actors) = response.get("actors").and_then(|a| a.as_array()) {
let actor_ids = actors
.iter()
.filter_map(|actor| {
actor
.get("id")
.and_then(|id| id.as_str().map(|s| s.to_string()))
})
.collect();
return Ok(actor_ids);
}
}
Ok(vec![])
}
async fn get_stored_actor_ids() -> CliResult<Vec<String>> {
Ok(vec![])
}