orcs_runtime/io/renderer.rs
1//! Console Renderer - View layer implementation for terminal output.
2//!
3//! The [`ConsoleRenderer`] receives [`IOOutput`] from the Bridge layer
4//! and renders it to the terminal using tracing and eprintln.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌───────────────────────────────────────────────────────────────┐
10//! │ IOBridge (Bridge) │
11//! │ │ │
12//! │ IOPort.output_tx │
13//! └───────────────────────────┼───────────────────────────────────┘
14//! │ IOOutput
15//! ▼
16//! ┌───────────────────────────────────────────────────────────────┐
17//! │ View Layer │
18//! │ ┌─────────────────────────────────────────────────────────┐ │
19//! │ │ IOOutputHandle │ │
20//! │ │ (receives IOOutput) │ │
21//! │ └─────────────────────────┬───────────────────────────────┘ │
22//! │ │ │
23//! │ ┌─────────────────────────▼───────────────────────────────┐ │
24//! │ │ ConsoleRenderer │ │
25//! │ │ - render_output() → eprintln! / tracing │ │
26//! │ │ - run() → background task │ │
27//! │ └─────────────────────────────────────────────────────────┘ │
28//! └───────────────────────────────────────────────────────────────┘
29//! ```
30//!
31//! # Example
32//!
33//! ```no_run
34//! use orcs_runtime::io::{IOPort, IOOutputHandle, ConsoleRenderer};
35//! use orcs_types::ChannelId;
36//!
37//! #[tokio::main]
38//! async fn main() {
39//! let channel_id = ChannelId::new();
40//! let (port, input_handle, output_handle) = IOPort::with_defaults(channel_id);
41//!
42//! // Spawn renderer task
43//! let renderer = ConsoleRenderer::new();
44//! tokio::spawn(renderer.run(output_handle));
45//!
46//! // Bridge layer sends output via port
47//! // Renderer receives and displays
48//! }
49//! ```
50
51use super::types::{IOOutput, OutputStyle};
52use super::IOOutputHandle;
53
54/// Console renderer for terminal output.
55///
56/// Renders [`IOOutput`] messages to the terminal using appropriate
57/// formatting and logging.
58pub struct ConsoleRenderer {
59 /// Show debug messages.
60 verbose: bool,
61}
62
63impl ConsoleRenderer {
64 /// Creates a new console renderer.
65 #[must_use]
66 pub fn new() -> Self {
67 Self { verbose: false }
68 }
69
70 /// Creates a console renderer with verbose mode.
71 #[must_use]
72 pub fn verbose() -> Self {
73 Self { verbose: true }
74 }
75
76 /// Sets verbose mode.
77 pub fn set_verbose(&mut self, verbose: bool) {
78 self.verbose = verbose;
79 }
80
81 /// Returns `true` if verbose mode is enabled.
82 #[must_use]
83 pub fn is_verbose(&self) -> bool {
84 self.verbose
85 }
86
87 /// Renders a single output to the console.
88 ///
89 /// This is the core rendering logic that can be called directly
90 /// or used within the `run` loop.
91 pub fn render_output(&self, output: &IOOutput) {
92 match output {
93 IOOutput::Print { text, style } => {
94 self.render_styled_text(text, *style);
95 }
96 IOOutput::Prompt { message } => {
97 eprint!("{} ", message);
98 }
99 IOOutput::ShowApprovalRequest {
100 id,
101 operation,
102 description,
103 } => {
104 tracing::info!(
105 target: "hil",
106 approval_id = %id,
107 operation = %operation,
108 "Awaiting approval: {}",
109 description
110 );
111 eprintln!();
112 eprintln!(" [{}] {} - {}", operation, id, description);
113 eprintln!(" Enter 'y' to approve, 'n' to reject:");
114 }
115 IOOutput::ShowApproved { approval_id } => {
116 tracing::info!(target: "hil", approval_id = %approval_id, "Approved");
117 eprintln!(" \u{2713} Approved: {}", approval_id);
118 }
119 IOOutput::ShowRejected {
120 approval_id,
121 reason,
122 } => {
123 if let Some(reason) = reason {
124 tracing::info!(target: "hil", approval_id = %approval_id, reason = %reason, "Rejected");
125 eprintln!(" \u{2717} Rejected: {} ({})", approval_id, reason);
126 } else {
127 tracing::info!(target: "hil", approval_id = %approval_id, "Rejected");
128 eprintln!(" \u{2717} Rejected: {}", approval_id);
129 }
130 }
131 IOOutput::ShowProcessing {
132 component,
133 operation,
134 } => {
135 use std::io::Write;
136 let mut out = std::io::stderr().lock();
137 let _ = writeln!(out, " [{component}] Processing ({operation})...");
138 let _ = out.flush();
139 }
140 IOOutput::Clear => {
141 // ANSI escape code to clear screen
142 eprint!("\x1B[2J\x1B[1;1H");
143 }
144 }
145 }
146
147 /// Renders styled text to the console.
148 ///
149 /// Normal output goes to stdout (with flush); warnings and errors go to stderr.
150 fn render_styled_text(&self, text: &str, style: OutputStyle) {
151 use std::io::Write;
152 match style {
153 OutputStyle::Normal | OutputStyle::Info => {
154 let mut out = std::io::stdout().lock();
155 let _ = writeln!(out, "{}", text);
156 let _ = out.flush();
157 }
158 OutputStyle::Warn => {
159 eprintln!("[WARN] {}", text);
160 }
161 OutputStyle::Error => {
162 eprintln!("[ERROR] {}", text);
163 }
164 OutputStyle::Success => {
165 let mut out = std::io::stdout().lock();
166 let _ = writeln!(out, "\x1B[32m{}\x1B[0m", text);
167 let _ = out.flush();
168 }
169 OutputStyle::Debug => {
170 if self.verbose {
171 tracing::debug!("{}", text);
172 }
173 }
174 }
175 }
176
177 /// Runs the renderer loop, consuming output from the handle.
178 ///
179 /// This is typically spawned as a background task.
180 /// The loop runs until the handle is closed (IOPort dropped).
181 ///
182 /// # Example
183 ///
184 /// ```no_run
185 /// use orcs_runtime::io::{IOPort, ConsoleRenderer};
186 /// use orcs_types::ChannelId;
187 ///
188 /// #[tokio::main]
189 /// async fn main() {
190 /// let channel_id = ChannelId::new();
191 /// let (port, _input_handle, output_handle) = IOPort::with_defaults(channel_id);
192 ///
193 /// let renderer = ConsoleRenderer::new();
194 /// tokio::spawn(renderer.run(output_handle));
195 /// }
196 /// ```
197 pub async fn run(self, mut output_handle: IOOutputHandle) {
198 while let Some(output) = output_handle.recv().await {
199 self.render_output(&output);
200 }
201 }
202
203 /// Drains and renders all available output without blocking.
204 ///
205 /// Returns the number of outputs rendered.
206 pub fn drain_and_render(&self, output_handle: &mut IOOutputHandle) -> usize {
207 let outputs = output_handle.drain();
208 for output in &outputs {
209 self.render_output(output);
210 }
211 outputs.len()
212 }
213}
214
215impl Default for ConsoleRenderer {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221impl std::fmt::Debug for ConsoleRenderer {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct("ConsoleRenderer")
224 .field("verbose", &self.verbose)
225 .finish()
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn renderer_creation() {
235 let renderer = ConsoleRenderer::new();
236 assert!(!renderer.is_verbose());
237
238 let renderer = ConsoleRenderer::verbose();
239 assert!(renderer.is_verbose());
240 }
241
242 #[test]
243 fn renderer_set_verbose() {
244 let mut renderer = ConsoleRenderer::new();
245 assert!(!renderer.is_verbose());
246
247 renderer.set_verbose(true);
248 assert!(renderer.is_verbose());
249 }
250
251 #[test]
252 fn renderer_default() {
253 let renderer = ConsoleRenderer::default();
254 assert!(!renderer.is_verbose());
255 }
256
257 #[test]
258 fn renderer_debug() {
259 let renderer = ConsoleRenderer::new();
260 let debug_str = format!("{:?}", renderer);
261 assert!(debug_str.contains("ConsoleRenderer"));
262 assert!(debug_str.contains("verbose"));
263 }
264
265 // Note: Actually testing render_output would require capturing stderr,
266 // which is complex. The important thing is that the code compiles and
267 // the logic is exercised through integration tests.
268
269 #[tokio::test]
270 async fn renderer_drain_and_render() {
271 use crate::io::IOPort;
272 use orcs_types::ChannelId;
273
274 let channel_id = ChannelId::new();
275 let (port, _input_handle, mut output_handle) = IOPort::with_defaults(channel_id);
276
277 // Send some outputs
278 port.send(IOOutput::info("test1"))
279 .await
280 .expect("send first output should succeed");
281 port.send(IOOutput::info("test2"))
282 .await
283 .expect("send second output should succeed");
284
285 // Small delay to ensure messages are received
286 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
287
288 let renderer = ConsoleRenderer::new();
289 let count = renderer.drain_and_render(&mut output_handle);
290 assert_eq!(count, 2);
291 }
292}