Skip to main content

rust_expect/send/
basic.rs

1//! Basic send operations.
2//!
3//! This module provides fundamental send operations for writing data
4//! to a session, including raw bytes, strings, lines, and control characters.
5
6use std::time::Duration;
7
8use tokio::io::AsyncWriteExt;
9
10use crate::config::LineEnding;
11use crate::error::Result;
12use crate::types::ControlChar;
13
14/// Trait for basic send operations.
15pub trait BasicSend: Send {
16    /// Send raw bytes.
17    fn send_bytes(&mut self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send;
18
19    /// Send a string.
20    fn send_str(&mut self, s: &str) -> impl std::future::Future<Output = Result<()>> + Send {
21        async move { self.send_bytes(s.as_bytes()).await }
22    }
23
24    /// Send a line with the specified line ending.
25    fn send_line_with(
26        &mut self,
27        line: &str,
28        ending: LineEnding,
29    ) -> impl std::future::Future<Output = Result<()>> + Send {
30        async move {
31            self.send_str(line).await?;
32            self.send_str(ending.as_str()).await
33        }
34    }
35
36    /// Send a line with LF ending.
37    fn send_line(&mut self, line: &str) -> impl std::future::Future<Output = Result<()>> + Send {
38        self.send_line_with(line, LineEnding::Lf)
39    }
40
41    /// Send a control character.
42    fn send_control(
43        &mut self,
44        ctrl: ControlChar,
45    ) -> impl std::future::Future<Output = Result<()>> + Send {
46        async move { self.send_bytes(&[ctrl.as_byte()]).await }
47    }
48
49    /// Send Ctrl+C (interrupt).
50    fn send_interrupt(&mut self) -> impl std::future::Future<Output = Result<()>> + Send {
51        self.send_control(ControlChar::CtrlC)
52    }
53
54    /// Send Ctrl+D (EOF).
55    fn send_eof(&mut self) -> impl std::future::Future<Output = Result<()>> + Send {
56        self.send_control(ControlChar::CtrlD)
57    }
58
59    /// Send Ctrl+Z (suspend).
60    fn send_suspend(&mut self) -> impl std::future::Future<Output = Result<()>> + Send {
61        self.send_control(ControlChar::CtrlZ)
62    }
63
64    /// Send Escape.
65    fn send_escape(&mut self) -> impl std::future::Future<Output = Result<()>> + Send {
66        self.send_control(ControlChar::Escape)
67    }
68
69    /// Send Tab (Ctrl+I).
70    fn send_tab(&mut self) -> impl std::future::Future<Output = Result<()>> + Send {
71        self.send_control(ControlChar::CtrlI)
72    }
73
74    /// Send Backspace (Ctrl+H).
75    fn send_backspace(&mut self) -> impl std::future::Future<Output = Result<()>> + Send {
76        self.send_control(ControlChar::CtrlH)
77    }
78
79    /// Send text using bracketed paste mode.
80    ///
81    /// Wraps the content in `\x1b[200~` and `\x1b[201~` markers. Terminal
82    /// applications that have enabled bracketed paste (DECSET 2004) treat
83    /// the enclosed content as pasted input rather than typed input — this
84    /// suppresses autocomplete, command-history scanning, and per-character
85    /// interpretation that can otherwise mangle multi-line or special-key
86    /// content (e.g. a leading `/` triggering a slash-command popup).
87    ///
88    /// The application must have requested bracketed paste; if it hasn't,
89    /// most terminals will ignore the markers and the inner text will be
90    /// delivered as-is, so the call is safe to use unconditionally.
91    fn send_paste(&mut self, text: &str) -> impl std::future::Future<Output = Result<()>> + Send {
92        async move {
93            self.send_bytes(b"\x1b[200~").await?;
94            self.send_str(text).await?;
95            self.send_bytes(b"\x1b[201~").await
96        }
97    }
98}
99
100/// A sender that wraps an async writer.
101pub struct Sender<W> {
102    writer: W,
103    line_ending: LineEnding,
104    /// Optional delay between characters.
105    char_delay: Option<Duration>,
106}
107
108impl<W: AsyncWriteExt + Unpin + Send> Sender<W> {
109    /// Create a new sender.
110    pub const fn new(writer: W) -> Self {
111        Self {
112            writer,
113            line_ending: LineEnding::Lf,
114            char_delay: None,
115        }
116    }
117
118    /// Set the line ending.
119    pub const fn set_line_ending(&mut self, ending: LineEnding) {
120        self.line_ending = ending;
121    }
122
123    /// Set character delay for slow typing.
124    pub const fn set_char_delay(&mut self, delay: Option<Duration>) {
125        self.char_delay = delay;
126    }
127
128    /// Get the line ending.
129    #[must_use]
130    pub const fn line_ending(&self) -> LineEnding {
131        self.line_ending
132    }
133
134    /// Send bytes with optional character delay.
135    pub async fn send_with_delay(&mut self, data: &[u8]) -> Result<()> {
136        if let Some(delay) = self.char_delay {
137            for byte in data {
138                self.writer
139                    .write_all(&[*byte])
140                    .await
141                    .map_err(crate::error::ExpectError::Io)?;
142                self.writer
143                    .flush()
144                    .await
145                    .map_err(crate::error::ExpectError::Io)?;
146                tokio::time::sleep(delay).await;
147            }
148        } else {
149            self.writer
150                .write_all(data)
151                .await
152                .map_err(crate::error::ExpectError::Io)?;
153            self.writer
154                .flush()
155                .await
156                .map_err(crate::error::ExpectError::Io)?;
157        }
158        Ok(())
159    }
160
161    /// Get mutable access to the underlying writer.
162    pub const fn writer_mut(&mut self) -> &mut W {
163        &mut self.writer
164    }
165}
166
167impl<W: AsyncWriteExt + Unpin + Send> BasicSend for Sender<W> {
168    async fn send_bytes(&mut self, data: &[u8]) -> Result<()> {
169        self.send_with_delay(data).await
170    }
171
172    async fn send_line(&mut self, line: &str) -> Result<()> {
173        self.send_line_with(line, self.line_ending).await
174    }
175}
176
177/// ANSI escape sequence helpers.
178pub struct AnsiSequences;
179
180impl AnsiSequences {
181    /// Cursor up.
182    pub const CURSOR_UP: &'static [u8] = b"\x1b[A";
183    /// Cursor down.
184    pub const CURSOR_DOWN: &'static [u8] = b"\x1b[B";
185    /// Cursor right.
186    pub const CURSOR_RIGHT: &'static [u8] = b"\x1b[C";
187    /// Cursor left.
188    pub const CURSOR_LEFT: &'static [u8] = b"\x1b[D";
189    /// Home key.
190    pub const HOME: &'static [u8] = b"\x1b[H";
191    /// End key.
192    pub const END: &'static [u8] = b"\x1b[F";
193    /// Page up.
194    pub const PAGE_UP: &'static [u8] = b"\x1b[5~";
195    /// Page down.
196    pub const PAGE_DOWN: &'static [u8] = b"\x1b[6~";
197    /// Insert key.
198    pub const INSERT: &'static [u8] = b"\x1b[2~";
199    /// Delete key.
200    pub const DELETE: &'static [u8] = b"\x1b[3~";
201    /// F1 key.
202    pub const F1: &'static [u8] = b"\x1bOP";
203    /// F2 key.
204    pub const F2: &'static [u8] = b"\x1bOQ";
205    /// F3 key.
206    pub const F3: &'static [u8] = b"\x1bOR";
207    /// F4 key.
208    pub const F4: &'static [u8] = b"\x1bOS";
209    /// F5 key.
210    pub const F5: &'static [u8] = b"\x1b[15~";
211    /// F6 key.
212    pub const F6: &'static [u8] = b"\x1b[17~";
213    /// F7 key.
214    pub const F7: &'static [u8] = b"\x1b[18~";
215    /// F8 key.
216    pub const F8: &'static [u8] = b"\x1b[19~";
217    /// F9 key.
218    pub const F9: &'static [u8] = b"\x1b[20~";
219    /// F10 key.
220    pub const F10: &'static [u8] = b"\x1b[21~";
221    /// F11 key.
222    pub const F11: &'static [u8] = b"\x1b[23~";
223    /// F12 key.
224    pub const F12: &'static [u8] = b"\x1b[24~";
225
226    /// Generate cursor movement sequence.
227    #[must_use]
228    pub fn cursor_move(rows: i32, cols: i32) -> Vec<u8> {
229        let mut result = Vec::new();
230
231        if rows != 0 {
232            let dir = if rows > 0 { 'B' } else { 'A' };
233            let count = rows.unsigned_abs();
234            result.extend(format!("\x1b[{count}{dir}").as_bytes());
235        }
236
237        if cols != 0 {
238            let dir = if cols > 0 { 'C' } else { 'D' };
239            let count = cols.unsigned_abs();
240            result.extend(format!("\x1b[{count}{dir}").as_bytes());
241        }
242
243        result
244    }
245
246    /// Generate cursor position sequence.
247    #[must_use]
248    pub fn cursor_position(row: u32, col: u32) -> Vec<u8> {
249        format!("\x1b[{row};{col}H").into_bytes()
250    }
251}
252
253/// Extension trait for sending ANSI sequences.
254pub trait AnsiSend: BasicSend {
255    /// Send cursor up.
256    fn send_cursor_up(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
257    where
258        Self: Send,
259    {
260        async move { self.send_bytes(AnsiSequences::CURSOR_UP).await }
261    }
262
263    /// Send cursor down.
264    fn send_cursor_down(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
265    where
266        Self: Send,
267    {
268        async move { self.send_bytes(AnsiSequences::CURSOR_DOWN).await }
269    }
270
271    /// Send cursor right.
272    fn send_cursor_right(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
273    where
274        Self: Send,
275    {
276        async move { self.send_bytes(AnsiSequences::CURSOR_RIGHT).await }
277    }
278
279    /// Send cursor left.
280    fn send_cursor_left(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
281    where
282        Self: Send,
283    {
284        async move { self.send_bytes(AnsiSequences::CURSOR_LEFT).await }
285    }
286
287    /// Send home key.
288    fn send_home(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
289    where
290        Self: Send,
291    {
292        async move { self.send_bytes(AnsiSequences::HOME).await }
293    }
294
295    /// Send end key.
296    fn send_end(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
297    where
298        Self: Send,
299    {
300        async move { self.send_bytes(AnsiSequences::END).await }
301    }
302
303    /// Send delete key.
304    fn send_delete(&mut self) -> impl std::future::Future<Output = Result<()>> + Send
305    where
306        Self: Send,
307    {
308        async move { self.send_bytes(AnsiSequences::DELETE).await }
309    }
310
311    /// Send a function key.
312    fn send_function_key(&mut self, n: u8) -> impl std::future::Future<Output = Result<()>> + Send
313    where
314        Self: Send,
315    {
316        async move {
317            let seq = match n {
318                1 => AnsiSequences::F1,
319                2 => AnsiSequences::F2,
320                3 => AnsiSequences::F3,
321                4 => AnsiSequences::F4,
322                5 => AnsiSequences::F5,
323                6 => AnsiSequences::F6,
324                7 => AnsiSequences::F7,
325                8 => AnsiSequences::F8,
326                9 => AnsiSequences::F9,
327                10 => AnsiSequences::F10,
328                11 => AnsiSequences::F11,
329                12 => AnsiSequences::F12,
330                _ => return Ok(()),
331            };
332            self.send_bytes(seq).await
333        }
334    }
335}
336
337impl<T: BasicSend> AnsiSend for T {}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn ansi_cursor_move() {
345        assert_eq!(AnsiSequences::cursor_move(3, 0), b"\x1b[3B");
346        assert_eq!(AnsiSequences::cursor_move(-2, 0), b"\x1b[2A");
347        assert_eq!(AnsiSequences::cursor_move(0, 5), b"\x1b[5C");
348        assert_eq!(AnsiSequences::cursor_move(0, -4), b"\x1b[4D");
349    }
350
351    #[test]
352    fn ansi_cursor_position() {
353        assert_eq!(AnsiSequences::cursor_position(1, 1), b"\x1b[1;1H");
354        assert_eq!(AnsiSequences::cursor_position(10, 20), b"\x1b[10;20H");
355    }
356}