Skip to main content

workflow_terminal/
cli.rs

1//!
2//! Cli trait for implementing a user-side command-line processor.
3//!
4
5use crate::error::Error;
6use crate::parse;
7pub use crate::result::Result;
8use crate::terminal::Terminal;
9use async_trait::async_trait;
10use downcast::{AnySync, downcast_sync};
11use std::{
12    collections::HashMap,
13    sync::{Arc, Mutex, MutexGuard},
14};
15pub use workflow_terminal_macros::{Handler, declare_handler, register_handlers};
16
17/// User-implemented command-line processor driven by a [`Terminal`].
18///
19/// An implementation receives the user's input lines and is responsible for
20/// parsing, executing, and providing completion candidates for commands.
21#[async_trait]
22pub trait Cli: Sync + Send {
23    /// Called once when the CLI is bound to a terminal, allowing setup.
24    fn init(self: Arc<Self>, _term: &Arc<Terminal>) -> Result<()> {
25        Ok(())
26    }
27    /// Processes a single command line entered by the user.
28    async fn digest(self: Arc<Self>, term: Arc<Terminal>, cmd: String) -> Result<()>;
29    /// Returns completion candidates for the current partial input, if any.
30    async fn complete(
31        self: Arc<Self>,
32        term: Arc<Terminal>,
33        cmd: String,
34    ) -> Result<Option<Vec<String>>>;
35    /// Returns the prompt string to display, or `None` for the default prompt.
36    fn prompt(&self) -> Option<String>;
37}
38
39/// Shared execution context passed to command handlers, providing access to
40/// the underlying terminal and allowing downcasting to a concrete type.
41pub trait Context: Sync + Send + AnySync {
42    /// Returns the terminal associated with this context.
43    fn term(&self) -> Arc<Terminal>;
44}
45downcast_sync!(dyn Context);
46downcast_sync!(dyn Context + Sync + Send);
47
48impl From<&dyn Context> for Arc<Terminal> {
49    fn from(ctx: &dyn Context) -> Arc<Terminal> {
50        ctx.term()
51    }
52}
53
54/// A single command handler, identified by a verb, that can be registered
55/// with a [`HandlerCli`] and invoked when its verb is entered.
56#[async_trait]
57pub trait Handler: Sync + Send + AnySync {
58    /// Returns the command verb this handler responds to, or `None` to disable it.
59    fn verb(&self, _ctx: &Arc<dyn Context>) -> Option<&'static str> {
60        None
61    }
62    /// Returns whether this handler should be registered in the given context.
63    fn condition(&self, ctx: &Arc<dyn Context>) -> bool {
64        self.verb(ctx).is_some()
65    }
66    /// Returns static help text describing the command.
67    fn help(&self, _ctx: &Arc<dyn Context>) -> &'static str {
68        ""
69    }
70    /// Returns dynamically generated help text, used when [`help`](Self::help) is empty.
71    fn dyn_help(&self, _ctx: &Arc<dyn Context>) -> String {
72        "".to_owned()
73    }
74    /// Returns completion candidates for the command's arguments, if any.
75    async fn complete(&self, _ctx: &Arc<dyn Context>, _cmd: &str) -> Result<Option<Vec<String>>> {
76        Ok(None)
77    }
78    /// Called when the owning [`HandlerCli`] starts, allowing setup.
79    async fn start(self: Arc<Self>, _ctx: &Arc<dyn Context>) -> Result<()> {
80        Ok(())
81    }
82    /// Called when the owning [`HandlerCli`] stops, allowing teardown.
83    async fn stop(self: Arc<Self>, _ctx: &Arc<dyn Context>) -> Result<()> {
84        Ok(())
85    }
86    /// Executes the command with the parsed argument vector and raw command line.
87    async fn handle(
88        self: Arc<Self>,
89        ctx: &Arc<dyn Context>,
90        argv: Vec<String>,
91        cmd: &str,
92    ) -> Result<()>;
93}
94
95downcast_sync!(dyn Handler);
96
97/// Returns the help text for a handler, preferring static [`Handler::help`]
98/// and falling back to [`Handler::dyn_help`] when the static text is empty.
99pub fn get_handler_help(handler: Arc<dyn Handler>, ctx: &Arc<dyn Context>) -> String {
100    let s = handler.help(ctx);
101    if s.is_empty() {
102        handler.dyn_help(ctx)
103    } else {
104        s.to_string()
105    }
106}
107
108#[derive(Default)]
109struct Inner {
110    handlers: HashMap<String, Arc<dyn Handler>>,
111}
112
113#[derive(Default)]
114/// A registry-based [`Cli`] implementation that dispatches command lines to
115/// registered [`Handler`]s keyed by their verb.
116pub struct HandlerCli {
117    inner: Arc<Mutex<Inner>>,
118}
119
120impl HandlerCli {
121    /// Creates a new, empty handler registry.
122    pub fn new() -> Self {
123        Self {
124            inner: Arc::new(Mutex::new(Inner::default())),
125        }
126    }
127
128    fn inner(&self) -> MutexGuard<'_, Inner> {
129        self.inner.lock().unwrap()
130    }
131
132    /// Returns all currently registered handlers.
133    pub fn collect(&self) -> Vec<Arc<dyn Handler>> {
134        self.inner().handlers.values().cloned().collect::<Vec<_>>()
135    }
136
137    /// Returns the handler registered under the given verb, if any.
138    pub fn get(&self, name: &str) -> Option<Arc<dyn Handler>> {
139        self.inner().handlers.get(name).cloned()
140    }
141
142    /// Registers a handler by value under its verb, if its
143    /// [`condition`](Handler::condition) is met in the given context.
144    pub fn register<T, H>(&self, ctx: &Arc<T>, handler: H)
145    where
146        T: Context + Sized,
147        H: Handler + Send + Sync + 'static,
148    {
149        let ctx: Arc<dyn Context> = ctx.clone();
150        match handler.verb(&ctx) {
151            Some(name) if handler.condition(&ctx) => {
152                self.inner()
153                    .handlers
154                    .insert(name.to_lowercase(), Arc::new(handler));
155            }
156            _ => {}
157        }
158    }
159
160    /// Registers an already-`Arc`-wrapped handler under its verb, if its
161    /// [`condition`](Handler::condition) is met in the given context.
162    pub fn register_arc<T, H>(&self, ctx: &Arc<T>, handler: &Arc<H>)
163    where
164        T: Context + Sized,
165        H: Handler + Send + Sync + 'static,
166    {
167        let ctx: Arc<dyn Context> = ctx.clone();
168        match handler.verb(&ctx) {
169            Some(name) if handler.condition(&ctx) => {
170                self.inner()
171                    .handlers
172                    .insert(name.to_lowercase(), handler.clone());
173            }
174            _ => {}
175        }
176    }
177
178    /// Removes and returns the handler registered under the given verb, if any.
179    pub fn unregister(&self, name: &str) -> Option<Arc<dyn Handler>> {
180        self.inner().handlers.remove(name)
181    }
182
183    /// Removes all registered handlers.
184    pub fn clear(&self) -> Result<()> {
185        self.inner().handlers.clear();
186        Ok(())
187    }
188
189    /// Invokes [`Handler::start`] on every registered handler.
190    pub async fn start<T>(&self, ctx: &Arc<T>) -> Result<()>
191    where
192        T: Context + Sized,
193    {
194        let ctx: Arc<dyn Context> = ctx.clone();
195        let handlers = self.collect();
196        for handler in handlers.iter() {
197            handler.clone().start(&ctx).await?;
198        }
199        Ok(())
200    }
201
202    /// Invokes the stop lifecycle on every registered handler.
203    pub async fn stop<T>(&self, ctx: &Arc<T>) -> Result<()>
204    where
205        T: Context + Sized,
206    {
207        let handlers = self.collect();
208        let ctx: Arc<dyn Context> = ctx.clone();
209        for handler in handlers.into_iter() {
210            handler.clone().start(&ctx).await?;
211        }
212        Ok(())
213    }
214
215    /// Parses the command line and dispatches it to the matching handler,
216    /// returning [`Error::CommandNotFound`] if no handler matches the verb.
217    pub async fn execute<T>(&self, ctx: &Arc<T>, cmd: &str) -> Result<()>
218    where
219        T: Context + Sized,
220    {
221        let ctx: Arc<dyn Context> = ctx.clone();
222
223        let argv = parse(cmd);
224        let action = argv[0].to_lowercase();
225
226        let handler = self.get(action.as_str());
227        if let Some(handler) = handler {
228            handler
229                .clone()
230                .handle(&ctx, argv[1..].to_vec(), cmd)
231                .await?;
232            Ok(())
233        } else {
234            Err(Error::CommandNotFound(action))
235        }
236    }
237
238    /// Returns completion candidates for the command line by delegating to the
239    /// matching handler, or [`Error::CommandNotFound`] if no handler matches.
240    pub async fn complete<T>(&self, ctx: &Arc<T>, cmd: &str) -> Result<Option<Vec<String>>>
241    where
242        T: Context + Sized,
243    {
244        let ctx: Arc<dyn Context> = ctx.clone();
245
246        let argv = parse(cmd);
247        let action = argv[0].to_lowercase();
248
249        let handler = self.get(action.as_str());
250        if let Some(handler) = handler {
251            Ok(handler.clone().complete(&ctx, cmd).await?)
252        } else {
253            Err(Error::CommandNotFound(action))
254        }
255    }
256}