diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 61ee164a9..0dda26a8d 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -490,17 +490,24 @@ pub fn init_scheme() -> Result<()> { } #[cfg(target_os = "linux")] pub fn init_scheme() -> Result<()> { - let output = std::process::Command::new("xdg-mime") - .arg("default") - .arg("clash-verge.desktop") - .arg("x-scheme-handler/clash") - .output()?; - if !output.status.success() { - return Err(anyhow::anyhow!( - "failed to set clash scheme, {}", - String::from_utf8_lossy(&output.stderr) - )); + const DESKTOP_FILE: &str = "clash-verge.desktop"; + + for scheme in DEEP_LINK_SCHEMES { + let handler = format!("x-scheme-handler/{scheme}"); + let output = std::process::Command::new("xdg-mime") + .arg("default") + .arg(DESKTOP_FILE) + .arg(&handler) + .output()?; + if !output.status.success() { + return Err(anyhow::anyhow!( + "failed to set {handler}, {}", + String::from_utf8_lossy(&output.stderr) + )); + } } + + crate::utils::linux::ensure_mimeapps_entries(DESKTOP_FILE, DEEP_LINK_SCHEMES)?; Ok(()) } #[cfg(target_os = "macos")] @@ -508,6 +515,9 @@ pub fn init_scheme() -> Result<()> { Ok(()) } +#[cfg(target_os = "linux")] +const DEEP_LINK_SCHEMES: &[&str] = &["clash", "clash-verge"]; + pub async fn startup_script() -> Result<()> { let app_handle = handle::Handle::app_handle(); let script_path = { diff --git a/src-tauri/src/utils/linux.rs b/src-tauri/src/utils/linux.rs index 2094811b3..678008597 100644 --- a/src-tauri/src/utils/linux.rs +++ b/src-tauri/src/utils/linux.rs @@ -1,5 +1,6 @@ use crate::logging; use crate::utils::logging::Type; +use anyhow::Result; use std::collections::HashSet; use std::env; use std::fs; @@ -472,6 +473,233 @@ fn extract_nvidia_driver_version(summary: &str) -> Option<&str> { .find(|token| token.chars().all(|c| c.is_ascii_digit() || c == '.')) } +pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<()> { + let Some(path) = mimeapps_list_path() else { + return Ok(()); + }; + + if !path.exists() { + return Ok(()); + } + + let original = fs::read_to_string(&path)?; + let mut changed = false; + + let mut output_lines: Vec = Vec::new(); + let mut current_section: Option = None; + let mut section_buffer: Vec = Vec::new(); + let mut default_present = false; + + for line in original.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('[') { + if let Some(kind) = current_section.take() { + flush_section( + &mut output_lines, + &mut section_buffer, + desktop_file, + schemes, + kind, + &mut changed, + ); + } + + if trimmed.eq_ignore_ascii_case("[Default Applications]") { + default_present = true; + current_section = Some(SectionKind::DefaultApplications); + output_lines.push("[Default Applications]".to_string()); + continue; + } else if trimmed.eq_ignore_ascii_case("[Added Associations]") { + current_section = Some(SectionKind::AddedAssociations); + output_lines.push("[Added Associations]".to_string()); + continue; + } + } + + if current_section.is_some() { + section_buffer.push(line.to_string()); + } else { + output_lines.push(line.to_string()); + } + } + + if let Some(kind) = current_section.take() { + flush_section( + &mut output_lines, + &mut section_buffer, + desktop_file, + schemes, + kind, + &mut changed, + ); + } + + if !default_present { + changed = true; + if !output_lines.is_empty() && !output_lines.last().unwrap().is_empty() { + output_lines.push(String::new()); + } + output_lines.push("[Default Applications]".to_string()); + for &scheme in schemes { + output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};")); + } + } + + if !changed { + return Ok(()); + } + + let mut new_content = output_lines.join("\n"); + if !new_content.ends_with('\n') { + new_content.push('\n'); + } + + fs::write(path, new_content)?; + Ok(()) +} + +fn mimeapps_list_path() -> Option { + let data_path = env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .or_else(|| { + env::var_os("HOME").map(PathBuf::from).map(|mut home| { + home.push(".local"); + home.push("share"); + home + }) + }) + .map(|mut dir| { + dir.push("applications"); + dir.push("mimeapps.list"); + dir + }); + + if let Some(ref path) = data_path { + if path.exists() { + return Some(path.clone()); + } + } + + let config_path = env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .or_else(|| { + env::var_os("HOME").map(PathBuf::from).map(|mut home| { + home.push(".config"); + home + }) + }) + .map(|mut dir| { + dir.push("mimeapps.list"); + dir + }); + + if let Some(ref path) = config_path { + if path.exists() { + return Some(path.clone()); + } + } + + config_path +} + +#[derive(Clone, Copy)] +enum SectionKind { + DefaultApplications, + AddedAssociations, +} + +fn flush_section( + output: &mut Vec, + section: &mut Vec, + desktop_file: &str, + schemes: &[&str], + kind: SectionKind, + changed: &mut bool, +) { + let mut seen: HashSet<&str> = HashSet::new(); + let mut processed: Vec = Vec::with_capacity(section.len()); + + for line in section.drain(..) { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + processed.push(line); + continue; + } + + let Some((raw_key, raw_value)) = trimmed.split_once('=') else { + processed.push(line); + continue; + }; + + if let Some(scheme) = match_scheme(raw_key.trim(), schemes) { + if !seen.insert(scheme) { + *changed = true; + continue; + } + + let prefix = line + .chars() + .take_while(|c| c.is_whitespace()) + .collect::(); + let mut values: Vec = raw_value + .split(';') + .filter_map(|value| { + let trimmed = value.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) + }) + .collect(); + + if let Some(pos) = values.iter().position(|value| value == desktop_file) { + if pos != 0 { + values.remove(pos); + values.insert(0, desktop_file.to_string()); + *changed = true; + } + } else { + values.insert(0, desktop_file.to_string()); + *changed = true; + } + + let mut new_line = format!("{prefix}x-scheme-handler/{scheme}="); + new_line.push_str(&values.join(";")); + new_line.push(';'); + + if new_line != line { + *changed = true; + } + + processed.push(new_line); + continue; + } + + processed.push(line); + } + + let ensure_all = matches!( + kind, + SectionKind::DefaultApplications | SectionKind::AddedAssociations + ); + + if ensure_all { + for &scheme in schemes { + if !seen.contains(scheme) { + processed.push(format!("x-scheme-handler/{scheme}={desktop_file};")); + *changed = true; + } + } + } + + output.extend(processed); +} + +fn match_scheme<'a>(key: &str, schemes: &'a [&str]) -> Option<&'a str> { + if let Some(rest) = key.strip_prefix("x-scheme-handler/") { + return schemes.iter().copied().find(|candidate| *candidate == rest); + } + + schemes.iter().copied().find(|candidate| *candidate == key) +} + pub fn configure_environment() { let session = SessionEnv::gather(); let overrides = DmabufOverrides::gather();