rust_expect/
types.rs

1//! Common types for rust-expect.
2//!
3//! This module defines core types used throughout the library including
4//! patterns, matches, and session state.
5
6use std::fmt;
7use std::time::Duration;
8
9/// A match result from an expect operation.
10#[derive(Debug, Clone)]
11pub struct Match {
12    /// The index of the pattern that matched (for multi-pattern expects).
13    pub pattern_index: usize,
14
15    /// The full text that matched.
16    pub matched: String,
17
18    /// Capture groups from regex patterns.
19    pub captures: Vec<String>,
20
21    /// Text before the match.
22    pub before: String,
23
24    /// Text after the match (remaining in buffer).
25    pub after: String,
26}
27
28impl Match {
29    /// Create a new match result.
30    #[must_use]
31    pub fn new(
32        pattern_index: usize,
33        matched: impl Into<String>,
34        before: impl Into<String>,
35        after: impl Into<String>,
36    ) -> Self {
37        Self {
38            pattern_index,
39            matched: matched.into(),
40            captures: Vec::new(),
41            before: before.into(),
42            after: after.into(),
43        }
44    }
45
46    /// Create a match with captures.
47    #[must_use]
48    pub fn with_captures(mut self, captures: Vec<String>) -> Self {
49        self.captures = captures;
50        self
51    }
52
53    /// Get a capture group by index.
54    #[must_use]
55    pub fn capture(&self, index: usize) -> Option<&str> {
56        self.captures.get(index).map(String::as_str)
57    }
58
59    /// Get the full matched text.
60    #[must_use]
61    pub fn as_str(&self) -> &str {
62        &self.matched
63    }
64}
65
66impl fmt::Display for Match {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "{}", self.matched)
69    }
70}
71
72/// Result of an expect operation with multiple patterns.
73#[derive(Debug, Clone)]
74pub enum ExpectResult {
75    /// A pattern matched.
76    Matched(Match),
77
78    /// End of file was reached.
79    Eof {
80        /// Buffer contents when EOF was reached.
81        buffer: String,
82    },
83
84    /// Timeout occurred.
85    Timeout {
86        /// The duration that elapsed.
87        duration: Duration,
88        /// Buffer contents at timeout.
89        buffer: String,
90    },
91}
92
93impl ExpectResult {
94    /// Check if this is a successful match.
95    #[must_use]
96    pub const fn is_match(&self) -> bool {
97        matches!(self, Self::Matched(_))
98    }
99
100    /// Check if this is an EOF.
101    #[must_use]
102    pub const fn is_eof(&self) -> bool {
103        matches!(self, Self::Eof { .. })
104    }
105
106    /// Check if this is a timeout.
107    #[must_use]
108    pub const fn is_timeout(&self) -> bool {
109        matches!(self, Self::Timeout { .. })
110    }
111
112    /// Get the match if this is a successful match.
113    #[must_use]
114    pub fn into_match(self) -> Option<Match> {
115        match self {
116            Self::Matched(m) => Some(m),
117            _ => None,
118        }
119    }
120
121    /// Get the buffer contents (for EOF or timeout).
122    #[must_use]
123    pub fn buffer(&self) -> Option<&str> {
124        match self {
125            Self::Eof { buffer } | Self::Timeout { buffer, .. } => Some(buffer),
126            Self::Matched(_) => None,
127        }
128    }
129}
130
131/// The state of a session.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum SessionState {
134    /// Session is starting up.
135    Starting,
136
137    /// Session is running and ready for operations.
138    Running,
139
140    /// Session is in interact mode.
141    Interacting,
142
143    /// Session is closing.
144    Closing,
145
146    /// Session is closed.
147    Closed,
148
149    /// Process has exited with status.
150    Exited(ProcessExitStatus),
151}
152
153impl SessionState {
154    /// Check if the session is usable for operations.
155    #[must_use]
156    pub const fn is_usable(&self) -> bool {
157        matches!(self, Self::Running | Self::Interacting)
158    }
159
160    /// Check if the session is closed or exited.
161    #[must_use]
162    pub const fn is_closed(&self) -> bool {
163        matches!(self, Self::Closed | Self::Exited(_))
164    }
165
166    /// Get the exit status if the session has exited.
167    #[must_use]
168    pub const fn exit_status(&self) -> Option<&ProcessExitStatus> {
169        if let Self::Exited(status) = self {
170            Some(status)
171        } else {
172            None
173        }
174    }
175}
176
177impl fmt::Display for SessionState {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        let s = match self {
180            Self::Starting => "starting".to_string(),
181            Self::Running => "running".to_string(),
182            Self::Interacting => "interacting".to_string(),
183            Self::Closing => "closing".to_string(),
184            Self::Closed => "closed".to_string(),
185            Self::Exited(status) => format!("exited ({status})"),
186        };
187        write!(f, "{s}")
188    }
189}
190
191/// Exit status of a process.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum ProcessExitStatus {
194    /// Process exited with a code.
195    Exited(i32),
196
197    /// Process was terminated by a signal (Unix).
198    Signaled(i32),
199
200    /// Exit status is unknown.
201    Unknown,
202}
203
204impl ProcessExitStatus {
205    /// Check if the process exited successfully (code 0).
206    #[must_use]
207    pub const fn success(self) -> bool {
208        matches!(self, Self::Exited(0))
209    }
210
211    /// Get the exit code if the process exited normally.
212    #[must_use]
213    pub const fn code(self) -> Option<i32> {
214        match self {
215            Self::Exited(code) => Some(code),
216            _ => None,
217        }
218    }
219
220    /// Get the signal number if the process was signaled.
221    #[must_use]
222    pub const fn signal(self) -> Option<i32> {
223        match self {
224            Self::Signaled(sig) => Some(sig),
225            _ => None,
226        }
227    }
228}
229
230impl fmt::Display for ProcessExitStatus {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        match self {
233            Self::Exited(code) => write!(f, "exited with code {code}"),
234            Self::Signaled(sig) => write!(f, "terminated by signal {sig}"),
235            Self::Unknown => write!(f, "unknown exit status"),
236        }
237    }
238}
239
240impl From<std::process::ExitStatus> for ProcessExitStatus {
241    fn from(status: std::process::ExitStatus) -> Self {
242        #[cfg(unix)]
243        {
244            use std::os::unix::process::ExitStatusExt;
245            if let Some(code) = status.code() {
246                Self::Exited(code)
247            } else if let Some(sig) = status.signal() {
248                Self::Signaled(sig)
249            } else {
250                Self::Unknown
251            }
252        }
253
254        #[cfg(not(unix))]
255        {
256            if let Some(code) = status.code() {
257                Self::Exited(code)
258            } else {
259                Self::Unknown
260            }
261        }
262    }
263}
264
265/// Terminal dimensions.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct Dimensions {
268    /// Width in columns.
269    pub cols: u16,
270
271    /// Height in rows.
272    pub rows: u16,
273}
274
275impl Dimensions {
276    /// Create new dimensions.
277    #[must_use]
278    pub const fn new(cols: u16, rows: u16) -> Self {
279        Self { cols, rows }
280    }
281
282    /// Standard 80x24 terminal.
283    pub const STANDARD: Self = Self::new(80, 24);
284
285    /// Wide terminal (120x40).
286    pub const WIDE: Self = Self::new(120, 40);
287}
288
289impl Default for Dimensions {
290    fn default() -> Self {
291        Self::STANDARD
292    }
293}
294
295impl From<(u16, u16)> for Dimensions {
296    fn from((cols, rows): (u16, u16)) -> Self {
297        Self::new(cols, rows)
298    }
299}
300
301impl From<Dimensions> for (u16, u16) {
302    fn from(dim: Dimensions) -> Self {
303        (dim.cols, dim.rows)
304    }
305}
306
307/// Control characters that can be sent to a terminal.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum ControlChar {
310    /// Ctrl+A (SOH)
311    CtrlA,
312    /// Ctrl+B (STX)
313    CtrlB,
314    /// Ctrl+C (ETX) - Interrupt
315    CtrlC,
316    /// Ctrl+D (EOT) - End of transmission / EOF
317    CtrlD,
318    /// Ctrl+E (ENQ)
319    CtrlE,
320    /// Ctrl+F (ACK)
321    CtrlF,
322    /// Ctrl+G (BEL) - Bell
323    CtrlG,
324    /// Ctrl+H (BS) - Backspace
325    CtrlH,
326    /// Ctrl+I (HT) - Tab
327    CtrlI,
328    /// Ctrl+J (LF) - Line feed
329    CtrlJ,
330    /// Ctrl+K (VT) - Vertical tab
331    CtrlK,
332    /// Ctrl+L (FF) - Form feed / Clear screen
333    CtrlL,
334    /// Ctrl+M (CR) - Carriage return
335    CtrlM,
336    /// Ctrl+N (SO)
337    CtrlN,
338    /// Ctrl+O (SI)
339    CtrlO,
340    /// Ctrl+P (DLE)
341    CtrlP,
342    /// Ctrl+Q (DC1) - XON / Resume
343    CtrlQ,
344    /// Ctrl+R (DC2)
345    CtrlR,
346    /// Ctrl+S (DC3) - XOFF / Pause
347    CtrlS,
348    /// Ctrl+T (DC4)
349    CtrlT,
350    /// Ctrl+U (NAK) - Kill line
351    CtrlU,
352    /// Ctrl+V (SYN)
353    CtrlV,
354    /// Ctrl+W (ETB) - Kill word
355    CtrlW,
356    /// Ctrl+X (CAN)
357    CtrlX,
358    /// Ctrl+Y (EM)
359    CtrlY,
360    /// Ctrl+Z (SUB) - Suspend
361    CtrlZ,
362    /// Escape
363    Escape,
364    /// Ctrl+\ (FS) - Quit
365    CtrlBackslash,
366    /// Ctrl+] (GS)
367    CtrlBracket,
368    /// Ctrl+^ (RS)
369    CtrlCaret,
370    /// Ctrl+_ (US)
371    CtrlUnderscore,
372}
373
374impl ControlChar {
375    /// Get the byte value of this control character.
376    #[must_use]
377    pub const fn as_byte(self) -> u8 {
378        match self {
379            Self::CtrlA => 0x01,
380            Self::CtrlB => 0x02,
381            Self::CtrlC => 0x03,
382            Self::CtrlD => 0x04,
383            Self::CtrlE => 0x05,
384            Self::CtrlF => 0x06,
385            Self::CtrlG => 0x07,
386            Self::CtrlH => 0x08,
387            Self::CtrlI => 0x09,
388            Self::CtrlJ => 0x0A,
389            Self::CtrlK => 0x0B,
390            Self::CtrlL => 0x0C,
391            Self::CtrlM => 0x0D,
392            Self::CtrlN => 0x0E,
393            Self::CtrlO => 0x0F,
394            Self::CtrlP => 0x10,
395            Self::CtrlQ => 0x11,
396            Self::CtrlR => 0x12,
397            Self::CtrlS => 0x13,
398            Self::CtrlT => 0x14,
399            Self::CtrlU => 0x15,
400            Self::CtrlV => 0x16,
401            Self::CtrlW => 0x17,
402            Self::CtrlX => 0x18,
403            Self::CtrlY => 0x19,
404            Self::CtrlZ => 0x1A,
405            Self::Escape => 0x1B,
406            Self::CtrlBackslash => 0x1C,
407            Self::CtrlBracket => 0x1D,
408            Self::CtrlCaret => 0x1E,
409            Self::CtrlUnderscore => 0x1F,
410        }
411    }
412
413    /// Create a control character from a regular character.
414    ///
415    /// For example, `ControlChar::from_char('c')` returns `Some(ControlChar::CtrlC)`.
416    #[must_use]
417    pub const fn from_char(c: char) -> Option<Self> {
418        match c.to_ascii_lowercase() {
419            'a' => Some(Self::CtrlA),
420            'b' => Some(Self::CtrlB),
421            'c' => Some(Self::CtrlC),
422            'd' => Some(Self::CtrlD),
423            'e' => Some(Self::CtrlE),
424            'f' => Some(Self::CtrlF),
425            'g' => Some(Self::CtrlG),
426            'h' => Some(Self::CtrlH),
427            'i' => Some(Self::CtrlI),
428            'j' => Some(Self::CtrlJ),
429            'k' => Some(Self::CtrlK),
430            'l' => Some(Self::CtrlL),
431            'm' => Some(Self::CtrlM),
432            'n' => Some(Self::CtrlN),
433            'o' => Some(Self::CtrlO),
434            'p' => Some(Self::CtrlP),
435            'q' => Some(Self::CtrlQ),
436            'r' => Some(Self::CtrlR),
437            's' => Some(Self::CtrlS),
438            't' => Some(Self::CtrlT),
439            'u' => Some(Self::CtrlU),
440            'v' => Some(Self::CtrlV),
441            'w' => Some(Self::CtrlW),
442            'x' => Some(Self::CtrlX),
443            'y' => Some(Self::CtrlY),
444            'z' => Some(Self::CtrlZ),
445            '[' => Some(Self::Escape),
446            '\\' => Some(Self::CtrlBackslash),
447            ']' => Some(Self::CtrlBracket),
448            '^' => Some(Self::CtrlCaret),
449            '_' => Some(Self::CtrlUnderscore),
450            _ => None,
451        }
452    }
453}
454
455impl From<ControlChar> for u8 {
456    fn from(c: ControlChar) -> Self {
457        c.as_byte()
458    }
459}
460
461impl From<ControlChar> for char {
462    fn from(c: ControlChar) -> Self {
463        c.as_byte() as Self
464    }
465}
466
467/// A unique identifier for a session.
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
469pub struct SessionId(u64);
470
471impl SessionId {
472    /// Create a new session ID.
473    #[must_use]
474    pub fn new() -> Self {
475        use std::sync::atomic::{AtomicU64, Ordering};
476        static NEXT_ID: AtomicU64 = AtomicU64::new(1);
477        Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
478    }
479
480    /// Get the inner value.
481    #[must_use]
482    pub const fn as_u64(self) -> u64 {
483        self.0
484    }
485}
486
487impl Default for SessionId {
488    fn default() -> Self {
489        Self::new()
490    }
491}
492
493impl fmt::Display for SessionId {
494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495        write!(f, "session-{}", self.0)
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn match_creation() {
505        let m =
506            Match::new(0, "hello", "before ", " after").with_captures(vec!["capture1".to_string()]);
507
508        assert_eq!(m.pattern_index, 0);
509        assert_eq!(m.as_str(), "hello");
510        assert_eq!(m.before, "before ");
511        assert_eq!(m.after, " after");
512        assert_eq!(m.capture(0), Some("capture1"));
513        assert_eq!(m.capture(1), None);
514    }
515
516    #[test]
517    fn session_state_checks() {
518        assert!(SessionState::Running.is_usable());
519        assert!(SessionState::Interacting.is_usable());
520        assert!(!SessionState::Closed.is_usable());
521
522        assert!(SessionState::Closed.is_closed());
523        assert!(SessionState::Exited(ProcessExitStatus::Unknown).is_closed());
524        assert!(!SessionState::Running.is_closed());
525    }
526
527    #[test]
528    fn process_exit_status() {
529        let success = ProcessExitStatus::Exited(0);
530        assert!(success.success());
531        assert_eq!(success.code(), Some(0));
532
533        let failure = ProcessExitStatus::Exited(1);
534        assert!(!failure.success());
535        assert_eq!(failure.code(), Some(1));
536
537        let signaled = ProcessExitStatus::Signaled(9);
538        assert!(!signaled.success());
539        assert_eq!(signaled.signal(), Some(9));
540    }
541
542    #[test]
543    fn control_char_from_char() {
544        assert_eq!(ControlChar::from_char('c'), Some(ControlChar::CtrlC));
545        assert_eq!(ControlChar::from_char('C'), Some(ControlChar::CtrlC));
546        assert_eq!(ControlChar::from_char('d'), Some(ControlChar::CtrlD));
547        assert_eq!(ControlChar::from_char('?'), None);
548    }
549
550    #[test]
551    fn control_char_as_byte() {
552        assert_eq!(ControlChar::CtrlC.as_byte(), 0x03);
553        assert_eq!(ControlChar::CtrlD.as_byte(), 0x04);
554        assert_eq!(ControlChar::Escape.as_byte(), 0x1B);
555    }
556
557    #[test]
558    fn session_id_unique() {
559        let id1 = SessionId::new();
560        let id2 = SessionId::new();
561        assert_ne!(id1, id2);
562    }
563
564    #[test]
565    fn dimensions_conversion() {
566        let dim = Dimensions::new(120, 40);
567        let tuple: (u16, u16) = dim.into();
568        assert_eq!(tuple, (120, 40));
569
570        let dim2: Dimensions = (80, 24).into();
571        assert_eq!(dim2, Dimensions::STANDARD);
572    }
573}