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}