mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
Compare commits
8 Commits
9d81a13c58
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bada1393e | ||
|
|
af0b0a28ca | ||
|
|
04bdc7c948 | ||
|
|
391f6fe0b7 | ||
|
|
ad6f302e21 | ||
|
|
f1fe2d7e8b | ||
|
|
07c5372dbb | ||
|
|
d6f9bb4256 |
17
src-tauri/Cargo.lock
generated
17
src-tauri/Cargo.lock
generated
@@ -1165,6 +1165,7 @@ dependencies = [
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
@@ -7655,6 +7656,22 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-deep-link",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.9.0"
|
||||
|
||||
@@ -114,6 +114,7 @@ signal-hook = "0.3.18"
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.5.1"
|
||||
tauri-plugin-global-shortcut = "2.3.1"
|
||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -13,11 +13,7 @@ pub mod utils;
|
||||
use crate::constants::files;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::utils::linux;
|
||||
use crate::{
|
||||
core::handle,
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use crate::{core::handle, process::AsyncHandler, utils::resolve};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use rust_i18n::i18n;
|
||||
@@ -32,21 +28,24 @@ i18n!("locales", fallback = "zh");
|
||||
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
use super::*;
|
||||
use crate::{module::lightweight, utils::window_manager::WindowManager};
|
||||
|
||||
/// Initialize singleton monitoring for other instances
|
||||
pub fn init_singleton_check() -> Result<()> {
|
||||
AsyncHandler::block_on(async move {
|
||||
logging!(info, Type::Setup, "开始检查单例实例...");
|
||||
server::check_singleton().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
use super::*;
|
||||
|
||||
/// Setup plugins for the Tauri builder
|
||||
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = builder
|
||||
.plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| {
|
||||
AsyncHandler::spawn(async || {
|
||||
logging!(info, Type::Window, "检测到从单例模式恢复应用窗口");
|
||||
if !lightweight::exit_lightweight_mode().await {
|
||||
WindowManager::show_main_window().await;
|
||||
} else {
|
||||
logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口");
|
||||
};
|
||||
});
|
||||
}))
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
@@ -84,10 +83,7 @@ mod app_init {
|
||||
/// Setup deep link handling
|
||||
pub fn setup_deep_links(app: &tauri::App) {
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
{
|
||||
logging!(info, Type::Setup, "注册深层链接...");
|
||||
let _ = app.deep_link().register_all();
|
||||
}
|
||||
let _ = app.deep_link().register_all();
|
||||
|
||||
app.deep_link().on_open_url(|event| {
|
||||
let urls = event.urls();
|
||||
@@ -222,10 +218,6 @@ mod app_init {
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
if app_init::init_singleton_check().is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -469,57 +469,6 @@ pub async fn init_resources() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize url scheme
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
use winreg::{RegKey, enums::HKEY_CURRENT_USER};
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_exe = app_exe.to_string_lossy().into_owned();
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
|
||||
clash.set_value("", &"Clash Verge")?;
|
||||
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
|
||||
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
|
||||
default_icon.set_value("", &app_exe)?;
|
||||
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
|
||||
command.set_value("", &format!("{app_exe} \"%1\""))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
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")]
|
||||
pub const 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 = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::logging;
|
||||
use crate::utils::logging::Type;
|
||||
use anyhow::Result;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -475,273 +474,6 @@ 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<String> = Vec::new();
|
||||
let mut current_section: Option<SectionKind> = None;
|
||||
let mut section_buffer: Vec<String> = 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.last().is_some_and(|line| !line.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<PathBuf> {
|
||||
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 config_path.as_ref().is_some_and(|path| path.exists()) {
|
||||
return config_path;
|
||||
}
|
||||
|
||||
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 data_path.as_ref().is_some_and(|path| path.exists()) {
|
||||
return data_path;
|
||||
}
|
||||
|
||||
config_path
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum SectionKind {
|
||||
DefaultApplications,
|
||||
AddedAssociations,
|
||||
}
|
||||
|
||||
fn flush_section(
|
||||
output: &mut Vec<String>,
|
||||
section: &mut Vec<String>,
|
||||
desktop_file: &str,
|
||||
schemes: &[&str],
|
||||
kind: SectionKind,
|
||||
changed: &mut bool,
|
||||
) {
|
||||
let mut seen: HashMap<&str, usize> = HashMap::new();
|
||||
let mut processed: Vec<String> = 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) {
|
||||
let mut values: Vec<String> = raw_value
|
||||
.split(';')
|
||||
.filter_map(|value| {
|
||||
let trimmed = value.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(&index) = seen.get(scheme) {
|
||||
let existing_line = &mut processed[index];
|
||||
let existing_prefix: String = existing_line
|
||||
.chars()
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.collect();
|
||||
let Some((_, existing_raw_value)) = existing_line.trim().split_once('=') else {
|
||||
processed.push(line);
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut merged_values: Vec<String> = existing_raw_value
|
||||
.split(';')
|
||||
.filter_map(|value| {
|
||||
let trimmed = value.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
for value in values {
|
||||
if !merged_values.iter().any(|existing| existing == &value) {
|
||||
merged_values.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pos) = merged_values.iter().position(|value| value == desktop_file) {
|
||||
if pos != 0 {
|
||||
let moved = merged_values.remove(pos);
|
||||
merged_values.insert(0, moved);
|
||||
}
|
||||
} else {
|
||||
merged_values.insert(0, desktop_file.to_string());
|
||||
}
|
||||
|
||||
let mut merged_line = format!("{existing_prefix}x-scheme-handler/{scheme}=");
|
||||
merged_line.push_str(&merged_values.join(";"));
|
||||
merged_line.push(';');
|
||||
|
||||
if *existing_line != merged_line {
|
||||
*existing_line = merged_line;
|
||||
}
|
||||
|
||||
// Dropping the duplicate entry alters the section even if nothing new was added.
|
||||
*changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
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 prefix = line
|
||||
.chars()
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.collect::<String>();
|
||||
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;
|
||||
}
|
||||
|
||||
let index = processed.len();
|
||||
processed.push(new_line);
|
||||
seen.insert(scheme, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
processed.push(line);
|
||||
}
|
||||
|
||||
let ensure_all = matches!(
|
||||
kind,
|
||||
SectionKind::DefaultApplications | SectionKind::AddedAssociations
|
||||
);
|
||||
|
||||
if ensure_all {
|
||||
for &scheme in schemes {
|
||||
if !seen.contains_key(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();
|
||||
|
||||
@@ -27,7 +27,6 @@ pub fn resolve_setup_handle() {
|
||||
|
||||
pub fn resolve_setup_sync() {
|
||||
AsyncHandler::spawn(|| async {
|
||||
AsyncHandler::spawn_blocking(init_scheme);
|
||||
AsyncHandler::spawn_blocking(init_embed_server);
|
||||
AsyncHandler::spawn_blocking(init_signal);
|
||||
});
|
||||
@@ -90,10 +89,6 @@ pub fn init_handle() {
|
||||
handle::Handle::global().init();
|
||||
}
|
||||
|
||||
pub(super) fn init_scheme() {
|
||||
logging_error!(Type::Setup, init::init_scheme());
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
pub(super) async fn resolve_setup_logger() {
|
||||
logging_error!(Type::Setup, init::init_logger().await);
|
||||
|
||||
@@ -1,70 +1,18 @@
|
||||
use super::resolve;
|
||||
use crate::{
|
||||
config::{Config, DEFAULT_PAC, IVerge},
|
||||
logging, logging_error,
|
||||
module::lightweight,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::{logging::Type, window_manager::WindowManager},
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use port_scanner::local_port_available;
|
||||
use reqwest::ClientBuilder;
|
||||
use smartstring::alias::String;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
use warp::Filter as _;
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct QueryParam {
|
||||
param: String,
|
||||
}
|
||||
|
||||
// 关闭 embedded server 的信号发送端
|
||||
static SHUTDOWN_SENDER: OnceCell<Mutex<Option<oneshot::Sender<()>>>> = OnceCell::new();
|
||||
|
||||
/// check whether there is already exists
|
||||
pub async fn check_singleton() -> Result<()> {
|
||||
let port = IVerge::get_singleton_port();
|
||||
if !local_port_available(port) {
|
||||
let client = ClientBuilder::new()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.build()?;
|
||||
// 需要确保 Send
|
||||
#[allow(clippy::needless_collect)]
|
||||
let argvs: Vec<std::string::String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let param = argvs[1].as_str();
|
||||
if param.starts_with("clash:") {
|
||||
client
|
||||
.get(format!(
|
||||
"http://127.0.0.1:{port}/commands/scheme?param={param}"
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.get(format!("http://127.0.0.1:{port}/commands/visible"))
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
"failed to setup singleton listen server"
|
||||
);
|
||||
bail!("app exists");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The embed server only be used to implement singleton process
|
||||
/// maybe it can be used as pac server later
|
||||
/// The embed server only be used as pac server
|
||||
pub fn embed_server() {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
#[allow(clippy::expect_used)]
|
||||
@@ -73,19 +21,6 @@ pub fn embed_server() {
|
||||
.expect("failed to set shutdown signal for embedded server");
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
let visible = warp::path!("commands" / "visible").and_then(|| async {
|
||||
logging!(info, Type::Window, "检测到从单例模式恢复应用窗口");
|
||||
if !lightweight::exit_lightweight_mode().await {
|
||||
WindowManager::show_main_window().await;
|
||||
} else {
|
||||
logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口");
|
||||
};
|
||||
Ok::<_, warp::Rejection>(warp::reply::with_status::<std::string::String>(
|
||||
"ok".to_string(),
|
||||
warp::http::StatusCode::OK,
|
||||
))
|
||||
});
|
||||
|
||||
let pac = warp::path!("commands" / "pac").and_then(|| async move {
|
||||
let verge_config = Config::verge().await;
|
||||
let clash_config = Config::clash().await;
|
||||
@@ -109,23 +44,8 @@ pub fn embed_server() {
|
||||
)
|
||||
});
|
||||
|
||||
// Use map instead of and_then to avoid Send issues
|
||||
let scheme = warp::path!("commands" / "scheme")
|
||||
.and(warp::query::<QueryParam>())
|
||||
.and_then(|query: QueryParam| async move {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await);
|
||||
});
|
||||
Ok::<_, warp::Rejection>(warp::reply::with_status::<std::string::String>(
|
||||
"ok".to_string(),
|
||||
warp::http::StatusCode::OK,
|
||||
))
|
||||
});
|
||||
|
||||
let commands = visible.or(scheme).or(pac);
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
warp::serve(commands)
|
||||
warp::serve(pac)
|
||||
.bind(([127, 0, 0, 1], port))
|
||||
.await
|
||||
.graceful(async {
|
||||
|
||||
Reference in New Issue
Block a user