clawless_core/event/mod.rs
1//! Event types for structured command output
2//!
3//! This module defines [`Event`], the structured message type that commands produce and the
4//! Presenter consumes. Events decouple output production from rendering: a command emits events
5//! through Output, and the Presenter decides how to render them based on its output mode and
6//! verbosity settings.
7//!
8//! The [`Artifact`] trait enables the [`Event::Artifact`] variant to carry a type-erased value that
9//! supports both text rendering ([`Display`]) and JSON serialization ([`Serialize`]). A blanket
10//! implementation covers any type satisfying the required bounds, so command authors derive the
11//! usual traits and pass values to Output without manual trait implementation.
12//!
13//! The [`Event::Process`] variant carries the [`ProcessEvent`] of an external program that a
14//! command runs. Those events arrive while the program runs, which lets a presenter show progress
15//! instead of waiting for the program to end.
16//!
17//! The [`Event::Prompt`] variant carries a [`PromptRequest`], which is a question for the user.
18//! The request arrives after the events that the command sent before it.
19//!
20//! [`ProcessEvent`]: process::ProcessEvent
21//! [`PromptRequest`]: prompt::PromptRequest
22
23use std::fmt::{Debug, Display};
24
25use serde::Serialize;
26
27pub use self::channel::{SendError, event_channel};
28use self::process::ProcessEvent;
29use self::prompt::PromptRequest;
30pub use self::receiver::EventReceiver;
31pub use self::sender::EventSender;
32
33pub mod process;
34pub mod prompt;
35
36/// Bounded channel that carries events from a command to its presenter
37mod channel;
38/// The half of the event channel that reads events
39mod receiver;
40/// The half of the event channel that sends events
41mod sender;
42
43/// Trait for artifact values that can be rendered as text or JSON
44///
45/// `Artifact` combines [`Display`] (for text rendering), [`Serialize`] (for JSON rendering), and
46/// [`Debug`] (for diagnostics). The Presenter uses the appropriate trait based on its output mode.
47///
48/// Command authors do not implement this trait directly. A blanket implementation covers any type
49/// that satisfies the required bounds.
50///
51/// # Examples
52///
53/// ```
54/// use std::fmt;
55///
56/// use serde::Serialize;
57///
58/// #[derive(Clone, Debug, Serialize)]
59/// struct UserCount {
60/// count: usize,
61/// }
62///
63/// impl fmt::Display for UserCount {
64/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65/// write!(f, "{} users", self.count)
66/// }
67/// }
68///
69/// // UserCount automatically implements Artifact — no manual impl needed.
70/// let artifact: Box<dyn clawless_core::event::Artifact> = Box::new(UserCount { count: 42 });
71/// assert_eq!(artifact.to_string(), "42 users");
72/// ```
73///
74// r[impl event.artifact.structured]
75// r[impl event.artifact.text]
76pub trait Artifact: Display + Debug + Send + Sync + erased_serde::Serialize {}
77
78// r[impl event.artifact.zero-cost]
79impl<T> Artifact for T where T: Display + Serialize + Debug + Send + Sync + 'static {}
80
81erased_serde::serialize_trait_object!(Artifact);
82
83/// Structured output event produced by commands
84///
85/// An `Event` represents a single piece of output that a command has produced. Events travel from
86/// the producer through an async channel to the Presenter, decoupling production from rendering.
87///
88/// Five variants:
89///
90/// - [`Message`] — informational text (shown at default verbosity and above).
91/// - [`Detail`] — supplementary text (shown only at verbose verbosity).
92/// - [`Event::Artifact`] — the primary data a command produces, carried as a trait object that the
93/// Presenter can render via [`Display`] or [`Serialize`].
94/// - [`Event::Process`] — one step in the run of an external program that the command started.
95/// - [`Event::Prompt`] — a question for the user, with the reply that answers it.
96///
97/// The Presenter decides which events to render based on its verbosity setting. Verbosity does
98/// not apply to a prompt, because the command waits for the answer. A presenter that cannot ask
99/// the user drops the request.
100///
101/// [`Detail`]: Event::Detail
102/// [`Message`]: Event::Message
103// r[impl event.safety.event-send]
104#[derive(Debug)]
105pub enum Event {
106 /// Informational message
107 // r[impl event.output.message]
108 Message(String),
109 /// Supplementary detail
110 // r[impl event.output.detail]
111 Detail(String),
112 /// Primary command output
113 // r[impl event.output.artifact]
114 Artifact(Box<dyn Artifact>),
115 /// One step in the run of an external program
116 ///
117 /// The event is boxed so that the size of an `Event` stays that of its text variants. A run
118 /// names its command in the events that start and end it, and an unboxed variant would make
119 /// every message in the channel as large as that name.
120 // r[impl event.output.process]
121 Process(Box<ProcessEvent>),
122 /// A question for the user
123 ///
124 /// The request is boxed so that the size of an `Event` stays that of its text variants.
125 Prompt(Box<PromptRequest>),
126}
127
128#[cfg(test)]
129mod tests {
130 // An assertion in a test panics by design. A `# Panics` section on every test
131 // would repeat that and give the reader no information.
132 #![allow(clippy::missing_panics_doc)]
133
134 use std::fmt;
135
136 use serde::Serialize;
137
138 use super::*;
139 use crate::event::process::RunId;
140 use crate::process::Invocation;
141 use crate::prompt::Confirm;
142
143 #[derive(Clone, Debug, Serialize)]
144 struct TestArtifact {
145 value: String,
146 }
147
148 impl fmt::Display for TestArtifact {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}", self.value)
151 }
152 }
153
154 fn test_artifact() -> TestArtifact {
155 TestArtifact {
156 value: "hello".to_string(),
157 }
158 }
159
160 #[test]
161 fn artifact_debug_delegates_to_inner_type() {
162 let event = Event::Artifact(Box::new(test_artifact()));
163
164 let debug = format!("{event:?}");
165
166 assert!(debug.contains("hello"));
167 }
168
169 // r[verify event.artifact.text]
170 #[test]
171 fn artifact_display_renders_via_display_trait() {
172 let boxed: Box<dyn Artifact> = Box::new(test_artifact());
173
174 let display = boxed.to_string();
175
176 assert_eq!(display, "hello");
177 }
178
179 // r[verify event.artifact.structured]
180 #[test]
181 fn artifact_serializes_via_erased_serde() {
182 let boxed: Box<dyn Artifact> = Box::new(test_artifact());
183
184 let json = serde_json::to_string(&boxed).expect("should serialize");
185
186 assert_eq!(json, r#"{"value":"hello"}"#);
187 }
188
189 // r[verify event.output.detail]
190 #[test]
191 fn detail_with_empty_string_is_valid() {
192 let event = Event::Detail(String::new());
193
194 let debug = format!("{event:?}");
195
196 assert!(debug.contains("Detail"));
197 }
198
199 // r[verify event.output.message]
200 #[test]
201 fn message_with_empty_string_is_valid() {
202 let event = Event::Message(String::new());
203
204 let debug = format!("{event:?}");
205
206 assert!(debug.contains("Message"));
207 }
208
209 // r[verify event.output.process]
210 #[test]
211 fn process_carries_the_event_of_a_run() {
212 let id = RunId::next();
213
214 let event = Event::Process(Box::new(ProcessEvent::Started {
215 id,
216 invocation: Invocation::new("git"),
217 process_id: None,
218 }));
219
220 assert_eq!(
221 match event {
222 Event::Process(event) => Some(event.id()),
223 Event::Message(_) => None,
224 Event::Detail(_) => None,
225 Event::Artifact(_) => None,
226 Event::Prompt(_) => None,
227 },
228 Some(id)
229 );
230 }
231
232 #[test]
233 fn prompt_carries_the_request_of_a_command() {
234 let (request, _pending) = PromptRequest::confirm(Confirm::new("Release?"));
235
236 let event = Event::Prompt(Box::new(request));
237
238 assert_eq!(
239 match event {
240 Event::Prompt(request) => Some(request.question().to_owned()),
241 Event::Message(_) => None,
242 Event::Detail(_) => None,
243 Event::Artifact(_) => None,
244 Event::Process(_) => None,
245 },
246 Some("Release?".to_owned())
247 );
248 }
249
250 // r[verify event.safety.event-send]
251 #[test]
252 fn trait_send() {
253 fn assert_send<T: Send>() {}
254 assert_send::<Event>();
255 }
256
257 #[test]
258 fn trait_sync() {
259 fn assert_sync<T: Sync>() {}
260 assert_sync::<Event>();
261 }
262
263 #[test]
264 fn trait_unpin() {
265 fn assert_unpin<T: Unpin>() {}
266 assert_unpin::<Event>();
267 }
268}