1use std::fmt;
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
11pub struct Match {
12 pub pattern_index: usize,
14
15 pub matched: String,
17
18 pub captures: Vec<String>,
20
21 pub before: String,
23
24 pub after: String,
26}
27
28impl Match {
29 #[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 #[must_use]
48 pub fn with_captures(mut self, captures: Vec<String>) -> Self {
49 self.captures = captures;
50 self
51 }
52
53 #[must_use]
55 pub fn capture(&self, index: usize) -> Option<&str> {
56 self.captures.get(index).map(String::as_str)
57 }
58
59 #[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#[derive(Debug, Clone)]
74pub enum ExpectResult {
75 Matched(Match),
77
78 Eof {
80 buffer: String,
82 },
83
84 Timeout {
86 duration: Duration,
88 buffer: String,
90 },
91}
92
93impl ExpectResult {
94 #[must_use]
96 pub const fn is_match(&self) -> bool {
97 matches!(self, Self::Matched(_))
98 }
99
100 #[must_use]
102 pub const fn is_eof(&self) -> bool {
103 matches!(self, Self::Eof { .. })
104 }
105
106 #[must_use]
108 pub const fn is_timeout(&self) -> bool {
109 matches!(self, Self::Timeout { .. })
110 }
111
112 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum SessionState {
134 Starting,
136
137 Running,
139
140 Interacting,
142
143 Closing,
145
146 Closed,
148
149 Exited(ProcessExitStatus),
151}
152
153impl SessionState {
154 #[must_use]
156 pub const fn is_usable(&self) -> bool {
157 matches!(self, Self::Running | Self::Interacting)
158 }
159
160 #[must_use]
162 pub const fn is_closed(&self) -> bool {
163 matches!(self, Self::Closed | Self::Exited(_))
164 }
165
166 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum ProcessExitStatus {
194 Exited(i32),
196
197 Signaled(i32),
199
200 Unknown,
202}
203
204impl ProcessExitStatus {
205 #[must_use]
207 pub const fn success(self) -> bool {
208 matches!(self, Self::Exited(0))
209 }
210
211 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct Dimensions {
268 pub cols: u16,
270
271 pub rows: u16,
273}
274
275impl Dimensions {
276 #[must_use]
278 pub const fn new(cols: u16, rows: u16) -> Self {
279 Self { cols, rows }
280 }
281
282 pub const STANDARD: Self = Self::new(80, 24);
284
285 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum ControlChar {
310 CtrlA,
312 CtrlB,
314 CtrlC,
316 CtrlD,
318 CtrlE,
320 CtrlF,
322 CtrlG,
324 CtrlH,
326 CtrlI,
328 CtrlJ,
330 CtrlK,
332 CtrlL,
334 CtrlM,
336 CtrlN,
338 CtrlO,
340 CtrlP,
342 CtrlQ,
344 CtrlR,
346 CtrlS,
348 CtrlT,
350 CtrlU,
352 CtrlV,
354 CtrlW,
356 CtrlX,
358 CtrlY,
360 CtrlZ,
362 Escape,
364 CtrlBackslash,
366 CtrlBracket,
368 CtrlCaret,
370 CtrlUnderscore,
372}
373
374impl ControlChar {
375 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
469pub struct SessionId(u64);
470
471impl SessionId {
472 #[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 #[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}