Skip to main content

limit_cli/tools/browser/
client.rs

1//! Browser client - shared API for browser automation
2//!
3//! Provides a high-level API for browser operations used by both
4//! the TUI command and the agent tool.
5
6use super::config::BrowserConfig;
7use super::executor::{BrowserError, BrowserExecutor};
8use std::sync::Arc;
9
10/// Browser client for high-level operations
11pub struct BrowserClient {
12    executor: Arc<dyn BrowserExecutor>,
13}
14
15impl BrowserClient {
16    /// Create a new browser client with the given executor
17    pub fn new(executor: Arc<dyn BrowserExecutor>) -> Self {
18        Self { executor }
19    }
20
21    /// Create a client with default configuration
22    pub fn with_default_config() -> Self {
23        let config = BrowserConfig::default();
24        let executor = Arc::new(super::executor::CliExecutor::new(config));
25        Self::new(executor)
26    }
27
28    /// Get reference to executor (for extension traits)
29    pub fn executor(&self) -> &Arc<dyn BrowserExecutor> {
30        &self.executor
31    }
32
33    /// Open a URL in the browser
34    pub async fn open(&self, url: &str) -> Result<(), BrowserError> {
35        self.validate_url(url)?;
36
37        let output = self.executor.execute(&["open", url]).await?;
38
39        if output.success {
40            Ok(())
41        } else {
42            Err(BrowserError::Other(format!(
43                "Failed to open URL: {}",
44                output.stderr
45            )))
46        }
47    }
48
49    /// Close the browser
50    pub async fn close(&self) -> Result<(), BrowserError> {
51        let output = self.executor.execute(&["close"]).await?;
52
53        if output.success {
54            Ok(())
55        } else {
56            Err(BrowserError::Other(format!(
57                "Failed to close browser: {}",
58                output.stderr
59            )))
60        }
61    }
62
63    /// Check if browser daemon is running
64    pub fn is_daemon_running(&self) -> bool {
65        self.executor.is_daemon_running()
66    }
67
68    /// Validate a URL
69    fn validate_url(&self, url: &str) -> Result<(), BrowserError> {
70        if url.is_empty() {
71            return Err(BrowserError::InvalidArguments(
72                "URL cannot be empty".to_string(),
73            ));
74        }
75
76        if !url.starts_with("http://") && !url.starts_with("https://") {
77            return Err(BrowserError::InvalidArguments(
78                "URL must start with http:// or https://".to_string(),
79            ));
80        }
81
82        Ok(())
83    }
84
85    /// Extract a field value from snapshot content
86    pub(crate) fn extract_field(content: &str, field_name: &str) -> Option<String> {
87        for line in content.lines() {
88            if let Some(stripped) = line.strip_prefix(field_name) {
89                return Some(stripped.trim().to_string());
90            }
91        }
92        None
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_extract_field() {
102        let content = "Title: Example\nURL: https://example.com\nOther content";
103        assert_eq!(
104            BrowserClient::extract_field(content, "Title:"),
105            Some("Example".to_string())
106        );
107        assert_eq!(
108            BrowserClient::extract_field(content, "URL:"),
109            Some("https://example.com".to_string())
110        );
111        assert_eq!(BrowserClient::extract_field(content, "Missing:"), None);
112    }
113}