Skip to main content

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}