1use std::collections::HashMap;
7use std::path::PathBuf;
8use std::time::Duration;
9
10pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
12
13pub const DEFAULT_BUFFER_SIZE: usize = 100 * 1024 * 1024;
15
16pub const DEFAULT_TERMINAL_WIDTH: u16 = 80;
18
19pub const DEFAULT_TERMINAL_HEIGHT: u16 = 24;
21
22pub const DEFAULT_TERM: &str = "xterm-256color";
24
25pub const DEFAULT_DELAY_BEFORE_SEND: Duration = Duration::from_millis(50);
27
28#[derive(Debug, Clone)]
30pub struct SessionConfig {
31 pub command: String,
33
34 pub args: Vec<String>,
36
37 pub env: HashMap<String, String>,
39
40 pub inherit_env: bool,
42
43 pub working_dir: Option<PathBuf>,
45
46 pub dimensions: (u16, u16),
48
49 pub timeout: TimeoutConfig,
51
52 pub buffer: BufferConfig,
54
55 pub logging: LoggingConfig,
57
58 pub line_ending: LineEnding,
60
61 pub encoding: EncodingConfig,
63
64 pub delay_before_send: Duration,
66}
67
68impl Default for SessionConfig {
69 fn default() -> Self {
70 let mut env = HashMap::new();
71 env.insert("TERM".to_string(), DEFAULT_TERM.to_string());
72
73 Self {
74 command: String::new(),
75 args: Vec::new(),
76 env,
77 inherit_env: true,
78 working_dir: None,
79 dimensions: (DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_HEIGHT),
80 timeout: TimeoutConfig::default(),
81 buffer: BufferConfig::default(),
82 logging: LoggingConfig::default(),
83 line_ending: LineEnding::default(),
84 encoding: EncodingConfig::default(),
85 delay_before_send: DEFAULT_DELAY_BEFORE_SEND,
86 }
87 }
88}
89
90impl SessionConfig {
91 #[must_use]
93 pub fn new(command: impl Into<String>) -> Self {
94 Self {
95 command: command.into(),
96 ..Default::default()
97 }
98 }
99
100 #[must_use]
102 pub fn args<I, S>(mut self, args: I) -> Self
103 where
104 I: IntoIterator<Item = S>,
105 S: Into<String>,
106 {
107 self.args = args.into_iter().map(Into::into).collect();
108 self
109 }
110
111 #[must_use]
113 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
114 self.env.insert(key.into(), value.into());
115 self
116 }
117
118 #[must_use]
120 pub const fn inherit_env(mut self, inherit: bool) -> Self {
121 self.inherit_env = inherit;
122 self
123 }
124
125 #[must_use]
127 pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
128 self.working_dir = Some(path.into());
129 self
130 }
131
132 #[must_use]
134 pub const fn dimensions(mut self, width: u16, height: u16) -> Self {
135 self.dimensions = (width, height);
136 self
137 }
138
139 #[must_use]
141 pub const fn timeout(mut self, timeout: Duration) -> Self {
142 self.timeout.default = timeout;
143 self
144 }
145
146 #[must_use]
148 pub const fn line_ending(mut self, line_ending: LineEnding) -> Self {
149 self.line_ending = line_ending;
150 self
151 }
152
153 #[must_use]
155 pub const fn delay_before_send(mut self, delay: Duration) -> Self {
156 self.delay_before_send = delay;
157 self
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct TimeoutConfig {
164 pub default: Duration,
166
167 pub spawn: Duration,
169
170 pub close: Duration,
172}
173
174impl Default for TimeoutConfig {
175 fn default() -> Self {
176 Self {
177 default: DEFAULT_TIMEOUT,
178 spawn: Duration::from_secs(60),
179 close: Duration::from_secs(10),
180 }
181 }
182}
183
184impl TimeoutConfig {
185 #[must_use]
187 pub fn new(default: Duration) -> Self {
188 Self {
189 default,
190 ..Default::default()
191 }
192 }
193
194 #[must_use]
196 pub const fn spawn(mut self, timeout: Duration) -> Self {
197 self.spawn = timeout;
198 self
199 }
200
201 #[must_use]
203 pub const fn close(mut self, timeout: Duration) -> Self {
204 self.close = timeout;
205 self
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct BufferConfig {
212 pub max_size: usize,
214
215 pub search_window: Option<usize>,
217
218 pub ring_buffer: bool,
220}
221
222impl Default for BufferConfig {
223 fn default() -> Self {
224 Self {
225 max_size: DEFAULT_BUFFER_SIZE,
226 search_window: None,
227 ring_buffer: true,
228 }
229 }
230}
231
232impl BufferConfig {
233 #[must_use]
235 pub fn new(max_size: usize) -> Self {
236 Self {
237 max_size,
238 ..Default::default()
239 }
240 }
241
242 #[must_use]
244 pub const fn search_window(mut self, size: usize) -> Self {
245 self.search_window = Some(size);
246 self
247 }
248
249 #[must_use]
251 pub const fn ring_buffer(mut self, enabled: bool) -> Self {
252 self.ring_buffer = enabled;
253 self
254 }
255}
256
257#[derive(Debug, Clone, Default)]
259pub struct LoggingConfig {
260 pub log_file: Option<PathBuf>,
262
263 pub log_user: bool,
265
266 pub format: LogFormat,
268
269 pub separate_io: bool,
271
272 pub redact_patterns: Vec<String>,
274}
275
276impl LoggingConfig {
277 #[must_use]
279 pub fn new() -> Self {
280 Self::default()
281 }
282
283 #[must_use]
285 pub fn log_file(mut self, path: impl Into<PathBuf>) -> Self {
286 self.log_file = Some(path.into());
287 self
288 }
289
290 #[must_use]
292 pub const fn log_user(mut self, enabled: bool) -> Self {
293 self.log_user = enabled;
294 self
295 }
296
297 #[must_use]
299 pub const fn format(mut self, format: LogFormat) -> Self {
300 self.format = format;
301 self
302 }
303
304 #[must_use]
306 pub fn redact(mut self, pattern: impl Into<String>) -> Self {
307 self.redact_patterns.push(pattern.into());
308 self
309 }
310}
311
312#[derive(Debug, Clone, Default, PartialEq, Eq)]
314pub enum LogFormat {
315 #[default]
317 Raw,
318
319 Timestamped,
321
322 Ndjson,
324
325 Asciicast,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum LineEnding {
332 Lf,
334
335 CrLf,
337
338 Cr,
340}
341
342impl Default for LineEnding {
356 fn default() -> Self {
357 #[cfg(windows)]
358 {
359 Self::Cr
360 }
361 #[cfg(not(windows))]
362 {
363 Self::Lf
364 }
365 }
366}
367
368impl LineEnding {
369 #[must_use]
371 pub const fn as_str(self) -> &'static str {
372 match self {
373 Self::Lf => "\n",
374 Self::CrLf => "\r\n",
375 Self::Cr => "\r",
376 }
377 }
378
379 #[must_use]
381 pub const fn as_bytes(self) -> &'static [u8] {
382 match self {
383 Self::Lf => b"\n",
384 Self::CrLf => b"\r\n",
385 Self::Cr => b"\r",
386 }
387 }
388
389 #[must_use]
391 pub const fn platform_default() -> Self {
392 if cfg!(windows) { Self::CrLf } else { Self::Lf }
393 }
394}
395
396#[derive(Debug, Clone)]
398pub struct EncodingConfig {
399 pub encoding: Encoding,
401
402 pub error_handling: EncodingErrorHandling,
404
405 pub normalize_line_endings: bool,
407}
408
409impl Default for EncodingConfig {
410 fn default() -> Self {
411 Self {
412 encoding: Encoding::Utf8,
413 error_handling: EncodingErrorHandling::Replace,
414 normalize_line_endings: false,
415 }
416 }
417}
418
419impl EncodingConfig {
420 #[must_use]
422 pub fn new(encoding: Encoding) -> Self {
423 Self {
424 encoding,
425 ..Default::default()
426 }
427 }
428
429 #[must_use]
431 pub const fn error_handling(mut self, mode: EncodingErrorHandling) -> Self {
432 self.error_handling = mode;
433 self
434 }
435
436 #[must_use]
438 pub const fn normalize_line_endings(mut self, normalize: bool) -> Self {
439 self.normalize_line_endings = normalize;
440 self
441 }
442}
443
444#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
446pub enum Encoding {
447 #[default]
449 Utf8,
450
451 Raw,
453
454 #[cfg(feature = "legacy-encoding")]
456 Latin1,
457
458 #[cfg(feature = "legacy-encoding")]
460 Windows1252,
461}
462
463#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
465pub enum EncodingErrorHandling {
466 #[default]
468 Replace,
469
470 Skip,
472
473 Strict,
475
476 Escape,
478}
479
480#[derive(Debug, Clone)]
482pub struct InteractConfig {
483 pub escape_char: Option<char>,
485
486 pub idle_timeout: Option<Duration>,
488
489 pub echo: bool,
491
492 pub output_hooks: Vec<InteractHook>,
494
495 pub input_hooks: Vec<InteractHook>,
497}
498
499impl Default for InteractConfig {
500 fn default() -> Self {
501 Self {
502 escape_char: Some('\x1d'), idle_timeout: None,
504 echo: true,
505 output_hooks: Vec::new(),
506 input_hooks: Vec::new(),
507 }
508 }
509}
510
511impl InteractConfig {
512 #[must_use]
514 pub fn new() -> Self {
515 Self::default()
516 }
517
518 #[must_use]
520 pub const fn escape_char(mut self, c: char) -> Self {
521 self.escape_char = Some(c);
522 self
523 }
524
525 #[must_use]
527 pub const fn no_escape(mut self) -> Self {
528 self.escape_char = None;
529 self
530 }
531
532 #[must_use]
534 pub const fn idle_timeout(mut self, timeout: Duration) -> Self {
535 self.idle_timeout = Some(timeout);
536 self
537 }
538
539 #[must_use]
541 pub const fn echo(mut self, enabled: bool) -> Self {
542 self.echo = enabled;
543 self
544 }
545}
546
547#[derive(Debug, Clone)]
549pub struct InteractHook {
550 pub pattern: String,
552
553 pub is_regex: bool,
555}
556
557impl InteractHook {
558 #[must_use]
560 pub fn literal(pattern: impl Into<String>) -> Self {
561 Self {
562 pattern: pattern.into(),
563 is_regex: false,
564 }
565 }
566
567 #[must_use]
569 pub fn regex(pattern: impl Into<String>) -> Self {
570 Self {
571 pattern: pattern.into(),
572 is_regex: true,
573 }
574 }
575}
576
577#[derive(Debug, Clone)]
579pub struct HumanTypingConfig {
580 pub base_delay: Duration,
582
583 pub variance: Duration,
585
586 pub typo_chance: f32,
588
589 pub correction_chance: f32,
591}
592
593impl Default for HumanTypingConfig {
594 fn default() -> Self {
595 Self {
596 base_delay: Duration::from_millis(100),
597 variance: Duration::from_millis(50),
598 typo_chance: 0.01,
599 correction_chance: 0.85,
600 }
601 }
602}
603
604impl HumanTypingConfig {
605 #[must_use]
607 pub fn new(base_delay: Duration, variance: Duration) -> Self {
608 Self {
609 base_delay,
610 variance,
611 ..Default::default()
612 }
613 }
614
615 #[must_use]
617 pub const fn typo_chance(mut self, chance: f32) -> Self {
618 self.typo_chance = chance;
619 self
620 }
621
622 #[must_use]
624 pub const fn correction_chance(mut self, chance: f32) -> Self {
625 self.correction_chance = chance;
626 self
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 #[test]
635 fn session_config_builder() {
636 let config = SessionConfig::new("bash")
637 .args(["-l", "-i"])
638 .env("MY_VAR", "value")
639 .dimensions(120, 40)
640 .timeout(Duration::from_secs(10));
641
642 assert_eq!(config.command, "bash");
643 assert_eq!(config.args, vec!["-l", "-i"]);
644 assert_eq!(config.env.get("MY_VAR"), Some(&"value".to_string()));
645 assert_eq!(config.dimensions, (120, 40));
646 assert_eq!(config.timeout.default, Duration::from_secs(10));
647 }
648
649 #[test]
650 fn line_ending_as_str() {
651 assert_eq!(LineEnding::Lf.as_str(), "\n");
652 assert_eq!(LineEnding::CrLf.as_str(), "\r\n");
653 assert_eq!(LineEnding::Cr.as_str(), "\r");
654 }
655
656 #[test]
657 fn default_config_has_term() {
658 let config = SessionConfig::default();
659 assert_eq!(config.env.get("TERM"), Some(&"xterm-256color".to_string()));
660 }
661
662 #[test]
663 fn logging_config_builder() {
664 let config = LoggingConfig::new()
665 .log_file("/tmp/session.log")
666 .log_user(true)
667 .format(LogFormat::Ndjson)
668 .redact("password");
669
670 assert_eq!(config.log_file, Some(PathBuf::from("/tmp/session.log")));
671 assert!(config.log_user);
672 assert_eq!(config.format, LogFormat::Ndjson);
673 assert_eq!(config.redact_patterns, vec!["password"]);
674 }
675}