Skip to main content

compio_term\command/
mod.rs

1use std::{fmt, io, mem};
2
3use compio_buf::{BufResult, IntoInner, IoBufExt};
4use compio_io::AsyncWrite;
5use crossterm::Command;
6
7mod multi;
8
9pub use multi::Commands;
10
11/// Starts an asynchronous command queue for a writer.
12pub trait Queueable: AsyncWrite {
13    /// Creates a queue that borrows this writer and adds `command` to it.
14    ///
15    /// No bytes are written until [`CommandQueue::flush`] is awaited.
16    fn queue(&mut self, command: impl Command) -> io::Result<CommandQueue<&mut Self>>;
17
18    /// Adds an ordered batch of commands without writing to the underlying
19    /// writer.
20    ///
21    /// If any command cannot render its ANSI representation, this method
22    /// removes every byte added by this call. Commands that were already queued
23    /// remain intact.
24    fn queue_many(&mut self, command: impl Commands) -> io::Result<CommandQueue<&mut Self>>;
25}
26
27impl<W> CommandQueue<W> {
28    /// Adds a command without writing to the underlying writer.
29    ///
30    /// If the command cannot render its ANSI representation, this method
31    /// removes any bytes that the failed command added. Commands that were
32    /// already queued remain intact.
33    pub fn queue(&mut self, command: impl Command) -> io::Result<&mut Self> {
34        self.append(|writer| command.write_ansi(writer))
35    }
36
37    /// Adds an ordered batch of commands without writing to the underlying
38    /// writer.
39    ///
40    /// If any command cannot render its ANSI representation, this method
41    /// removes every byte added by this call. Commands that were already queued
42    /// remain intact.
43    pub fn queue_many(&mut self, commands: impl Commands) -> io::Result<&mut Self> {
44        self.append(|writer| commands.write_ansi(writer))
45    }
46}
47
48impl<W: AsyncWrite + ?Sized> Queueable for W {
49    fn queue(&mut self, command: impl Command) -> io::Result<CommandQueue<&mut Self>> {
50        let mut queue = CommandQueue::new(self);
51        queue.queue(command)?;
52        Ok(queue)
53    }
54
55    fn queue_many(&mut self, command: impl Commands) -> io::Result<CommandQueue<&mut Self>> {
56        let mut queue = CommandQueue::new(self);
57        queue.queue_many(command)?;
58        Ok(queue)
59    }
60}
61
62/// A buffered queue of terminal commands for a Compio asynchronous writer.
63///
64/// Commands are encoded with
65/// [`Command::write_ansi`](crate::Command::write_ansi) and written in one
66/// ordered batch when [`flush`](Self::flush) is awaited. The writer can be
67/// Compio's standard output handle or any other type that implements
68/// [`compio_io::AsyncWrite`].
69///
70/// Call [`flush`](Self::flush) before this value is dropped. Dropping it
71/// discards commands that are still queued.
72///
73/// # Example
74///
75/// ```no_run
76/// use std::io;
77///
78/// use compio_term::{Queueable, cursor::MoveTo, style::Print};
79///
80/// #[compio_macros::main]
81/// async fn main() -> io::Result<()> {
82///     let mut stdout = compio_fs::stdout();
83///     let mut output = stdout.queue(MoveTo(0, 0))?;
84///     output.queue_many((Print("ready"), Print("\r\n")))?;
85///     output.flush().await
86/// }
87/// ```
88#[derive(Debug)]
89#[must_use = "queued commands are discarded unless flush is awaited"]
90pub struct CommandQueue<W> {
91    writer: W,
92    buffer: Vec<u8>,
93    written: usize,
94}
95
96impl<W> CommandQueue<W> {
97    /// Creates an empty command queue for `writer`.
98    pub fn new(writer: W) -> Self {
99        Self {
100            writer,
101            buffer: Vec::new(),
102            written: 0,
103        }
104    }
105
106    /// Creates an empty command queue with space for at least `capacity` bytes.
107    pub fn with_capacity(capacity: usize, writer: W) -> Self {
108        Self {
109            writer,
110            buffer: Vec::with_capacity(capacity),
111            written: 0,
112        }
113    }
114
115    /// Returns the number of command bytes that have not been written.
116    pub fn buffered_len(&self) -> usize {
117        self.buffer.len() - self.written
118    }
119
120    /// Returns `true` when no command bytes are waiting to be written.
121    pub fn is_empty(&self) -> bool {
122        self.buffered_len() == 0
123    }
124
125    fn append(
126        &mut self,
127        write: impl FnOnce(&mut AnsiBuffer<'_>) -> fmt::Result,
128    ) -> io::Result<&mut Self> {
129        let original_len = self.buffer.len();
130        if write(&mut AnsiBuffer(&mut self.buffer)).is_err() {
131            self.buffer.truncate(original_len);
132            return Err(io::Error::other(
133                "terminal command failed to render its ANSI representation",
134            ));
135        }
136        Ok(self)
137    }
138}
139
140impl CommandQueue<compio_fs::Stdout> {
141    /// Creates an empty command queue that writes to standard output.
142    pub fn stdout() -> Self {
143        Self::new(compio_fs::stdout())
144    }
145}
146
147impl CommandQueue<compio_fs::Stderr> {
148    /// Creates an empty command queue that writes to standard error.
149    pub fn stderr() -> Self {
150        Self::new(compio_fs::stderr())
151    }
152}
153
154impl<W: AsyncWrite> CommandQueue<W> {
155    /// Writes all queued commands and flushes the underlying writer.
156    ///
157    /// A partial write is resumed at the first unwritten byte if this method is
158    /// called again after a write error. Commands queued after that error stay
159    /// after the remaining bytes from the failed batch.
160    pub async fn flush(&mut self) -> io::Result<()> {
161        while self.written < self.buffer.len() {
162            let buffer = mem::take(&mut self.buffer).slice(self.written..);
163            let BufResult(result, buffer) = self.writer.write(buffer).await;
164            self.buffer = buffer.into_inner();
165
166            match result {
167                Ok(0) => {
168                    return Err(io::Error::new(
169                        io::ErrorKind::WriteZero,
170                        "failed to write queued terminal commands",
171                    ));
172                }
173                Ok(written) => {
174                    let remaining = self.buffer.len() - self.written;
175                    if written > remaining {
176                        return Err(io::Error::new(
177                            io::ErrorKind::InvalidData,
178                            "asynchronous writer reported too many written bytes",
179                        ));
180                    }
181                    self.written += written;
182                }
183                Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
184                Err(error) => return Err(error),
185            }
186        }
187
188        self.buffer.clear();
189        self.written = 0;
190        self.writer.flush().await
191    }
192}
193
194struct AnsiBuffer<'a>(&'a mut Vec<u8>);
195
196impl fmt::Write for AnsiBuffer<'_> {
197    fn write_str(&mut self, value: &str) -> fmt::Result {
198        self.0.extend_from_slice(value.as_bytes());
199        Ok(())
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use std::{cell::RefCell, fmt, io, rc::Rc};
206
207    use compio_buf::{BufResult, IoBuf, IoBufExt};
208    use compio_io::AsyncWrite;
209    use futures_util::FutureExt;
210
211    use super::CommandQueue;
212    use crate::{Command, cursor::MoveTo, style::Print};
213
214    #[test]
215    fn queues_commands_until_flush_in_order() {
216        let writer = TestWriter::new(2, None);
217        let state = writer.state();
218        let mut queue = CommandQueue::new(writer);
219        let expected = b"\x1b[5;4Hready";
220
221        queue
222            .queue(MoveTo(3, 4))
223            .unwrap()
224            .queue(Print("ready"))
225            .unwrap();
226
227        assert_eq!(queue.buffered_len(), expected.len());
228        assert!(state.borrow().output.is_empty());
229        flush(&mut queue).unwrap();
230        assert_eq!(&state.borrow().output, expected);
231        assert!(queue.is_empty());
232    }
233
234    #[test]
235    fn failed_command_does_not_leave_partial_bytes() {
236        let writer = TestWriter::new(usize::MAX, None);
237        let state = writer.state();
238        let mut queue = CommandQueue::new(writer);
239
240        queue.queue(Print("before")).unwrap();
241        let error = queue
242            .queue(FailingCommand)
243            .err()
244            .expect("failing command must return an error");
245        queue.queue(Print("after")).unwrap();
246        flush(&mut queue).unwrap();
247
248        assert_eq!(error.kind(), io::ErrorKind::Other);
249        assert_eq!(&state.borrow().output, b"beforeafter");
250    }
251
252    #[test]
253    fn retry_resumes_after_a_partial_write() {
254        let writer = TestWriter::new(2, Some(2));
255        let state = writer.state();
256        let mut queue = CommandQueue::new(writer);
257        queue.queue(Print("abcdef")).unwrap();
258
259        let error = flush(&mut queue).unwrap_err();
260        assert_eq!(error.kind(), io::ErrorKind::Other);
261        assert_eq!(&state.borrow().output, b"ab");
262
263        queue.queue(Print("gh")).unwrap();
264        flush(&mut queue).unwrap();
265        assert_eq!(&state.borrow().output, b"abcdefgh");
266    }
267
268    fn flush(queue: &mut CommandQueue<TestWriter>) -> io::Result<()> {
269        queue
270            .flush()
271            .now_or_never()
272            .expect("test writer must not yield")
273    }
274
275    struct FailingCommand;
276
277    impl Command for FailingCommand {
278        fn write_ansi(&self, writer: &mut impl fmt::Write) -> fmt::Result {
279            fmt::Write::write_str(writer, "partial")?;
280            Err(fmt::Error)
281        }
282
283        #[cfg(windows)]
284        fn execute_winapi(&self) -> io::Result<()> {
285            Ok(())
286        }
287    }
288
289    #[derive(Clone)]
290    struct TestWriter {
291        state: Rc<RefCell<WriterState>>,
292    }
293
294    impl TestWriter {
295        fn new(chunk_size: usize, fail_at: Option<usize>) -> Self {
296            Self {
297                state: Rc::new(RefCell::new(WriterState {
298                    output: Vec::new(),
299                    chunk_size,
300                    writes: 0,
301                    fail_at,
302                })),
303            }
304        }
305
306        fn state(&self) -> Rc<RefCell<WriterState>> {
307            Rc::clone(&self.state)
308        }
309    }
310
311    struct WriterState {
312        output: Vec<u8>,
313        chunk_size: usize,
314        writes: usize,
315        fail_at: Option<usize>,
316    }
317
318    impl AsyncWrite for TestWriter {
319        async fn write<T: IoBuf>(&mut self, buffer: T) -> BufResult<usize, T> {
320            let mut state = self.state.borrow_mut();
321            state.writes += 1;
322            if state.fail_at == Some(state.writes) {
323                state.fail_at = None;
324                return BufResult(Err(io::Error::other("injected write failure")), buffer);
325            }
326
327            let written = state.chunk_size.min(buffer.buf_len());
328            state.output.extend_from_slice(&buffer.as_init()[..written]);
329            BufResult(Ok(written), buffer)
330        }
331
332        async fn flush(&mut self) -> io::Result<()> {
333            Ok(())
334        }
335
336        async fn shutdown(&mut self) -> io::Result<()> {
337            Ok(())
338        }
339    }
340}