diff --git a/crates/gyandoot-browser/src/client.rs b/crates/gyandoot-browser/src/client.rs index 6e5354d..7378be0 100644 --- a/crates/gyandoot-browser/src/client.rs +++ b/crates/gyandoot-browser/src/client.rs @@ -330,7 +330,11 @@ impl BrowserClient { let (reader, mut writer) = stream.into_split(); writer.write_all(format!("{}\n", payload).as_bytes()).await.map_err(BrowserError::Io)?; - writer.shutdown().await.map_err(BrowserError::Io)?; + // Don't call writer.shutdown() — it sends EOF to the Node.js daemon, + // which causes the socket to close before the async response arrives. + // The daemon's safeWrite() checks socket.destroyed and bails silently. + // The socket pair (reader/writer) will be dropped after we read the response. + drop(writer); let mut buf_reader = BufReader::new(reader); let mut line = String::new(); @@ -729,12 +733,58 @@ impl BrowserClient { } } - /// Search via macro. + /// Search via macro — resolves macro names to actual search URLs + /// since the camoufox-browser daemon does not handle macros internally. pub async fn search(&self, macro_name: &str, query: &str) -> BrowserResult { + let url = match macro_name { + "@google_search" | "google" => { + format!("https://www.google.com/search?q={}&hl=en", urlencoding(query)) + } + "@youtube_search" | "youtube" => { + format!("https://www.youtube.com/results?search_query={}", urlencoding(query)) + } + "@amazon_search" | "amazon" => { + format!("https://www.amazon.com/s?k={}", urlencoding(query)) + } + "@reddit_search" | "reddit" => { + format!("https://www.reddit.com/search/?q={}", urlencoding(query)) + } + "@wikipedia_search" | "wikipedia" => { + format!("https://en.wikipedia.org/w/index.php?search={}", urlencoding(query)) + } + "@twitter_search" | "@x_search" | "twitter" | "x" => { + format!("https://x.com/search?q={}&src=typed_query", urlencoding(query)) + } + "@yelp_search" | "yelp" => { + format!("https://www.yelp.com/search?find_desc={}", urlencoding(query)) + } + "@spotify_search" | "spotify" => { + format!("https://open.spotify.com/search/{}", urlencoding(query)) + } + "@linkedin_search" | "linkedin" => { + format!("https://www.linkedin.com/search/results/all/?keywords={}", urlencoding(query)) + } + "@instagram_search" | "instagram" => { + format!("https://www.instagram.com/explore/search/keyword/?q={}", urlencoding(query)) + } + "@tiktok_search" | "tiktok" => { + format!("https://www.tiktok.com/search?q={}", urlencoding(query)) + } + "@twitch_search" | "twitch" => { + format!("https://www.twitch.tv/search?term={}", urlencoding(query)) + } + // If it looks like a URL already, use it as-is + s if s.starts_with("http://") || s.starts_with("https://") => s.to_string(), + // Default: Google search + _ => { + warn!("Unknown macro '{}', falling back to Google search", macro_name); + format!("https://www.google.com/search?q={}&hl=en", urlencoding(query)) + } + }; + let resp = self.send_command("navigate", |cmd| { - cmd.macro_name = Some(macro_name.to_string()); - cmd.query = Some(query.to_string()); - cmd.wait = Some(3000); + cmd.url = Some(url); + cmd.wait = Some(5000); }).await?; let data = self.check_response(resp)?; Ok(NavigationResult { @@ -763,6 +813,24 @@ impl BrowserClient { } } +/// URL-encode a string for use in a search query. +fn urlencoding(input: &str) -> String { + // Simple URL encoding — replaces spaces with + and encodes special chars + let mut result = String::with_capacity(input.len()); + for byte in input.bytes() { + match byte { + b' ' => result.push('+'), + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + result.push(byte as char); + } + _ => { + result.push_str(&format!("%{:02X}", byte)); + } + } + } + result +} + impl Default for BrowserClient { fn default() -> Self { Self::new()