theater_cli/commands/
dynamic_completion.rs

1use clap::Parser;
2use std::net::SocketAddr;
3use tracing::debug;
4
5use crate::error::{CliError, CliResult};
6use crate::CommandContext;
7
8#[derive(Debug, Parser)]
9pub struct DynamicCompletionArgs {
10    /// The command line being completed
11    #[arg(required = true)]
12    pub line: String,
13
14    /// The current word being completed
15    #[arg(required = true)]
16    pub current: String,
17
18    /// Address of the theater server for dynamic completions
19    #[arg(short, long, default_value = "127.0.0.1:9000")]
20    pub address: SocketAddr,
21}
22
23/// Generate dynamic completions based on current theater state
24pub async fn execute_async(args: &DynamicCompletionArgs, ctx: &CommandContext) -> CliResult<()> {
25    debug!("Generating dynamic completion for: '{}'", args.line);
26    debug!("Current word: '{}'", args.current);
27
28    let completions = generate_dynamic_completions(args, ctx).await?;
29
30    // Output completions one per line for shell consumption
31    for completion in completions {
32        println!("{}", completion);
33    }
34
35    Ok(())
36}
37
38/// Generate completions based on context
39async fn generate_dynamic_completions(
40    args: &DynamicCompletionArgs,
41    _ctx: &CommandContext,
42) -> CliResult<Vec<String>> {
43    let words: Vec<&str> = args.line.split_whitespace().collect();
44
45    // Determine what we're completing based on the command structure
46    match words.as_slice() {
47        // theater <command>
48        ["theater"] => Ok(get_command_completions(&args.current)),
49
50        // theater start <manifest_or_actor_id>
51        ["theater", "start"] => get_manifest_completions(&args.current).await,
52
53        // theater stop <actor_id>
54        ["theater", "stop"] => get_actor_id_completions(&args.current, args.address).await,
55
56        // theater state <actor_id>
57        ["theater", "state"] => get_actor_id_completions(&args.current, args.address).await,
58
59        // theater inspect <actor_id>
60        ["theater", "inspect"] => get_actor_id_completions(&args.current, args.address).await,
61
62        // theater message <actor_id>
63        ["theater", "message"] => get_actor_id_completions(&args.current, args.address).await,
64
65        // theater events <actor_id>
66        ["theater", "events"] => get_actor_id_completions(&args.current, args.address).await,
67
68        // theater channel open <actor_id>
69        ["theater", "channel", "open"] => {
70            get_actor_id_completions(&args.current, args.address).await
71        }
72
73        // theater create <template>
74        ["theater", "create"] => Ok(get_template_completions(&args.current)),
75
76        // theater completion <shell>
77        ["theater", "completion"] => Ok(get_shell_completions(&args.current)),
78
79        _ => Ok(vec![]),
80    }
81}
82
83/// Get available command completions
84fn get_command_completions(current: &str) -> Vec<String> {
85    let commands = vec![
86        "build",
87        "channel",
88        "completion",
89        "create",
90        "events",
91        "inspect",
92        "list",
93        "list-stored",
94        "message",
95        "start",
96        "state",
97        "stop",
98        "subscribe",
99    ];
100
101    commands
102        .into_iter()
103        .filter(|cmd| cmd.starts_with(current))
104        .map(|s| s.to_string())
105        .collect()
106}
107
108/// Get template completions
109fn get_template_completions(current: &str) -> Vec<String> {
110    let templates = vec!["basic", "http"];
111
112    templates
113        .into_iter()
114        .filter(|tmpl| tmpl.starts_with(current))
115        .map(|s| s.to_string())
116        .collect()
117}
118
119/// Get shell completions
120fn get_shell_completions(current: &str) -> Vec<String> {
121    let shells = vec!["bash", "zsh", "fish", "powershell", "elvish"];
122
123    shells
124        .into_iter()
125        .filter(|shell| shell.starts_with(current))
126        .map(|s| s.to_string())
127        .collect()
128}
129
130/// Get manifest file completions
131async fn get_manifest_completions(current: &str) -> CliResult<Vec<String>> {
132    // Look for manifest.toml files in current directory and subdirectories
133    let mut completions = Vec::new();
134
135    // Add manifest files
136    if let Ok(entries) = std::fs::read_dir(".") {
137        for entry in entries.flatten() {
138            if let Some(name) = entry.file_name().to_str() {
139                if name == "manifest.toml" || name.ends_with(".toml") {
140                    if name.starts_with(current) {
141                        completions.push(name.to_string());
142                    }
143                }
144            }
145        }
146    }
147
148    // Also add any stored actor IDs
149    if let Ok(stored_actors) = get_stored_actor_ids().await {
150        for actor_id in stored_actors {
151            if actor_id.starts_with(current) {
152                completions.push(actor_id);
153            }
154        }
155    }
156
157    Ok(completions)
158}
159
160/// Get running actor ID completions
161async fn get_actor_id_completions(current: &str, address: SocketAddr) -> CliResult<Vec<String>> {
162    debug!("Getting actor completions from server at: {}", address);
163
164    match get_running_actor_ids(address).await {
165        Ok(actor_ids) => {
166            let completions = actor_ids
167                .into_iter()
168                .filter(|id| id.starts_with(current))
169                .collect();
170            Ok(completions)
171        }
172        Err(e) => {
173            debug!("Failed to get running actors: {}", e);
174            // Fallback to stored actor IDs if server is unavailable
175            get_stored_actor_ids().await.map(|ids| {
176                ids.into_iter()
177                    .filter(|id| id.starts_with(current))
178                    .collect()
179            })
180        }
181    }
182}
183
184/// Get running actor IDs from the server
185async fn get_running_actor_ids(address: SocketAddr) -> CliResult<Vec<String>> {
186    use bytes::Bytes;
187    use futures::{SinkExt, StreamExt};
188    use tokio::net::TcpStream;
189    use tokio_util::codec::{Framed, LengthDelimitedCodec};
190
191    let socket = TcpStream::connect(address)
192        .await
193        .map_err(|e| CliError::connection_failed(address, e))?;
194
195    let mut framed = Framed::new(socket, LengthDelimitedCodec::new());
196
197    // Send list request
198    let request = serde_json::json!({
199        "type": "list_actors"
200    });
201
202    let request_bytes = Bytes::from(serde_json::to_vec(&request).unwrap());
203    framed
204        .send(request_bytes)
205        .await
206        .map_err(|e| CliError::NetworkError {
207            operation: "send list request".to_string(),
208            source: Box::new(e),
209        })?;
210
211    // Read response
212    if let Some(response_frame) = framed.next().await {
213        let response_bytes = response_frame.map_err(|e| CliError::NetworkError {
214            operation: "receive list response".to_string(),
215            source: Box::new(e),
216        })?;
217
218        let response: serde_json::Value =
219            serde_json::from_slice(&response_bytes).map_err(|e| CliError::InvalidResponse {
220                message: "Failed to parse actor list response".to_string(),
221                source: Some(Box::new(e)),
222            })?;
223
224        if let Some(actors) = response.get("actors").and_then(|a| a.as_array()) {
225            let actor_ids = actors
226                .iter()
227                .filter_map(|actor| {
228                    actor
229                        .get("id")
230                        .and_then(|id| id.as_str().map(|s| s.to_string()))
231                })
232                .collect();
233
234            return Ok(actor_ids);
235        }
236    }
237
238    Ok(vec![])
239}
240
241/// Get stored actor IDs from filesystem
242async fn get_stored_actor_ids() -> CliResult<Vec<String>> {
243    // This would read from your stored actors directory
244    // For now, return empty vec as a placeholder
245    Ok(vec![])
246}