umsh_cli/
io.rs

1//! Transport-agnostic line I/O for the CLI.
2//!
3//! Split into two halves — [`CliInput`] for line-by-line reading, and
4//! [`CliOutput`] for writing + flushing. Splitting the halves lets the
5//! [`CliSession::run`](crate::CliSession::run) loop hold a long-lived
6//! read future across `select!` iterations while the session simultaneously
7//! uses the output half to service inbound events. A unified trait would
8//! force the read future to borrow the whole transport, blocking concurrent
9//! writes.
10
11use core::future::Future;
12
13/// Line-reading half of the CLI transport.
14///
15/// Read futures returned by [`CliInput::read_line`] are NOT required to be
16/// cancellation-safe. The driver keeps each read future alive across multiple
17/// wake events and only drops it when it completes, so implementations may
18/// safely park partial-line state inside the returned future.
19pub trait CliInput {
20    type Error: core::fmt::Debug;
21
22    /// Read one line (terminator stripped) into `buf`. `Ok(None)` on EOF.
23    /// `Err` on overflow (line > buf.len()) or invalid UTF-8.
24    ///
25    /// The returned `&'buf str` borrows from `buf` only, not from `self`.
26    fn read_line<'io, 'buf>(
27        &'io mut self,
28        buf: &'buf mut [u8],
29    ) -> impl Future<Output = Result<Option<&'buf str>, Self::Error>> + 'io
30    where
31        'buf: 'io;
32}
33
34/// Line-writing half of the CLI transport.
35pub trait CliOutput {
36    type Error: core::fmt::Debug;
37
38    fn write_line(&mut self, line: &str) -> impl Future<Output = Result<(), Self::Error>>;
39
40    fn flush(&mut self) -> impl Future<Output = Result<(), Self::Error>>;
41}
42
43#[cfg(feature = "tokio-stdio")]
44pub use stdio::{StdioInput, StdioOutput, stdio_split};
45
46#[cfg(feature = "tokio-stdio")]
47mod stdio {
48    use super::{CliInput, CliOutput};
49    use std::io;
50    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Stdin, Stdout};
51
52    /// Tokio-based stdin reader.
53    pub struct StdioInput {
54        reader: BufReader<Stdin>,
55    }
56
57    /// Tokio-based stdout writer.
58    pub struct StdioOutput {
59        writer: Stdout,
60    }
61
62    /// Construct the stdin/stdout halves for the CLI.
63    pub fn stdio_split() -> (StdioInput, StdioOutput) {
64        (
65            StdioInput {
66                reader: BufReader::new(tokio::io::stdin()),
67            },
68            StdioOutput {
69                writer: tokio::io::stdout(),
70            },
71        )
72    }
73
74    impl CliInput for StdioInput {
75        type Error = io::Error;
76
77        async fn read_line<'io, 'buf>(
78            &'io mut self,
79            buf: &'buf mut [u8],
80        ) -> Result<Option<&'buf str>, Self::Error>
81        where
82            'buf: 'io,
83        {
84            // The driver keeps this future alive across wake events, so we
85            // don't need an external partial-line accumulator — tokio's
86            // internal `Vec<u8>` inside `ReadLine` retains consumed bytes
87            // until the line is complete.
88            let mut line = String::new();
89            let n = self.reader.read_line(&mut line).await?;
90            if n == 0 && line.is_empty() {
91                return Ok(None);
92            }
93            let trimmed_len = {
94                let s = line.as_str();
95                let mut end = s.len();
96                if s.ends_with('\n') {
97                    end -= 1;
98                    if s[..end].ends_with('\r') {
99                        end -= 1;
100                    }
101                }
102                end
103            };
104            let bytes = &line.as_bytes()[..trimmed_len];
105            if bytes.len() > buf.len() {
106                return Err(io::Error::new(
107                    io::ErrorKind::InvalidData,
108                    "input line exceeds buffer size",
109                ));
110            }
111            buf[..bytes.len()].copy_from_slice(bytes);
112            let line_len = bytes.len();
113            let out = core::str::from_utf8(&buf[..line_len]).expect("utf8 in = utf8 out");
114            Ok(Some(out))
115        }
116    }
117
118    impl CliOutput for StdioOutput {
119        type Error = io::Error;
120
121        async fn write_line(&mut self, line: &str) -> Result<(), Self::Error> {
122            self.writer.write_all(line.as_bytes()).await?;
123            self.writer.write_all(b"\n").await?;
124            Ok(())
125        }
126
127        async fn flush(&mut self) -> Result<(), Self::Error> {
128            self.writer.flush().await
129        }
130    }
131}