use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_BUFFER_SIZE: usize = 100 * 1024 * 1024;
pub const DEFAULT_TERMINAL_WIDTH: u16 = 80;
pub const DEFAULT_TERMINAL_HEIGHT: u16 = 24;
pub const DEFAULT_TERM: &str = "xterm-256color";
pub const DEFAULT_DELAY_BEFORE_SEND: Duration = Duration::from_millis(50);
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub command: String,
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub inherit_env: bool,
pub working_dir: Option<PathBuf>,
pub dimensions: (u16, u16),
pub timeout: TimeoutConfig,
pub buffer: BufferConfig,
pub logging: LoggingConfig,
pub line_ending: LineEnding,
pub encoding: EncodingConfig,
pub delay_before_send: Duration,
}
impl Default for SessionConfig {
fn default() -> Self {
let mut env = HashMap::new();
env.insert("TERM".to_string(), DEFAULT_TERM.to_string());
Self {
command: String::new(),
args: Vec::new(),
env,
inherit_env: true,
working_dir: None,
dimensions: (DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_HEIGHT),
timeout: TimeoutConfig::default(),
buffer: BufferConfig::default(),
logging: LoggingConfig::default(),
line_ending: LineEnding::default(),
encoding: EncodingConfig::default(),
delay_before_send: DEFAULT_DELAY_BEFORE_SEND,
}
}
}
impl SessionConfig {
#[must_use]
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
..Default::default()
}
}
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args = args.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
#[must_use]
pub const fn inherit_env(mut self, inherit: bool) -> Self {
self.inherit_env = inherit;
self
}
#[must_use]
pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.working_dir = Some(path.into());
self
}
#[must_use]
pub const fn dimensions(mut self, width: u16, height: u16) -> Self {
self.dimensions = (width, height);
self
}
#[must_use]
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout.default = timeout;
self
}
#[must_use]
pub const fn line_ending(mut self, line_ending: LineEnding) -> Self {
self.line_ending = line_ending;
self
}
#[must_use]
pub const fn delay_before_send(mut self, delay: Duration) -> Self {
self.delay_before_send = delay;
self
}
}
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
pub default: Duration,
pub spawn: Duration,
pub close: Duration,
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
default: DEFAULT_TIMEOUT,
spawn: Duration::from_secs(60),
close: Duration::from_secs(10),
}
}
}
impl TimeoutConfig {
#[must_use]
pub fn new(default: Duration) -> Self {
Self {
default,
..Default::default()
}
}
#[must_use]
pub const fn spawn(mut self, timeout: Duration) -> Self {
self.spawn = timeout;
self
}
#[must_use]
pub const fn close(mut self, timeout: Duration) -> Self {
self.close = timeout;
self
}
}
#[derive(Debug, Clone)]
pub struct BufferConfig {
pub max_size: usize,
pub search_window: Option<usize>,
pub ring_buffer: bool,
}
impl Default for BufferConfig {
fn default() -> Self {
Self {
max_size: DEFAULT_BUFFER_SIZE,
search_window: None,
ring_buffer: true,
}
}
}
impl BufferConfig {
#[must_use]
pub fn new(max_size: usize) -> Self {
Self {
max_size,
..Default::default()
}
}
#[must_use]
pub const fn search_window(mut self, size: usize) -> Self {
self.search_window = Some(size);
self
}
#[must_use]
pub const fn ring_buffer(mut self, enabled: bool) -> Self {
self.ring_buffer = enabled;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct LoggingConfig {
pub log_file: Option<PathBuf>,
pub log_user: bool,
pub format: LogFormat,
pub separate_io: bool,
pub redact_patterns: Vec<String>,
}
impl LoggingConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn log_file(mut self, path: impl Into<PathBuf>) -> Self {
self.log_file = Some(path.into());
self
}
#[must_use]
pub const fn log_user(mut self, enabled: bool) -> Self {
self.log_user = enabled;
self
}
#[must_use]
pub const fn format(mut self, format: LogFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn redact(mut self, pattern: impl Into<String>) -> Self {
self.redact_patterns.push(pattern.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum LogFormat {
#[default]
Raw,
Timestamped,
Ndjson,
Asciicast,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LineEnding {
#[default]
Lf,
CrLf,
Cr,
}
impl LineEnding {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Lf => "\n",
Self::CrLf => "\r\n",
Self::Cr => "\r",
}
}
#[must_use]
pub const fn as_bytes(self) -> &'static [u8] {
match self {
Self::Lf => b"\n",
Self::CrLf => b"\r\n",
Self::Cr => b"\r",
}
}
#[must_use]
pub const fn platform_default() -> Self {
if cfg!(windows) { Self::CrLf } else { Self::Lf }
}
}
#[derive(Debug, Clone)]
pub struct EncodingConfig {
pub encoding: Encoding,
pub error_handling: EncodingErrorHandling,
pub normalize_line_endings: bool,
}
impl Default for EncodingConfig {
fn default() -> Self {
Self {
encoding: Encoding::Utf8,
error_handling: EncodingErrorHandling::Replace,
normalize_line_endings: false,
}
}
}
impl EncodingConfig {
#[must_use]
pub fn new(encoding: Encoding) -> Self {
Self {
encoding,
..Default::default()
}
}
#[must_use]
pub const fn error_handling(mut self, mode: EncodingErrorHandling) -> Self {
self.error_handling = mode;
self
}
#[must_use]
pub const fn normalize_line_endings(mut self, normalize: bool) -> Self {
self.normalize_line_endings = normalize;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Encoding {
#[default]
Utf8,
Raw,
#[cfg(feature = "legacy-encoding")]
Latin1,
#[cfg(feature = "legacy-encoding")]
Windows1252,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum EncodingErrorHandling {
#[default]
Replace,
Skip,
Strict,
Escape,
}
#[derive(Debug, Clone)]
pub struct InteractConfig {
pub escape_char: Option<char>,
pub idle_timeout: Option<Duration>,
pub echo: bool,
pub output_hooks: Vec<InteractHook>,
pub input_hooks: Vec<InteractHook>,
}
impl Default for InteractConfig {
fn default() -> Self {
Self {
escape_char: Some('\x1d'), idle_timeout: None,
echo: true,
output_hooks: Vec::new(),
input_hooks: Vec::new(),
}
}
}
impl InteractConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn escape_char(mut self, c: char) -> Self {
self.escape_char = Some(c);
self
}
#[must_use]
pub const fn no_escape(mut self) -> Self {
self.escape_char = None;
self
}
#[must_use]
pub const fn idle_timeout(mut self, timeout: Duration) -> Self {
self.idle_timeout = Some(timeout);
self
}
#[must_use]
pub const fn echo(mut self, enabled: bool) -> Self {
self.echo = enabled;
self
}
}
#[derive(Debug, Clone)]
pub struct InteractHook {
pub pattern: String,
pub is_regex: bool,
}
impl InteractHook {
#[must_use]
pub fn literal(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
is_regex: false,
}
}
#[must_use]
pub fn regex(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
is_regex: true,
}
}
}
#[derive(Debug, Clone)]
pub struct HumanTypingConfig {
pub base_delay: Duration,
pub variance: Duration,
pub typo_chance: f32,
pub correction_chance: f32,
}
impl Default for HumanTypingConfig {
fn default() -> Self {
Self {
base_delay: Duration::from_millis(100),
variance: Duration::from_millis(50),
typo_chance: 0.01,
correction_chance: 0.85,
}
}
}
impl HumanTypingConfig {
#[must_use]
pub fn new(base_delay: Duration, variance: Duration) -> Self {
Self {
base_delay,
variance,
..Default::default()
}
}
#[must_use]
pub const fn typo_chance(mut self, chance: f32) -> Self {
self.typo_chance = chance;
self
}
#[must_use]
pub const fn correction_chance(mut self, chance: f32) -> Self {
self.correction_chance = chance;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_config_builder() {
let config = SessionConfig::new("bash")
.args(["-l", "-i"])
.env("MY_VAR", "value")
.dimensions(120, 40)
.timeout(Duration::from_secs(10));
assert_eq!(config.command, "bash");
assert_eq!(config.args, vec!["-l", "-i"]);
assert_eq!(config.env.get("MY_VAR"), Some(&"value".to_string()));
assert_eq!(config.dimensions, (120, 40));
assert_eq!(config.timeout.default, Duration::from_secs(10));
}
#[test]
fn line_ending_as_str() {
assert_eq!(LineEnding::Lf.as_str(), "\n");
assert_eq!(LineEnding::CrLf.as_str(), "\r\n");
assert_eq!(LineEnding::Cr.as_str(), "\r");
}
#[test]
fn default_config_has_term() {
let config = SessionConfig::default();
assert_eq!(config.env.get("TERM"), Some(&"xterm-256color".to_string()));
}
#[test]
fn logging_config_builder() {
let config = LoggingConfig::new()
.log_file("/tmp/session.log")
.log_user(true)
.format(LogFormat::Ndjson)
.redact("password");
assert_eq!(config.log_file, Some(PathBuf::from("/tmp/session.log")));
assert!(config.log_user);
assert_eq!(config.format, LogFormat::Ndjson);
assert_eq!(config.redact_patterns, vec!["password"]);
}
}