Skip to main content

orcs_runtime/io/
console.rs

1//! Console I/O - Complete terminal interaction implementation.
2//!
3//! Provides [`Console`] which integrates:
4//! - [`ConsoleInputReader`] - stdin reading with signal handling
5//! - [`ConsoleRenderer`] - terminal output rendering
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌───────────────────────────────────────────────────────────────┐
11//! │                         Console                                │
12//! │  ┌─────────────────────────────────────────────────────────┐  │
13//! │  │               ConsoleInputReader                         │  │
14//! │  │  - Reads stdin line by line                              │  │
15//! │  │  - Handles Ctrl+C → SignalKind::Veto                     │  │
16//! │  │  - Sends IOInput to IOInputHandle                        │  │
17//! │  └─────────────────────────────────────────────────────────┘  │
18//! │                              │                                  │
19//! │                       IOInputHandle                             │
20//! │                              │                                  │
21//! │  ┌─────────────────────────────────────────────────────────┐  │
22//! │  │               ConsoleRenderer                            │  │
23//! │  │  - Receives IOOutput from IOOutputHandle                 │  │
24//! │  │  - Renders to terminal with styling                      │  │
25//! │  └─────────────────────────────────────────────────────────┘  │
26//! └───────────────────────────────────────────────────────────────┘
27//! ```
28//!
29//! # Example
30//!
31//! ```no_run
32//! use orcs_runtime::io::{Console, IOPort};
33//! use orcs_runtime::components::IOBridge;
34//! use orcs_types::ChannelId;
35//!
36//! #[tokio::main]
37//! async fn main() {
38//!     let channel_id = ChannelId::new();
39//!
40//!     // Create IO port and handles
41//!     let (port, input_handle, output_handle) = IOPort::with_defaults(channel_id);
42//!
43//!     // Create console with handles
44//!     let console = Console::new(input_handle, output_handle);
45//!
46//!     // Create IOBridge (Bridge) - principal is now provided when calling methods
47//!     let bridge = IOBridge::new(port);
48//!
49//!     // Run console (spawns input reader and renderer tasks)
50//!     console.run().await;
51//! }
52//! ```
53
54use super::renderer::ConsoleRenderer;
55use super::types::IOInput;
56use super::{IOInputHandle, IOOutputHandle};
57use orcs_event::SignalKind;
58use std::io::BufRead;
59use tokio::sync::mpsc;
60
61/// Console input reader.
62///
63/// Reads lines from stdin and sends them to the IOInputHandle.
64/// Also handles Ctrl+C to send Veto signals.
65pub struct ConsoleInputReader {
66    input_handle: IOInputHandle,
67}
68
69impl ConsoleInputReader {
70    /// Creates a new console input reader.
71    #[must_use]
72    pub fn new(input_handle: IOInputHandle) -> Self {
73        Self { input_handle }
74    }
75
76    /// Runs the input reader loop.
77    ///
78    /// This is a blocking operation that reads from stdin.
79    /// Should be spawned in a blocking task.
80    ///
81    /// Returns when stdin is closed or an error occurs.
82    pub fn run_blocking(self) {
83        let stdin = std::io::stdin();
84        let handle = stdin.lock();
85
86        for line in handle.lines() {
87            match line {
88                Ok(line) => {
89                    let input = IOInput::line(line);
90                    // Use blocking send since we're in a sync context
91                    if self.input_handle.try_send(input).is_err() {
92                        // Channel closed, exit
93                        break;
94                    }
95                }
96                Err(_) => {
97                    // EOF or error, send Eof and exit
98                    let _ = self.input_handle.try_send(IOInput::Eof);
99                    break;
100                }
101            }
102        }
103    }
104
105    /// Runs the input reader as an async task.
106    ///
107    /// Spawns a blocking task internally to read from stdin.
108    pub async fn run(self) {
109        // Spawn blocking task for stdin reading
110        tokio::task::spawn_blocking(move || {
111            self.run_blocking();
112        })
113        .await
114        .ok();
115    }
116
117    /// Sends a Veto signal.
118    ///
119    /// Called when Ctrl+C is detected.
120    pub async fn send_veto(&self) -> Result<(), mpsc::error::SendError<IOInput>> {
121        self.input_handle
122            .send(IOInput::Signal(SignalKind::Veto))
123            .await
124    }
125
126    /// Returns `true` if the channel is closed.
127    #[must_use]
128    pub fn is_closed(&self) -> bool {
129        self.input_handle.is_closed()
130    }
131}
132
133impl std::fmt::Debug for ConsoleInputReader {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct("ConsoleInputReader")
136            .field("channel_id", &self.input_handle.channel_id())
137            .finish_non_exhaustive()
138    }
139}
140
141/// Complete console I/O facade.
142///
143/// Integrates input reading and output rendering for terminal interaction.
144pub struct Console {
145    input_handle: IOInputHandle,
146    output_handle: IOOutputHandle,
147    renderer: ConsoleRenderer,
148}
149
150impl Console {
151    /// Creates a new console.
152    #[must_use]
153    pub fn new(input_handle: IOInputHandle, output_handle: IOOutputHandle) -> Self {
154        Self {
155            input_handle,
156            output_handle,
157            renderer: ConsoleRenderer::new(),
158        }
159    }
160
161    /// Creates a console with verbose output.
162    #[must_use]
163    pub fn verbose(input_handle: IOInputHandle, output_handle: IOOutputHandle) -> Self {
164        Self {
165            input_handle,
166            output_handle,
167            renderer: ConsoleRenderer::verbose(),
168        }
169    }
170
171    /// Sets verbose mode for the renderer.
172    pub fn set_verbose(&mut self, verbose: bool) {
173        self.renderer.set_verbose(verbose);
174    }
175
176    /// Runs the console.
177    ///
178    /// This starts:
179    /// 1. Input reader task (reads stdin, sends to IOInputHandle)
180    /// 2. Renderer task (receives from IOOutputHandle, renders to terminal)
181    ///
182    /// Returns when both tasks complete (typically when the channel is closed).
183    pub async fn run(self) {
184        let input_reader = ConsoleInputReader::new(self.input_handle);
185        let renderer = self.renderer;
186        let output_handle = self.output_handle;
187
188        // Run input reader and renderer concurrently
189        tokio::join!(input_reader.run(), renderer.run(output_handle),);
190    }
191
192    /// Spawns the console as background tasks.
193    ///
194    /// Returns handles to the spawned tasks.
195    ///
196    /// # Returns
197    ///
198    /// A tuple of (input_task, renderer_task) JoinHandles.
199    pub fn spawn(self) -> (tokio::task::JoinHandle<()>, tokio::task::JoinHandle<()>) {
200        let input_reader = ConsoleInputReader::new(self.input_handle);
201        let renderer = self.renderer;
202        let output_handle = self.output_handle;
203
204        let input_task = tokio::spawn(async move {
205            input_reader.run().await;
206        });
207
208        let renderer_task = tokio::spawn(async move {
209            renderer.run(output_handle).await;
210        });
211
212        (input_task, renderer_task)
213    }
214
215    /// Splits the console into its components.
216    ///
217    /// Useful when you need separate control over input and output.
218    #[must_use]
219    pub fn split(self) -> (ConsoleInputReader, ConsoleRenderer, IOOutputHandle) {
220        (
221            ConsoleInputReader::new(self.input_handle),
222            self.renderer,
223            self.output_handle,
224        )
225    }
226}
227
228impl std::fmt::Debug for Console {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        f.debug_struct("Console")
231            .field("channel_id", &self.input_handle.channel_id())
232            .field("verbose", &self.renderer.is_verbose())
233            .finish_non_exhaustive()
234    }
235}
236
237/// Ctrl+C handler that sends Veto signal.
238///
239/// Sets up a Ctrl+C handler that sends a Veto signal through the input handle.
240///
241/// # Example
242///
243/// ```no_run
244/// use orcs_runtime::io::{IOPort, setup_ctrlc_handler};
245/// use orcs_types::ChannelId;
246///
247/// #[tokio::main]
248/// async fn main() {
249///     let channel_id = ChannelId::new();
250///     let (_port, input_handle, _output_handle) = IOPort::with_defaults(channel_id);
251///
252///     setup_ctrlc_handler(input_handle.clone());
253///
254///     // Ctrl+C will now send Veto signal through the input handle
255/// }
256/// ```
257pub fn setup_ctrlc_handler(input_handle: IOInputHandle) {
258    // Note: This requires the ctrlc crate or similar
259    // For now, we document the pattern but don't implement the actual handler
260    // since it requires additional dependencies
261
262    // Example implementation with ctrlc crate:
263    // ctrlc::set_handler(move || {
264    //     let _ = input_handle.try_send(IOInput::Signal(SignalKind::Veto));
265    // }).expect("Error setting Ctrl-C handler");
266
267    // Using tokio signal handling:
268    let handle = input_handle;
269    tokio::spawn(async move {
270        while tokio::signal::ctrl_c().await.is_ok() {
271            // Send Veto signal
272            if handle.try_send(IOInput::Signal(SignalKind::Veto)).is_err() {
273                // Channel closed, exit handler
274                break;
275            }
276        }
277    });
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::io::IOPort;
284    use orcs_types::ChannelId;
285
286    #[test]
287    fn console_creation() {
288        let channel_id = ChannelId::new();
289        let (_, input_handle, output_handle) = IOPort::with_defaults(channel_id);
290
291        let console = Console::new(input_handle, output_handle);
292        assert!(!console.renderer.is_verbose());
293    }
294
295    #[test]
296    fn console_verbose() {
297        let channel_id = ChannelId::new();
298        let (_, input_handle, output_handle) = IOPort::with_defaults(channel_id);
299
300        let console = Console::verbose(input_handle, output_handle);
301        assert!(console.renderer.is_verbose());
302    }
303
304    #[test]
305    fn console_set_verbose() {
306        let channel_id = ChannelId::new();
307        let (_, input_handle, output_handle) = IOPort::with_defaults(channel_id);
308
309        let mut console = Console::new(input_handle, output_handle);
310        assert!(!console.renderer.is_verbose());
311
312        console.set_verbose(true);
313        assert!(console.renderer.is_verbose());
314    }
315
316    #[test]
317    fn console_split() {
318        let channel_id = ChannelId::new();
319        // NOTE: `_port` must be kept alive. Using `_` would drop IOPort immediately,
320        // closing the channel and causing `input_reader.is_closed()` to return true.
321        let (_port, input_handle, output_handle) = IOPort::with_defaults(channel_id);
322
323        let console = Console::new(input_handle, output_handle);
324        let (input_reader, renderer, _) = console.split();
325
326        assert!(!input_reader.is_closed());
327        assert!(!renderer.is_verbose());
328    }
329
330    #[test]
331    fn console_debug() {
332        let channel_id = ChannelId::new();
333        let (_, input_handle, output_handle) = IOPort::with_defaults(channel_id);
334
335        let console = Console::new(input_handle, output_handle);
336        let debug_str = format!("{:?}", console);
337        assert!(debug_str.contains("Console"));
338    }
339
340    #[test]
341    fn input_reader_debug() {
342        let channel_id = ChannelId::new();
343        let (_port, input_handle, _output_handle) = IOPort::with_defaults(channel_id);
344
345        let reader = ConsoleInputReader::new(input_handle);
346        let debug_str = format!("{:?}", reader);
347        assert!(debug_str.contains("ConsoleInputReader"));
348    }
349
350    #[tokio::test]
351    async fn input_reader_closed_detection() {
352        let channel_id = ChannelId::new();
353        let (port, input_handle, _output_handle) = IOPort::with_defaults(channel_id);
354
355        let reader = ConsoleInputReader::new(input_handle);
356        assert!(!reader.is_closed());
357
358        // Drop port to close channel
359        drop(port);
360
361        assert!(reader.is_closed());
362    }
363
364    #[tokio::test]
365    async fn send_veto() {
366        let channel_id = ChannelId::new();
367        let (mut port, input_handle, _output_handle) = IOPort::with_defaults(channel_id);
368
369        let reader = ConsoleInputReader::new(input_handle);
370        reader
371            .send_veto()
372            .await
373            .expect("send veto signal should succeed");
374
375        let input = port.recv().await.expect("should receive veto input");
376        assert!(matches!(input, IOInput::Signal(SignalKind::Veto)));
377    }
378}