Skip to main content

clawless_core/
output.rs

1//! Command output interface
2//!
3//! This module defines [`Output`], the interface commands use to produce events. `Output` wraps an
4//! [`EventSender`] and provides typed methods for each event kind, decoupling commands from the
5//! rendering strategy. Commands call [`message`], [`detail`], or [`artifact`] to emit events into
6//! the channel; the Presenter consumes them and decides how to render.
7//!
8//! [`process_event`] carries the steps of an external program that a command runs. Commands do not
9//! usually call it, because [`Process`] reports a run through it.
10//!
11//! [`prompt`] sends a question for the user.
12//!
13//! [`Process`]: crate::process::Process
14//! [`artifact`]: Output::artifact
15//! [`detail`]: Output::detail
16//! [`message`]: Output::message
17//! [`process_event`]: Output::process_event
18//! [`prompt`]: Output::prompt
19
20use std::fmt::{Debug, Display};
21
22use serde::Serialize;
23
24use crate::event::process::ProcessEvent;
25use crate::event::prompt::PromptRequest;
26use crate::event::{Event, EventSender, SendError};
27
28/// Command output interface that wraps an event channel sender
29///
30/// `Output` provides typed methods for each event kind, decoupling commands from the event channel
31/// API. Commands call [`message`], [`detail`], or [`artifact`] to emit events; the Presenter
32/// consumes them for rendering.
33///
34/// `Output` is cheaply clonable. Cloning an `Output` produces another handle to the same
35/// underlying channel, not an independent channel.
36///
37/// # Examples
38///
39/// ```
40/// use clawless_core::event::event_channel;
41/// use clawless_core::output::Output;
42///
43/// # #[tokio::main]
44/// # async fn main() {
45/// let (sender, mut receiver) = event_channel();
46/// let output = Output::new(sender);
47///
48/// output.message("hello").await.expect("should send");
49/// # }
50/// ```
51///
52/// [`artifact`]: Output::artifact
53/// [`detail`]: Output::detail
54/// [`message`]: Output::message
55// r[impl output.safety.clone]
56// r[impl output.safety.send]
57// r[impl output.safety.concurrent]
58#[derive(Clone, Debug)]
59pub struct Output {
60    /// Channel that carries the emitted events
61    sender: EventSender,
62}
63
64impl Output {
65    /// Creates a new `Output` wrapping the given event sender
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use clawless_core::event::event_channel;
71    /// use clawless_core::output::Output;
72    ///
73    /// let (sender, _receiver) = event_channel();
74    /// let output = Output::new(sender);
75    /// ```
76    pub fn new(sender: EventSender) -> Self {
77        Self { sender }
78    }
79
80    /// Sends an informational message event
81    ///
82    /// Converts the value to a string via [`Display`] and sends it as an [`Event::Message`].
83    ///
84    /// # Errors
85    ///
86    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use clawless_core::event::event_channel;
92    /// use clawless_core::output::Output;
93    ///
94    /// # #[tokio::main]
95    /// # async fn main() {
96    /// let (sender, _receiver) = event_channel();
97    /// let output = Output::new(sender);
98    ///
99    /// output.message("processing files").await.expect("should send");
100    /// output.message(format!("found {} items", 42)).await.expect("should send");
101    /// # }
102    /// ```
103    ///
104    /// [`EventReceiver`]: crate::event::EventReceiver
105    // r[impl output.send.message]
106    // r[impl output.send.async]
107    pub async fn message(&self, message: impl Display) -> Result<(), SendError> {
108        self.sender.send(Event::Message(message.to_string())).await
109    }
110
111    /// Sends a supplementary detail event
112    ///
113    /// Converts the value to a string via [`Display`] and sends it as an [`Event::Detail`].
114    /// Details carry lower-priority information that the Presenter may suppress at default
115    /// verbosity.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use clawless_core::event::event_channel;
125    /// use clawless_core::output::Output;
126    ///
127    /// # #[tokio::main]
128    /// # async fn main() {
129    /// let (sender, _receiver) = event_channel();
130    /// let output = Output::new(sender);
131    ///
132    /// output.detail("reading config from ~/.config/app.toml").await.expect("should send");
133    /// # }
134    /// ```
135    ///
136    /// [`EventReceiver`]: crate::event::EventReceiver
137    // r[impl output.send.detail]
138    pub async fn detail(&self, detail: impl Display) -> Result<(), SendError> {
139        self.sender.send(Event::Detail(detail.to_string())).await
140    }
141
142    /// Sends a structured artifact event
143    ///
144    /// Wraps the value in a [`Box`] and sends it as an [`Event::Artifact`]. The value must
145    /// implement [`Display`] (for text rendering), [`Serialize`] (for JSON rendering), and
146    /// [`Debug`] (for diagnostics).
147    ///
148    /// # Errors
149    ///
150    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use std::fmt;
156    ///
157    /// use serde::Serialize;
158    ///
159    /// use clawless_core::event::event_channel;
160    /// use clawless_core::output::Output;
161    ///
162    /// #[derive(Clone, Debug, Serialize)]
163    /// struct UserCount {
164    ///     count: usize,
165    /// }
166    ///
167    /// impl fmt::Display for UserCount {
168    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169    ///         write!(f, "{} users", self.count)
170    ///     }
171    /// }
172    ///
173    /// # #[tokio::main]
174    /// # async fn main() {
175    /// let (sender, _receiver) = event_channel();
176    /// let output = Output::new(sender);
177    ///
178    /// output.artifact(UserCount { count: 42 }).await.expect("should send");
179    /// # }
180    /// ```
181    ///
182    /// [`EventReceiver`]: crate::event::EventReceiver
183    // r[impl output.send.artifact]
184    pub async fn artifact<T>(&self, value: T) -> Result<(), SendError>
185    where
186        T: Display + Serialize + Debug + Send + Sync + 'static,
187    {
188        self.sender.send(Event::Artifact(Box::new(value))).await
189    }
190
191    /// Sends one step in the run of an external program
192    ///
193    /// [`Process`] calls this for every step of a run, so a command that runs a program through
194    /// Clawless does not call it. An application that drives a program itself, and wants its
195    /// output to reach the presenter in the same shape, reports the run through this method.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use clawless_core::event::event_channel;
205    /// use clawless_core::event::process::{ProcessEvent, RunId};
206    /// use clawless_core::output::Output;
207    /// use clawless_core::process::Invocation;
208    ///
209    /// # #[tokio::main]
210    /// # async fn main() {
211    /// let (sender, _receiver) = event_channel();
212    /// let output = Output::new(sender);
213    ///
214    /// output
215    ///     .process_event(ProcessEvent::Started {
216    ///         id: RunId::next(),
217    ///         invocation: Invocation::new("git").arg("status"),
218    ///         process_id: None,
219    ///     })
220    ///     .await
221    ///     .expect("should send");
222    /// # }
223    /// ```
224    ///
225    /// [`EventReceiver`]: crate::event::EventReceiver
226    /// [`Process`]: crate::process::Process
227    // r[impl output.send.process]
228    pub async fn process_event(&self, event: ProcessEvent) -> Result<(), SendError> {
229        self.sender.send(Event::Process(Box::new(event))).await
230    }
231
232    /// Sends a question for the user
233    ///
234    /// The request arrives after every event that this output sent before it. The answer
235    /// arrives at the [`PendingAnswer`] that was created together with the request.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`SendError`] if the [`EventReceiver`] has been dropped.
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use clawless_core::event::event_channel;
245    /// use clawless_core::event::prompt::PromptRequest;
246    /// use clawless_core::output::Output;
247    /// use clawless_core::prompt::Confirm;
248    ///
249    /// # #[tokio::main]
250    /// # async fn main() {
251    /// let (sender, _receiver) = event_channel();
252    /// let output = Output::new(sender);
253    /// let (request, _pending) = PromptRequest::confirm(Confirm::new("Delete the branch?"));
254    ///
255    /// output.prompt(request).await.expect("should send");
256    /// # }
257    /// ```
258    ///
259    /// [`EventReceiver`]: crate::event::EventReceiver
260    /// [`PendingAnswer`]: crate::event::prompt::PendingAnswer
261    pub async fn prompt(&self, request: PromptRequest) -> Result<(), SendError> {
262        self.sender.send(Event::Prompt(Box::new(request))).await
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    // An assertion in a test panics by design. A `# Panics` section on every test
269    // would repeat that and give the reader no information.
270    #![allow(clippy::missing_panics_doc)]
271
272    use std::fmt;
273
274    use serde::Serialize;
275
276    use super::*;
277    use crate::event::event_channel;
278    use crate::event::process::RunId;
279    use crate::process::Invocation;
280    use crate::prompt::Confirm;
281
282    #[derive(Clone, Debug, Serialize)]
283    struct TestArtifact {
284        value: String,
285    }
286
287    impl fmt::Display for TestArtifact {
288        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289            write!(f, "{}", self.value)
290        }
291    }
292
293    // r[verify output.send.artifact]
294    #[tokio::test]
295    async fn artifact_sends_artifact_event() {
296        let (sender, mut receiver) = event_channel();
297        let output = Output::new(sender);
298
299        output
300            .artifact(TestArtifact {
301                value: "result".to_string(),
302            })
303            .await
304            .expect("should send");
305
306        let event = receiver.recv().await.expect("should receive");
307
308        assert!(matches!(event, Event::Artifact(ref a) if a.to_string() == "result"));
309    }
310
311    #[tokio::test]
312    async fn artifact_to_closed_channel_returns_error() {
313        let (sender, receiver) = event_channel();
314        let output = Output::new(sender);
315        drop(receiver);
316
317        let error = output
318            .artifact(TestArtifact {
319                value: "lost".to_string(),
320            })
321            .await
322            .expect_err("should fail");
323
324        assert_eq!(error.to_string(), "event channel closed");
325    }
326
327    // r[verify output.safety.clone]
328    #[tokio::test]
329    async fn clone_produces_independent_handle() {
330        let (sender, mut receiver) = event_channel();
331        let output = Output::new(sender);
332        let cloned = output.clone();
333
334        output.message("from original").await.expect("should send");
335        cloned.message("from clone").await.expect("should send");
336
337        let first = receiver.recv().await.expect("should receive first");
338        let second = receiver.recv().await.expect("should receive second");
339
340        assert!(matches!(first, Event::Message(ref s) if s == "from original"));
341        assert!(matches!(second, Event::Message(ref s) if s == "from clone"));
342    }
343
344    // r[verify output.send.process]
345    #[tokio::test]
346    async fn process_event_sends_process_event() {
347        let (sender, mut receiver) = event_channel();
348        let output = Output::new(sender);
349        let invocation = Invocation::new("git").arg("status");
350
351        output
352            .process_event(ProcessEvent::Started {
353                id: RunId::next(),
354                invocation: invocation.clone(),
355                process_id: None,
356            })
357            .await
358            .expect("should send");
359
360        assert_eq!(
361            receiver.recv().await.map(|event| match event {
362                Event::Process(event) => event.to_string(),
363                Event::Message(text) => text,
364                Event::Detail(text) => text,
365                Event::Artifact(artifact) => artifact.to_string(),
366                Event::Prompt(request) => format!("{request:?}"),
367            }),
368            Some(format!("$ {invocation}"))
369        );
370    }
371
372    #[tokio::test]
373    async fn prompt_sends_prompt_event() {
374        let (sender, mut receiver) = event_channel();
375        let output = Output::new(sender);
376        let (request, _pending) = PromptRequest::confirm(Confirm::new("Release?"));
377
378        output.prompt(request).await.expect("should send");
379
380        assert_eq!(
381            receiver.recv().await.map(|event| match event {
382                Event::Prompt(request) => request.question().to_owned(),
383                Event::Process(event) => event.to_string(),
384                Event::Message(text) => text,
385                Event::Detail(text) => text,
386                Event::Artifact(artifact) => artifact.to_string(),
387            }),
388            Some("Release?".to_owned())
389        );
390    }
391
392    // r[verify output.send.detail]
393    #[tokio::test]
394    async fn detail_sends_detail_event() {
395        let (sender, mut receiver) = event_channel();
396        let output = Output::new(sender);
397
398        output
399            .detail("supplementary info")
400            .await
401            .expect("should send");
402
403        let event = receiver.recv().await.expect("should receive");
404
405        assert!(matches!(event, Event::Detail(ref s) if s == "supplementary info"));
406    }
407
408    #[tokio::test]
409    async fn detail_to_closed_channel_returns_error() {
410        let (sender, receiver) = event_channel();
411        let output = Output::new(sender);
412        drop(receiver);
413
414        let error = output.detail("lost").await.expect_err("should fail");
415
416        assert_eq!(error.to_string(), "event channel closed");
417    }
418
419    // r[verify output.send.message]
420    // r[verify output.send.async]
421    #[tokio::test]
422    async fn message_sends_message_event() {
423        let (sender, mut receiver) = event_channel();
424        let output = Output::new(sender);
425
426        output.message("hello").await.expect("should send");
427
428        let event = receiver.recv().await.expect("should receive");
429
430        assert!(matches!(event, Event::Message(ref s) if s == "hello"));
431    }
432
433    // r[verify output.send.error]
434    #[tokio::test]
435    async fn message_to_closed_channel_returns_error() {
436        let (sender, receiver) = event_channel();
437        let output = Output::new(sender);
438        drop(receiver);
439
440        let error = output.message("lost").await.expect_err("should fail");
441
442        assert_eq!(error.to_string(), "event channel closed");
443    }
444
445    #[tokio::test]
446    async fn message_with_format_args_sends_formatted_string() {
447        let (sender, mut receiver) = event_channel();
448        let output = Output::new(sender);
449
450        output
451            .message(format!("count: {}", 42))
452            .await
453            .expect("should send");
454
455        let event = receiver.recv().await.expect("should receive");
456
457        assert!(matches!(event, Event::Message(ref s) if s == "count: 42"));
458    }
459
460    #[test]
461    fn trait_send() {
462        fn assert_send<T: Send>() {}
463        assert_send::<Output>();
464    }
465
466    #[test]
467    fn trait_sync() {
468        fn assert_sync<T: Sync>() {}
469        assert_sync::<Output>();
470    }
471
472    #[test]
473    fn trait_unpin() {
474        fn assert_unpin<T: Unpin>() {}
475        assert_unpin::<Output>();
476    }
477}