diff --git a/src/cli.rs b/src/cli.rs index bf778e7..87c4ff7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,7 +16,10 @@ pub enum LinuxBackend { version )] pub struct Cli { - #[arg(long, help = "Optional startup URL (defaults to https://duckduckgo.com)")] + #[arg( + long, + help = "Optional startup URL (defaults to https://duckduckgo.com)" + )] pub url: Option, #[arg(long, help = "Set a custom window title")] @@ -32,7 +35,7 @@ pub struct Cli { long, value_enum, default_value_t = LinuxBackend::Auto, - help = "Linux only: prefer a windowing backend (auto, x11, wayland)" + help = "Linux only: select a GTK windowing backend (auto, x11, wayland)" )] pub linux_backend: LinuxBackend, diff --git a/src/webview.rs b/src/webview.rs index 90c0d4a..27ba488 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -2,6 +2,8 @@ use crate::cli::Cli; use anyhow::{Result, anyhow}; use base64::Engine; +#[cfg(target_os = "linux")] +use gtk::prelude::*; use serde_json::json; use std::collections::HashSet; use std::fs; @@ -9,13 +11,11 @@ use std::path::{Path, PathBuf}; use std::thread; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(target_os = "linux")] -use gtk::prelude::*; -#[cfg(target_os = "linux")] -use tao::platform::unix::WindowExtUnix; use tao::dpi::LogicalSize; use tao::event::{Event, WindowEvent}; use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}; +#[cfg(target_os = "linux")] +use tao::platform::unix::WindowExtUnix; use tao::window::{Icon, Window, WindowBuilder}; #[cfg(target_os = "linux")] use wry::WebViewBuilderExtUnix; @@ -38,13 +38,31 @@ const MAX_TAB_NAV_ENTRIES: usize = 100; #[derive(Debug)] enum UserEvent { ChromeIpc(String), - TitleChanged { tab_id: u32, title: String }, - PageLoaded { tab_id: u32, url: String }, - DownloadStarted { url: String, file_path: String }, - DownloadTotal { file_path: String, total_bytes: Option }, + TitleChanged { + tab_id: u32, + title: String, + }, + PageLoaded { + tab_id: u32, + url: String, + }, + DownloadStarted { + url: String, + file_path: String, + }, + DownloadTotal { + file_path: String, + total_bytes: Option, + }, DownloadTick, - DownloadCompleted { url: String, file_path: Option, success: bool }, - OpenInNewTab { url: String }, + DownloadCompleted { + url: String, + file_path: Option, + success: bool, + }, + OpenInNewTab { + url: String, + }, Quit, } @@ -713,7 +731,10 @@ pub fn launch_webkit(args: &Cli) -> Result<()> { .as_deref() .map(normalize_url) .unwrap_or_else(|| DEFAULT_HOME.to_string()); - let title = args.title.clone().unwrap_or_else(|| "RustyWolf".to_string()); + let title = args + .title + .clone() + .unwrap_or_else(|| "RustyWolf".to_string()); if args.dry_run { println!( @@ -773,10 +794,12 @@ pub fn launch_webkit(args: &Cli) -> Result<()> { open_tab(&mut state, &window, &proxy, start_url)?; let tick_proxy = proxy.clone(); - thread::spawn(move || loop { - thread::sleep(Duration::from_millis(300)); - if tick_proxy.send_event(UserEvent::DownloadTick).is_err() { - break; + thread::spawn(move || { + loop { + thread::sleep(Duration::from_millis(300)); + if tick_proxy.send_event(UserEvent::DownloadTick).is_err() { + break; + } } }); @@ -790,10 +813,16 @@ pub fn launch_webkit(args: &Cli) -> Result<()> { eprintln!("event error: {e:#}"); } } - Event::WindowEvent { event: WindowEvent::Resized(_size), .. } => { + Event::WindowEvent { + event: WindowEvent::Resized(_size), + .. + } => { reflow_layout(&window, &state); } - Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { *control_flow = ControlFlow::Exit; } _ => {} @@ -808,18 +837,14 @@ fn apply_linux_runtime_overrides(_args: &Cli) { match _args.linux_backend { crate::cli::LinuxBackend::Auto => {} - crate::cli::LinuxBackend::X11 => { - unsafe { - std::env::set_var("WINIT_UNIX_BACKEND", "x11"); - std::env::set_var("GDK_BACKEND", "x11"); - } - } - crate::cli::LinuxBackend::Wayland => { - unsafe { - std::env::set_var("WINIT_UNIX_BACKEND", "wayland"); - std::env::set_var("GDK_BACKEND", "wayland"); - } - } + crate::cli::LinuxBackend::X11 => unsafe { + std::env::set_var("WINIT_UNIX_BACKEND", "x11"); + std::env::set_var("GDK_BACKEND", "x11"); + }, + crate::cli::LinuxBackend::Wayland => unsafe { + std::env::set_var("WINIT_UNIX_BACKEND", "wayland"); + std::env::set_var("GDK_BACKEND", "wayland"); + }, } if _args.linux_disable_dmabuf { @@ -844,7 +869,9 @@ fn sanitize_gtk_modules_env() { match std::env::var(name) { Ok(raw) => { let mut kept = Vec::new(); - for token in raw.split(|c: char| c == ':' || c == ';' || c == ',' || c.is_ascii_whitespace()) { + for token in + raw.split(|c: char| c == ':' || c == ';' || c == ',' || c.is_ascii_whitespace()) + { let trimmed = token.trim(); if trimmed.is_empty() || is_appmenu_module(trimmed) { continue; @@ -882,27 +909,8 @@ fn sanitize_gtk_modules_env() { fn resolve_webview_host_mode(args: &Cli) -> WebViewHostMode { #[cfg(target_os = "linux")] { - return match args.linux_backend { - crate::cli::LinuxBackend::X11 => WebViewHostMode::Child, - crate::cli::LinuxBackend::Wayland => WebViewHostMode::Window, - crate::cli::LinuxBackend::Auto => { - let forced = std::env::var("WINIT_UNIX_BACKEND").ok(); - if forced.as_deref() == Some("x11") { - return WebViewHostMode::Child; - } - if forced.as_deref() == Some("wayland") { - return WebViewHostMode::Window; - } - - let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default(); - let has_wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some(); - if session_type.eq_ignore_ascii_case("wayland") || has_wayland_display { - WebViewHostMode::Window - } else { - WebViewHostMode::Child - } - } - }; + let _ = args; + WebViewHostMode::Window } #[cfg(not(target_os = "linux"))] @@ -956,7 +964,11 @@ fn handle_event( state.tabs[i].url.clone(), ); } - let enabled = if state.adblock_enabled { "true" } else { "false" }; + let enabled = if state.adblock_enabled { + "true" + } else { + "false" + }; if let Some(webview) = state.tabs[i].webview.as_ref() { let _ = webview.evaluate_script(&format!( "window.__rustywolfSetAdblockEnabled && window.__rustywolfSetAdblockEnabled({enabled});" @@ -979,7 +991,10 @@ fn handle_event( trim_downloads(state); push_downloads(state); } - UserEvent::DownloadTotal { file_path, total_bytes } => { + UserEvent::DownloadTotal { + file_path, + total_bytes, + } => { if let Some(entry) = state .downloads .iter_mut() @@ -1005,7 +1020,11 @@ fn handle_event( push_downloads(state); } } - UserEvent::DownloadCompleted { url, file_path, success } => { + UserEvent::DownloadCompleted { + url, + file_path, + success, + } => { let path = file_path.unwrap_or_else(|| "Unknown destination".to_string()); if let Some(entry) = state .downloads @@ -1262,25 +1281,15 @@ fn build_chrome( WebViewHostMode::Window => { #[cfg(target_os = "linux")] { - if let Some(container) = gtk_chrome_container { - match builder().build_gtk(container) { - Ok(webview) => return Ok((webview, WebViewHostMode::Window)), - Err(gtk_err) => { - return builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|fallback_err| { - anyhow!( - "Failed to create chrome webview: gtk hosting failed ({gtk_err}); window fallback failed ({fallback_err})" - ) - }); - } - } - } - return builder() - .build(window) + let container = gtk_chrome_container.ok_or_else(|| { + anyhow!("Failed to create chrome webview: missing Linux GTK chrome container") + })?; + builder() + .build_gtk(container) .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|err| anyhow!("Failed to create chrome webview: {err}")); + .map_err(|err| { + anyhow!("Failed to create chrome webview with GTK hosting: {err}") + }) } #[cfg(not(target_os = "linux"))] @@ -1302,28 +1311,18 @@ fn build_chrome( #[cfg(target_os = "linux")] { if let Some(container) = gtk_chrome_container { - match builder().build_gtk(container) { - Ok(webview) => return Ok((webview, WebViewHostMode::Window)), - Err(gtk_err) => { - return builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|fallback_err| { - anyhow!( - "Failed to create chrome webview: child failed ({err_text}); gtk fallback failed ({gtk_err}); window fallback failed ({fallback_err})" - ) - }); - } - } + return builder() + .build_gtk(container) + .map(|webview| (webview, WebViewHostMode::Window)) + .map_err(|gtk_err| { + anyhow!( + "Failed to create chrome webview: child failed ({err_text}); GTK fallback failed ({gtk_err})" + ) + }); } - return builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|fallback_err| { - anyhow!( - "Failed to create chrome webview: child failed ({err_text}); window fallback failed ({fallback_err})" - ) - }); + return Err(anyhow!( + "Failed to create chrome webview: child failed ({err_text}); missing Linux GTK chrome container" + )); } #[cfg(not(target_os = "linux"))] @@ -1405,7 +1404,11 @@ fn build_tab_webview( } *destination = path.clone(); let path_string = path_to_string(&path); - fetch_download_total(url.clone(), path_string.clone(), download_total_proxy.clone()); + fetch_download_total( + url.clone(), + path_string.clone(), + download_total_proxy.clone(), + ); let _ = download_started_proxy.send_event(UserEvent::DownloadStarted { url, file_path: path_string, @@ -1425,25 +1428,13 @@ fn build_tab_webview( WebViewHostMode::Window => { #[cfg(target_os = "linux")] { - if let Some(container) = gtk_content_container { - match builder().build_gtk(container) { - Ok(webview) => return Ok((webview, WebViewHostMode::Window)), - Err(gtk_err) => { - return builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|fallback_err| { - anyhow!( - "Failed to create tab webview: gtk hosting failed ({gtk_err}); window fallback failed ({fallback_err})" - ) - }); - } - } - } - return builder() - .build(window) + let container = gtk_content_container.ok_or_else(|| { + anyhow!("Failed to create tab webview: missing Linux GTK content container") + })?; + builder() + .build_gtk(container) .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|err| anyhow!("Failed to create tab webview: {err}")); + .map_err(|err| anyhow!("Failed to create tab webview with GTK hosting: {err}")) } #[cfg(not(target_os = "linux"))] @@ -1465,28 +1456,18 @@ fn build_tab_webview( #[cfg(target_os = "linux")] { if let Some(container) = gtk_content_container { - match builder().build_gtk(container) { - Ok(webview) => return Ok((webview, WebViewHostMode::Window)), - Err(gtk_err) => { - return builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|fallback_err| { - anyhow!( - "Failed to create tab webview: child failed ({err_text}); gtk fallback failed ({gtk_err}); window fallback failed ({fallback_err})" - ) - }); - } - } + return builder() + .build_gtk(container) + .map(|webview| (webview, WebViewHostMode::Window)) + .map_err(|gtk_err| { + anyhow!( + "Failed to create tab webview: child failed ({err_text}); GTK fallback failed ({gtk_err})" + ) + }); } - return builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|fallback_err| { - anyhow!( - "Failed to create tab webview: child failed ({err_text}); window fallback failed ({fallback_err})" - ) - }); + return Err(anyhow!( + "Failed to create tab webview: child failed ({err_text}); missing Linux GTK content container" + )); } #[cfg(not(target_os = "linux"))] @@ -1580,7 +1561,9 @@ fn close_tab( proxy: &EventLoopProxy, tab_id: u32, ) -> Result { - let Some(index) = state.find_index(tab_id) else { return Ok(false); }; + let Some(index) = state.find_index(tab_id) else { + return Ok(false); + }; state.tabs.remove(index); if state.tabs.is_empty() { @@ -1669,7 +1652,9 @@ fn apply_visibility(state: &BrowserState) { } fn push_tabs(state: &BrowserState) { - if state.tabs.is_empty() { return; } + if state.tabs.is_empty() { + return; + } let payload = json!({ "tabs": state.tabs.iter().map(|t| json!({ "id": t.id, @@ -1705,15 +1690,19 @@ fn push_downloads(state: &BrowserState) { .collect::>(), }) .to_string(); - let script = format!( - "window.__rustywolfSetDownloads && window.__rustywolfSetDownloads({payload});" - ); + let script = + format!("window.__rustywolfSetDownloads && window.__rustywolfSetDownloads({payload});"); let _ = state.chrome.evaluate_script(&script); } fn push_adblock(state: &BrowserState) { - let enabled = if state.adblock_enabled { "true" } else { "false" }; - let script = format!("window.__rustywolfSetAdblock && window.__rustywolfSetAdblock({enabled});"); + let enabled = if state.adblock_enabled { + "true" + } else { + "false" + }; + let script = + format!("window.__rustywolfSetAdblock && window.__rustywolfSetAdblock({enabled});"); let _ = state.chrome.evaluate_script(&script); } @@ -1726,14 +1715,17 @@ fn push_popup_prompt(state: &BrowserState) { None => json!({ "visible": false }), } .to_string(); - let script = format!( - "window.__rustywolfSetPopupPrompt && window.__rustywolfSetPopupPrompt({payload});" - ); + let script = + format!("window.__rustywolfSetPopupPrompt && window.__rustywolfSetPopupPrompt({payload});"); let _ = state.chrome.evaluate_script(&script); } fn apply_adblock_setting_to_tabs(state: &BrowserState) { - let enabled = if state.adblock_enabled { "true" } else { "false" }; + let enabled = if state.adblock_enabled { + "true" + } else { + "false" + }; let script = format!( "window.__rustywolfSetAdblockEnabled && window.__rustywolfSetAdblockEnabled({enabled});" ); @@ -1860,7 +1852,9 @@ fn chrome_height(state: &BrowserState) -> f64 { fn normalize_url(input: &str) -> String { let trimmed = input.trim(); - if trimmed == "about:blank" { return trimmed.to_string(); } + if trimmed == "about:blank" { + return trimmed.to_string(); + } if trimmed.starts_with("http://") || trimmed.starts_with("https://") { return trimmed.to_string(); } @@ -1963,8 +1957,7 @@ fn render_properties_html(state: &BrowserState) -> String {

Browser Properties

Session history and download activity.

\

Browsing History

{}

{history_rows}
VisitedTitleURL
\

Download History

{}

{download_rows}
UpdatedStatusSaved ToSource URL
\ - " - , + ", if history_is_truncated { format!("Showing latest {PROPERTIES_MAX_ROWS} entries.") } else { @@ -2105,8 +2098,8 @@ fn path_string_to_file_url(path: &str) -> String { fn sanitize_filename(raw: &str) -> String { let mut out = String::with_capacity(raw.len()); for ch in raw.chars() { - let invalid = matches!(ch, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*') - || ch.is_control(); + let invalid = + matches!(ch, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*') || ch.is_control(); out.push(if invalid { '_' } else { ch }); } let trimmed = out.trim_matches('.').trim(); @@ -2119,10 +2112,10 @@ fn sanitize_filename(raw: &str) -> String { fn fetch_download_total(url: String, file_path: String, proxy: EventLoopProxy) { thread::spawn(move || { - let total_bytes = ureq::head(&url) - .call() - .ok() - .and_then(|resp| resp.header("Content-Length").and_then(|v| v.parse::().ok())); + let total_bytes = ureq::head(&url).call().ok().and_then(|resp| { + resp.header("Content-Length") + .and_then(|v| v.parse::().ok()) + }); let _ = proxy.send_event(UserEvent::DownloadTotal { file_path, total_bytes, @@ -2162,7 +2155,10 @@ mod tests { #[test] fn converts_search_terms_to_duckduckgo_query() { - assert_eq!(normalize_url("privacy browser"), "https://duckduckgo.com/?q=privacy+browser"); + assert_eq!( + normalize_url("privacy browser"), + "https://duckduckgo.com/?q=privacy+browser" + ); } #[test] @@ -2172,7 +2168,10 @@ mod tests { #[test] fn trims_before_search() { - assert_eq!(normalize_url(" rust webview "), "https://duckduckgo.com/?q=rust+webview"); + assert_eq!( + normalize_url(" rust webview "), + "https://duckduckgo.com/?q=rust+webview" + ); } #[test] @@ -2183,9 +2182,13 @@ mod tests { #[test] fn blocks_duckduckgo_internal_popup_paths() { - assert!(!should_open_new_tab_url("https://duckduckgo.com/post3.html")); + assert!(!should_open_new_tab_url( + "https://duckduckgo.com/post3.html" + )); assert!(!should_open_new_tab_url("https://duckduckgo.com/page3")); - assert!(!should_open_new_tab_url("https://duckduckgo.com/page3.html")); + assert!(!should_open_new_tab_url( + "https://duckduckgo.com/page3.html" + )); } #[test]