theater_cli/commands/channel/
open.rs1use anyhow::Result;
2use clap::Parser;
3use console::style;
4use rustyline::error::ReadlineError;
5use rustyline::DefaultEditor;
6use std::fs;
7use std::net::SocketAddr;
8use std::path::PathBuf;
9use tokio::sync::mpsc;
10use tracing::{debug, error};
11
12use crate::{error::CliError, output::formatters::ChannelOpened, CommandContext};
13use theater::id::TheaterId;
14
15#[derive(Debug, Parser)]
16pub struct OpenArgs {
17 #[arg(required = true)]
19 pub actor_id: String,
20
21 #[arg(short, long)]
23 pub message: Option<String>,
24
25 #[arg(short, long, conflicts_with = "message")]
27 pub file: Option<PathBuf>,
28
29 #[arg(short, long)]
31 pub address: Option<SocketAddr>,
32}
33
34pub async fn execute_async(args: &OpenArgs, ctx: &CommandContext) -> Result<(), CliError> {
36 debug!("Opening channel to actor: {}", args.actor_id);
37
38 let initial_message = if let Some(message) = &args.message {
40 message.clone().into_bytes()
41 } else if let Some(file_path) = &args.file {
42 debug!("Reading initial message from file: {:?}", file_path);
43 fs::read(file_path).map_err(|e| {
44 CliError::file_operation_failed("read message file", file_path.display().to_string(), e)
45 })?
46 } else {
47 serde_json::to_vec(&serde_json::json!({
49 "message_type": "channel_init",
50 "payload": {
51 "timestamp": chrono::Utc::now().timestamp_millis(),
52 }
53 }))
54 .map_err(|e| {
55 CliError::invalid_input(
56 "initial_message",
57 "json",
58 format!("Failed to create default initial message: {}", e),
59 )
60 })?
61 };
62
63 debug!("Initial message size: {} bytes", initial_message.len());
64
65 let actor_id = TheaterId::parse(&args.actor_id)
67 .map_err(|_e| CliError::invalid_actor_id(&args.actor_id))?;
68
69 let address = ctx.server_address(args.address);
71 debug!("Connecting to server at: {}", address);
72
73 run_channel_session(address, actor_id, initial_message, ctx).await
75}
76
77async fn run_channel_session(
78 server_addr: SocketAddr,
79 actor_id: TheaterId,
80 initial_message: Vec<u8>,
81 ctx: &CommandContext,
82) -> Result<(), CliError> {
83 let client = ctx.create_client();
85 client
86 .connect()
87 .await
88 .map_err(|e| CliError::connection_failed(server_addr, e))?;
89
90 let initial_message_size = initial_message.len();
92
93 let channel_id = client
95 .open_channel(&actor_id.to_string(), initial_message)
96 .await
97 .map_err(|e| {
98 CliError::actor_not_found(format!(
99 "Failed to open channel to actor {}: {}",
100 actor_id, e
101 ))
102 })?;
103 let channel_info = ChannelOpened {
105 actor_id: actor_id.clone(),
106 channel_id: channel_id.clone(),
107 address: server_addr.to_string(),
108 initial_message_size,
109 is_interactive: !ctx.json,
110 };
111
112 ctx.output.output(&channel_info, None)?;
114
115 if ctx.json {
117 return Ok(());
118 }
119
120 let mut rl = DefaultEditor::new().map_err(|e| {
122 CliError::invalid_input(
123 "readline",
124 "setup",
125 format!("Failed to setup readline: {}", e),
126 )
127 })?;
128
129 println!(
130 "{} Enter commands ('help' for available commands, 'exit' to quit)",
131 style("i").blue().bold()
132 );
133
134 let (input_tx, mut input_rx) = mpsc::channel::<String>(32);
136 let input_task = tokio::spawn(async move {
137 loop {
138 let readline = rl.readline("channel> ");
139 match readline {
140 Ok(line) => {
141 let _ = rl.add_history_entry(line.as_str());
142 if let Err(_) = input_tx.send(line).await {
143 break;
144 }
145 }
146 Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
147 println!("\rClosing channel and exiting...");
148 break;
149 }
150 Err(err) => {
151 println!("\rError: {}", err);
152 break;
153 }
154 }
155 }
156 });
157
158 let mut running = true;
160
161 while running {
162 tokio::select! {
163 Some(line) = input_rx.recv() => {
165 let trimmed = line.trim();
166 if trimmed.is_empty() {
167 continue;
168 }
169
170 let parts: Vec<&str> = trimmed.split_whitespace().collect();
172 let cmd = parts[0].to_lowercase();
173
174 match cmd.as_str() {
175 "send" => {
176 if parts.len() < 2 {
178 println!("Error: send requires a message or --file option");
179 continue;
180 }
181
182 let message = if parts[1] == "--file" || parts[1] == "-f" {
183 println!("{} Reading message from file...", style(">").green().bold());
184 if parts.len() < 3 {
185 println!("Error: --file option requires a file path");
186 continue;
187 }
188
189 let file_path = parts[2];
190 match fs::read(file_path) {
191 Ok(content) => {
192 println!("{} Read {} bytes from file",
193 style("✓").green().bold(), content.len());
194 content
195 }
196 Err(e) => {
197 println!("Error reading file: {}", e);
198 continue;
199 }
200 }
201 } else {
202 let message_text = trimmed[5..].trim(); let text = if message_text.starts_with('"')
207 && message_text.ends_with('"')
208 && message_text.len() >= 2
209 {
210 &message_text[1..message_text.len() - 1]
211 } else {
212 message_text
213 };
214
215 text.as_bytes().to_vec()
216 };
217
218 debug!("Sending message on channel: {} bytes", message.len());
219
220 match client.send_on_channel(&channel_id, message).await {
222 Ok(_) => {
223 if ctx.verbose {
224 println!("{} Message sent", style("✓").green().bold());
225 }
226 }
227 Err(e) => {
228 println!("{} Error sending message: {}",
229 style("✗").red().bold(), e);
230 }
231 }
232 }
233 "exit" | "quit" => {
234 running = false;
235 println!("Closing channel and exiting...");
236 }
237 "help" => {
238 println!("Available commands:");
239 println!(" send \"message\" - Send a text message");
240 println!(" send --file path - Send contents of a file");
241 println!(" exit | quit - Close channel and exit");
242 println!(" help - Show this help");
243 }
244 _ => {
245 println!("Unknown command: {}. Type 'help' for available commands.", cmd);
246 }
247 }
248 },
249 result = client.receive_channel_message() => {
251 match result {
252 Ok(response) => {
253 if let Some((id, message)) = response {
254 if id == channel_id {
256 match std::str::from_utf8(&message) {
258 Ok(text) => {
259 if let Ok(json) = serde_json::from_str::<serde_json::Value>(text) {
260 println!("\r{}", serde_json::to_string_pretty(&json)
261 .unwrap_or_else(|_| text.to_string()));
262 } else {
263 println!("\r{}", text);
264 }
265 },
266 Err(_) => {
267 println!("\r[Binary message of {} bytes]", message.len());
268 if ctx.verbose {
269 println!("\r{:?}", message);
270 }
271 }
272 }
273 print!("channel> ");
275 let _ = std::io::Write::flush(&mut std::io::stdout());
276 }
277 }
278 }
279 Err(e) => {
280 error!("Error receiving channel message: {}", e);
281 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
283 }
284 }
285 }
286 }
287 }
288
289 input_task.abort();
291
292 client
294 .close_channel(&channel_id)
295 .await
296 .map_err(|e| CliError::actor_not_found(format!("Failed to close channel: {}", e)))?;
297
298 println!("{} Channel closed", style("✓").green().bold());
299
300 Ok(())
301}
302
303pub fn execute(args: &OpenArgs, verbose: bool, json: bool) -> Result<()> {
305 let runtime = tokio::runtime::Runtime::new()?;
306 runtime.block_on(async {
307 let config = crate::config::Config::load().unwrap_or_default();
308 let output = crate::output::OutputManager::new(config.output.clone());
309 let ctx = crate::CommandContext {
310 config,
311 output,
312 verbose,
313 json,
314 };
315 execute_async(args, &ctx)
316 .await
317 .map_err(|e| anyhow::Error::from(e))
318 })
319}