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}