use anyhow::Result;
use clap::Parser;
use console::style;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
use tokio::time;
use tracing::{debug, info};
use crate::client::ManagementResponse;
use crate::utils::event_display::{display_events, display_single_event, EventDisplayOptions};
use crate::{error::CliError, output::formatters::EventSubscription, CommandContext};
use theater::id::TheaterId;
#[derive(Debug, Parser)]
pub struct SubscribeArgs {
#[arg(required = true)]
pub actor_id: String,
#[arg(short, long)]
pub address: Option<SocketAddr>,
#[arg(short, long)]
pub event_type: Option<String>,
#[arg(short, long)]
pub detailed: bool,
#[arg(short, long, default_value = "0")]
pub limit: usize,
#[arg(short, long, default_value = "0")]
pub timeout: u64,
#[arg(short, long, default_value = "compact")]
pub format: String,
#[arg(short = 'H', long)]
pub history: bool,
#[arg(long, default_value = "0")]
pub history_limit: usize,
}
pub async fn execute_async(args: &SubscribeArgs, ctx: &CommandContext) -> Result<(), CliError> {
let actor_id_str = if args.actor_id == "-" {
let mut input = String::new();
std::io::stdin().read_line(&mut input).map_err(|e| {
CliError::invalid_input("actor_id", "-", format!("Failed to read from stdin: {}", e))
})?;
input.trim().to_string()
} else {
args.actor_id.clone()
};
debug!("Subscribing to events for actor: {}", actor_id_str);
let actor_id = TheaterId::from_str(&actor_id_str)
.map_err(|_| CliError::invalid_actor_id(&actor_id_str))?;
let address = ctx.server_address(args.address);
debug!("Connecting to server at: {}", address);
let client = ctx.create_client();
client
.connect()
.await
.map_err(|e| CliError::connection_failed(address, e))?;
let display_options = EventDisplayOptions {
format: args.format.clone(),
detailed: args.detailed,
json: ctx.json,
};
let mut events_count = 0;
let mut subscription_info = EventSubscription {
actor_id: actor_id.clone(),
address: address.to_string(),
event_type_filter: args.event_type.clone(),
limit: args.limit,
timeout: args.timeout,
format: args.format.clone(),
show_history: args.history,
history_limit: args.history_limit,
detailed: args.detailed,
events_received: 0,
subscription_id: None,
is_active: false,
};
if !ctx.json {
ctx.output.output(&subscription_info, None)?;
}
if args.history {
let mut events = client
.get_actor_events(&actor_id.to_string())
.await
.map_err(|e| {
CliError::actor_not_found(format!(
"Failed to get events for actor {}: {}",
actor_id, e
))
})?;
if let Some(filter) = &args.event_type {
events.retain(|e| e.event_type.contains(filter));
}
if args.history_limit > 0 && events.len() > args.history_limit {
let skip_count = events.len() - args.history_limit;
events = events.into_iter().skip(skip_count).collect();
}
if !events.is_empty() {
events_count = display_events(&events, Some(&actor_id), &display_options, 0)
.map_err(|e| CliError::invalid_input("event_display", "events", e.to_string()))?;
}
}
let event_stream = client
.subscribe_to_events(&actor_id.to_string())
.await
.map_err(|e| {
CliError::actor_not_found(format!("Failed to subscribe to actor {}: {}", actor_id, e))
})?;
let subscription_id = event_stream.subscription_id();
subscription_info.subscription_id = Some(subscription_id.to_string());
subscription_info.is_active = true;
info!(
"Subscribed to actor events with subscription ID: {}",
subscription_id
);
if !args.history || events_count == 0 {
if display_options.format == "compact" && !display_options.json {
println!(
"{:<12} {:<12} {:<25} {}",
"HASH", "PARENT", "EVENT TYPE", "DESCRIPTION"
);
println!("{}", style("─".repeat(100)).dim());
}
if !args.history && !ctx.json {
println!("{} Waiting for events...\n", style("⏳").yellow().bold());
}
}
let mut last_event_time = std::time::Instant::now();
loop {
if args.timeout > 0 {
let timeout_duration = Duration::from_secs(args.timeout);
if last_event_time.elapsed() > timeout_duration {
if !ctx.json {
println!(
"\n{} No events received for {} seconds, exiting.",
style("⏱").yellow().bold(),
args.timeout
);
}
break;
}
}
let response = match time::timeout(Duration::from_secs(1), client.next_response()).await {
Ok(result) => match result {
Ok(response) => response,
Err(e) => {
if !e.to_string().contains("Connection closed") {
return Err(CliError::connection_failed(address, e));
}
continue;
}
},
Err(_) => continue, };
match response {
ManagementResponse::ActorEvent { event } => {
if let Some(filter) = &args.event_type {
if !event.event_type.contains(filter) {
continue;
}
}
last_event_time = std::time::Instant::now();
events_count += 1;
subscription_info.events_received = events_count;
display_single_event(
&event,
if display_options.json {
"json"
} else {
&display_options.format
},
)
.map_err(|e| CliError::invalid_input("event_display", "event", e.to_string()))?;
if args.limit > 0 && events_count >= args.limit {
if !ctx.json {
println!(
"\n{} Reached event limit ({}), exiting.",
style("ℹ").blue().bold(),
args.limit
);
}
break;
}
}
ManagementResponse::ActorError { error } => {
if ctx.json {
let output = serde_json::json!({
"actor_id": actor_id.to_string(),
"error": error,
});
println!(
"{}",
serde_json::to_string_pretty(&output).map_err(|e| {
CliError::invalid_input("json_output", "error", e.to_string())
})?
);
} else {
println!("{} Actor error: {}", style("ERROR").bold().red(), error);
}
}
ManagementResponse::Error { error } => {
return Err(CliError::actor_not_found(format!(
"Server error: {:?}",
error
)));
}
_ => {
debug!("Received unexpected response: {:?}", response);
}
}
}
if let Err(e) = client
.unsubscribe_from_actor(&actor_id.to_string(), subscription_id)
.await
{
debug!("Failed to unsubscribe: {}", e);
}
subscription_info.is_active = false;
if !ctx.json && !args.history {
subscription_info.events_received = events_count;
}
Ok(())
}