diff --git a/Cargo.lock b/Cargo.lock index 29f0c43..682222a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2626,6 +2626,7 @@ dependencies = [ "log", "pop-telemetry", "portable-pty", + "regex", "serde", "serde_json", "tokio", diff --git a/Cargo.toml b/Cargo.toml index a79e302..d4eba24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ env_logger = "0.11" libc = "0.2" pop-telemetry = { git = "https://github.com/jasonwitty/pop-cli", branch = "main" } dirs = "5.0" +regex = "1.10" [lib] name = "webterm" diff --git a/src/lib.rs b/src/lib.rs index 7a82673..c88e43d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,6 @@ use serde_json::json; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); -const IDLE_TIMEOUT: Duration = Duration::from_secs(300); // 5 minutes const IDLE_CHECK_INTERVAL: Duration = Duration::from_secs(30); // Check every 30 seconds pub mod analytics; @@ -229,6 +228,7 @@ pub struct Terminal { last_activity: Instant, idle_timeout: Duration, analytics: Option, + command_buffer: String, } impl Terminal { @@ -240,8 +240,9 @@ impl Terminal { ws, command, last_activity: Instant::now(), - idle_timeout: IDLE_TIMEOUT, + idle_timeout: Duration::from_secs(300), analytics: None, + command_buffer: String::new(), } } @@ -253,8 +254,9 @@ impl Terminal { ws, command, last_activity: Instant::now(), - idle_timeout: IDLE_TIMEOUT, + idle_timeout: Duration::from_secs(300), analytics: Some(analytics), + command_buffer: String::new(), } } } @@ -417,13 +419,50 @@ impl Handler for Terminal { // Reset idle timer on user input self.last_activity = Instant::now(); - // Track command in analytics + // Buffer input and track command only when Enter is pressed if let Some(analytics) = &self.analytics { - let command = String::from_utf8_lossy(&io.0).to_string(); - let analytics_clone = analytics.clone(); - actix::spawn(async move { - let _ = analytics_clone.track_command(&command, None).await; - }); + let input = String::from_utf8_lossy(&io.0).to_string(); + + // Check if input contains newline (Enter key) + if input.contains('\n') || input.contains('\r') { + // Strip ANSI escape sequences and control codes + // Pattern matches: ESC[ followed by any characters until a letter + let mut cleaned_command = self.command_buffer.clone(); + + // Remove ANSI escape sequences like [<35;2;1M + // This regex pattern matches ESC [ followed by any non-letter chars and ending with a letter + let escape_pattern = regex::Regex::new(r"\x1b\[[^\x1b]*?[a-zA-Z]").unwrap(); + cleaned_command = + escape_pattern.replace_all(&cleaned_command, "").to_string(); + + // Remove CSI sequences without ESC prefix like [<35;2;1M + let csi_pattern = regex::Regex::new(r"\[<[0-9;]+[a-zA-Z]").unwrap(); + cleaned_command = csi_pattern.replace_all(&cleaned_command, "").to_string(); + + // Remove any remaining control characters + cleaned_command = cleaned_command + .chars() + .filter(|c| !c.is_control() || c.is_ascii_whitespace()) + .collect(); + + let command = cleaned_command.trim().to_string(); + + // Track if command has actual content (alphanumeric chars) + if !command.is_empty() && command.chars().any(|c| c.is_ascii_alphanumeric()) + { + log::info!("Tracking command: '{}'", command); + let analytics_clone = analytics.clone(); + actix::spawn(async move { + let _ = analytics_clone.track_command(&command, None).await; + }); + } + + // Clear buffer for next command + self.command_buffer.clear(); + } else { + // Accumulate input in buffer + self.command_buffer.push_str(&input); + } } let writer = match &mut self.pty_writer {