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, Default, PartialEq, Eq)]
331pub enum LineEnding {
332 #[default]
334 Lf,
335
336 CrLf,
338
339 Cr,
341}
342
343impl LineEnding {
344 #[must_use]
346 pub const fn as_str(self) -> &'static str {
347 match self {
348 Self::Lf => "\n",
349 Self::CrLf => "\r\n",
350 Self::Cr => "\r",
351 }
352 }
353
354 #[must_use]
356 pub const fn as_bytes(self) -> &'static [u8] {
357 match self {
358 Self::Lf => b"\n",
359 Self::CrLf => b"\r\n",
360 Self::Cr => b"\r",
361 }
362 }
363
364 #[must_use]
366 pub const fn platform_default() -> Self {
367 if cfg!(windows) { Self::CrLf } else { Self::Lf }
368 }
369}
370
371#[derive(Debug, Clone)]
373pub struct EncodingConfig {
374 pub encoding: Encoding,
376
377 pub error_handling: EncodingErrorHandling,
379
380 pub normalize_line_endings: bool,
382}
383
384impl Default for EncodingConfig {
385 fn default() -> Self {
386 Self {
387 encoding: Encoding::Utf8,
388 error_handling: EncodingErrorHandling::Replace,
389 normalize_line_endings: false,
390 }
391 }
392}
393
394impl EncodingConfig {
395 #[must_use]
397 pub fn new(encoding: Encoding) -> Self {
398 Self {
399 encoding,
400 ..Default::default()
401 }
402 }
403
404 #[must_use]
406 pub const fn error_handling(mut self, mode: EncodingErrorHandling) -> Self {
407 self.error_handling = mode;
408 self
409 }
410
411 #[must_use]
413 pub const fn normalize_line_endings(mut self, normalize: bool) -> Self {
414 self.normalize_line_endings = normalize;
415 self
416 }
417}
418
419#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
421pub enum Encoding {
422 #[default]
424 Utf8,
425
426 Raw,
428
429 #[cfg(feature = "legacy-encoding")]
431 Latin1,
432
433 #[cfg(feature = "legacy-encoding")]
435 Windows1252,
436}
437
438#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
440pub enum EncodingErrorHandling {
441 #[default]
443 Replace,
444
445 Skip,
447
448 Strict,
450
451 Escape,
453}
454
455#[derive(Debug, Clone)]
457pub struct InteractConfig {
458 pub escape_char: Option<char>,
460
461 pub idle_timeout: Option<Duration>,
463
464 pub echo: bool,
466
467 pub output_hooks: Vec<InteractHook>,
469
470 pub input_hooks: Vec<InteractHook>,
472}
473
474impl Default for InteractConfig {
475 fn default() -> Self {
476 Self {
477 escape_char: Some('\x1d'), idle_timeout: None,
479 echo: true,
480 output_hooks: Vec::new(),
481 input_hooks: Vec::new(),
482 }
483 }
484}
485
486impl InteractConfig {
487 #[must_use]
489 pub fn new() -> Self {
490 Self::default()
491 }
492
493 #[must_use]
495 pub const fn escape_char(mut self, c: char) -> Self {
496 self.escape_char = Some(c);
497 self
498 }
499
500 #[must_use]
502 pub const fn no_escape(mut self) -> Self {
503 self.escape_char = None;
504 self
505 }
506
507 #[must_use]
509 pub const fn idle_timeout(mut self, timeout: Duration) -> Self {
510 self.idle_timeout = Some(timeout);
511 self
512 }
513
514 #[must_use]
516 pub const fn echo(mut self, enabled: bool) -> Self {
517 self.echo = enabled;
518 self
519 }
520}
521
522#[derive(Debug, Clone)]
524pub struct InteractHook {
525 pub pattern: String,
527
528 pub is_regex: bool,
530}
531
532impl InteractHook {
533 #[must_use]
535 pub fn literal(pattern: impl Into<String>) -> Self {
536 Self {
537 pattern: pattern.into(),
538 is_regex: false,
539 }
540 }
541
542 #[must_use]
544 pub fn regex(pattern: impl Into<String>) -> Self {
545 Self {
546 pattern: pattern.into(),
547 is_regex: true,
548 }
549 }
550}
551
552#[derive(Debug, Clone)]
554pub struct HumanTypingConfig {
555 pub base_delay: Duration,
557
558 pub variance: Duration,
560
561 pub typo_chance: f32,
563
564 pub correction_chance: f32,
566}
567
568impl Default for HumanTypingConfig {
569 fn default() -> Self {
570 Self {
571 base_delay: Duration::from_millis(100),
572 variance: Duration::from_millis(50),
573 typo_chance: 0.01,
574 correction_chance: 0.85,
575 }
576 }
577}
578
579impl HumanTypingConfig {
580 #[must_use]
582 pub fn new(base_delay: Duration, variance: Duration) -> Self {
583 Self {
584 base_delay,
585 variance,
586 ..Default::default()
587 }
588 }
589
590 #[must_use]
592 pub const fn typo_chance(mut self, chance: f32) -> Self {
593 self.typo_chance = chance;
594 self
595 }
596
597 #[must_use]
599 pub const fn correction_chance(mut self, chance: f32) -> Self {
600 self.correction_chance = chance;
601 self
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn session_config_builder() {
611 let config = SessionConfig::new("bash")
612 .args(["-l", "-i"])
613 .env("MY_VAR", "value")
614 .dimensions(120, 40)
615 .timeout(Duration::from_secs(10));
616
617 assert_eq!(config.command, "bash");
618 assert_eq!(config.args, vec!["-l", "-i"]);
619 assert_eq!(config.env.get("MY_VAR"), Some(&"value".to_string()));
620 assert_eq!(config.dimensions, (120, 40));
621 assert_eq!(config.timeout.default, Duration::from_secs(10));
622 }
623
624 #[test]
625 fn line_ending_as_str() {
626 assert_eq!(LineEnding::Lf.as_str(), "\n");
627 assert_eq!(LineEnding::CrLf.as_str(), "\r\n");
628 assert_eq!(LineEnding::Cr.as_str(), "\r");
629 }
630
631 #[test]
632 fn default_config_has_term() {
633 let config = SessionConfig::default();
634 assert_eq!(config.env.get("TERM"), Some(&"xterm-256color".to_string()));
635 }
636
637 #[test]
638 fn logging_config_builder() {
639 let config = LoggingConfig::new()
640 .log_file("/tmp/session.log")
641 .log_user(true)
642 .format(LogFormat::Ndjson)
643 .redact("password");
644
645 assert_eq!(config.log_file, Some(PathBuf::from("/tmp/session.log")));
646 assert!(config.log_user);
647 assert_eq!(config.format, LogFormat::Ndjson);
648 assert_eq!(config.redact_patterns, vec!["password"]);
649 }
650}