chore: format rust code

This commit is contained in:
GyDi 2022-11-12 11:37:23 +08:00
parent a211fc7c97
commit 4668be6e24
No known key found for this signature in database
GPG Key ID: 9C3AD40F1F99880A
31 changed files with 3268 additions and 3262 deletions

View File

@ -5,3 +5,9 @@ charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 2 indent_size = 2
insert_final_newline = true insert_final_newline = true
[*.rs]
charset = utf-8
end_of_line = lf
indent_size = 4
insert_final_newline = true

View File

@ -1,3 +1,3 @@
fn main() { fn main() {
tauri_build::build() tauri_build::build()
} }

View File

@ -1,6 +1,6 @@
max_width = 100 max_width = 100
hard_tabs = false hard_tabs = false
tab_spaces = 2 tab_spaces = 4
newline_style = "Auto" newline_style = "Auto"
use_small_heuristics = "Default" use_small_heuristics = "Default"
reorder_imports = true reorder_imports = true

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
core::Core, core::Core,
data::{ClashInfo, Data, PrfItem, PrfOption, Profiles, Verge}, data::{ClashInfo, Data, PrfItem, PrfOption, Profiles, Verge},
utils::{dirs, help}, utils::{dirs, help},
}; };
use crate::{log_if_err, ret_err, wrap_err}; use crate::{log_if_err, ret_err, wrap_err};
use anyhow::Result; use anyhow::Result;
@ -14,27 +14,27 @@ type CmdResult<T = ()> = Result<T, String>;
/// get all profiles from `profiles.yaml` /// get all profiles from `profiles.yaml`
#[tauri::command] #[tauri::command]
pub fn get_profiles() -> CmdResult<Profiles> { pub fn get_profiles() -> CmdResult<Profiles> {
let global = Data::global(); let global = Data::global();
let profiles = global.profiles.lock(); let profiles = global.profiles.lock();
Ok(profiles.clone()) Ok(profiles.clone())
} }
/// manually exec enhanced profile /// manually exec enhanced profile
#[tauri::command] #[tauri::command]
pub fn enhance_profiles() -> CmdResult { pub fn enhance_profiles() -> CmdResult {
let core = Core::global(); let core = Core::global();
wrap_err!(core.activate()) wrap_err!(core.activate())
} }
/// import the profile from url /// import the profile from url
/// and save to `profiles.yaml` /// and save to `profiles.yaml`
#[tauri::command] #[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult { pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
wrap_err!(profiles.append_item(item)) wrap_err!(profiles.append_item(item))
} }
/// new a profile /// new a profile
@ -42,166 +42,166 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
/// view the temp profile file by using vscode or other editor /// view the temp profile file by using vscode or other editor
#[tauri::command] #[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult { pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?; let item = wrap_err!(PrfItem::from(item, file_data).await)?;
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
wrap_err!(profiles.append_item(item)) wrap_err!(profiles.append_item(item))
} }
/// Update the profile /// Update the profile
#[tauri::command] #[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult { pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
let core = Core::global(); let core = Core::global();
wrap_err!(core.update_profile_item(index, option).await) wrap_err!(core.update_profile_item(index, option).await)
} }
/// change the current profile /// change the current profile
#[tauri::command] #[tauri::command]
pub fn select_profile(index: String) -> CmdResult { pub fn select_profile(index: String) -> CmdResult {
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
wrap_err!(profiles.put_current(index))?; wrap_err!(profiles.put_current(index))?;
drop(profiles); drop(profiles);
let core = Core::global(); let core = Core::global();
wrap_err!(core.activate()) wrap_err!(core.activate())
} }
/// change the profile chain /// change the profile chain
#[tauri::command] #[tauri::command]
pub fn change_profile_chain(chain: Option<Vec<String>>) -> CmdResult { pub fn change_profile_chain(chain: Option<Vec<String>>) -> CmdResult {
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
wrap_err!(profiles.put_chain(chain))?; wrap_err!(profiles.put_chain(chain))?;
drop(profiles); drop(profiles);
let core = Core::global(); let core = Core::global();
wrap_err!(core.activate()) wrap_err!(core.activate())
} }
/// change the profile valid fields /// change the profile valid fields
#[tauri::command] #[tauri::command]
pub fn change_profile_valid(valid: Option<Vec<String>>) -> CmdResult { pub fn change_profile_valid(valid: Option<Vec<String>>) -> CmdResult {
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
wrap_err!(profiles.put_valid(valid))?; wrap_err!(profiles.put_valid(valid))?;
drop(profiles); drop(profiles);
let core = Core::global(); let core = Core::global();
wrap_err!(core.activate()) wrap_err!(core.activate())
} }
/// delete profile item /// delete profile item
#[tauri::command] #[tauri::command]
pub fn delete_profile(index: String) -> CmdResult { pub fn delete_profile(index: String) -> CmdResult {
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
if wrap_err!(profiles.delete_item(index))? { if wrap_err!(profiles.delete_item(index))? {
drop(profiles); drop(profiles);
let core = Core::global(); let core = Core::global();
log_if_err!(core.activate()); log_if_err!(core.activate());
} }
Ok(()) Ok(())
} }
/// patch the profile config /// patch the profile config
#[tauri::command] #[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
let global = Data::global(); let global = Data::global();
let mut profiles = global.profiles.lock(); let mut profiles = global.profiles.lock();
wrap_err!(profiles.patch_item(index, profile))?; wrap_err!(profiles.patch_item(index, profile))?;
drop(profiles); drop(profiles);
// update cron task // update cron task
let core = Core::global(); let core = Core::global();
let mut timer = core.timer.lock(); let mut timer = core.timer.lock();
wrap_err!(timer.refresh()) wrap_err!(timer.refresh())
} }
/// run vscode command to edit the profile /// run vscode command to edit the profile
#[tauri::command] #[tauri::command]
pub fn view_profile(index: String) -> CmdResult { pub fn view_profile(index: String) -> CmdResult {
let global = Data::global(); let global = Data::global();
let profiles = global.profiles.lock(); let profiles = global.profiles.lock();
let item = wrap_err!(profiles.get_item(&index))?; let item = wrap_err!(profiles.get_item(&index))?;
let file = item.file.clone(); let file = item.file.clone();
if file.is_none() { if file.is_none() {
ret_err!("file is null"); ret_err!("file is null");
} }
let path = dirs::app_profiles_dir().join(file.unwrap()); let path = dirs::app_profiles_dir().join(file.unwrap());
if !path.exists() { if !path.exists() {
ret_err!("file not found"); ret_err!("file not found");
} }
wrap_err!(help::open_file(path)) wrap_err!(help::open_file(path))
} }
/// read the profile item file data /// read the profile item file data
#[tauri::command] #[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> { pub fn read_profile_file(index: String) -> CmdResult<String> {
let global = Data::global(); let global = Data::global();
let profiles = global.profiles.lock(); let profiles = global.profiles.lock();
let item = wrap_err!(profiles.get_item(&index))?; let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?; let data = wrap_err!(item.read_file())?;
Ok(data) Ok(data)
} }
/// save the profile item file data /// save the profile item file data
#[tauri::command] #[tauri::command]
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult { pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if file_data.is_none() { if file_data.is_none() {
return Ok(()); return Ok(());
} }
let global = Data::global(); let global = Data::global();
let profiles = global.profiles.lock(); let profiles = global.profiles.lock();
let item = wrap_err!(profiles.get_item(&index))?; let item = wrap_err!(profiles.get_item(&index))?;
wrap_err!(item.save_file(file_data.unwrap())) wrap_err!(item.save_file(file_data.unwrap()))
} }
/// get the clash core info from the state /// get the clash core info from the state
/// the caller can also get the infomation by clash's api /// the caller can also get the infomation by clash's api
#[tauri::command] #[tauri::command]
pub fn get_clash_info() -> CmdResult<ClashInfo> { pub fn get_clash_info() -> CmdResult<ClashInfo> {
let global = Data::global(); let global = Data::global();
let clash = global.clash.lock(); let clash = global.clash.lock();
Ok(clash.info.clone()) Ok(clash.info.clone())
} }
/// get the runtime clash config mapping /// get the runtime clash config mapping
#[tauri::command] #[tauri::command]
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> { pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
let core = Core::global(); let core = Core::global();
let rt = core.runtime.lock(); let rt = core.runtime.lock();
Ok(rt.config.clone()) Ok(rt.config.clone())
} }
/// get the runtime clash config yaml string /// get the runtime clash config yaml string
#[tauri::command] #[tauri::command]
pub fn get_runtime_yaml() -> CmdResult<Option<String>> { pub fn get_runtime_yaml() -> CmdResult<Option<String>> {
let core = Core::global(); let core = Core::global();
let rt = core.runtime.lock(); let rt = core.runtime.lock();
Ok(rt.config_yaml.clone()) Ok(rt.config_yaml.clone())
} }
/// get the runtime config exists keys /// get the runtime config exists keys
#[tauri::command] #[tauri::command]
pub fn get_runtime_exists() -> CmdResult<Vec<String>> { pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
let core = Core::global(); let core = Core::global();
let rt = core.runtime.lock(); let rt = core.runtime.lock();
Ok(rt.exists_keys.clone()) Ok(rt.exists_keys.clone())
} }
/// get the runtime enhanced chain log /// get the runtime enhanced chain log
#[tauri::command] #[tauri::command]
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> { pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
let core = Core::global(); let core = Core::global();
let rt = core.runtime.lock(); let rt = core.runtime.lock();
Ok(rt.chain_logs.clone()) Ok(rt.chain_logs.clone())
} }
/// update the clash core config /// update the clash core config
@ -209,153 +209,153 @@ pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
/// then we should save the latest config /// then we should save the latest config
#[tauri::command] #[tauri::command]
pub fn patch_clash_config(payload: Mapping) -> CmdResult { pub fn patch_clash_config(payload: Mapping) -> CmdResult {
let core = Core::global(); let core = Core::global();
wrap_err!(core.patch_clash(payload)) wrap_err!(core.patch_clash(payload))
} }
#[tauri::command] #[tauri::command]
pub fn get_verge_config() -> CmdResult<Verge> { pub fn get_verge_config() -> CmdResult<Verge> {
let global = Data::global(); let global = Data::global();
let verge = global.verge.lock(); let verge = global.verge.lock();
Ok(verge.clone()) Ok(verge.clone())
} }
/// patch the verge config /// patch the verge config
/// this command only save the config and not responsible for other things /// this command only save the config and not responsible for other things
#[tauri::command] #[tauri::command]
pub fn patch_verge_config(payload: Verge) -> CmdResult { pub fn patch_verge_config(payload: Verge) -> CmdResult {
let core = Core::global(); let core = Core::global();
wrap_err!(core.patch_verge(payload)) wrap_err!(core.patch_verge(payload))
} }
#[tauri::command] #[tauri::command]
pub fn update_hotkeys(hotkeys: Vec<String>) -> CmdResult { pub fn update_hotkeys(hotkeys: Vec<String>) -> CmdResult {
let core = Core::global(); let core = Core::global();
let mut hotkey = core.hotkey.lock(); let mut hotkey = core.hotkey.lock();
wrap_err!(hotkey.update(hotkeys)) wrap_err!(hotkey.update(hotkeys))
} }
/// change clash core /// change clash core
#[tauri::command] #[tauri::command]
pub fn change_clash_core(clash_core: Option<String>) -> CmdResult { pub fn change_clash_core(clash_core: Option<String>) -> CmdResult {
let core = Core::global(); let core = Core::global();
wrap_err!(core.change_core(clash_core)) wrap_err!(core.change_core(clash_core))
} }
/// restart the sidecar /// restart the sidecar
#[tauri::command] #[tauri::command]
pub fn restart_sidecar() -> CmdResult { pub fn restart_sidecar() -> CmdResult {
let core = Core::global(); let core = Core::global();
wrap_err!(core.restart_clash()) wrap_err!(core.restart_clash())
} }
/// kill all sidecars when update app /// kill all sidecars when update app
#[tauri::command] #[tauri::command]
pub fn kill_sidecar() { pub fn kill_sidecar() {
tauri::api::process::kill_children(); tauri::api::process::kill_children();
} }
/// get the system proxy /// get the system proxy
#[tauri::command] #[tauri::command]
pub fn get_sys_proxy() -> CmdResult<Mapping> { pub fn get_sys_proxy() -> CmdResult<Mapping> {
let current = wrap_err!(Sysproxy::get_system_proxy())?; let current = wrap_err!(Sysproxy::get_system_proxy())?;
let mut map = Mapping::new(); let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into()); map.insert("enable".into(), current.enable.into());
map.insert( map.insert(
"server".into(), "server".into(),
format!("{}:{}", current.host, current.port).into(), format!("{}:{}", current.host, current.port).into(),
); );
map.insert("bypass".into(), current.bypass.into()); map.insert("bypass".into(), current.bypass.into());
Ok(map) Ok(map)
} }
#[tauri::command] #[tauri::command]
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> { pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
let core = Core::global(); let core = Core::global();
let service = core.service.lock(); let service = core.service.lock();
Ok(service.get_logs()) Ok(service.get_logs())
} }
/// open app config dir /// open app config dir
#[tauri::command] #[tauri::command]
pub fn open_app_dir() -> CmdResult<()> { pub fn open_app_dir() -> CmdResult<()> {
let app_dir = dirs::app_home_dir(); let app_dir = dirs::app_home_dir();
wrap_err!(open::that(app_dir)) wrap_err!(open::that(app_dir))
} }
/// open logs dir /// open logs dir
#[tauri::command] #[tauri::command]
pub fn open_logs_dir() -> CmdResult<()> { pub fn open_logs_dir() -> CmdResult<()> {
let log_dir = dirs::app_logs_dir(); let log_dir = dirs::app_logs_dir();
wrap_err!(open::that(log_dir)) wrap_err!(open::that(log_dir))
} }
/// open url /// open url
#[tauri::command] #[tauri::command]
pub fn open_web_url(url: String) -> CmdResult<()> { pub fn open_web_url(url: String) -> CmdResult<()> {
wrap_err!(open::that(url)) wrap_err!(open::that(url))
} }
/// service mode /// service mode
#[cfg(windows)] #[cfg(windows)]
pub mod service { pub mod service {
use super::*; use super::*;
use crate::core::win_service::JsonResponse; use crate::core::win_service::JsonResponse;
#[tauri::command] #[tauri::command]
pub async fn start_service() -> CmdResult<()> { pub async fn start_service() -> CmdResult<()> {
wrap_err!(crate::core::Service::start_service().await) wrap_err!(crate::core::Service::start_service().await)
}
#[tauri::command]
pub async fn stop_service() -> CmdResult<()> {
wrap_err!(crate::core::Service::stop_service().await)
}
#[tauri::command]
pub async fn check_service() -> CmdResult<JsonResponse> {
// no log
match crate::core::Service::check_service().await {
Ok(res) => Ok(res),
Err(err) => Err(err.to_string()),
} }
}
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult<()> { pub async fn stop_service() -> CmdResult<()> {
wrap_err!(crate::core::Service::install_service().await) wrap_err!(crate::core::Service::stop_service().await)
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult<()> { pub async fn check_service() -> CmdResult<JsonResponse> {
wrap_err!(crate::core::Service::uninstall_service().await) // no log
} match crate::core::Service::check_service().await {
Ok(res) => Ok(res),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn install_service() -> CmdResult<()> {
wrap_err!(crate::core::Service::install_service().await)
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult<()> {
wrap_err!(crate::core::Service::uninstall_service().await)
}
} }
#[cfg(not(windows))] #[cfg(not(windows))]
pub mod service { pub mod service {
use super::*; use super::*;
#[tauri::command] #[tauri::command]
pub async fn start_service() -> CmdResult<()> { pub async fn start_service() -> CmdResult<()> {
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn stop_service() -> CmdResult<()> { pub async fn stop_service() -> CmdResult<()> {
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn check_service() -> CmdResult<()> { pub async fn check_service() -> CmdResult<()> {
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult<()> { pub async fn install_service() -> CmdResult<()> {
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult<()> { pub async fn uninstall_service() -> CmdResult<()> {
Ok(()) Ok(())
} }
} }

View File

@ -1,119 +1,119 @@
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
pub const HANDLE_FIELDS: [&str; 9] = [ pub const HANDLE_FIELDS: [&str; 9] = [
"port", "port",
"socks-port", "socks-port",
"mixed-port", "mixed-port",
"mode", "mode",
"ipv6", "ipv6",
"log-level", "log-level",
"allow-lan", "allow-lan",
"external-controller", "external-controller",
"secret", "secret",
]; ];
pub const DEFAULT_FIELDS: [&str; 5] = [ pub const DEFAULT_FIELDS: [&str; 5] = [
"proxies", "proxies",
"proxy-groups", "proxy-groups",
"rules", "rules",
"proxy-providers", "proxy-providers",
"rule-providers", "rule-providers",
]; ];
pub const OTHERS_FIELDS: [&str; 21] = [ pub const OTHERS_FIELDS: [&str; 21] = [
"tun", "tun",
"dns", "dns",
"ebpf", "ebpf",
"hosts", "hosts",
"script", "script",
"profile", "profile",
"payload", "payload",
"auto-redir", "auto-redir",
"experimental", "experimental",
"interface-name", "interface-name",
"routing-mark", "routing-mark",
"redir-port", "redir-port",
"tproxy-port", "tproxy-port",
"iptables", "iptables",
"external-ui", "external-ui",
"bind-address", "bind-address",
"authentication", "authentication",
"sniffer", // meta "sniffer", // meta
"sub-rules", // meta "sub-rules", // meta
"geodata-mode", // meta "geodata-mode", // meta
"tcp-concurrent", // meta "tcp-concurrent", // meta
]; ];
pub fn use_clash_fields() -> Vec<String> { pub fn use_clash_fields() -> Vec<String> {
DEFAULT_FIELDS DEFAULT_FIELDS
.into_iter() .into_iter()
.chain(HANDLE_FIELDS) .chain(HANDLE_FIELDS)
.chain(OTHERS_FIELDS) .chain(OTHERS_FIELDS)
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect() .collect()
} }
pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> { pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
let others = Vec::from(OTHERS_FIELDS); let others = Vec::from(OTHERS_FIELDS);
valid.iter_mut().for_each(|s| s.make_ascii_lowercase()); valid.iter_mut().for_each(|s| s.make_ascii_lowercase());
valid valid
.into_iter() .into_iter()
.filter(|s| others.contains(&s.as_str())) .filter(|s| others.contains(&s.as_str()))
.chain(DEFAULT_FIELDS.iter().map(|s| s.to_string())) .chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))
.collect() .collect()
} }
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping { pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
let mut ret = Mapping::new(); let mut ret = Mapping::new();
for (key, value) in config.into_iter() { for (key, value) in config.into_iter() {
if let Some(key) = key.as_str() { if let Some(key) = key.as_str() {
if filter.contains(&key.to_string()) { if filter.contains(&key.to_string()) {
ret.insert(Value::from(key), value); ret.insert(Value::from(key), value);
} }
}
} }
} ret
ret
} }
pub fn use_lowercase(config: Mapping) -> Mapping { pub fn use_lowercase(config: Mapping) -> Mapping {
let mut ret = Mapping::new(); let mut ret = Mapping::new();
for (key, value) in config.into_iter() { for (key, value) in config.into_iter() {
if let Some(key_str) = key.as_str() { if let Some(key_str) = key.as_str() {
let mut key_str = String::from(key_str); let mut key_str = String::from(key_str);
key_str.make_ascii_lowercase(); key_str.make_ascii_lowercase();
ret.insert(Value::from(key_str), value); ret.insert(Value::from(key_str), value);
}
} }
} ret
ret
} }
pub fn use_sort(config: Mapping) -> Mapping { pub fn use_sort(config: Mapping) -> Mapping {
let mut ret = Mapping::new(); let mut ret = Mapping::new();
HANDLE_FIELDS HANDLE_FIELDS
.into_iter() .into_iter()
.chain(OTHERS_FIELDS) .chain(OTHERS_FIELDS)
.chain(DEFAULT_FIELDS) .chain(DEFAULT_FIELDS)
.for_each(|key| { .for_each(|key| {
let key = Value::from(key); let key = Value::from(key);
config.get(&key).map(|value| { config.get(&key).map(|value| {
ret.insert(key, value.clone()); ret.insert(key, value.clone());
}); });
}); });
ret ret
} }
pub fn use_keys(config: &Mapping) -> Vec<String> { pub fn use_keys(config: &Mapping) -> Vec<String> {
config config
.iter() .iter()
.filter_map(|(key, _)| key.as_str()) .filter_map(|(key, _)| key.as_str())
.map(|s| { .map(|s| {
let mut s = s.to_string(); let mut s = s.to_string();
s.make_ascii_lowercase(); s.make_ascii_lowercase();
return s; return s;
}) })
.collect() .collect()
} }

View File

@ -3,60 +3,60 @@ use serde_yaml::{self, Mapping, Sequence, Value};
#[allow(unused)] #[allow(unused)]
const MERGE_FIELDS: [&str; 6] = [ const MERGE_FIELDS: [&str; 6] = [
"prepend-rules", "prepend-rules",
"append-rules", "append-rules",
"prepend-proxies", "prepend-proxies",
"append-proxies", "append-proxies",
"prepend-proxy-groups", "prepend-proxy-groups",
"append-proxy-groups", "append-proxy-groups",
]; ];
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping { pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
// 直接覆盖原字段 // 直接覆盖原字段
use_lowercase(merge.clone()) use_lowercase(merge.clone())
.into_iter() .into_iter()
.for_each(|(key, value)| { .for_each(|(key, value)| {
config.insert(key, value); config.insert(key, value);
}); });
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string()); let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
let merge = use_filter(merge, &merge_list.collect()); let merge = use_filter(merge, &merge_list.collect());
["rules", "proxies", "proxy-groups"] ["rules", "proxies", "proxy-groups"]
.iter() .iter()
.for_each(|key_str| { .for_each(|key_str| {
let key_val = Value::from(key_str.to_string()); let key_val = Value::from(key_str.to_string());
let mut list = Sequence::default(); let mut list = Sequence::default();
list = config.get(&key_val).map_or(list.clone(), |val| { list = config.get(&key_val).map_or(list.clone(), |val| {
val.as_sequence().map_or(list, |v| v.clone()) val.as_sequence().map_or(list, |v| v.clone())
}); });
let pre_key = Value::from(format!("prepend-{key_str}")); let pre_key = Value::from(format!("prepend-{key_str}"));
let post_key = Value::from(format!("append-{key_str}")); let post_key = Value::from(format!("append-{key_str}"));
if let Some(pre_val) = merge.get(&pre_key) { if let Some(pre_val) = merge.get(&pre_key) {
if pre_val.is_sequence() { if pre_val.is_sequence() {
let mut pre_val = pre_val.as_sequence().unwrap().clone(); let mut pre_val = pre_val.as_sequence().unwrap().clone();
pre_val.extend(list); pre_val.extend(list);
list = pre_val; list = pre_val;
} }
} }
if let Some(post_val) = merge.get(&post_key) { if let Some(post_val) = merge.get(&post_key) {
if post_val.is_sequence() { if post_val.is_sequence() {
list.extend(post_val.as_sequence().unwrap().clone()); list.extend(post_val.as_sequence().unwrap().clone());
} }
} }
config.insert(key_val, Value::from(list)); config.insert(key_val, Value::from(list));
}); });
config config
} }
#[test] #[test]
fn test_merge() -> anyhow::Result<()> { fn test_merge() -> anyhow::Result<()> {
let merge = r" let merge = r"
prepend-rules: prepend-rules:
- prepend - prepend
- 1123123 - 1123123
@ -76,18 +76,18 @@ fn test_merge() -> anyhow::Result<()> {
enable: true enable: true
"; ";
let config = r" let config = r"
rules: rules:
- aaaaa - aaaaa
script1: test script1: test
"; ";
let merge = serde_yaml::from_str::<Mapping>(merge)?; let merge = serde_yaml::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?; let config = serde_yaml::from_str::<Mapping>(config)?;
let result = serde_yaml::to_string(&use_merge(merge, config))?; let result = serde_yaml::to_string(&use_merge(merge, config))?;
println!("{result}"); println!("{result}");
Ok(()) Ok(())
} }

View File

@ -16,54 +16,54 @@ use std::collections::HashSet;
type ResultLog = Vec<(String, String)>; type ResultLog = Vec<(String, String)>;
pub fn enhance_config( pub fn enhance_config(
clash_config: Mapping, clash_config: Mapping,
profile_config: Mapping, profile_config: Mapping,
chain: Vec<ChainItem>, chain: Vec<ChainItem>,
valid: Vec<String>, valid: Vec<String>,
tun_mode: bool, tun_mode: bool,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) { ) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
let mut config = profile_config; let mut config = profile_config;
let mut result_map = HashMap::new(); let mut result_map = HashMap::new();
let mut exists_keys = use_keys(&config); let mut exists_keys = use_keys(&config);
let valid = use_valid_fields(valid); let valid = use_valid_fields(valid);
chain.into_iter().for_each(|item| match item.data { chain.into_iter().for_each(|item| match item.data {
ChainType::Merge(merge) => { ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge)); exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned()); config = use_merge(merge, config.to_owned());
config = use_filter(config.to_owned(), &valid); config = use_filter(config.to_owned(), &valid);
}
ChainType::Script(script) => {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = use_filter(res_config, &valid);
logs.extend(res_logs);
} }
Err(err) => logs.push(("exception".into(), err.to_string())), ChainType::Script(script) => {
} let mut logs = vec![];
result_map.insert(item.uid, logs); match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = use_filter(res_config, &valid);
logs.extend(res_logs);
}
Err(err) => logs.push(("exception".into(), err.to_string())),
}
result_map.insert(item.uid, logs);
}
});
config = use_filter(config, &valid);
for (key, value) in clash_config.into_iter() {
config.insert(key, value);
} }
});
config = use_filter(config, &valid); let clash_fields = use_clash_fields();
config = use_filter(config, &clash_fields);
config = use_tun(config, tun_mode);
config = use_sort(config);
for (key, value) in clash_config.into_iter() { let mut exists_set = HashSet::new();
config.insert(key, value); exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
} exists_keys = exists_set.into_iter().collect();
let clash_fields = use_clash_fields(); (config, exists_keys, result_map)
config = use_filter(config, &clash_fields);
config = use_tun(config, tun_mode);
config = use_sort(config);
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
exists_keys = exists_set.into_iter().collect();
(config, exists_keys, result_map)
} }

View File

@ -3,66 +3,66 @@ use anyhow::Result;
use serde_yaml::Mapping; use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> { pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
use rquickjs::{Context, Func, Runtime}; use rquickjs::{Context, Func, Runtime};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
let runtime = Runtime::new().unwrap(); let runtime = Runtime::new().unwrap();
let context = Context::full(&runtime).unwrap(); let context = Context::full(&runtime).unwrap();
let outputs = Arc::new(Mutex::new(vec![])); let outputs = Arc::new(Mutex::new(vec![]));
let copy_outputs = outputs.clone(); let copy_outputs = outputs.clone();
let result = context.with(|ctx| -> Result<Mapping> { let result = context.with(|ctx| -> Result<Mapping> {
ctx.globals().set( ctx.globals().set(
"__verge_log__", "__verge_log__",
Func::from(move |level: String, data: String| { Func::from(move |level: String, data: String| {
let mut out = copy_outputs.lock().unwrap(); let mut out = copy_outputs.lock().unwrap();
out.push((level, data)); out.push((level, data));
}), }),
)?; )?;
ctx.eval( ctx.eval(
r#"var console = Object.freeze({ r#"var console = Object.freeze({
log(data){__verge_log__("log",JSON.stringify(data))}, log(data){__verge_log__("log",JSON.stringify(data))},
info(data){__verge_log__("info",JSON.stringify(data))}, info(data){__verge_log__("info",JSON.stringify(data))},
error(data){__verge_log__("error",JSON.stringify(data))}, error(data){__verge_log__("error",JSON.stringify(data))},
debug(data){__verge_log__("debug",JSON.stringify(data))}, debug(data){__verge_log__("debug",JSON.stringify(data))},
});"#, });"#,
)?; )?;
let config = use_lowercase(config.clone()); let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?; let config_str = serde_json::to_string(&config)?;
let code = format!( let code = format!(
r#"try{{ r#"try{{
{script}; {script};
JSON.stringify(main({config_str})||'') JSON.stringify(main({config_str})||'')
}} catch(err) {{ }} catch(err) {{
`__error_flag__ ${{err.toString()}}` `__error_flag__ ${{err.toString()}}`
}}"# }}"#
); );
let result: String = ctx.eval(code.as_str())?; let result: String = ctx.eval(code.as_str())?;
if result.starts_with("__error_flag__") { if result.starts_with("__error_flag__") {
anyhow::bail!(result[15..].to_owned()); anyhow::bail!(result[15..].to_owned());
} }
if result == "\"\"" { if result == "\"\"" {
anyhow::bail!("main function should return object"); anyhow::bail!("main function should return object");
} }
return Ok(serde_json::from_str::<Mapping>(result.as_str())?); return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
}); });
let mut out = outputs.lock().unwrap(); let mut out = outputs.lock().unwrap();
match result { match result {
Ok(config) => Ok((use_lowercase(config), out.to_vec())), Ok(config) => Ok((use_lowercase(config), out.to_vec())),
Err(err) => { Err(err) => {
out.push(("exception".into(), err.to_string())); out.push(("exception".into(), err.to_string()));
Ok((config, out.to_vec())) Ok((config, out.to_vec()))
}
} }
}
} }
#[test] #[test]
fn test_script() { fn test_script() {
let script = r#" let script = r#"
function main(config) { function main(config) {
if (Array.isArray(config.rules)) { if (Array.isArray(config.rules)) {
config.rules = [...config.rules, "add"]; config.rules = [...config.rules, "add"];
@ -73,7 +73,7 @@ fn test_script() {
} }
"#; "#;
let config = r#" let config = r#"
rules: rules:
- 111 - 111
- 222 - 222
@ -83,12 +83,12 @@ fn test_script() {
enable: false enable: false
"#; "#;
let config = serde_yaml::from_str(config).unwrap(); let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config).unwrap(); let (config, results) = use_script(script.into(), config).unwrap();
let config_str = serde_yaml::to_string(&config).unwrap(); let config_str = serde_yaml::to_string(&config).unwrap();
println!("{config_str}"); println!("{config_str}");
dbg!(results); dbg!(results);
} }

View File

@ -1,81 +1,81 @@
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
macro_rules! revise { macro_rules! revise {
($map: expr, $key: expr, $val: expr) => { ($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into()); let ret_key = Value::String($key.into());
$map.insert(ret_key, Value::from($val)); $map.insert(ret_key, Value::from($val));
}; };
} }
// if key not exists then append value // if key not exists then append value
macro_rules! append { macro_rules! append {
($map: expr, $key: expr, $val: expr) => { ($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into()); let ret_key = Value::String($key.into());
if !$map.contains_key(&ret_key) { if !$map.contains_key(&ret_key) {
$map.insert(ret_key, Value::from($val)); $map.insert(ret_key, Value::from($val));
} }
}; };
} }
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun"); let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key); let tun_val = config.get(&tun_key);
if !enable && tun_val.is_none() { if !enable && tun_val.is_none() {
return config; return config;
} }
let mut tun_val = tun_val.map_or(Mapping::new(), |val| { let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new()) val.as_mapping().cloned().unwrap_or(Mapping::new())
}); });
revise!(tun_val, "enable", enable); revise!(tun_val, "enable", enable);
if enable { if enable {
append!(tun_val, "stack", "gvisor"); append!(tun_val, "stack", "gvisor");
append!(tun_val, "dns-hijack", vec!["198.18.0.2:53"]); append!(tun_val, "dns-hijack", vec!["198.18.0.2:53"]);
append!(tun_val, "auto-route", true); append!(tun_val, "auto-route", true);
append!(tun_val, "auto-detect-interface", true); append!(tun_val, "auto-detect-interface", true);
} }
revise!(config, "tun", tun_val); revise!(config, "tun", tun_val);
if enable { if enable {
use_dns_for_tun(config) use_dns_for_tun(config)
} else { } else {
config config
} }
} }
fn use_dns_for_tun(mut config: Mapping) -> Mapping { fn use_dns_for_tun(mut config: Mapping) -> Mapping {
let dns_key = Value::from("dns"); let dns_key = Value::from("dns");
let dns_val = config.get(&dns_key); let dns_val = config.get(&dns_key);
let mut dns_val = dns_val.map_or(Mapping::new(), |val| { let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new()) val.as_mapping().cloned().unwrap_or(Mapping::new())
}); });
// 开启tun将同时开启dns // 开启tun将同时开启dns
revise!(dns_val, "enable", true); revise!(dns_val, "enable", true);
append!(dns_val, "enhanced-mode", "fake-ip"); append!(dns_val, "enhanced-mode", "fake-ip");
append!(dns_val, "fake-ip-range", "198.18.0.1/16"); append!(dns_val, "fake-ip-range", "198.18.0.1/16");
append!( append!(
dns_val, dns_val,
"nameserver", "nameserver",
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"] vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
); );
append!(dns_val, "fallback", vec![] as Vec<&str>); append!(dns_val, "fallback", vec![] as Vec<&str>);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
append!( append!(
dns_val, dns_val,
"fake-ip-filter", "fake-ip-filter",
vec![ vec![
"dns.msftncsi.com", "dns.msftncsi.com",
"www.msftncsi.com", "www.msftncsi.com",
"www.msftconnecttest.com" "www.msftconnecttest.com"
] ]
); );
revise!(config, "dns", dns_val); revise!(config, "dns", dns_val);
config config
} }

View File

@ -5,62 +5,61 @@ use tauri::{AppHandle, Manager, Window};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Handle { pub struct Handle {
pub app_handle: Option<AppHandle>, pub app_handle: Option<AppHandle>,
} }
impl Handle { impl Handle {
pub fn set_inner(&mut self, app_handle: AppHandle) { pub fn set_inner(&mut self, app_handle: AppHandle) {
self.app_handle = Some(app_handle); self.app_handle = Some(app_handle);
}
pub fn get_window(&self) -> Option<Window> {
self
.app_handle
.as_ref()
.map_or(None, |a| a.get_window("main"))
}
pub fn refresh_clash(&self) {
if let Some(window) = self.get_window() {
log_if_err!(window.emit("verge://refresh-clash-config", "yes"));
} }
}
pub fn refresh_verge(&self) { pub fn get_window(&self) -> Option<Window> {
if let Some(window) = self.get_window() { self.app_handle
log_if_err!(window.emit("verge://refresh-verge-config", "yes")); .as_ref()
.map_or(None, |a| a.get_window("main"))
} }
}
#[allow(unused)] pub fn refresh_clash(&self) {
pub fn refresh_profiles(&self) { if let Some(window) = self.get_window() {
if let Some(window) = self.get_window() { log_if_err!(window.emit("verge://refresh-clash-config", "yes"));
log_if_err!(window.emit("verge://refresh-profiles-config", "yes")); }
} }
}
pub fn notice_message(&self, status: String, msg: String) { pub fn refresh_verge(&self) {
if let Some(window) = self.get_window() { if let Some(window) = self.get_window() {
log_if_err!(window.emit("verge://notice-message", (status, msg))); log_if_err!(window.emit("verge://refresh-verge-config", "yes"));
}
} }
}
pub fn update_systray(&self) -> Result<()> { #[allow(unused)]
if self.app_handle.is_none() { pub fn refresh_profiles(&self) {
bail!("update_systray unhandle error"); if let Some(window) = self.get_window() {
log_if_err!(window.emit("verge://refresh-profiles-config", "yes"));
}
} }
let app_handle = self.app_handle.as_ref().unwrap();
Tray::update_systray(app_handle)?;
Ok(())
}
/// update the system tray state pub fn notice_message(&self, status: String, msg: String) {
pub fn update_systray_part(&self) -> Result<()> { if let Some(window) = self.get_window() {
if self.app_handle.is_none() { log_if_err!(window.emit("verge://notice-message", (status, msg)));
bail!("update_systray unhandle error"); }
}
pub fn update_systray(&self) -> Result<()> {
if self.app_handle.is_none() {
bail!("update_systray unhandle error");
}
let app_handle = self.app_handle.as_ref().unwrap();
Tray::update_systray(app_handle)?;
Ok(())
}
/// update the system tray state
pub fn update_systray_part(&self) -> Result<()> {
if self.app_handle.is_none() {
bail!("update_systray unhandle error");
}
let app_handle = self.app_handle.as_ref().unwrap();
Tray::update_part(app_handle)?;
Ok(())
} }
let app_handle = self.app_handle.as_ref().unwrap();
Tray::update_part(app_handle)?;
Ok(())
}
} }

View File

@ -4,150 +4,150 @@ use std::collections::HashMap;
use tauri::{AppHandle, GlobalShortcutManager}; use tauri::{AppHandle, GlobalShortcutManager};
pub struct Hotkey { pub struct Hotkey {
current: Vec<String>, // 保存当前的热键设置 current: Vec<String>, // 保存当前的热键设置
manager: Option<AppHandle>, manager: Option<AppHandle>,
} }
impl Hotkey { impl Hotkey {
pub fn new() -> Hotkey { pub fn new() -> Hotkey {
Hotkey { Hotkey {
current: Vec::new(), current: Vec::new(),
manager: None, manager: None,
}
}
pub fn init(&mut self, app_handle: AppHandle) -> Result<()> {
self.manager = Some(app_handle);
let data = Data::global();
let verge = data.verge.lock();
if let Some(hotkeys) = verge.hotkeys.as_ref() {
for hotkey in hotkeys.iter() {
let mut iter = hotkey.split(',');
let func = iter.next();
let key = iter.next();
if func.is_some() && key.is_some() {
log_if_err!(self.register(key.unwrap(), func.unwrap()));
} else {
log::error!(target: "app", "invalid hotkey \"{}\":\"{}\"", key.unwrap_or("None"), func.unwrap_or("None"));
} }
}
self.current = hotkeys.clone();
} }
Ok(()) pub fn init(&mut self, app_handle: AppHandle) -> Result<()> {
} self.manager = Some(app_handle);
let data = Data::global();
let verge = data.verge.lock();
fn get_manager(&self) -> Result<impl GlobalShortcutManager> { if let Some(hotkeys) = verge.hotkeys.as_ref() {
if self.manager.is_none() { for hotkey in hotkeys.iter() {
bail!("failed to get hotkey manager"); let mut iter = hotkey.split(',');
} let func = iter.next();
Ok(self.manager.as_ref().unwrap().global_shortcut_manager()) let key = iter.next();
}
fn register(&mut self, hotkey: &str, func: &str) -> Result<()> { if func.is_some() && key.is_some() {
let mut manager = self.get_manager()?; log_if_err!(self.register(key.unwrap(), func.unwrap()));
} else {
if manager.is_registered(hotkey)? { log::error!(target: "app", "invalid hotkey \"{}\":\"{}\"", key.unwrap_or("None"), func.unwrap_or("None"));
manager.unregister(hotkey)?; }
} }
self.current = hotkeys.clone();
let f = match func.trim() {
"clash_mode_rule" => || feat::change_clash_mode("rule"),
"clash_mode_global" => || feat::change_clash_mode("global"),
"clash_mode_direct" => || feat::change_clash_mode("direct"),
"clash_mode_script" => || feat::change_clash_mode("script"),
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"enable_system_proxy" => || feat::enable_system_proxy(),
"disable_system_proxy" => || feat::disable_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(),
"enable_tun_mode" => || feat::enable_tun_mode(),
"disable_tun_mode" => || feat::disable_tun_mode(),
_ => bail!("invalid function \"{func}\""),
};
manager.register(hotkey, f)?;
log::info!(target: "app", "register hotkey {hotkey} {func}");
Ok(())
}
fn unregister(&mut self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(&hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
pub fn update(&mut self, new_hotkeys: Vec<String>) -> Result<()> {
let current = self.current.to_owned();
let old_map = Self::get_map_from_vec(&current);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
del.iter().for_each(|key| {
let _ = self.unregister(key);
});
add.iter().for_each(|(key, func)| {
log_if_err!(self.register(key, func));
});
self.current = new_hotkeys;
Ok(())
}
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
let mut map = HashMap::new();
hotkeys.iter().for_each(|hotkey| {
let mut iter = hotkey.split(',');
let func = iter.next();
let key = iter.next();
if func.is_some() && key.is_some() {
let func = func.unwrap().trim();
let key = key.unwrap().trim();
map.insert(key, func);
}
});
map
}
fn get_diff<'a>(
old_map: HashMap<&'a str, &'a str>,
new_map: HashMap<&'a str, &'a str>,
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
let mut del_list = vec![];
let mut add_list = vec![];
old_map.iter().for_each(|(&key, func)| {
match new_map.get(key) {
Some(new_func) => {
if new_func != func {
del_list.push(key);
add_list.push((key, *new_func));
}
} }
None => del_list.push(key),
};
});
new_map.iter().for_each(|(&key, &func)| { Ok(())
if old_map.get(key).is_none() { }
add_list.push((key, func));
}
});
(del_list, add_list) fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
} if self.manager.is_none() {
bail!("failed to get hotkey manager");
}
Ok(self.manager.as_ref().unwrap().global_shortcut_manager())
}
fn register(&mut self, hotkey: &str, func: &str) -> Result<()> {
let mut manager = self.get_manager()?;
if manager.is_registered(hotkey)? {
manager.unregister(hotkey)?;
}
let f = match func.trim() {
"clash_mode_rule" => || feat::change_clash_mode("rule"),
"clash_mode_global" => || feat::change_clash_mode("global"),
"clash_mode_direct" => || feat::change_clash_mode("direct"),
"clash_mode_script" => || feat::change_clash_mode("script"),
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"enable_system_proxy" => || feat::enable_system_proxy(),
"disable_system_proxy" => || feat::disable_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(),
"enable_tun_mode" => || feat::enable_tun_mode(),
"disable_tun_mode" => || feat::disable_tun_mode(),
_ => bail!("invalid function \"{func}\""),
};
manager.register(hotkey, f)?;
log::info!(target: "app", "register hotkey {hotkey} {func}");
Ok(())
}
fn unregister(&mut self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(&hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
pub fn update(&mut self, new_hotkeys: Vec<String>) -> Result<()> {
let current = self.current.to_owned();
let old_map = Self::get_map_from_vec(&current);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
del.iter().for_each(|key| {
let _ = self.unregister(key);
});
add.iter().for_each(|(key, func)| {
log_if_err!(self.register(key, func));
});
self.current = new_hotkeys;
Ok(())
}
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
let mut map = HashMap::new();
hotkeys.iter().for_each(|hotkey| {
let mut iter = hotkey.split(',');
let func = iter.next();
let key = iter.next();
if func.is_some() && key.is_some() {
let func = func.unwrap().trim();
let key = key.unwrap().trim();
map.insert(key, func);
}
});
map
}
fn get_diff<'a>(
old_map: HashMap<&'a str, &'a str>,
new_map: HashMap<&'a str, &'a str>,
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
let mut del_list = vec![];
let mut add_list = vec![];
old_map.iter().for_each(|(&key, func)| {
match new_map.get(key) {
Some(new_func) => {
if new_func != func {
del_list.push(key);
add_list.push((key, *new_func));
}
}
None => del_list.push(key),
};
});
new_map.iter().for_each(|(&key, &func)| {
if old_map.get(key).is_none() {
add_list.push((key, func));
}
});
(del_list, add_list)
}
} }
impl Drop for Hotkey { impl Drop for Hotkey {
fn drop(&mut self) { fn drop(&mut self) {
if let Ok(mut manager) = self.get_manager() { if let Ok(mut manager) = self.get_manager() {
let _ = manager.unregister_all(); let _ = manager.unregister_all();
}
} }
}
} }

View File

@ -22,328 +22,328 @@ pub use self::service::*;
#[derive(Clone)] #[derive(Clone)]
pub struct Core { pub struct Core {
pub service: Arc<Mutex<Service>>, pub service: Arc<Mutex<Service>>,
pub sysopt: Arc<Mutex<Sysopt>>, pub sysopt: Arc<Mutex<Sysopt>>,
pub timer: Arc<Mutex<Timer>>, pub timer: Arc<Mutex<Timer>>,
pub hotkey: Arc<Mutex<Hotkey>>, pub hotkey: Arc<Mutex<Hotkey>>,
pub runtime: Arc<Mutex<RuntimeResult>>, pub runtime: Arc<Mutex<RuntimeResult>>,
pub handle: Arc<Mutex<Handle>>, pub handle: Arc<Mutex<Handle>>,
} }
impl Core { impl Core {
pub fn global() -> &'static Core { pub fn global() -> &'static Core {
static CORE: OnceCell<Core> = OnceCell::new(); static CORE: OnceCell<Core> = OnceCell::new();
CORE.get_or_init(|| Core { CORE.get_or_init(|| Core {
service: Arc::new(Mutex::new(Service::new())), service: Arc::new(Mutex::new(Service::new())),
sysopt: Arc::new(Mutex::new(Sysopt::new())), sysopt: Arc::new(Mutex::new(Sysopt::new())),
timer: Arc::new(Mutex::new(Timer::new())), timer: Arc::new(Mutex::new(Timer::new())),
hotkey: Arc::new(Mutex::new(Hotkey::new())), hotkey: Arc::new(Mutex::new(Hotkey::new())),
runtime: Arc::new(Mutex::new(RuntimeResult::default())), runtime: Arc::new(Mutex::new(RuntimeResult::default())),
handle: Arc::new(Mutex::new(Handle::default())), handle: Arc::new(Mutex::new(Handle::default())),
}) })
}
/// initialize the core state
pub fn init(&self, app_handle: tauri::AppHandle) {
// kill old clash process
Service::kill_old_clash();
let mut handle = self.handle.lock();
handle.set_inner(app_handle.clone());
drop(handle);
let mut service = self.service.lock();
log_if_err!(service.start());
drop(service);
log_if_err!(self.activate());
let mut sysopt = self.sysopt.lock();
log_if_err!(sysopt.init_launch());
log_if_err!(sysopt.init_sysproxy());
drop(sysopt);
let handle = self.handle.lock();
log_if_err!(handle.update_systray_part());
drop(handle);
let mut hotkey = self.hotkey.lock();
log_if_err!(hotkey.init(app_handle));
drop(hotkey);
// timer initialize
let mut timer = self.timer.lock();
log_if_err!(timer.restore());
}
/// restart the clash sidecar
pub fn restart_clash(&self) -> Result<()> {
let mut service = self.service.lock();
service.restart()?;
drop(service);
self.activate()
}
/// change the clash core
pub fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.unwrap_or("clash".into());
if &clash_core != "clash" && &clash_core != "clash-meta" {
bail!("invalid clash core name \"{clash_core}\"");
} }
let global = Data::global(); /// initialize the core state
let mut verge = global.verge.lock(); pub fn init(&self, app_handle: tauri::AppHandle) {
verge.patch_config(Verge { // kill old clash process
clash_core: Some(clash_core.clone()), Service::kill_old_clash();
..Verge::default()
})?;
drop(verge);
let mut service = self.service.lock(); let mut handle = self.handle.lock();
service.clear_logs(); handle.set_inner(app_handle.clone());
service.restart()?; drop(handle);
drop(service);
self.activate() let mut service = self.service.lock();
} log_if_err!(service.start());
drop(service);
/// Patch Clash log_if_err!(self.activate());
/// handle the clash config changed
pub fn patch_clash(&self, patch: Mapping) -> Result<()> {
let patch_cloned = patch.clone();
let clash_mode = patch.get("mode");
let mixed_port = patch.get("mixed-port");
let external = patch.get("external-controller");
let secret = patch.get("secret");
let valid_port = { let mut sysopt = self.sysopt.lock();
let global = Data::global(); log_if_err!(sysopt.init_launch());
let mut clash = global.clash.lock(); log_if_err!(sysopt.init_sysproxy());
clash.patch_config(patch_cloned)?; drop(sysopt);
clash.info.port.is_some()
};
// todo: port check let handle = self.handle.lock();
if (mixed_port.is_some() && valid_port) || external.is_some() || secret.is_some() { log_if_err!(handle.update_systray_part());
let mut service = self.service.lock(); drop(handle);
service.restart()?;
drop(service);
self.activate()?; let mut hotkey = self.hotkey.lock();
log_if_err!(hotkey.init(app_handle));
drop(hotkey);
let mut sysopt = self.sysopt.lock(); // timer initialize
sysopt.init_sysproxy()?; let mut timer = self.timer.lock();
log_if_err!(timer.restore());
} }
if clash_mode.is_some() { /// restart the clash sidecar
let handle = self.handle.lock(); pub fn restart_clash(&self) -> Result<()> {
handle.update_systray_part()?;
}
Ok(())
}
/// Patch Verge
pub fn patch_verge(&self, patch: Verge) -> Result<()> {
// save the patch
let global = Data::global();
let mut verge = global.verge.lock();
verge.patch_config(patch.clone())?;
drop(verge);
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
let proxy_bypass = patch.system_proxy_bypass;
let proxy_guard = patch.enable_proxy_guard;
let language = patch.language;
#[cfg(target_os = "windows")]
{
let service_mode = patch.enable_service_mode;
// 重启服务
if service_mode.is_some() {
let mut service = self.service.lock(); let mut service = self.service.lock();
service.restart()?; service.restart()?;
drop(service); drop(service);
} self.activate()
}
if tun_mode.is_some() && *tun_mode.as_ref().unwrap_or(&false) { /// change the clash core
let wintun_dll = crate::utils::dirs::app_home_dir().join("wintun.dll"); pub fn change_core(&self, clash_core: Option<String>) -> Result<()> {
if !wintun_dll.exists() { let clash_core = clash_core.unwrap_or("clash".into());
bail!("failed to enable TUN for missing `wintun.dll`");
if &clash_core != "clash" && &clash_core != "clash-meta" {
bail!("invalid clash core name \"{clash_core}\"");
} }
}
if service_mode.is_some() || tun_mode.is_some() { let global = Data::global();
self.activate()?; let mut verge = global.verge.lock();
} verge.patch_config(Verge {
clash_core: Some(clash_core.clone()),
..Verge::default()
})?;
drop(verge);
let mut service = self.service.lock();
service.clear_logs();
service.restart()?;
drop(service);
self.activate()
} }
#[cfg(not(target_os = "windows"))] /// Patch Clash
if tun_mode.is_some() { /// handle the clash config changed
self.activate()?; pub fn patch_clash(&self, patch: Mapping) -> Result<()> {
} let patch_cloned = patch.clone();
let clash_mode = patch.get("mode");
let mixed_port = patch.get("mixed-port");
let external = patch.get("external-controller");
let secret = patch.get("secret");
let mut sysopt = self.sysopt.lock(); let valid_port = {
let global = Data::global();
let mut clash = global.clash.lock();
clash.patch_config(patch_cloned)?;
clash.info.port.is_some()
};
if auto_launch.is_some() { // todo: port check
sysopt.update_launch()?; if (mixed_port.is_some() && valid_port) || external.is_some() || secret.is_some() {
} let mut service = self.service.lock();
if system_proxy.is_some() || proxy_bypass.is_some() { service.restart()?;
sysopt.update_sysproxy()?; drop(service);
sysopt.guard_proxy();
}
if proxy_guard.unwrap_or(false) {
sysopt.guard_proxy();
}
// 更新tray self.activate()?;
if language.is_some() {
let handle = self.handle.lock();
handle.update_systray()?;
} else if system_proxy.is_some() || tun_mode.is_some() {
let handle = self.handle.lock();
handle.update_systray_part()?;
}
if patch.hotkeys.is_some() { let mut sysopt = self.sysopt.lock();
let mut hotkey = self.hotkey.lock(); sysopt.init_sysproxy()?;
hotkey.update(patch.hotkeys.unwrap())?;
}
Ok(())
}
// update rule/global/direct/script mode
pub fn update_mode(&self, mode: &str) -> Result<()> {
// save config to file
let info = {
let global = Data::global();
let mut clash = global.clash.lock();
clash.config.insert(Value::from("mode"), Value::from(mode));
clash.save_config()?;
clash.info.clone()
};
let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), Value::from(mode));
let handle = self.handle.clone();
tauri::async_runtime::spawn(async move {
log_if_err!(Service::patch_config(info, mapping.to_owned()).await);
// update tray
let handle = handle.lock();
handle.refresh_clash();
log_if_err!(handle.update_systray_part());
});
Ok(())
}
/// activate the profile
/// auto activate enhanced profile
/// 触发clash配置更新
pub fn activate(&self) -> Result<()> {
let global = Data::global();
let verge = global.verge.lock();
let clash = global.clash.lock();
let profiles = global.profiles.lock();
let tun_mode = verge.enable_tun_mode.clone().unwrap_or(false);
let profile_activate = profiles.gen_activate()?;
let clash_config = clash.config.clone();
let clash_info = clash.info.clone();
drop(clash);
drop(verge);
drop(profiles);
let (config, exists_keys, logs) = enhance_config(
clash_config,
profile_activate.current,
profile_activate.chain,
profile_activate.valid,
tun_mode,
);
let mut runtime = self.runtime.lock();
*runtime = RuntimeResult {
config: Some(config.clone()),
config_yaml: Some(serde_yaml::to_string(&config).unwrap_or("".into())),
exists_keys,
chain_logs: logs,
};
drop(runtime);
let mut service = self.service.lock();
service.check_start()?;
drop(service);
let handle = self.handle.clone();
tauri::async_runtime::spawn(async move {
match Service::set_config(clash_info, config).await {
Ok(_) => {
let handle = handle.lock();
handle.refresh_clash();
handle.notice_message("set_config::ok".into(), "ok".into());
} }
Err(err) => {
let handle = handle.lock(); if clash_mode.is_some() {
handle.notice_message("set_config::error".into(), format!("{err}")); let handle = self.handle.lock();
log::error!(target: "app", "{err}") handle.update_systray_part()?;
} }
}
});
Ok(()) Ok(())
} }
/// Static function /// Patch Verge
/// update profile item pub fn patch_verge(&self, patch: Verge) -> Result<()> {
pub async fn update_profile_item(&self, uid: String, option: Option<PrfOption>) -> Result<()> { // save the patch
let global = Data::global(); let global = Data::global();
let mut verge = global.verge.lock();
verge.patch_config(patch.clone())?;
drop(verge);
let (url, opt) = { let tun_mode = patch.enable_tun_mode;
let profiles = global.profiles.lock(); let auto_launch = patch.enable_auto_launch;
let item = profiles.get_item(&uid)?; let system_proxy = patch.enable_system_proxy;
let proxy_bypass = patch.system_proxy_bypass;
let proxy_guard = patch.enable_proxy_guard;
let language = patch.language;
if let Some(typ) = item.itype.as_ref() { #[cfg(target_os = "windows")]
// maybe only valid for `local` profile {
if *typ != "remote" { let service_mode = patch.enable_service_mode;
// reactivate the config
if Some(uid) == profiles.get_current() { // 重启服务
if service_mode.is_some() {
let mut service = self.service.lock();
service.restart()?;
drop(service);
}
if tun_mode.is_some() && *tun_mode.as_ref().unwrap_or(&false) {
let wintun_dll = crate::utils::dirs::app_home_dir().join("wintun.dll");
if !wintun_dll.exists() {
bail!("failed to enable TUN for missing `wintun.dll`");
}
}
if service_mode.is_some() || tun_mode.is_some() {
self.activate()?;
}
}
#[cfg(not(target_os = "windows"))]
if tun_mode.is_some() {
self.activate()?;
}
let mut sysopt = self.sysopt.lock();
if auto_launch.is_some() {
sysopt.update_launch()?;
}
if system_proxy.is_some() || proxy_bypass.is_some() {
sysopt.update_sysproxy()?;
sysopt.guard_proxy();
}
if proxy_guard.unwrap_or(false) {
sysopt.guard_proxy();
}
// 更新tray
if language.is_some() {
let handle = self.handle.lock();
handle.update_systray()?;
} else if system_proxy.is_some() || tun_mode.is_some() {
let handle = self.handle.lock();
handle.update_systray_part()?;
}
if patch.hotkeys.is_some() {
let mut hotkey = self.hotkey.lock();
hotkey.update(patch.hotkeys.unwrap())?;
}
Ok(())
}
// update rule/global/direct/script mode
pub fn update_mode(&self, mode: &str) -> Result<()> {
// save config to file
let info = {
let global = Data::global();
let mut clash = global.clash.lock();
clash.config.insert(Value::from("mode"), Value::from(mode));
clash.save_config()?;
clash.info.clone()
};
let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), Value::from(mode));
let handle = self.handle.clone();
tauri::async_runtime::spawn(async move {
log_if_err!(Service::patch_config(info, mapping.to_owned()).await);
// update tray
let handle = handle.lock();
handle.refresh_clash();
log_if_err!(handle.update_systray_part());
});
Ok(())
}
/// activate the profile
/// auto activate enhanced profile
/// 触发clash配置更新
pub fn activate(&self) -> Result<()> {
let global = Data::global();
let verge = global.verge.lock();
let clash = global.clash.lock();
let profiles = global.profiles.lock();
let tun_mode = verge.enable_tun_mode.clone().unwrap_or(false);
let profile_activate = profiles.gen_activate()?;
let clash_config = clash.config.clone();
let clash_info = clash.info.clone();
drop(clash);
drop(verge);
drop(profiles);
let (config, exists_keys, logs) = enhance_config(
clash_config,
profile_activate.current,
profile_activate.chain,
profile_activate.valid,
tun_mode,
);
let mut runtime = self.runtime.lock();
*runtime = RuntimeResult {
config: Some(config.clone()),
config_yaml: Some(serde_yaml::to_string(&config).unwrap_or("".into())),
exists_keys,
chain_logs: logs,
};
drop(runtime);
let mut service = self.service.lock();
service.check_start()?;
drop(service);
let handle = self.handle.clone();
tauri::async_runtime::spawn(async move {
match Service::set_config(clash_info, config).await {
Ok(_) => {
let handle = handle.lock();
handle.refresh_clash();
handle.notice_message("set_config::ok".into(), "ok".into());
}
Err(err) => {
let handle = handle.lock();
handle.notice_message("set_config::error".into(), format!("{err}"));
log::error!(target: "app", "{err}")
}
}
});
Ok(())
}
/// Static function
/// update profile item
pub async fn update_profile_item(&self, uid: String, option: Option<PrfOption>) -> Result<()> {
let global = Data::global();
let (url, opt) = {
let profiles = global.profiles.lock();
let item = profiles.get_item(&uid)?;
if let Some(typ) = item.itype.as_ref() {
// maybe only valid for `local` profile
if *typ != "remote" {
// reactivate the config
if Some(uid) == profiles.get_current() {
drop(profiles);
self.activate()?;
}
return Ok(());
}
}
if item.url.is_none() {
bail!("failed to get the profile item url");
}
(item.url.clone().unwrap(), item.option.clone())
};
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let mut profiles = global.profiles.lock();
profiles.update_item(uid.clone(), item)?;
// reactivate the profile
if Some(uid) == profiles.get_current() {
drop(profiles); drop(profiles);
self.activate()?; self.activate()?;
}
return Ok(());
} }
}
if item.url.is_none() {
bail!("failed to get the profile item url");
}
(item.url.clone().unwrap(), item.option.clone())
};
let merged_opt = PrfOption::merge(opt, option); Ok(())
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let mut profiles = global.profiles.lock();
profiles.update_item(uid.clone(), item)?;
// reactivate the profile
if Some(uid) == profiles.get_current() {
drop(profiles);
self.activate()?;
} }
Ok(())
}
} }

View File

@ -9,8 +9,8 @@ use std::fs;
use std::io::Write; use std::io::Write;
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
time::Duration, time::Duration,
}; };
use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep; use tokio::time::sleep;
@ -19,209 +19,209 @@ const LOGS_QUEUE_LEN: usize = 100;
#[derive(Debug)] #[derive(Debug)]
pub struct Service { pub struct Service {
sidecar: Option<CommandChild>, sidecar: Option<CommandChild>,
logs: Arc<RwLock<VecDeque<String>>>, logs: Arc<RwLock<VecDeque<String>>>,
#[allow(unused)] #[allow(unused)]
use_service_mode: bool, use_service_mode: bool,
} }
impl Service { impl Service {
pub fn new() -> Service { pub fn new() -> Service {
let queue = VecDeque::with_capacity(LOGS_QUEUE_LEN + 10); let queue = VecDeque::with_capacity(LOGS_QUEUE_LEN + 10);
Service { Service {
sidecar: None, sidecar: None,
logs: Arc::new(RwLock::new(queue)), logs: Arc::new(RwLock::new(queue)),
use_service_mode: false, use_service_mode: false,
}
}
pub fn start(&mut self) -> Result<()> {
#[cfg(not(target_os = "windows"))]
self.start_clash_by_sidecar()?;
#[cfg(target_os = "windows")]
{
let enable = {
let data = Data::global();
let verge = data.verge.lock();
verge.enable_service_mode.clone().unwrap_or(false)
};
self.use_service_mode = enable;
if !enable {
return self.start_clash_by_sidecar();
}
tauri::async_runtime::spawn(async move {
match Self::check_service().await {
Ok(status) => {
// 未启动clash
if status.code != 0 {
log_if_err!(Self::start_clash_by_service().await);
}
}
Err(err) => log::error!(target: "app", "{err}"),
} }
});
} }
Ok(()) pub fn start(&mut self) -> Result<()> {
} #[cfg(not(target_os = "windows"))]
self.start_clash_by_sidecar()?;
pub fn stop(&mut self) -> Result<()> { #[cfg(target_os = "windows")]
#[cfg(not(target_os = "windows"))] {
self.stop_clash_by_sidecar()?; let enable = {
let data = Data::global();
let verge = data.verge.lock();
verge.enable_service_mode.clone().unwrap_or(false)
};
#[cfg(target_os = "windows")] self.use_service_mode = enable;
{
let _ = self.stop_clash_by_sidecar();
if self.use_service_mode { if !enable {
tauri::async_runtime::block_on(async move { return self.start_clash_by_sidecar();
log_if_err!(Self::stop_clash_by_service().await); }
});
} tauri::async_runtime::spawn(async move {
match Self::check_service().await {
Ok(status) => {
// 未启动clash
if status.code != 0 {
log_if_err!(Self::start_clash_by_service().await);
}
}
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
Ok(())
} }
Ok(()) pub fn stop(&mut self) -> Result<()> {
} #[cfg(not(target_os = "windows"))]
self.stop_clash_by_sidecar()?;
pub fn restart(&mut self) -> Result<()> { #[cfg(target_os = "windows")]
self.stop()?; {
self.start() let _ = self.stop_clash_by_sidecar();
}
pub fn get_logs(&self) -> VecDeque<String> { if self.use_service_mode {
self.logs.read().clone() tauri::async_runtime::block_on(async move {
} log_if_err!(Self::stop_clash_by_service().await);
});
}
}
#[allow(unused)] Ok(())
pub fn set_logs(&self, text: String) {
let mut logs = self.logs.write();
if logs.len() > LOGS_QUEUE_LEN {
(*logs).pop_front();
}
(*logs).push_back(text);
}
pub fn clear_logs(&self) {
let mut logs = self.logs.write();
(*logs).clear();
}
/// start the clash sidecar
fn start_clash_by_sidecar(&mut self) -> Result<()> {
if self.sidecar.is_some() {
let sidecar = self.sidecar.take().unwrap();
let _ = sidecar.kill();
} }
let clash_core: String = { pub fn restart(&mut self) -> Result<()> {
let global = Data::global(); self.stop()?;
let verge = global.verge.lock(); self.start()
verge.clash_core.clone().unwrap_or("clash".into()) }
};
let app_dir = dirs::app_home_dir(); pub fn get_logs(&self) -> VecDeque<String> {
let app_dir = app_dir.as_os_str().to_str().unwrap(); self.logs.read().clone()
}
// fix #212 #[allow(unused)]
let args = match clash_core.as_str() { pub fn set_logs(&self, text: String) {
"clash-meta" => vec!["-m", "-d", app_dir], let mut logs = self.logs.write();
_ => vec!["-d", app_dir], if logs.len() > LOGS_QUEUE_LEN {
}; (*logs).pop_front();
let cmd = Command::new_sidecar(clash_core)?;
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
// 将pid写入文件中
let pid = cmd_child.pid();
log_if_err!(|| -> Result<()> {
let path = dirs::clash_pid_path();
fs::File::create(path)?.write(format!("{pid}").as_bytes())?;
Ok(())
}());
self.sidecar = Some(cmd_child);
// clash log
let logs = self.logs.clone();
tauri::async_runtime::spawn(async move {
let write_log = |text: String| {
let mut logs = logs.write();
if logs.len() >= LOGS_QUEUE_LEN {
(*logs).pop_front();
} }
(*logs).push_back(text); (*logs).push_back(text);
}; }
while let Some(event) = rx.recv().await { pub fn clear_logs(&self) {
match event { let mut logs = self.logs.write();
CommandEvent::Stdout(line) => { (*logs).clear();
let can_short = line.starts_with("time=") && line.len() > 33; }
let stdout = if can_short { &line[33..] } else { &line };
log::info!(target: "app" ,"[clash]: {}", stdout); /// start the clash sidecar
write_log(line); fn start_clash_by_sidecar(&mut self) -> Result<()> {
} if self.sidecar.is_some() {
CommandEvent::Stderr(err) => { let sidecar = self.sidecar.take().unwrap();
log::error!(target: "app" ,"[clash error]: {}", err); let _ = sidecar.kill();
write_log(err);
}
CommandEvent::Error(err) => log::error!(target: "app" ,"{err}"),
CommandEvent::Terminated(_) => break,
_ => {}
} }
}
});
Ok(()) let clash_core: String = {
} let global = Data::global();
let verge = global.verge.lock();
verge.clash_core.clone().unwrap_or("clash".into())
};
/// stop the clash sidecar let app_dir = dirs::app_home_dir();
fn stop_clash_by_sidecar(&mut self) -> Result<()> { let app_dir = app_dir.as_os_str().to_str().unwrap();
if let Some(sidecar) = self.sidecar.take() {
sidecar.kill()?;
}
Ok(())
}
pub fn check_start(&mut self) -> Result<()> { // fix #212
#[cfg(target_os = "windows")] let args = match clash_core.as_str() {
{ "clash-meta" => vec!["-m", "-d", app_dir],
let global = Data::global(); _ => vec!["-d", app_dir],
let verge = global.verge.lock(); };
let service_mode = verge.enable_service_mode.unwrap_or(false);
if !service_mode && self.sidecar.is_none() { let cmd = Command::new_sidecar(clash_core)?;
self.start()?;
} let (mut rx, cmd_child) = cmd.args(args).spawn()?;
// 将pid写入文件中
let pid = cmd_child.pid();
log_if_err!(|| -> Result<()> {
let path = dirs::clash_pid_path();
fs::File::create(path)?.write(format!("{pid}").as_bytes())?;
Ok(())
}());
self.sidecar = Some(cmd_child);
// clash log
let logs = self.logs.clone();
tauri::async_runtime::spawn(async move {
let write_log = |text: String| {
let mut logs = logs.write();
if logs.len() >= LOGS_QUEUE_LEN {
(*logs).pop_front();
}
(*logs).push_back(text);
};
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let can_short = line.starts_with("time=") && line.len() > 33;
let stdout = if can_short { &line[33..] } else { &line };
log::info!(target: "app" ,"[clash]: {}", stdout);
write_log(line);
}
CommandEvent::Stderr(err) => {
log::error!(target: "app" ,"[clash error]: {}", err);
write_log(err);
}
CommandEvent::Error(err) => log::error!(target: "app" ,"{err}"),
CommandEvent::Terminated(_) => break,
_ => {}
}
}
});
Ok(())
} }
#[cfg(not(target_os = "windows"))] /// stop the clash sidecar
if self.sidecar.is_none() { fn stop_clash_by_sidecar(&mut self) -> Result<()> {
self.start()?; if let Some(sidecar) = self.sidecar.take() {
sidecar.kill()?;
}
Ok(())
} }
Ok(()) pub fn check_start(&mut self) -> Result<()> {
} #[cfg(target_os = "windows")]
{
let global = Data::global();
let verge = global.verge.lock();
let service_mode = verge.enable_service_mode.unwrap_or(false);
/// update clash config if !service_mode && self.sidecar.is_none() {
/// using PUT methods self.start()?;
pub async fn set_config(info: ClashInfo, config: Mapping) -> Result<()> { }
let temp_path = dirs::profiles_temp_path(); }
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
let (server, headers) = Self::clash_client_info(info)?; #[cfg(not(target_os = "windows"))]
if self.sidecar.is_none() {
self.start()?;
}
let mut data = HashMap::new(); Ok(())
data.insert("path", temp_path.as_os_str().to_str().unwrap()); }
macro_rules! report_err { /// update clash config
/// using PUT methods
pub async fn set_config(info: ClashInfo, config: Mapping) -> Result<()> {
let temp_path = dirs::profiles_temp_path();
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
let (server, headers) = Self::clash_client_info(info)?;
let mut data = HashMap::new();
data.insert("path", temp_path.as_os_str().to_str().unwrap());
macro_rules! report_err {
($i: expr, $e: expr) => { ($i: expr, $e: expr) => {
match $i { match $i {
4 => bail!($e), 4 => bail!($e),
@ -230,306 +230,308 @@ impl Service {
}; };
} }
// retry 5 times // retry 5 times
for i in 0..5 { for i in 0..5 {
let headers = headers.clone(); let headers = headers.clone();
match reqwest::ClientBuilder::new().no_proxy().build() { match reqwest::ClientBuilder::new().no_proxy().build() {
Ok(client) => { Ok(client) => {
let builder = client.put(&server).headers(headers).json(&data); let builder = client.put(&server).headers(headers).json(&data);
match builder.send().await { match builder.send().await {
Ok(resp) => match resp.status().as_u16() { Ok(resp) => match resp.status().as_u16() {
204 => break, 204 => break,
// 配置有问题不重试 // 配置有问题不重试
400 => bail!("failed to update clash config with status 400"), 400 => bail!("failed to update clash config with status 400"),
status @ _ => report_err!(i, "failed to activate clash with status \"{status}\""), status @ _ => {
}, report_err!(i, "failed to activate clash with status \"{status}\"")
Err(err) => report_err!(i, "{err}"), }
} },
Err(err) => report_err!(i, "{err}"),
}
}
Err(err) => report_err!(i, "{err}"),
}
sleep(Duration::from_millis(500)).await;
} }
Err(err) => report_err!(i, "{err}"),
} Ok(())
sleep(Duration::from_millis(500)).await;
} }
Ok(()) /// patch clash config
} pub async fn patch_config(info: ClashInfo, config: Mapping) -> Result<()> {
let (server, headers) = Self::clash_client_info(info)?;
/// patch clash config let client = reqwest::ClientBuilder::new().no_proxy().build()?;
pub async fn patch_config(info: ClashInfo, config: Mapping) -> Result<()> { let builder = client.patch(&server).headers(headers.clone()).json(&config);
let (server, headers) = Self::clash_client_info(info)?; builder.send().await?;
Ok(())
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.patch(&server).headers(headers.clone()).json(&config);
builder.send().await?;
Ok(())
}
/// get clash client url and headers from clash info
fn clash_client_info(info: ClashInfo) -> Result<(String, HeaderMap)> {
if info.server.is_none() {
let status = &info.status;
if info.port.is_none() {
bail!("failed to parse config.yaml file with status {status}");
} else {
bail!("failed to parse the server with status {status}");
}
} }
let server = info.server.unwrap(); /// get clash client url and headers from clash info
let server = format!("http://{server}/configs"); fn clash_client_info(info: ClashInfo) -> Result<(String, HeaderMap)> {
if info.server.is_none() {
let mut headers = HeaderMap::new(); let status = &info.status;
headers.insert("Content-Type", "application/json".parse().unwrap()); if info.port.is_none() {
bail!("failed to parse config.yaml file with status {status}");
if let Some(secret) = info.secret.as_ref() { } else {
let secret = format!("Bearer {}", secret.clone()).parse().unwrap(); bail!("failed to parse the server with status {status}");
headers.insert("Authorization", secret); }
} }
Ok((server, headers)) let server = info.server.unwrap();
} let server = format!("http://{server}/configs");
/// kill old clash process let mut headers = HeaderMap::new();
pub fn kill_old_clash() { headers.insert("Content-Type", "application/json".parse().unwrap());
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
if let Some(secret) = info.secret.as_ref() {
if let Ok(pid) = fs::read(dirs::clash_pid_path()) { let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
if let Ok(pid) = String::from_utf8_lossy(&pid).parse() { headers.insert("Authorization", secret);
let mut system = System::new(); }
system.refresh_all();
Ok((server, headers))
let proc = system.process(Pid::from_u32(pid)); }
if let Some(proc) = proc {
proc.kill(); /// kill old clash process
pub fn kill_old_clash() {
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
if let Ok(pid) = fs::read(dirs::clash_pid_path()) {
if let Ok(pid) = String::from_utf8_lossy(&pid).parse() {
let mut system = System::new();
system.refresh_all();
let proc = system.process(Pid::from_u32(pid));
if let Some(proc) = proc {
proc.kill();
}
}
} }
}
} }
}
} }
impl Drop for Service { impl Drop for Service {
fn drop(&mut self) { fn drop(&mut self) {
log_if_err!(self.stop()); log_if_err!(self.stop());
} }
} }
/// ### Service Mode /// ### Service Mode
/// ///
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod win_service { pub mod win_service {
use super::*; use super::*;
use anyhow::Context; use anyhow::Context;
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
use std::{env::current_exe, process::Command as StdCommand}; use std::{env::current_exe, process::Command as StdCommand};
const SERVICE_NAME: &str = "clash_verge_service"; const SERVICE_NAME: &str = "clash_verge_service";
const SERVICE_URL: &str = "http://127.0.0.1:33211"; const SERVICE_URL: &str = "http://127.0.0.1:33211";
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody { pub struct ResponseBody {
pub bin_path: String, pub bin_path: String,
pub config_dir: String, pub config_dir: String,
pub log_file: String, pub log_file: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse {
pub code: u64,
pub msg: String,
pub data: Option<ResponseBody>,
}
impl Service {
/// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
pub async fn install_service() -> Result<()> {
let binary_path = dirs::service_path();
let install_path = binary_path.with_file_name("install-service.exe");
if !install_path.exists() {
bail!("installer exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).status()?,
_ => StdCommand::new(install_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
} }
/// Uninstall the Clash Verge Service #[derive(Debug, Deserialize, Serialize, Clone)]
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程 pub struct JsonResponse {
pub async fn uninstall_service() -> Result<()> { pub code: u64,
let binary_path = dirs::service_path(); pub msg: String,
let uninstall_path = binary_path.with_file_name("uninstall-service.exe"); pub data: Option<ResponseBody>,
if !uninstall_path.exists() {
bail!("uninstaller exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
);
}
Ok(())
} }
/// [deprecated] impl Service {
/// start service /// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
pub async fn start_service() -> Result<()> { pub async fn install_service() -> Result<()> {
let token = Token::with_current_process()?; let binary_path = dirs::service_path();
let level = token.privilege_level()?; let install_path = binary_path.with_file_name("install-service.exe");
let args = ["start", SERVICE_NAME]; if !install_path.exists() {
bail!("installer exe not found");
}
let status = match level { let token = Token::with_current_process()?;
PrivilegeLevel::NotPrivileged => RunasCommand::new("sc").args(&args).status()?, let level = token.privilege_level()?;
_ => StdCommand::new("sc").args(&args).status()?,
};
match status.success() { let status = match level {
true => Ok(()), PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).status()?,
false => bail!( _ => StdCommand::new(install_path)
"failed to start service with status {}", .creation_flags(0x08000000)
status.code().unwrap() .status()?,
), };
}
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
pub async fn uninstall_service() -> Result<()> {
let binary_path = dirs::service_path();
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !uninstall_path.exists() {
bail!("uninstaller exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// [deprecated]
/// start service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
pub async fn start_service() -> Result<()> {
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let args = ["start", SERVICE_NAME];
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new("sc").args(&args).status()?,
_ => StdCommand::new("sc").args(&args).status()?,
};
match status.success() {
true => Ok(()),
false => bail!(
"failed to start service with status {}",
status.code().unwrap()
),
}
}
/// stop service
pub async fn stop_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_service");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// check the windows service status
pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.get(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
Ok(response)
}
/// start the clash by service
pub(super) async fn start_clash_by_service() -> Result<()> {
let status = Self::check_service().await?;
if status.code == 0 {
Self::stop_clash_by_service().await?;
sleep(Duration::from_secs(1)).await;
}
let clash_core = {
let global = Data::global();
let verge = global.verge.lock();
verge.clash_core.clone().unwrap_or("clash".into())
};
let clash_bin = format!("{clash_core}.exe");
let bin_path = current_exe().unwrap().with_file_name(clash_bin);
let bin_path = bin_path.as_os_str().to_str().unwrap();
let config_dir = dirs::app_home_dir();
let config_dir = config_dir.as_os_str().to_str().unwrap();
let log_path = dirs::service_log_file();
let log_path = log_path.as_os_str().to_str().unwrap();
let mut map = HashMap::new();
map.insert("bin_path", bin_path);
map.insert("config_dir", config_dir);
map.insert("log_file", log_path);
let url = format!("{SERVICE_URL}/start_clash");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.json(&map)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// stop the clash by service
pub(super) async fn stop_clash_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_clash");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
} }
/// stop service
pub async fn stop_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_service");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// check the windows service status
pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.get(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
Ok(response)
}
/// start the clash by service
pub(super) async fn start_clash_by_service() -> Result<()> {
let status = Self::check_service().await?;
if status.code == 0 {
Self::stop_clash_by_service().await?;
sleep(Duration::from_secs(1)).await;
}
let clash_core = {
let global = Data::global();
let verge = global.verge.lock();
verge.clash_core.clone().unwrap_or("clash".into())
};
let clash_bin = format!("{clash_core}.exe");
let bin_path = current_exe().unwrap().with_file_name(clash_bin);
let bin_path = bin_path.as_os_str().to_str().unwrap();
let config_dir = dirs::app_home_dir();
let config_dir = config_dir.as_os_str().to_str().unwrap();
let log_path = dirs::service_log_file();
let log_path = log_path.as_os_str().to_str().unwrap();
let mut map = HashMap::new();
map.insert("bin_path", bin_path);
map.insert("config_dir", config_dir);
map.insert("log_file", log_path);
let url = format!("{SERVICE_URL}/start_clash");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.json(&map)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// stop the clash by service
pub(super) async fn stop_clash_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_clash");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
}
} }

View File

@ -6,18 +6,18 @@ use sysproxy::Sysproxy;
use tauri::{async_runtime::Mutex, utils::platform::current_exe}; use tauri::{async_runtime::Mutex, utils::platform::current_exe};
pub struct Sysopt { pub struct Sysopt {
/// current system proxy setting /// current system proxy setting
cur_sysproxy: Option<Sysproxy>, cur_sysproxy: Option<Sysproxy>,
/// record the original system proxy /// record the original system proxy
/// recover it when exit /// recover it when exit
old_sysproxy: Option<Sysproxy>, old_sysproxy: Option<Sysproxy>,
/// helps to auto launch the app /// helps to auto launch the app
auto_launch: Option<AutoLaunch>, auto_launch: Option<AutoLaunch>,
/// record whether the guard async is running or not /// record whether the guard async is running or not
guard_state: Arc<Mutex<bool>>, guard_state: Arc<Mutex<bool>>,
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -28,258 +28,258 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>"; static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
impl Sysopt { impl Sysopt {
pub fn new() -> Sysopt { pub fn new() -> Sysopt {
Sysopt { Sysopt {
cur_sysproxy: None, cur_sysproxy: None,
old_sysproxy: None, old_sysproxy: None,
auto_launch: None, auto_launch: None,
guard_state: Arc::new(Mutex::new(false)), guard_state: Arc::new(Mutex::new(false)),
} }
}
/// init the sysproxy
pub fn init_sysproxy(&mut self) -> Result<()> {
let data = Data::global();
let clash = data.clash.lock();
let port = clash.info.port.clone();
if port.is_none() {
bail!("clash port is none");
} }
let verge = data.verge.lock(); /// init the sysproxy
pub fn init_sysproxy(&mut self) -> Result<()> {
let data = Data::global();
let clash = data.clash.lock();
let port = clash.info.port.clone();
let enable = verge.enable_system_proxy.clone().unwrap_or(false); if port.is_none() {
let bypass = verge.system_proxy_bypass.clone(); bail!("clash port is none");
let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into()); }
let port = port.unwrap().parse::<u16>()?; let verge = data.verge.lock();
let host = String::from("127.0.0.1");
self.cur_sysproxy = Some(Sysproxy {
enable,
host,
port,
bypass,
});
if enable {
self.old_sysproxy = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
}
// run the system proxy guard
self.guard_proxy();
Ok(())
}
/// update the system proxy
pub fn update_sysproxy(&mut self) -> Result<()> {
if self.cur_sysproxy.is_none() || self.old_sysproxy.is_none() {
return self.init_sysproxy();
}
let data = Data::global();
let verge = data.verge.lock();
let enable = verge.enable_system_proxy.clone().unwrap_or(false);
let bypass = verge.system_proxy_bypass.clone();
let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
let mut sysproxy = self.cur_sysproxy.take().unwrap();
sysproxy.enable = enable;
sysproxy.bypass = bypass;
self.cur_sysproxy = Some(sysproxy);
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
Ok(())
}
/// reset the sysproxy
pub fn reset_sysproxy(&mut self) -> Result<()> {
let cur = self.cur_sysproxy.take();
if let Some(mut old) = self.old_sysproxy.take() {
// 如果原代理和当前代理 端口一致就disable关闭否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let port_same = cur.map_or(true, |cur| old.port == cur.port);
if old.enable && port_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_system_proxy()?;
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur {
// 没有原代理就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_system_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
Ok(())
}
/// init the auto launch
pub fn init_launch(&mut self) -> Result<()> {
let data = Data::global();
let verge = data.verge.lock();
let enable = verge.enable_auto_launch.clone().unwrap_or(false);
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
.ok_or(anyhow!("failed to get file stem"))?;
let app_path = app_exe
.as_os_str()
.to_str()
.ok_or(anyhow!("failed to get app_path"))?
.to_string();
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
// use the /Applications/Clash Verge.app path
#[cfg(target_os = "macos")]
let app_path = (|| -> Option<String> {
let path = std::path::PathBuf::from(&app_path);
let path = path.parent()?.parent()?.parent()?;
let extension = path.extension()?.to_str()?;
match extension == "app" {
true => Some(path.as_os_str().to_str()?.to_string()),
false => None,
}
})()
.unwrap_or(app_path);
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()?;
self.auto_launch = Some(auto);
// 避免在开发时将自启动关了
#[cfg(feature = "verge-dev")]
if !enable {
return Ok(());
}
let auto = self.auto_launch.as_ref().unwrap();
// macos每次启动都更新登录项避免重复设置登录项
#[cfg(target_os = "macos")]
{
let _ = auto.disable();
if enable {
auto.enable()?;
}
}
#[cfg(not(target_os = "macos"))]
{
match enable {
true => auto.enable()?,
false => auto.disable()?,
};
}
Ok(())
}
/// update the startup
pub fn update_launch(&mut self) -> Result<()> {
if self.auto_launch.is_none() {
return self.init_launch();
}
let data = Data::global();
let verge = data.verge.lock();
let enable = verge.enable_auto_launch.clone().unwrap_or(false);
let auto_launch = self.auto_launch.as_ref().unwrap();
match enable {
true => auto_launch.enable()?,
false => crate::log_if_err!(auto_launch.disable()), // 忽略关闭的错误
};
Ok(())
}
/// launch a system proxy guard
/// read config from file directly
pub fn guard_proxy(&self) {
use tokio::time::{sleep, Duration};
let guard_state = self.guard_state.clone();
tauri::async_runtime::spawn(async move {
// if it is running, exit
let mut state = guard_state.lock().await;
if *state {
return;
}
*state = true;
drop(state);
// default duration is 10s
let mut wait_secs = 10u64;
loop {
sleep(Duration::from_secs(wait_secs)).await;
let global = Data::global();
let verge = global.verge.lock();
let enable = verge.enable_system_proxy.clone().unwrap_or(false); let enable = verge.enable_system_proxy.clone().unwrap_or(false);
let guard = verge.enable_proxy_guard.clone().unwrap_or(false);
let guard_duration = verge.proxy_guard_duration.clone().unwrap_or(10);
let bypass = verge.system_proxy_bypass.clone(); let bypass = verge.system_proxy_bypass.clone();
drop(verge); let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
// stop loop let port = port.unwrap().parse::<u16>()?;
if !enable || !guard { let host = String::from("127.0.0.1");
break;
self.cur_sysproxy = Some(Sysproxy {
enable,
host,
port,
bypass,
});
if enable {
self.old_sysproxy = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
} }
// update duration // run the system proxy guard
wait_secs = guard_duration; self.guard_proxy();
Ok(())
}
let clash = global.clash.lock(); /// update the system proxy
let port = clash.info.port.clone(); pub fn update_sysproxy(&mut self) -> Result<()> {
let port = port.unwrap_or("".into()).parse::<u16>(); if self.cur_sysproxy.is_none() || self.old_sysproxy.is_none() {
drop(clash); return self.init_sysproxy();
}
log::debug!(target: "app", "try to guard the system proxy"); let data = Data::global();
let verge = data.verge.lock();
match port { let enable = verge.enable_system_proxy.clone().unwrap_or(false);
Ok(port) => { let bypass = verge.system_proxy_bypass.clone();
let sysproxy = Sysproxy { let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
enable: true,
host: "127.0.0.1".into(), let mut sysproxy = self.cur_sysproxy.take().unwrap();
port,
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()), sysproxy.enable = enable;
sysproxy.bypass = bypass;
self.cur_sysproxy = Some(sysproxy);
self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
Ok(())
}
/// reset the sysproxy
pub fn reset_sysproxy(&mut self) -> Result<()> {
let cur = self.cur_sysproxy.take();
if let Some(mut old) = self.old_sysproxy.take() {
// 如果原代理和当前代理 端口一致就disable关闭否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let port_same = cur.map_or(true, |cur| old.port == cur.port);
if old.enable && port_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_system_proxy()?;
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur {
// 没有原代理就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_system_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
Ok(())
}
/// init the auto launch
pub fn init_launch(&mut self) -> Result<()> {
let data = Data::global();
let verge = data.verge.lock();
let enable = verge.enable_auto_launch.clone().unwrap_or(false);
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
.ok_or(anyhow!("failed to get file stem"))?;
let app_path = app_exe
.as_os_str()
.to_str()
.ok_or(anyhow!("failed to get app_path"))?
.to_string();
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
// use the /Applications/Clash Verge.app path
#[cfg(target_os = "macos")]
let app_path = (|| -> Option<String> {
let path = std::path::PathBuf::from(&app_path);
let path = path.parent()?.parent()?.parent()?;
let extension = path.extension()?.to_str()?;
match extension == "app" {
true => Some(path.as_os_str().to_str()?.to_string()),
false => None,
}
})()
.unwrap_or(app_path);
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()?;
self.auto_launch = Some(auto);
// 避免在开发时将自启动关了
#[cfg(feature = "verge-dev")]
if !enable {
return Ok(());
}
let auto = self.auto_launch.as_ref().unwrap();
// macos每次启动都更新登录项避免重复设置登录项
#[cfg(target_os = "macos")]
{
let _ = auto.disable();
if enable {
auto.enable()?;
}
}
#[cfg(not(target_os = "macos"))]
{
match enable {
true => auto.enable()?,
false => auto.disable()?,
}; };
log_if_err!(sysproxy.set_system_proxy());
}
Err(_) => log::error!(target: "app", "failed to parse clash port"),
} }
}
let mut state = guard_state.lock().await; Ok(())
*state = false; }
});
} /// update the startup
pub fn update_launch(&mut self) -> Result<()> {
if self.auto_launch.is_none() {
return self.init_launch();
}
let data = Data::global();
let verge = data.verge.lock();
let enable = verge.enable_auto_launch.clone().unwrap_or(false);
let auto_launch = self.auto_launch.as_ref().unwrap();
match enable {
true => auto_launch.enable()?,
false => crate::log_if_err!(auto_launch.disable()), // 忽略关闭的错误
};
Ok(())
}
/// launch a system proxy guard
/// read config from file directly
pub fn guard_proxy(&self) {
use tokio::time::{sleep, Duration};
let guard_state = self.guard_state.clone();
tauri::async_runtime::spawn(async move {
// if it is running, exit
let mut state = guard_state.lock().await;
if *state {
return;
}
*state = true;
drop(state);
// default duration is 10s
let mut wait_secs = 10u64;
loop {
sleep(Duration::from_secs(wait_secs)).await;
let global = Data::global();
let verge = global.verge.lock();
let enable = verge.enable_system_proxy.clone().unwrap_or(false);
let guard = verge.enable_proxy_guard.clone().unwrap_or(false);
let guard_duration = verge.proxy_guard_duration.clone().unwrap_or(10);
let bypass = verge.system_proxy_bypass.clone();
drop(verge);
// stop loop
if !enable || !guard {
break;
}
// update duration
wait_secs = guard_duration;
let clash = global.clash.lock();
let port = clash.info.port.clone();
let port = port.unwrap_or("".into()).parse::<u16>();
drop(clash);
log::debug!(target: "app", "try to guard the system proxy");
match port {
Ok(port) => {
let sysproxy = Sysproxy {
enable: true,
host: "127.0.0.1".into(),
port,
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
};
log_if_err!(sysproxy.set_system_proxy());
}
Err(_) => log::error!(target: "app", "failed to parse clash port"),
}
}
let mut state = guard_state.lock().await;
*state = false;
});
}
} }

View File

@ -8,165 +8,165 @@ use std::collections::HashMap;
type TaskID = u64; type TaskID = u64;
pub struct Timer { pub struct Timer {
/// cron manager /// cron manager
delay_timer: DelayTimer, delay_timer: DelayTimer,
/// save the current state /// save the current state
timer_map: HashMap<String, (TaskID, u64)>, timer_map: HashMap<String, (TaskID, u64)>,
/// increment id /// increment id
timer_count: TaskID, timer_count: TaskID,
} }
impl Timer { impl Timer {
pub fn new() -> Self { pub fn new() -> Self {
Timer { Timer {
delay_timer: DelayTimerBuilder::default().build(), delay_timer: DelayTimerBuilder::default().build(),
timer_map: HashMap::new(), timer_map: HashMap::new(),
timer_count: 1, timer_count: 1,
}
}
/// Correctly update all cron tasks
pub fn refresh(&mut self) -> Result<()> {
let diff_map = self.gen_diff();
for (uid, diff) in diff_map.into_iter() {
match diff {
DiffFlag::Del(tid) => {
let _ = self.timer_map.remove(&uid);
log_if_err!(self.delay_timer.remove_task(tid));
} }
DiffFlag::Add(tid, val) => {
let _ = self.timer_map.insert(uid.clone(), (tid, val));
log_if_err!(self.add_task(uid, tid, val));
}
DiffFlag::Mod(tid, val) => {
let _ = self.timer_map.insert(uid.clone(), (tid, val));
log_if_err!(self.delay_timer.remove_task(tid));
log_if_err!(self.add_task(uid, tid, val));
}
}
} }
Ok(()) /// Correctly update all cron tasks
} pub fn refresh(&mut self) -> Result<()> {
let diff_map = self.gen_diff();
/// restore timer for (uid, diff) in diff_map.into_iter() {
pub fn restore(&mut self) -> Result<()> { match diff {
self.refresh()?; DiffFlag::Del(tid) => {
let _ = self.timer_map.remove(&uid);
let cur_timestamp = get_now(); // seconds log_if_err!(self.delay_timer.remove_task(tid));
}
let global = Data::global(); DiffFlag::Add(tid, val) => {
let profiles = global.profiles.lock(); let _ = self.timer_map.insert(uid.clone(), (tid, val));
log_if_err!(self.add_task(uid, tid, val));
profiles }
.get_items() DiffFlag::Mod(tid, val) => {
.unwrap_or(&vec![]) let _ = self.timer_map.insert(uid.clone(), (tid, val));
.iter() log_if_err!(self.delay_timer.remove_task(tid));
.filter(|item| item.uid.is_some() && item.updated.is_some() && item.option.is_some()) log_if_err!(self.add_task(uid, tid, val));
.filter(|item| { }
// mins to seconds }
let interval = item.option.as_ref().unwrap().update_interval.unwrap_or(0) as usize * 60;
let updated = item.updated.unwrap();
return interval > 0 && cur_timestamp - updated >= interval;
})
.for_each(|item| {
let uid = item.uid.as_ref().unwrap();
if let Some((task_id, _)) = self.timer_map.get(uid) {
log_if_err!(self.delay_timer.advance_task(*task_id));
} }
});
Ok(()) Ok(())
}
/// generate a uid -> update_interval map
fn gen_map(&self) -> HashMap<String, u64> {
let global = Data::global();
let profiles = global.profiles.lock();
let mut new_map = HashMap::new();
if let Some(items) = profiles.get_items() {
for item in items.iter() {
if item.option.is_some() {
let option = item.option.as_ref().unwrap();
let interval = option.update_interval.unwrap_or(0);
if interval > 0 {
new_map.insert(item.uid.clone().unwrap(), interval);
}
}
}
} }
new_map /// restore timer
} pub fn restore(&mut self) -> Result<()> {
self.refresh()?;
/// generate the diff map for refresh let cur_timestamp = get_now(); // seconds
fn gen_diff(&mut self) -> HashMap<String, DiffFlag> {
let mut diff_map = HashMap::new();
let new_map = self.gen_map(); let global = Data::global();
let cur_map = &self.timer_map; let profiles = global.profiles.lock();
cur_map.iter().for_each(|(uid, (tid, val))| { profiles
let new_val = new_map.get(uid).unwrap_or(&0); .get_items()
.unwrap_or(&vec![])
.iter()
.filter(|item| item.uid.is_some() && item.updated.is_some() && item.option.is_some())
.filter(|item| {
// mins to seconds
let interval =
item.option.as_ref().unwrap().update_interval.unwrap_or(0) as usize * 60;
let updated = item.updated.unwrap();
return interval > 0 && cur_timestamp - updated >= interval;
})
.for_each(|item| {
let uid = item.uid.as_ref().unwrap();
if let Some((task_id, _)) = self.timer_map.get(uid) {
log_if_err!(self.delay_timer.advance_task(*task_id));
}
});
if *new_val == 0 { Ok(())
diff_map.insert(uid.clone(), DiffFlag::Del(*tid)); }
} else if new_val != val {
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
}
});
let mut count = self.timer_count; /// generate a uid -> update_interval map
fn gen_map(&self) -> HashMap<String, u64> {
let global = Data::global();
let profiles = global.profiles.lock();
new_map.iter().for_each(|(uid, val)| { let mut new_map = HashMap::new();
if cur_map.get(uid).is_none() {
diff_map.insert(uid.clone(), DiffFlag::Add(count, *val));
count += 1; if let Some(items) = profiles.get_items() {
} for item in items.iter() {
}); if item.option.is_some() {
let option = item.option.as_ref().unwrap();
let interval = option.update_interval.unwrap_or(0);
self.timer_count = count; if interval > 0 {
new_map.insert(item.uid.clone().unwrap(), interval);
}
}
}
}
diff_map new_map
} }
/// add a cron task /// generate the diff map for refresh
fn add_task(&self, uid: String, tid: TaskID, minutes: u64) -> Result<()> { fn gen_diff(&mut self) -> HashMap<String, DiffFlag> {
let core = Core::global(); let mut diff_map = HashMap::new();
let task = TaskBuilder::default() let new_map = self.gen_map();
.set_task_id(tid) let cur_map = &self.timer_map;
.set_maximum_parallel_runnable_num(1)
.set_frequency_repeated_by_minutes(minutes)
// .set_frequency_repeated_by_seconds(minutes) // for test
.spawn_async_routine(move || Self::async_task(core.to_owned(), uid.to_owned()))
.context("failed to create timer task")?;
self cur_map.iter().for_each(|(uid, (tid, val))| {
.delay_timer let new_val = new_map.get(uid).unwrap_or(&0);
.add_task(task)
.context("failed to add timer task")?;
Ok(()) if *new_val == 0 {
} diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
} else if new_val != val {
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
}
});
/// the task runner let mut count = self.timer_count;
async fn async_task(core: Core, uid: String) {
log::info!(target: "app", "running timer task `{uid}`"); new_map.iter().for_each(|(uid, val)| {
log_if_err!(core.update_profile_item(uid, None).await); if cur_map.get(uid).is_none() {
} diff_map.insert(uid.clone(), DiffFlag::Add(count, *val));
count += 1;
}
});
self.timer_count = count;
diff_map
}
/// add a cron task
fn add_task(&self, uid: String, tid: TaskID, minutes: u64) -> Result<()> {
let core = Core::global();
let task = TaskBuilder::default()
.set_task_id(tid)
.set_maximum_parallel_runnable_num(1)
.set_frequency_repeated_by_minutes(minutes)
// .set_frequency_repeated_by_seconds(minutes) // for test
.spawn_async_routine(move || Self::async_task(core.to_owned(), uid.to_owned()))
.context("failed to create timer task")?;
self.delay_timer
.add_task(task)
.context("failed to add timer task")?;
Ok(())
}
/// the task runner
async fn async_task(core: Core, uid: String) {
log::info!(target: "app", "running timer task `{uid}`");
log_if_err!(core.update_profile_item(uid, None).await);
}
} }
#[derive(Debug)] #[derive(Debug)]
enum DiffFlag { enum DiffFlag {
Del(TaskID), Del(TaskID),
Add(TaskID, u64), Add(TaskID, u64),
Mod(TaskID, u64), Mod(TaskID, u64),
} }

View File

@ -1,124 +1,130 @@
use crate::{data::Data, feat, utils::resolve}; use crate::{data::Data, feat, utils::resolve};
use anyhow::{Ok, Result}; use anyhow::{Ok, Result};
use tauri::{ use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu, SystemTraySubmenu,
}; };
pub struct Tray {} pub struct Tray {}
impl Tray { impl Tray {
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu { pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
let data = Data::global(); let data = Data::global();
let zh = { let zh = {
let verge = data.verge.lock(); let verge = data.verge.lock();
verge.language == Some("zh".into()) verge.language == Some("zh".into())
}; };
let version = app_handle.package_info().version.to_string(); let version = app_handle.package_info().version.to_string();
if zh { if zh {
SystemTrayMenu::new() SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", "打开面板")) .add_item(CustomMenuItem::new("open_window", "打开面板"))
.add_native_item(SystemTrayMenuItem::Separator) .add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("rule_mode", "规则模式")) .add_item(CustomMenuItem::new("rule_mode", "规则模式"))
.add_item(CustomMenuItem::new("global_mode", "全局模式")) .add_item(CustomMenuItem::new("global_mode", "全局模式"))
.add_item(CustomMenuItem::new("direct_mode", "直连模式")) .add_item(CustomMenuItem::new("direct_mode", "直连模式"))
.add_item(CustomMenuItem::new("script_mode", "脚本模式")) .add_item(CustomMenuItem::new("script_mode", "脚本模式"))
.add_native_item(SystemTrayMenuItem::Separator) .add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("system_proxy", "系统代理")) .add_item(CustomMenuItem::new("system_proxy", "系统代理"))
.add_item(CustomMenuItem::new("tun_mode", "TUN 模式")) .add_item(CustomMenuItem::new("tun_mode", "TUN 模式"))
.add_submenu(SystemTraySubmenu::new( .add_submenu(SystemTraySubmenu::new(
"更多", "更多",
SystemTrayMenu::new() SystemTrayMenu::new()
.add_item(CustomMenuItem::new("restart_clash", "重启 Clash")) .add_item(CustomMenuItem::new("restart_clash", "重启 Clash"))
.add_item(CustomMenuItem::new("restart_app", "重启应用")) .add_item(CustomMenuItem::new("restart_app", "重启应用"))
.add_item(CustomMenuItem::new("app_version", format!("Version {version}")).disabled()), .add_item(
)) CustomMenuItem::new("app_version", format!("Version {version}"))
.add_native_item(SystemTrayMenuItem::Separator) .disabled(),
.add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q")) ),
} else { ))
SystemTrayMenu::new() .add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("open_window", "Dashboard")) .add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q"))
.add_native_item(SystemTrayMenuItem::Separator) } else {
.add_item(CustomMenuItem::new("rule_mode", "Rule Mode")) SystemTrayMenu::new()
.add_item(CustomMenuItem::new("global_mode", "Global Mode")) .add_item(CustomMenuItem::new("open_window", "Dashboard"))
.add_item(CustomMenuItem::new("direct_mode", "Direct Mode")) .add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("script_mode", "Script Mode")) .add_item(CustomMenuItem::new("rule_mode", "Rule Mode"))
.add_native_item(SystemTrayMenuItem::Separator) .add_item(CustomMenuItem::new("global_mode", "Global Mode"))
.add_item(CustomMenuItem::new("system_proxy", "System Proxy")) .add_item(CustomMenuItem::new("direct_mode", "Direct Mode"))
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode")) .add_item(CustomMenuItem::new("script_mode", "Script Mode"))
.add_submenu(SystemTraySubmenu::new( .add_native_item(SystemTrayMenuItem::Separator)
"More", .add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
SystemTrayMenu::new() .add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash")) .add_submenu(SystemTraySubmenu::new(
.add_item(CustomMenuItem::new("restart_app", "Restart App")) "More",
.add_item(CustomMenuItem::new("app_version", format!("Version {version}")).disabled()), SystemTrayMenu::new()
)) .add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
.add_native_item(SystemTrayMenuItem::Separator) .add_item(CustomMenuItem::new("restart_app", "Restart App"))
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q")) .add_item(
} CustomMenuItem::new("app_version", format!("Version {version}"))
} .disabled(),
),
pub fn update_systray(app_handle: &AppHandle) -> Result<()> { ))
app_handle .add_native_item(SystemTrayMenuItem::Separator)
.tray_handle() .add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"))
.set_menu(Tray::tray_menu(app_handle))?; }
Tray::update_part(app_handle)?; }
Ok(())
} pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
app_handle
pub fn update_part(app_handle: &AppHandle) -> Result<()> { .tray_handle()
let global = Data::global(); .set_menu(Tray::tray_menu(app_handle))?;
let clash = global.clash.lock(); Tray::update_part(app_handle)?;
let mode = clash Ok(())
.config }
.get(&serde_yaml::Value::from("mode"))
.map(|val| val.as_str().unwrap_or("rule")) pub fn update_part(app_handle: &AppHandle) -> Result<()> {
.unwrap_or("rule"); let global = Data::global();
let clash = global.clash.lock();
let tray = app_handle.tray_handle(); let mode = clash
.config
let _ = tray.get_item("rule_mode").set_selected(mode == "rule"); .get(&serde_yaml::Value::from("mode"))
let _ = tray.get_item("global_mode").set_selected(mode == "global"); .map(|val| val.as_str().unwrap_or("rule"))
let _ = tray.get_item("direct_mode").set_selected(mode == "direct"); .unwrap_or("rule");
let _ = tray.get_item("script_mode").set_selected(mode == "script");
let tray = app_handle.tray_handle();
let verge = global.verge.lock();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let _ = tray.get_item("global_mode").set_selected(mode == "global");
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
let _ = tray.get_item("system_proxy").set_selected(*system_proxy); let _ = tray.get_item("script_mode").set_selected(mode == "script");
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
let verge = global.verge.lock();
Ok(()) let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
} let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) { let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
match event { let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => { Ok(())
let mode = &mode[0..mode.len() - 5]; }
feat::change_clash_mode(mode);
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode);
}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => api::process::restart(&app_handle.env()),
"quit" => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
_ => {}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
resolve::create_window(app_handle);
}
_ => {}
} }
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => api::process::restart(&app_handle.env()),
"quit" => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
_ => {}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
resolve::create_window(app_handle);
}
_ => {}
} }
}
} }

View File

@ -5,163 +5,163 @@ use serde_yaml::{Mapping, Value};
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct ClashInfo { pub struct ClashInfo {
/// clash sidecar status /// clash sidecar status
pub status: String, pub status: String,
/// clash core port /// clash core port
pub port: Option<String>, pub port: Option<String>,
/// same as `external-controller` /// same as `external-controller`
pub server: Option<String>, pub server: Option<String>,
/// clash secret /// clash secret
pub secret: Option<String>, pub secret: Option<String>,
} }
impl ClashInfo { impl ClashInfo {
/// parse the clash's config.yaml /// parse the clash's config.yaml
/// get some information /// get some information
pub fn from(config: &Mapping) -> ClashInfo { pub fn from(config: &Mapping) -> ClashInfo {
let key_port_1 = Value::from("mixed-port"); let key_port_1 = Value::from("mixed-port");
let key_port_2 = Value::from("port"); let key_port_2 = Value::from("port");
let key_server = Value::from("external-controller"); let key_server = Value::from("external-controller");
let key_secret = Value::from("secret"); let key_secret = Value::from("secret");
let mut status: u32 = 0; let mut status: u32 = 0;
let port = match config.get(&key_port_1) { let port = match config.get(&key_port_1) {
Some(value) => match value { Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()), Value::String(val_str) => Some(val_str.clone()),
Value::Number(val_num) => Some(val_num.to_string()), Value::Number(val_num) => Some(val_num.to_string()),
_ => { _ => {
status |= 0b1; status |= 0b1;
None None
}
},
_ => {
status |= 0b10;
None
}
};
let port = match port {
Some(_) => port,
None => match config.get(&key_port_2) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => {
status |= 0b100;
None
}
},
_ => {
status |= 0b1000;
None
}
},
};
// `external-controller` could be
// "127.0.0.1:9090" or ":9090"
let server = match config.get(&key_server) {
Some(value) => match value.as_str() {
Some(val_str) => {
if val_str.starts_with(":") {
Some(format!("127.0.0.1{val_str}"))
} else if val_str.starts_with("0.0.0.0:") {
Some(format!("127.0.0.1:{}", &val_str[8..]))
} else if val_str.starts_with("[::]:") {
Some(format!("127.0.0.1:{}", &val_str[5..]))
} else {
Some(val_str.into())
}
}
None => {
status |= 0b10000;
None
}
},
None => {
status |= 0b100000;
None
}
};
let secret = match config.get(&key_secret) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
},
_ => None,
};
ClashInfo {
status: format!("{status}"),
port,
server,
secret,
} }
},
_ => {
status |= 0b10;
None
}
};
let port = match port {
Some(_) => port,
None => match config.get(&key_port_2) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => {
status |= 0b100;
None
}
},
_ => {
status |= 0b1000;
None
}
},
};
// `external-controller` could be
// "127.0.0.1:9090" or ":9090"
let server = match config.get(&key_server) {
Some(value) => match value.as_str() {
Some(val_str) => {
if val_str.starts_with(":") {
Some(format!("127.0.0.1{val_str}"))
} else if val_str.starts_with("0.0.0.0:") {
Some(format!("127.0.0.1:{}", &val_str[8..]))
} else if val_str.starts_with("[::]:") {
Some(format!("127.0.0.1:{}", &val_str[5..]))
} else {
Some(val_str.into())
}
}
None => {
status |= 0b10000;
None
}
},
None => {
status |= 0b100000;
None
}
};
let secret = match config.get(&key_secret) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
},
_ => None,
};
ClashInfo {
status: format!("{status}"),
port,
server,
secret,
} }
}
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Clash { pub struct Clash {
/// maintain the clash config /// maintain the clash config
pub config: Mapping, pub config: Mapping,
/// some info /// some info
pub info: ClashInfo, pub info: ClashInfo,
} }
impl Clash { impl Clash {
pub fn new() -> Clash { pub fn new() -> Clash {
let config = Clash::read_config(); let config = Clash::read_config();
let info = ClashInfo::from(&config); let info = ClashInfo::from(&config);
Clash { config, info } Clash { config, info }
}
/// get clash config
pub fn read_config() -> Mapping {
config::read_merge_mapping(dirs::clash_path())
}
/// save the clash config
pub fn save_config(&self) -> Result<()> {
config::save_yaml(
dirs::clash_path(),
&self.config,
Some("# Default Config For Clash Core\n\n"),
)
}
/// patch update the clash config
/// if the port is changed then return true
pub fn patch_config(&mut self, patch: Mapping) -> Result<()> {
let port_key = Value::from("mixed-port");
let server_key = Value::from("external-controller");
let secret_key = Value::from("secret");
let change_info = patch.contains_key(&port_key)
|| patch.contains_key(&server_key)
|| patch.contains_key(&secret_key);
for (key, value) in patch.into_iter() {
self.config.insert(key, value);
} }
if change_info { /// get clash config
self.info = ClashInfo::from(&self.config); pub fn read_config() -> Mapping {
config::read_merge_mapping(dirs::clash_path())
} }
self.save_config() /// save the clash config
} pub fn save_config(&self) -> Result<()> {
config::save_yaml(
dirs::clash_path(),
&self.config,
Some("# Default Config For Clash Core\n\n"),
)
}
/// patch update the clash config
/// if the port is changed then return true
pub fn patch_config(&mut self, patch: Mapping) -> Result<()> {
let port_key = Value::from("mixed-port");
let server_key = Value::from("external-controller");
let secret_key = Value::from("secret");
let change_info = patch.contains_key(&port_key)
|| patch.contains_key(&server_key)
|| patch.contains_key(&secret_key);
for (key, value) in patch.into_iter() {
self.config.insert(key, value);
}
if change_info {
self.info = ClashInfo::from(&self.config);
}
self.save_config()
}
} }
impl Default for Clash { impl Default for Clash {
fn default() -> Self { fn default() -> Self {
Clash::new() Clash::new()
} }
} }

View File

@ -14,19 +14,19 @@ use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Data { pub struct Data {
pub clash: Arc<Mutex<Clash>>, pub clash: Arc<Mutex<Clash>>,
pub verge: Arc<Mutex<Verge>>, pub verge: Arc<Mutex<Verge>>,
pub profiles: Arc<Mutex<Profiles>>, pub profiles: Arc<Mutex<Profiles>>,
} }
impl Data { impl Data {
pub fn global() -> &'static Data { pub fn global() -> &'static Data {
static DATA: OnceCell<Data> = OnceCell::new(); static DATA: OnceCell<Data> = OnceCell::new();
DATA.get_or_init(|| Data { DATA.get_or_init(|| Data {
clash: Arc::new(Mutex::new(Clash::new())), clash: Arc::new(Mutex::new(Clash::new())),
verge: Arc::new(Mutex::new(Verge::new())), verge: Arc::new(Mutex::new(Verge::new())),
profiles: Arc::new(Mutex::new(Profiles::new())), profiles: Arc::new(Mutex::new(Profiles::new())),
}) })
} }
} }

View File

@ -8,399 +8,399 @@ use sysproxy::Sysproxy;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PrfItem { pub struct PrfItem {
pub uid: Option<String>, pub uid: Option<String>,
/// profile item type /// profile item type
/// enum value: remote | local | script | merge /// enum value: remote | local | script | merge
#[serde(rename = "type")] #[serde(rename = "type")]
pub itype: Option<String>, pub itype: Option<String>,
/// profile name /// profile name
pub name: Option<String>, pub name: Option<String>,
/// profile file /// profile file
pub file: Option<String>, pub file: Option<String>,
/// profile description /// profile description
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>, pub desc: Option<String>,
/// source url /// source url
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>, pub url: Option<String>,
/// selected infomation /// selected infomation
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<Vec<PrfSelected>>, pub selected: Option<Vec<PrfSelected>>,
/// subscription user info /// subscription user info
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<PrfExtra>, pub extra: Option<PrfExtra>,
/// updated time /// updated time
pub updated: Option<usize>, pub updated: Option<usize>,
/// some options of the item /// some options of the item
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub option: Option<PrfOption>, pub option: Option<PrfOption>,
/// the file data /// the file data
#[serde(skip)] #[serde(skip)]
pub file_data: Option<String>, pub file_data: Option<String>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct PrfSelected { pub struct PrfSelected {
pub name: Option<String>, pub name: Option<String>,
pub now: Option<String>, pub now: Option<String>,
} }
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
pub struct PrfExtra { pub struct PrfExtra {
pub upload: usize, pub upload: usize,
pub download: usize, pub download: usize,
pub total: usize, pub total: usize,
pub expire: usize, pub expire: usize,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct PrfOption { pub struct PrfOption {
/// for `remote` profile's http request /// for `remote` profile's http request
/// see issue #13 /// see issue #13
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>, pub user_agent: Option<String>,
/// for `remote` profile /// for `remote` profile
/// use system proxy /// use system proxy
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub with_proxy: Option<bool>, pub with_proxy: Option<bool>,
/// for `remote` profile /// for `remote` profile
/// use self proxy /// use self proxy
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub self_proxy: Option<bool>, pub self_proxy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub update_interval: Option<u64>, pub update_interval: Option<u64>,
} }
impl PrfOption { impl PrfOption {
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> { pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
match (one, other) { match (one, other) {
(Some(mut a), Some(b)) => { (Some(mut a), Some(b)) => {
a.user_agent = b.user_agent.or(a.user_agent); a.user_agent = b.user_agent.or(a.user_agent);
a.with_proxy = b.with_proxy.or(a.with_proxy); a.with_proxy = b.with_proxy.or(a.with_proxy);
a.self_proxy = b.self_proxy.or(a.self_proxy); a.self_proxy = b.self_proxy.or(a.self_proxy);
a.update_interval = b.update_interval.or(a.update_interval); a.update_interval = b.update_interval.or(a.update_interval);
Some(a) Some(a)
} }
t @ _ => t.0.or(t.1), t @ _ => t.0.or(t.1),
}
} }
}
} }
impl Default for PrfItem { impl Default for PrfItem {
fn default() -> Self { fn default() -> Self {
PrfItem { PrfItem {
uid: None, uid: None,
itype: None, itype: None,
name: None, name: None,
desc: None, desc: None,
file: None, file: None,
url: None, url: None,
selected: None, selected: None,
extra: None, extra: None,
updated: None, updated: None,
option: None, option: None,
file_data: None, file_data: None,
}
} }
}
} }
impl PrfItem { impl PrfItem {
/// From partial item /// From partial item
/// must contain `itype` /// must contain `itype`
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> { pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
if item.itype.is_none() { if item.itype.is_none() {
bail!("type should not be null"); bail!("type should not be null");
}
match item.itype.unwrap().as_str() {
"remote" => {
if item.url.is_none() {
bail!("url should not be null");
} }
let url = item.url.as_ref().unwrap().as_str();
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, item.option).await
}
"local" => {
let name = item.name.unwrap_or("Local File".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc, file_data)
}
"merge" => {
let name = item.name.unwrap_or("Merge".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_merge(name, desc)
}
"script" => {
let name = item.name.unwrap_or("Script".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
}
typ @ _ => bail!("invalid profile item type \"{typ}\""),
}
}
/// ## Local type match item.itype.unwrap().as_str() {
/// create a new item from name/desc "remote" => {
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> { if item.url.is_none() {
let uid = help::get_uid("l"); bail!("url should not be null");
let file = format!("{uid}.yaml"); }
let url = item.url.as_ref().unwrap().as_str();
Ok(PrfItem { let name = item.name;
uid: Some(uid), let desc = item.desc;
itype: Some("local".into()), PrfItem::from_url(url, name, desc, item.option).await
name: Some(name), }
desc: Some(desc), "local" => {
file: Some(file), let name = item.name.unwrap_or("Local File".into());
url: None, let desc = item.desc.unwrap_or("".into());
selected: None, PrfItem::from_local(name, desc, file_data)
extra: None, }
option: None, "merge" => {
updated: Some(help::get_now()), let name = item.name.unwrap_or("Merge".into());
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), let desc = item.desc.unwrap_or("".into());
}) PrfItem::from_merge(name, desc)
} }
"script" => {
/// ## Remote type let name = item.name.unwrap_or("Script".into());
/// create a new item from url let desc = item.desc.unwrap_or("".into());
pub async fn from_url( PrfItem::from_script(name, desc)
url: &str, }
name: Option<String>, typ @ _ => bail!("invalid profile item type \"{typ}\""),
desc: Option<String>,
option: Option<PrfOption>,
) -> Result<PrfItem> {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
let mut builder = reqwest::ClientBuilder::new().no_proxy();
// 使用软件自己的代理
if self_proxy {
let data = super::Data::global();
let port = data.clash.lock().info.port.clone();
let port = port.ok_or(anyhow::anyhow!("failed to get clash info port"))?;
let proxy_scheme = format!("http://127.0.0.1:{port}");
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
// 使用系统代理
else if with_proxy {
match Sysproxy::get_system_proxy() {
Ok(p @ Sysproxy { enable: true, .. }) => {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
} }
_ => {}
};
} }
let version = unsafe { dirs::APP_VERSION }; /// ## Local type
let version = format!("clash-verge/{version}"); /// create a new item from name/desc
builder = builder.user_agent(user_agent.unwrap_or(version)); pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
let uid = help::get_uid("l");
let file = format!("{uid}.yaml");
let resp = builder.build()?.get(url).send().await?; Ok(PrfItem {
uid: Some(uid),
let status_code = resp.status(); itype: Some("local".into()),
if !StatusCode::is_success(&status_code) { name: Some(name),
bail!("failed to fetch remote profile with status {status_code}") desc: Some(desc),
} file: Some(file),
url: None,
let header = resp.headers(); selected: None,
extra: None,
// parse the Subscription UserInfo option: None,
let extra = match header.get("Subscription-Userinfo") { updated: Some(help::get_now()),
Some(value) => { file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
download: help::parse_str(sub_info, "download=").unwrap_or(0),
total: help::parse_str(sub_info, "total=").unwrap_or(0),
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
}) })
}
None => None,
};
// parse the Content-Disposition
let filename = match header.get("Content-Disposition") {
Some(value) => {
let filename = value.to_str().unwrap_or("");
help::parse_str::<String>(filename, "filename=")
}
None => None,
};
// parse the profile-update-interval
let option = match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Err(_) => None,
},
None => None,
};
let uid = help::get_uid("r");
let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?;
// check the data whether the valid yaml format
let yaml = serde_yaml::from_str::<Mapping>(&data) //
.context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
bail!("profile does not contain `proxies` or `proxy-providers`");
} }
Ok(PrfItem { /// ## Remote type
uid: Some(uid), /// create a new item from url
itype: Some("remote".into()), pub async fn from_url(
name: Some(name), url: &str,
desc, name: Option<String>,
file: Some(file), desc: Option<String>,
url: Some(url.into()), option: Option<PrfOption>,
selected: None, ) -> Result<PrfItem> {
extra, let opt_ref = option.as_ref();
option, let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
updated: Some(help::get_now()), let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
file_data: Some(data), let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
})
}
/// ## Merge type (enhance) let mut builder = reqwest::ClientBuilder::new().no_proxy();
/// create the enhanced item by using `merge` rule
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
Ok(PrfItem { // 使用软件自己的代理
uid: Some(uid), if self_proxy {
itype: Some("merge".into()), let data = super::Data::global();
name: Some(name), let port = data.clash.lock().info.port.clone();
desc: Some(desc), let port = port.ok_or(anyhow::anyhow!("failed to get clash info port"))?;
file: Some(file), let proxy_scheme = format!("http://127.0.0.1:{port}");
url: None,
selected: None,
extra: None,
option: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_MERGE.into()),
})
}
/// ## Script type (enhance) if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
/// create the enhanced item by using javascript(browserjs) builder = builder.proxy(proxy);
pub fn from_script(name: String, desc: String) -> Result<PrfItem> { }
let uid = help::get_uid("s"); if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
let file = format!("{uid}.js"); // js ext builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
// 使用系统代理
else if with_proxy {
match Sysproxy::get_system_proxy() {
Ok(p @ Sysproxy { enable: true, .. }) => {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
Ok(PrfItem { if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
uid: Some(uid), builder = builder.proxy(proxy);
itype: Some("script".into()), }
name: Some(name), if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
desc: Some(desc), builder = builder.proxy(proxy);
file: Some(file), }
url: None, if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
selected: None, builder = builder.proxy(proxy);
extra: None, }
option: None, }
updated: Some(help::get_now()), _ => {}
file_data: Some(tmpl::ITEM_SCRIPT.into()), };
}) }
}
/// get the file data let version = unsafe { dirs::APP_VERSION };
pub fn read_file(&self) -> Result<String> { let version = format!("clash-verge/{version}");
if self.file.is_none() { builder = builder.user_agent(user_agent.unwrap_or(version));
bail!("could not find the file");
let resp = builder.build()?.get(url).send().await?;
let status_code = resp.status();
if !StatusCode::is_success(&status_code) {
bail!("failed to fetch remote profile with status {status_code}")
}
let header = resp.headers();
// parse the Subscription UserInfo
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
download: help::parse_str(sub_info, "download=").unwrap_or(0),
total: help::parse_str(sub_info, "total=").unwrap_or(0),
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
})
}
None => None,
};
// parse the Content-Disposition
let filename = match header.get("Content-Disposition") {
Some(value) => {
let filename = value.to_str().unwrap_or("");
help::parse_str::<String>(filename, "filename=")
}
None => None,
};
// parse the profile-update-interval
let option = match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Err(_) => None,
},
None => None,
};
let uid = help::get_uid("r");
let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?;
// check the data whether the valid yaml format
let yaml = serde_yaml::from_str::<Mapping>(&data) //
.context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
bail!("profile does not contain `proxies` or `proxy-providers`");
}
Ok(PrfItem {
uid: Some(uid),
itype: Some("remote".into()),
name: Some(name),
desc,
file: Some(file),
url: Some(url.into()),
selected: None,
extra,
option,
updated: Some(help::get_now()),
file_data: Some(data),
})
} }
let file = self.file.clone().unwrap(); /// ## Merge type (enhance)
let path = dirs::app_profiles_dir().join(file); /// create the enhanced item by using `merge` rule
fs::read_to_string(path).context("failed to read the file") pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
} let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
/// save the file data Ok(PrfItem {
pub fn save_file(&self, data: String) -> Result<()> { uid: Some(uid),
if self.file.is_none() { itype: Some("merge".into()),
bail!("could not find the file"); name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_MERGE.into()),
})
} }
let file = self.file.clone().unwrap(); /// ## Script type (enhance)
let path = dirs::app_profiles_dir().join(file); /// create the enhanced item by using javascript(browserjs)
fs::write(path, data.as_bytes()).context("failed to save the file") pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
} let uid = help::get_uid("s");
let file = format!("{uid}.js"); // js ext
/// get the data for enhanced mode Ok(PrfItem {
pub fn to_enhance(&self) -> Option<ChainItem> { uid: Some(uid),
let itype = self.itype.as_ref()?.as_str(); itype: Some("script".into()),
let file = self.file.clone()?; name: Some(name),
let uid = self.uid.clone().unwrap_or("".into()); desc: Some(desc),
let path = dirs::app_profiles_dir().join(file); file: Some(file),
url: None,
if !path.exists() { selected: None,
return None; extra: None,
option: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_SCRIPT.into()),
})
} }
match itype { /// get the file data
"script" => Some(ChainItem { pub fn read_file(&self) -> Result<String> {
uid, if self.file.is_none() {
data: ChainType::Script(fs::read_to_string(path).unwrap_or("".into())), bail!("could not find the file");
}), }
"merge" => Some(ChainItem {
uid, let file = self.file.clone().unwrap();
data: ChainType::Merge(config::read_merge_mapping(path)), let path = dirs::app_profiles_dir().join(file);
}), fs::read_to_string(path).context("failed to read the file")
_ => None, }
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(file);
fs::write(path, data.as_bytes()).context("failed to save the file")
}
/// get the data for enhanced mode
pub fn to_enhance(&self) -> Option<ChainItem> {
let itype = self.itype.as_ref()?.as_str();
let file = self.file.clone()?;
let uid = self.uid.clone().unwrap_or("".into());
let path = dirs::app_profiles_dir().join(file);
if !path.exists() {
return None;
}
match itype {
"script" => Some(ChainItem {
uid,
data: ChainType::Script(fs::read_to_string(path).unwrap_or("".into())),
}),
"merge" => Some(ChainItem {
uid,
data: ChainType::Merge(config::read_merge_mapping(path)),
}),
_ => None,
}
} }
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ChainItem { pub struct ChainItem {
pub uid: String, pub uid: String,
pub data: ChainType, pub data: ChainType,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ChainType { pub enum ChainType {
Merge(Mapping), Merge(Mapping),
Script(String), Script(String),
} }

View File

@ -14,315 +14,316 @@ use std::{fs, io::Write};
/// ///
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct Profiles { pub struct Profiles {
/// same as PrfConfig.current /// same as PrfConfig.current
current: Option<String>, current: Option<String>,
/// same as PrfConfig.chain /// same as PrfConfig.chain
chain: Option<Vec<String>>, chain: Option<Vec<String>>,
/// record valid fields for clash /// record valid fields for clash
valid: Option<Vec<String>>, valid: Option<Vec<String>>,
/// profile list /// profile list
items: Option<Vec<PrfItem>>, items: Option<Vec<PrfItem>>,
} }
macro_rules! patch { macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => { ($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() { if ($rv.$key).is_some() {
$lv.$key = $rv.$key; $lv.$key = $rv.$key;
} }
}; };
} }
impl Profiles { impl Profiles {
pub fn new() -> Self { pub fn new() -> Self {
Profiles::read_file() Profiles::read_file()
}
/// read the config from the file
pub fn read_file() -> Self {
let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
if profiles.items.is_none() {
profiles.items = Some(vec![]);
} }
// compatiable with the old old old version /// read the config from the file
profiles.items.as_mut().map(|items| { pub fn read_file() -> Self {
for mut item in items.iter_mut() { let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
if profiles.items.is_none() {
profiles.items = Some(vec![]);
}
// compatiable with the old old old version
profiles.items.as_mut().map(|items| {
for mut item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
}
}
});
profiles
}
/// save the config to the file
pub fn save_file(&self) -> Result<()> {
config::save_yaml(
dirs::profiles_path(),
self,
Some("# Profiles Config for Clash Verge\n\n"),
)
}
/// get the current uid
pub fn get_current(&self) -> Option<String> {
self.current.clone()
}
/// only change the main to the target id
pub fn put_current(&mut self, uid: String) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
let items = self.items.as_ref().unwrap();
let some_uid = Some(uid.clone());
if items.iter().find(|&each| each.uid == some_uid).is_some() {
self.current = some_uid;
return self.save_file();
}
bail!("invalid uid \"{uid}\"");
}
/// just change the `chain`
pub fn put_chain(&mut self, chain: Option<Vec<String>>) -> Result<()> {
self.chain = chain;
self.save_file()
}
/// just change the `field`
pub fn put_valid(&mut self, valid: Option<Vec<String>>) -> Result<()> {
self.valid = valid;
self.save_file()
}
/// get items ref
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref()
}
/// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if self.items.is_some() {
let items = self.items.as_ref().unwrap();
let some_uid = Some(uid.clone());
for each in items.iter() {
if each.uid == some_uid {
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{uid}\"");
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() { if item.uid.is_none() {
item.uid = Some(help::get_uid("d")); bail!("the uid should not be null");
} }
}
});
profiles // save the file data
} // move the field value after save
if let Some(file_data) = item.file_data.take() {
/// save the config to the file if item.file.is_none() {
pub fn save_file(&self) -> Result<()> { bail!("the file should not be null");
config::save_yaml( }
dirs::profiles_path(),
self,
Some("# Profiles Config for Clash Verge\n\n"),
)
}
/// get the current uid
pub fn get_current(&self) -> Option<String> {
self.current.clone()
}
/// only change the main to the target id
pub fn put_current(&mut self, uid: String) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
let items = self.items.as_ref().unwrap();
let some_uid = Some(uid.clone());
if items.iter().find(|&each| each.uid == some_uid).is_some() {
self.current = some_uid;
return self.save_file();
}
bail!("invalid uid \"{uid}\"");
}
/// just change the `chain`
pub fn put_chain(&mut self, chain: Option<Vec<String>>) -> Result<()> {
self.chain = chain;
self.save_file()
}
/// just change the `field`
pub fn put_valid(&mut self, valid: Option<Vec<String>>) -> Result<()> {
self.valid = valid;
self.save_file()
}
/// get items ref
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref()
}
/// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if self.items.is_some() {
let items = self.items.as_ref().unwrap();
let some_uid = Some(uid.clone());
for each in items.iter() {
if each.uid == some_uid {
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{uid}\"");
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() {
bail!("the uid should not be null");
}
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
if item.file.is_none() {
bail!("the file should not be null");
}
let file = item.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(&file);
fs::File::create(path)
.context(format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.context(format!("failed to write to file \"{}\"", file))?;
}
if self.items.is_none() {
self.items = Some(vec![]);
}
self.items.as_mut().map(|items| items.push(item));
self.save_file()
}
/// update the item value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]);
for mut each in items.iter_mut() {
if each.uid == Some(uid.clone()) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
self.items = Some(items);
return self.save_file();
}
}
self.items = Some(items);
bail!("failed to find the profile item \"uid:{uid}\"")
}
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
// find the item
let _ = self.get_item(&uid)?;
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
for mut each in items.iter_mut() {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
let file = each.file.take();
let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
// the file must exists
each.file = Some(file.clone());
let file = item.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(&file); let path = dirs::app_profiles_dir().join(&file);
fs::File::create(path) fs::File::create(path)
.context(format!("failed to create file \"{}\"", file))? .context(format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes()) .write(file_data.as_bytes())
.context(format!("failed to write to file \"{}\"", file))?; .context(format!("failed to write to file \"{}\"", file))?;
}
break;
} }
}
}
self.save_file() if self.items.is_none() {
} self.items = Some(vec![]);
/// delete item
/// if delete the current then return true
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
let mut items = self.items.take().unwrap_or(vec![]);
let mut index = None;
// get the index
for i in 0..items.len() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
}
}
if let Some(index) = index {
items.remove(index).file.map(|file| {
let path = dirs::app_profiles_dir().join(file);
if path.exists() {
let _ = fs::remove_file(path);
} }
});
self.items.as_mut().map(|items| items.push(item));
self.save_file()
} }
// delete the original uid /// update the item value
if current == uid { pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
self.current = match items.len() > 0 { let mut items = self.items.take().unwrap_or(vec![]);
true => items[0].uid.clone(),
false => None, for mut each in items.iter_mut() {
}; if each.uid == Some(uid.clone()) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
self.items = Some(items);
return self.save_file();
}
}
self.items = Some(items);
bail!("failed to find the profile item \"uid:{uid}\"")
} }
self.items = Some(items); /// be used to update the remote item
self.save_file()?; /// only patch `updated` `extra` `file_data`
Ok(current == uid) pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
} if self.items.is_none() {
self.items = Some(vec![]);
}
/// generate the current Mapping data // find the item
fn gen_current(&self) -> Result<Mapping> { let _ = self.get_item(&uid)?;
let config = Mapping::new();
if self.current.is_none() || self.items.is_none() { if let Some(items) = self.items.as_mut() {
return Ok(config); let some_uid = Some(uid.clone());
for mut each in items.iter_mut() {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
let file = each.file.take();
let file =
file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
// the file must exists
each.file = Some(file.clone());
let path = dirs::app_profiles_dir().join(&file);
fs::File::create(path)
.context(format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.context(format!("failed to write to file \"{}\"", file))?;
}
break;
}
}
}
self.save_file()
} }
let current = self.current.clone().unwrap(); /// delete item
for item in self.items.as_ref().unwrap().iter() { /// if delete the current then return true
if item.uid == Some(current.clone()) { pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let file_path = match item.file.clone() { let current = self.current.as_ref().unwrap_or(&uid);
Some(file) => dirs::app_profiles_dir().join(file), let current = current.clone();
None => bail!("failed to get the file field"),
let mut items = self.items.take().unwrap_or(vec![]);
let mut index = None;
// get the index
for i in 0..items.len() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
}
}
if let Some(index) = index {
items.remove(index).file.map(|file| {
let path = dirs::app_profiles_dir().join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
// delete the original uid
if current == uid {
self.current = match items.len() > 0 {
true => items[0].uid.clone(),
false => None,
};
}
self.items = Some(items);
self.save_file()?;
Ok(current == uid)
}
/// generate the current Mapping data
fn gen_current(&self) -> Result<Mapping> {
let config = Mapping::new();
if self.current.is_none() || self.items.is_none() {
return Ok(config);
}
let current = self.current.clone().unwrap();
for item in self.items.as_ref().unwrap().iter() {
if item.uid == Some(current.clone()) {
let file_path = match item.file.clone() {
Some(file) => dirs::app_profiles_dir().join(file),
None => bail!("failed to get the file field"),
};
if !file_path.exists() {
bail!("failed to read the file \"{}\"", file_path.display());
}
return Ok(config::read_merge_mapping(file_path.clone()));
}
}
bail!("failed to find current profile \"uid:{current}\"");
}
/// generate the data for activate clash config
pub fn gen_activate(&self) -> Result<PrfActivate> {
let current = self.gen_current()?;
let chain = match self.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| self.get_item(uid).ok())
.filter_map(|item| item.to_enhance())
.collect::<Vec<ChainItem>>(),
None => vec![],
}; };
let valid = self.valid.clone().unwrap_or(vec![]);
if !file_path.exists() { Ok(PrfActivate {
bail!("failed to read the file \"{}\"", file_path.display()); current,
} chain,
valid,
return Ok(config::read_merge_mapping(file_path.clone())); })
}
} }
bail!("failed to find current profile \"uid:{current}\"");
}
/// generate the data for activate clash config
pub fn gen_activate(&self) -> Result<PrfActivate> {
let current = self.gen_current()?;
let chain = match self.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| self.get_item(uid).ok())
.filter_map(|item| item.to_enhance())
.collect::<Vec<ChainItem>>(),
None => vec![],
};
let valid = self.valid.clone().unwrap_or(vec![]);
Ok(PrfActivate {
current,
chain,
valid,
})
}
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct PrfActivate { pub struct PrfActivate {
pub current: Mapping, pub current: Mapping,
pub chain: Vec<ChainItem>, pub chain: Vec<ChainItem>,
pub valid: Vec<String>, pub valid: Vec<String>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct RuntimeResult { pub struct RuntimeResult {
pub config: Option<Mapping>, pub config: Option<Mapping>,
pub config_yaml: Option<String>, pub config_yaml: Option<String>,
// 记录在配置中包括merge和script生成的出现过的keys // 记录在配置中包括merge和script生成的出现过的keys
// 这些keys不一定都生效 // 这些keys不一定都生效
pub exists_keys: Vec<String>, pub exists_keys: Vec<String>,
pub chain_logs: HashMap<String, Vec<(String, String)>>, pub chain_logs: HashMap<String, Vec<(String, String)>>,
} }

View File

@ -5,132 +5,132 @@ use serde::{Deserialize, Serialize};
/// ### `verge.yaml` schema /// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct Verge { pub struct Verge {
/// app listening port /// app listening port
/// for app singleton /// for app singleton
pub app_singleton_port: Option<u16>, pub app_singleton_port: Option<u16>,
// i18n // i18n
pub language: Option<String>, pub language: Option<String>,
/// `light` or `dark` or `system` /// `light` or `dark` or `system`
pub theme_mode: Option<String>, pub theme_mode: Option<String>,
/// enable blur mode /// enable blur mode
/// maybe be able to set the alpha /// maybe be able to set the alpha
pub theme_blur: Option<bool>, pub theme_blur: Option<bool>,
/// enable traffic graph default is true /// enable traffic graph default is true
pub traffic_graph: Option<bool>, pub traffic_graph: Option<bool>,
/// clash tun mode /// clash tun mode
pub enable_tun_mode: Option<bool>, pub enable_tun_mode: Option<bool>,
/// windows service mode /// windows service mode
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub enable_service_mode: Option<bool>, pub enable_service_mode: Option<bool>,
/// can the app auto startup /// can the app auto startup
pub enable_auto_launch: Option<bool>, pub enable_auto_launch: Option<bool>,
/// not show the window on launch /// not show the window on launch
pub enable_silent_start: Option<bool>, pub enable_silent_start: Option<bool>,
/// set system proxy /// set system proxy
pub enable_system_proxy: Option<bool>, pub enable_system_proxy: Option<bool>,
/// enable proxy guard /// enable proxy guard
pub enable_proxy_guard: Option<bool>, pub enable_proxy_guard: Option<bool>,
/// set system proxy bypass /// set system proxy bypass
pub system_proxy_bypass: Option<String>, pub system_proxy_bypass: Option<String>,
/// proxy guard duration /// proxy guard duration
pub proxy_guard_duration: Option<u64>, pub proxy_guard_duration: Option<u64>,
/// theme setting /// theme setting
pub theme_setting: Option<VergeTheme>, pub theme_setting: Option<VergeTheme>,
/// web ui list /// web ui list
pub web_ui_list: Option<Vec<String>>, pub web_ui_list: Option<Vec<String>>,
/// clash core path /// clash core path
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub clash_core: Option<String>, pub clash_core: Option<String>,
/// hotkey map /// hotkey map
/// format: {func},{key} /// format: {func},{key}
pub hotkeys: Option<Vec<String>>, pub hotkeys: Option<Vec<String>>,
/// 切换代理时自动关闭连接 /// 切换代理时自动关闭连接
pub auto_close_connection: Option<bool>, pub auto_close_connection: Option<bool>,
/// 默认的延迟测试连接 /// 默认的延迟测试连接
pub default_latency_test: Option<String>, pub default_latency_test: Option<String>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct VergeTheme { pub struct VergeTheme {
pub primary_color: Option<String>, pub primary_color: Option<String>,
pub secondary_color: Option<String>, pub secondary_color: Option<String>,
pub primary_text: Option<String>, pub primary_text: Option<String>,
pub secondary_text: Option<String>, pub secondary_text: Option<String>,
pub info_color: Option<String>, pub info_color: Option<String>,
pub error_color: Option<String>, pub error_color: Option<String>,
pub warning_color: Option<String>, pub warning_color: Option<String>,
pub success_color: Option<String>, pub success_color: Option<String>,
pub font_family: Option<String>, pub font_family: Option<String>,
pub css_injection: Option<String>, pub css_injection: Option<String>,
} }
impl Verge { impl Verge {
pub fn new() -> Self { pub fn new() -> Self {
config::read_yaml::<Verge>(dirs::verge_path()) config::read_yaml::<Verge>(dirs::verge_path())
}
/// Save Verge App Config
pub fn save_file(&self) -> Result<()> {
config::save_yaml(
dirs::verge_path(),
self,
Some("# The Config for Clash Verge App\n\n"),
)
}
/// patch verge config
/// only save to file
pub fn patch_config(&mut self, patch: Verge) -> Result<()> {
macro_rules! patch {
($key: tt) => {
if patch.$key.is_some() {
self.$key = patch.$key;
}
};
} }
patch!(language); /// Save Verge App Config
patch!(theme_mode); pub fn save_file(&self) -> Result<()> {
patch!(theme_blur); config::save_yaml(
patch!(traffic_graph); dirs::verge_path(),
self,
Some("# The Config for Clash Verge App\n\n"),
)
}
patch!(enable_tun_mode); /// patch verge config
patch!(enable_service_mode); /// only save to file
patch!(enable_auto_launch); pub fn patch_config(&mut self, patch: Verge) -> Result<()> {
patch!(enable_silent_start); macro_rules! patch {
patch!(enable_system_proxy); ($key: tt) => {
patch!(enable_proxy_guard); if patch.$key.is_some() {
patch!(system_proxy_bypass); self.$key = patch.$key;
patch!(proxy_guard_duration); }
};
}
patch!(theme_setting); patch!(language);
patch!(web_ui_list); patch!(theme_mode);
patch!(clash_core); patch!(theme_blur);
patch!(hotkeys); patch!(traffic_graph);
patch!(auto_close_connection); patch!(enable_tun_mode);
patch!(default_latency_test); patch!(enable_service_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_system_proxy);
patch!(enable_proxy_guard);
patch!(system_proxy_bypass);
patch!(proxy_guard_duration);
self.save_file() patch!(theme_setting);
} patch!(web_ui_list);
patch!(clash_core);
patch!(hotkeys);
patch!(auto_close_connection);
patch!(default_latency_test);
self.save_file()
}
} }

View File

@ -4,99 +4,99 @@ use crate::log_if_err;
// 重启clash // 重启clash
pub fn restart_clash_core() { pub fn restart_clash_core() {
let core = Core::global(); let core = Core::global();
let mut service = core.service.lock(); let mut service = core.service.lock();
log_if_err!(service.restart()); log_if_err!(service.restart());
drop(service); drop(service);
log_if_err!(core.activate()); log_if_err!(core.activate());
} }
// 切换模式 // 切换模式
pub fn change_clash_mode(mode: &str) { pub fn change_clash_mode(mode: &str) {
let core = Core::global(); let core = Core::global();
log_if_err!(core.update_mode(mode)); log_if_err!(core.update_mode(mode));
} }
// 切换系统代理 // 切换系统代理
pub fn toggle_system_proxy() { pub fn toggle_system_proxy() {
let core = Core::global(); let core = Core::global();
let data = Data::global(); let data = Data::global();
let verge = data.verge.lock(); let verge = data.verge.lock();
let enable = !verge.enable_system_proxy.clone().unwrap_or(false); let enable = !verge.enable_system_proxy.clone().unwrap_or(false);
drop(verge); drop(verge);
log_if_err!(core.patch_verge(Verge { log_if_err!(core.patch_verge(Verge {
enable_system_proxy: Some(enable), enable_system_proxy: Some(enable),
..Verge::default() ..Verge::default()
})); }));
let handle = core.handle.lock(); let handle = core.handle.lock();
let _ = handle.refresh_verge(); let _ = handle.refresh_verge();
} }
// 打开系统代理 // 打开系统代理
pub fn enable_system_proxy() { pub fn enable_system_proxy() {
let core = Core::global(); let core = Core::global();
log_if_err!(core.patch_verge(Verge { log_if_err!(core.patch_verge(Verge {
enable_system_proxy: Some(true), enable_system_proxy: Some(true),
..Verge::default() ..Verge::default()
})); }));
let handle = core.handle.lock(); let handle = core.handle.lock();
let _ = handle.refresh_verge(); let _ = handle.refresh_verge();
} }
// 关闭系统代理 // 关闭系统代理
pub fn disable_system_proxy() { pub fn disable_system_proxy() {
let core = Core::global(); let core = Core::global();
log_if_err!(core.patch_verge(Verge { log_if_err!(core.patch_verge(Verge {
enable_system_proxy: Some(false), enable_system_proxy: Some(false),
..Verge::default() ..Verge::default()
})); }));
let handle = core.handle.lock(); let handle = core.handle.lock();
let _ = handle.refresh_verge(); let _ = handle.refresh_verge();
} }
// 切换tun模式 // 切换tun模式
pub fn toggle_tun_mode() { pub fn toggle_tun_mode() {
let core = Core::global(); let core = Core::global();
let data = Data::global(); let data = Data::global();
let verge = data.verge.lock(); let verge = data.verge.lock();
let enable = !verge.enable_tun_mode.clone().unwrap_or(false); let enable = !verge.enable_tun_mode.clone().unwrap_or(false);
drop(verge); drop(verge);
log_if_err!(core.patch_verge(Verge { log_if_err!(core.patch_verge(Verge {
enable_tun_mode: Some(enable), enable_tun_mode: Some(enable),
..Verge::default() ..Verge::default()
})); }));
let handle = core.handle.lock(); let handle = core.handle.lock();
let _ = handle.refresh_verge(); let _ = handle.refresh_verge();
} }
// 打开tun模式 // 打开tun模式
pub fn enable_tun_mode() { pub fn enable_tun_mode() {
let core = Core::global(); let core = Core::global();
log_if_err!(core.patch_verge(Verge { log_if_err!(core.patch_verge(Verge {
enable_tun_mode: Some(true), enable_tun_mode: Some(true),
..Verge::default() ..Verge::default()
})); }));
let handle = core.handle.lock(); let handle = core.handle.lock();
let _ = handle.refresh_verge(); let _ = handle.refresh_verge();
} }
// 关闭tun模式 // 关闭tun模式
pub fn disable_tun_mode() { pub fn disable_tun_mode() {
let core = Core::global(); let core = Core::global();
log_if_err!(core.patch_verge(Verge { log_if_err!(core.patch_verge(Verge {
enable_tun_mode: Some(false), enable_tun_mode: Some(false),
..Verge::default() ..Verge::default()
})); }));
let handle = core.handle.lock(); let handle = core.handle.lock();
let _ = handle.refresh_verge(); let _ = handle.refresh_verge();
} }

View File

@ -1,6 +1,6 @@
#![cfg_attr( #![cfg_attr(
all(not(debug_assertions), target_os = "windows"), all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
mod cmds; mod cmds;
@ -14,129 +14,129 @@ use crate::utils::{init, resolve, server};
use tauri::{api, Manager, SystemTray}; use tauri::{api, Manager, SystemTray};
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
// 单例检测 // 单例检测
if server::check_singleton().is_err() { if server::check_singleton().is_err() {
println!("app exists"); println!("app exists");
return Ok(()); return Ok(());
}
#[cfg(target_os = "windows")]
unsafe {
use crate::utils::dirs;
dirs::init_portable_flag();
}
crate::log_if_err!(init::init_config());
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.setup(|app| Ok(resolve::resolve_setup(app)))
.system_tray(SystemTray::new())
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
.invoke_handler(tauri::generate_handler![
// common
cmds::get_sys_proxy,
cmds::open_app_dir,
cmds::open_logs_dir,
cmds::open_web_url,
cmds::kill_sidecar,
cmds::restart_sidecar,
// clash
cmds::get_clash_info,
cmds::get_clash_logs,
cmds::patch_clash_config,
cmds::change_clash_core,
cmds::get_runtime_config,
cmds::get_runtime_yaml,
cmds::get_runtime_exists,
cmds::get_runtime_logs,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::update_hotkeys,
// profile
cmds::view_profile,
cmds::patch_profile,
cmds::create_profile,
cmds::import_profile,
cmds::update_profile,
cmds::delete_profile,
cmds::select_profile,
cmds::get_profiles,
cmds::enhance_profiles,
cmds::change_profile_chain,
cmds::change_profile_valid,
cmds::read_profile_file,
cmds::save_profile_file,
// service mode
cmds::service::start_service,
cmds::service::stop_service,
cmds::service::check_service,
cmds::service::install_service,
cmds::service::uninstall_service,
]);
#[cfg(target_os = "macos")]
{
use tauri::{Menu, MenuItem, Submenu};
builder = builder.menu(
Menu::new().add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::CloseWindow)
.add_native_item(MenuItem::Quit)
)),
);
}
#[allow(unused_mut)]
let mut app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let app_handle = app.app_handle();
ctrlc::set_handler(move || {
resolve::resolve_reset();
app_handle.exit(0);
})
.expect("error while exiting.");
#[allow(unused)]
app.run(|app_handle, e| match e {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
} }
tauri::RunEvent::Exit => {
resolve::resolve_reset(); #[cfg(target_os = "windows")]
api::process::kill_children(); unsafe {
app_handle.exit(0); use crate::utils::dirs;
dirs::init_portable_flag();
} }
crate::log_if_err!(init::init_config());
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.setup(|app| Ok(resolve::resolve_setup(app)))
.system_tray(SystemTray::new())
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
.invoke_handler(tauri::generate_handler![
// common
cmds::get_sys_proxy,
cmds::open_app_dir,
cmds::open_logs_dir,
cmds::open_web_url,
cmds::kill_sidecar,
cmds::restart_sidecar,
// clash
cmds::get_clash_info,
cmds::get_clash_logs,
cmds::patch_clash_config,
cmds::change_clash_core,
cmds::get_runtime_config,
cmds::get_runtime_yaml,
cmds::get_runtime_exists,
cmds::get_runtime_logs,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::update_hotkeys,
// profile
cmds::view_profile,
cmds::patch_profile,
cmds::create_profile,
cmds::import_profile,
cmds::update_profile,
cmds::delete_profile,
cmds::select_profile,
cmds::get_profiles,
cmds::enhance_profiles,
cmds::change_profile_chain,
cmds::change_profile_valid,
cmds::read_profile_file,
cmds::save_profile_file,
// service mode
cmds::service::start_service,
cmds::service::stop_service,
cmds::service::check_service,
cmds::service::install_service,
cmds::service::uninstall_service,
]);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
tauri::RunEvent::WindowEvent { label, event, .. } => { {
if label == "main" { use tauri::{Menu, MenuItem, Submenu};
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
app_handle.get_window("main").map(|win| {
let _ = win.hide();
});
}
_ => {}
}
}
}
_ => {}
});
Ok(()) builder = builder.menu(
Menu::new().add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::CloseWindow)
.add_native_item(MenuItem::Quit),
)),
);
}
#[allow(unused_mut)]
let mut app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let app_handle = app.app_handle();
ctrlc::set_handler(move || {
resolve::resolve_reset();
app_handle.exit(0);
})
.expect("error while exiting.");
#[allow(unused)]
app.run(|app_handle, e| match e {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
tauri::RunEvent::Exit => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
#[cfg(target_os = "macos")]
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
app_handle.get_window("main").map(|win| {
let _ = win.hide();
});
}
_ => {}
}
}
}
_ => {}
});
Ok(())
} }

View File

@ -5,55 +5,55 @@ use std::{fs, path::PathBuf};
/// read data from yaml as struct T /// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T { pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
if !path.exists() { if !path.exists() {
log::error!(target: "app", "file not found \"{}\"", path.display()); log::error!(target: "app", "file not found \"{}\"", path.display());
return T::default(); return T::default();
} }
let yaml_str = fs::read_to_string(&path).unwrap_or("".into()); let yaml_str = fs::read_to_string(&path).unwrap_or("".into());
match serde_yaml::from_str::<T>(&yaml_str) { match serde_yaml::from_str::<T>(&yaml_str) {
Ok(val) => val, Ok(val) => val,
Err(_) => { Err(_) => {
log::error!(target: "app", "failed to read yaml file \"{}\"", path.display()); log::error!(target: "app", "failed to read yaml file \"{}\"", path.display());
T::default() T::default()
}
} }
}
} }
/// read mapping from yaml fix #165 /// read mapping from yaml fix #165
pub fn read_merge_mapping(path: PathBuf) -> Mapping { pub fn read_merge_mapping(path: PathBuf) -> Mapping {
let map = Mapping::new(); let map = Mapping::new();
if !path.exists() { if !path.exists() {
log::error!(target: "app", "file not found \"{}\"", path.display()); log::error!(target: "app", "file not found \"{}\"", path.display());
return map; return map;
}
let yaml_str = fs::read_to_string(&path).unwrap_or("".into());
match serde_yaml::from_str::<Value>(&yaml_str) {
Ok(mut val) => {
crate::log_if_err!(val.apply_merge());
val.as_mapping().unwrap_or(&map).to_owned()
} }
Err(_) => {
log::error!(target: "app", "failed to read yaml file \"{}\"", path.display()); let yaml_str = fs::read_to_string(&path).unwrap_or("".into());
map
match serde_yaml::from_str::<Value>(&yaml_str) {
Ok(mut val) => {
crate::log_if_err!(val.apply_merge());
val.as_mapping().unwrap_or(&map).to_owned()
}
Err(_) => {
log::error!(target: "app", "failed to read yaml file \"{}\"", path.display());
map
}
} }
}
} }
/// save the data to the file /// save the data to the file
/// can set `prefix` string to add some comments /// can set `prefix` string to add some comments
pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
let data_str = serde_yaml::to_string(data)?; let data_str = serde_yaml::to_string(data)?;
let yaml_str = match prefix { let yaml_str = match prefix {
Some(prefix) => format!("{prefix}{data_str}"), Some(prefix) => format!("{prefix}{data_str}"),
None => data_str, None => data_str,
}; };
let path_str = path.as_os_str().to_string_lossy().to_string(); let path_str = path.as_os_str().to_string_lossy().to_string();
fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\"")) fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
} }

View File

@ -1,8 +1,8 @@
use std::env::temp_dir; use std::env::temp_dir;
use std::path::PathBuf; use std::path::PathBuf;
use tauri::{ use tauri::{
api::path::{home_dir, resource_dir}, api::path::{home_dir, resource_dir},
Env, PackageInfo, Env, PackageInfo,
}; };
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
@ -26,89 +26,89 @@ pub static mut APP_VERSION: &str = "v1.1.1";
/// initialize portable flag /// initialize portable flag
#[allow(unused)] #[allow(unused)]
pub unsafe fn init_portable_flag() { pub unsafe fn init_portable_flag() {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
use tauri::utils::platform::current_exe; use tauri::utils::platform::current_exe;
let exe = current_exe().unwrap(); let exe = current_exe().unwrap();
let dir = exe.parent().unwrap(); let dir = exe.parent().unwrap();
let dir = PathBuf::from(dir).join(".config/PORTABLE"); let dir = PathBuf::from(dir).join(".config/PORTABLE");
if dir.exists() { if dir.exists() {
PORTABLE_FLAG = true; PORTABLE_FLAG = true;
}
} }
}
} }
/// get the verge app home dir /// get the verge app home dir
pub fn app_home_dir() -> PathBuf { pub fn app_home_dir() -> PathBuf {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
unsafe { unsafe {
use tauri::utils::platform::current_exe; use tauri::utils::platform::current_exe;
if !PORTABLE_FLAG { if !PORTABLE_FLAG {
home_dir().unwrap().join(".config").join(APP_DIR) home_dir().unwrap().join(".config").join(APP_DIR)
} else { } else {
let app_exe = current_exe().unwrap(); let app_exe = current_exe().unwrap();
let app_exe = dunce::canonicalize(app_exe).unwrap(); let app_exe = dunce::canonicalize(app_exe).unwrap();
let app_dir = app_exe.parent().unwrap(); let app_dir = app_exe.parent().unwrap();
PathBuf::from(app_dir).join(".config").join(APP_DIR) PathBuf::from(app_dir).join(".config").join(APP_DIR)
}
} }
}
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
home_dir().unwrap().join(".config").join(APP_DIR) home_dir().unwrap().join(".config").join(APP_DIR)
} }
/// get the resources dir /// get the resources dir
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf { pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
let res_dir = resource_dir(package_info, &Env::default()) let res_dir = resource_dir(package_info, &Env::default())
.unwrap() .unwrap()
.join("resources"); .join("resources");
unsafe { unsafe {
RESOURCE_DIR = Some(res_dir.clone()); RESOURCE_DIR = Some(res_dir.clone());
let ver = package_info.version.to_string(); let ver = package_info.version.to_string();
let ver_str = format!("v{ver}"); let ver_str = format!("v{ver}");
APP_VERSION = Box::leak(Box::new(ver_str)); APP_VERSION = Box::leak(Box::new(ver_str));
} }
res_dir res_dir
} }
/// profiles dir /// profiles dir
pub fn app_profiles_dir() -> PathBuf { pub fn app_profiles_dir() -> PathBuf {
app_home_dir().join("profiles") app_home_dir().join("profiles")
} }
/// logs dir /// logs dir
pub fn app_logs_dir() -> PathBuf { pub fn app_logs_dir() -> PathBuf {
app_home_dir().join("logs") app_home_dir().join("logs")
} }
pub fn clash_path() -> PathBuf { pub fn clash_path() -> PathBuf {
app_home_dir().join(CLASH_CONFIG) app_home_dir().join(CLASH_CONFIG)
} }
pub fn verge_path() -> PathBuf { pub fn verge_path() -> PathBuf {
app_home_dir().join(VERGE_CONFIG) app_home_dir().join(VERGE_CONFIG)
} }
pub fn profiles_path() -> PathBuf { pub fn profiles_path() -> PathBuf {
app_home_dir().join(PROFILE_YAML) app_home_dir().join(PROFILE_YAML)
} }
pub fn profiles_temp_path() -> PathBuf { pub fn profiles_temp_path() -> PathBuf {
#[cfg(not(feature = "debug-yml"))] #[cfg(not(feature = "debug-yml"))]
return temp_dir().join(PROFILE_TEMP); return temp_dir().join(PROFILE_TEMP);
#[cfg(feature = "debug-yml")] #[cfg(feature = "debug-yml")]
return app_home_dir().join(PROFILE_TEMP); return app_home_dir().join(PROFILE_TEMP);
} }
pub fn clash_pid_path() -> PathBuf { pub fn clash_pid_path() -> PathBuf {
unsafe { RESOURCE_DIR.clone().unwrap().join("clash.pid") } unsafe { RESOURCE_DIR.clone().unwrap().join("clash.pid") }
} }
#[cfg(windows)] #[cfg(windows)]
@ -116,23 +116,23 @@ static SERVICE_PATH: &str = "clash-verge-service.exe";
#[cfg(windows)] #[cfg(windows)]
pub fn service_path() -> PathBuf { pub fn service_path() -> PathBuf {
unsafe { unsafe {
let res_dir = RESOURCE_DIR.clone().unwrap(); let res_dir = RESOURCE_DIR.clone().unwrap();
res_dir.join(SERVICE_PATH) res_dir.join(SERVICE_PATH)
} }
} }
#[cfg(windows)] #[cfg(windows)]
pub fn service_log_file() -> PathBuf { pub fn service_log_file() -> PathBuf {
use chrono::Local; use chrono::Local;
let log_dir = app_logs_dir().join("service"); let log_dir = app_logs_dir().join("service");
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string(); let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
let log_file = format!("{}.log", local_time); let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file); let log_file = log_dir.join(log_file);
std::fs::create_dir_all(&log_dir).unwrap(); std::fs::create_dir_all(&log_dir).unwrap();
log_file log_file
} }

View File

@ -6,67 +6,67 @@ use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
pub fn get_now() -> usize { pub fn get_now() -> usize {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs() as _ .as_secs() as _
} }
const ALPHABET: [char; 62] = [ const ALPHABET: [char; 62] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z', 'V', 'W', 'X', 'Y', 'Z',
]; ];
/// generate the uid /// generate the uid
pub fn get_uid(prefix: &str) -> String { pub fn get_uid(prefix: &str) -> String {
let id = nanoid!(11, &ALPHABET); let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}") format!("{prefix}{id}")
} }
/// parse the string /// parse the string
/// xxx=123123; => 123123 /// xxx=123123; => 123123
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> { pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
target.find(key).and_then(|idx| { target.find(key).and_then(|idx| {
let idx = idx + key.len(); let idx = idx + key.len();
let value = &target[idx..]; let value = &target[idx..];
match value.split(';').nth(0) { match value.split(';').nth(0) {
Some(value) => value.trim().parse(), Some(value) => value.trim().parse(),
None => value.trim().parse(), None => value.trim().parse(),
} }
.ok() .ok()
}) })
} }
/// open file /// open file
/// use vscode by default /// use vscode by default
pub fn open_file(path: PathBuf) -> Result<()> { pub fn open_file(path: PathBuf) -> Result<()> {
// use vscode first // use vscode first
if let Ok(code) = which::which("code") { if let Ok(code) = which::which("code") {
let mut command = Command::new(&code); let mut command = Command::new(&code);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
if let Err(err) = command.creation_flags(0x08000000).arg(&path).spawn() { if let Err(err) = command.creation_flags(0x08000000).arg(&path).spawn() {
log::error!(target: "app", "failed to open with VScode `{err}`"); log::error!(target: "app", "failed to open with VScode `{err}`");
open::that(path)?; open::that(path)?;
} }
}
#[cfg(not(target_os = "windows"))]
if let Err(err) = command.arg(&path).spawn() {
log::error!(target: "app", "failed to open with VScode `{err}`");
open::that(path)?;
}
return Ok(());
} }
#[cfg(not(target_os = "windows"))] open::that(path)?;
if let Err(err) = command.arg(&path).spawn() { Ok(())
log::error!(target: "app", "failed to open with VScode `{err}`");
open::that(path)?;
}
return Ok(());
}
open::that(path)?;
Ok(())
} }
#[macro_export] #[macro_export]
@ -96,27 +96,27 @@ macro_rules! wrap_err {
/// return the string literal error /// return the string literal error
#[macro_export] #[macro_export]
macro_rules! ret_err { macro_rules! ret_err {
($str: expr) => { ($str: expr) => {
return Err($str.into()) return Err($str.into())
}; };
} }
#[test] #[test]
fn test_parse_value() { fn test_parse_value() {
let test_1 = "upload=111; download=2222; total=3333; expire=444"; let test_1 = "upload=111; download=2222; total=3333; expire=444";
let test_2 = "attachment; filename=Clash.yaml"; let test_2 = "attachment; filename=Clash.yaml";
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111); assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222); assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333); assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444); assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
assert_eq!( assert_eq!(
parse_str::<String>(test_2, "filename=").unwrap(), parse_str::<String>(test_2, "filename=").unwrap(),
format!("Clash.yaml") format!("Clash.yaml")
); );
assert_eq!(parse_str::<usize>(test_1, "aaa="), None); assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
assert_eq!(parse_str::<usize>(test_1, "upload1="), None); assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
assert_eq!(parse_str::<usize>(test_1, "expire1="), None); assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
assert_eq!(parse_str::<usize>(test_2, "attachment="), None); assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
} }

View File

@ -12,94 +12,94 @@ use tauri::PackageInfo;
/// initialize this instance's log file /// initialize this instance's log file
fn init_log() -> Result<()> { fn init_log() -> Result<()> {
let log_dir = dirs::app_logs_dir(); let log_dir = dirs::app_logs_dir();
if !log_dir.exists() { if !log_dir.exists() {
let _ = fs::create_dir_all(&log_dir); let _ = fs::create_dir_all(&log_dir);
} }
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string(); let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
let log_file = format!("{}.log", local_time); let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file); let log_file = log_dir.join(log_file);
let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}"; let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}";
let stdout = ConsoleAppender::builder() let stdout = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new(time_format))) .encoder(Box::new(PatternEncoder::new(time_format)))
.build(); .build();
let tofile = FileAppender::builder() let tofile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(time_format))) .encoder(Box::new(PatternEncoder::new(time_format)))
.build(log_file)?; .build(log_file)?;
let config = Config::builder() let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout))) .appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile))) .appender(Appender::builder().build("file", Box::new(tofile)))
.logger( .logger(
Logger::builder() Logger::builder()
.appenders(["file", "stdout"]) .appenders(["file", "stdout"])
.additive(false) .additive(false)
.build("app", LevelFilter::Info), .build("app", LevelFilter::Info),
) )
.build(Root::builder().appender("stdout").build(LevelFilter::Info))?; .build(Root::builder().appender("stdout").build(LevelFilter::Info))?;
log4rs::init_config(config)?; log4rs::init_config(config)?;
Ok(()) Ok(())
} }
/// Initialize all the files from resources /// Initialize all the files from resources
pub fn init_config() -> Result<()> { pub fn init_config() -> Result<()> {
let _ = init_log(); let _ = init_log();
let app_dir = dirs::app_home_dir(); let app_dir = dirs::app_home_dir();
let profiles_dir = dirs::app_profiles_dir(); let profiles_dir = dirs::app_profiles_dir();
if !app_dir.exists() { if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir); let _ = fs::create_dir_all(&app_dir);
} }
if !profiles_dir.exists() { if !profiles_dir.exists() {
let _ = fs::create_dir_all(&profiles_dir); let _ = fs::create_dir_all(&profiles_dir);
} }
// target path // target path
let clash_path = app_dir.join("config.yaml"); let clash_path = app_dir.join("config.yaml");
let verge_path = app_dir.join("verge.yaml"); let verge_path = app_dir.join("verge.yaml");
let profile_path = app_dir.join("profiles.yaml"); let profile_path = app_dir.join("profiles.yaml");
if !clash_path.exists() { if !clash_path.exists() {
fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?; fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?;
} }
if !verge_path.exists() { if !verge_path.exists() {
fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?; fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?;
} }
if !profile_path.exists() { if !profile_path.exists() {
fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?; fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?;
} }
Ok(()) Ok(())
} }
/// initialize app /// initialize app
pub fn init_resources(package_info: &PackageInfo) { pub fn init_resources(package_info: &PackageInfo) {
// create app dir // create app dir
let app_dir = dirs::app_home_dir(); let app_dir = dirs::app_home_dir();
let res_dir = dirs::app_resources_dir(package_info); let res_dir = dirs::app_resources_dir(package_info);
if !app_dir.exists() { if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir); let _ = fs::create_dir_all(&app_dir);
} }
// copy the resource file // copy the resource file
let mmdb_path = app_dir.join("Country.mmdb"); let mmdb_path = app_dir.join("Country.mmdb");
let mmdb_tmpl = res_dir.join("Country.mmdb"); let mmdb_tmpl = res_dir.join("Country.mmdb");
if !mmdb_path.exists() && mmdb_tmpl.exists() { if !mmdb_path.exists() && mmdb_tmpl.exists() {
let _ = fs::copy(mmdb_tmpl, mmdb_path); let _ = fs::copy(mmdb_tmpl, mmdb_path);
} }
// copy the wintun.dll // copy the wintun.dll
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
let wintun_path = app_dir.join("wintun.dll"); let wintun_path = app_dir.join("wintun.dll");
let wintun_tmpl = res_dir.join("wintun.dll"); let wintun_tmpl = res_dir.join("wintun.dll");
if !wintun_path.exists() && wintun_tmpl.exists() { if !wintun_path.exists() && wintun_tmpl.exists() {
let _ = fs::copy(wintun_tmpl, wintun_path); let _ = fs::copy(wintun_tmpl, wintun_path);
}
} }
}
} }

View File

@ -1,110 +1,110 @@
use crate::{ use crate::{
core::{tray, Core}, core::{tray, Core},
data::Data, data::Data,
utils::init, utils::init,
utils::server, utils::server,
}; };
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
/// handle something when start app /// handle something when start app
pub fn resolve_setup(app: &App) { pub fn resolve_setup(app: &App) {
let _ = app let _ = app
.tray_handle() .tray_handle()
.set_menu(tray::Tray::tray_menu(&app.app_handle())); .set_menu(tray::Tray::tray_menu(&app.app_handle()));
init::init_resources(app.package_info()); init::init_resources(app.package_info());
let silent_start = { let silent_start = {
let global = Data::global(); let global = Data::global();
let verge = global.verge.lock(); let verge = global.verge.lock();
let singleton = verge.app_singleton_port.clone(); let singleton = verge.app_singleton_port.clone();
// setup a simple http server for singleton // setup a simple http server for singleton
server::embed_server(&app.app_handle(), singleton); server::embed_server(&app.app_handle(), singleton);
verge.enable_silent_start.clone().unwrap_or(false) verge.enable_silent_start.clone().unwrap_or(false)
}; };
// core should be initialized after init_app fix #122 // core should be initialized after init_app fix #122
let core = Core::global(); let core = Core::global();
core.init(app.app_handle()); core.init(app.app_handle());
if !silent_start { if !silent_start {
create_window(&app.app_handle()); create_window(&app.app_handle());
} }
} }
/// reset system proxy /// reset system proxy
pub fn resolve_reset() { pub fn resolve_reset() {
let core = Core::global(); let core = Core::global();
let mut sysopt = core.sysopt.lock(); let mut sysopt = core.sysopt.lock();
crate::log_if_err!(sysopt.reset_sysproxy()); crate::log_if_err!(sysopt.reset_sysproxy());
drop(sysopt); drop(sysopt);
let mut service = core.service.lock(); let mut service = core.service.lock();
crate::log_if_err!(service.stop()); crate::log_if_err!(service.stop());
} }
/// create main window /// create main window
pub fn create_window(app_handle: &AppHandle) { pub fn create_window(app_handle: &AppHandle) {
if let Some(window) = app_handle.get_window("main") { if let Some(window) = app_handle.get_window("main") {
let _ = window.unminimize(); let _ = window.unminimize();
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
return; return;
}
let builder = tauri::window::WindowBuilder::new(
app_handle,
"main".to_string(),
tauri::WindowUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.fullscreen(false)
.min_inner_size(600.0, 520.0);
#[cfg(target_os = "windows")]
{
use crate::utils::winhelp;
use std::time::Duration;
use tokio::time::sleep;
use window_shadows::set_shadow;
use window_vibrancy::apply_blur;
match builder
.decorations(false)
.transparent(true)
.inner_size(800.0, 636.0)
.build()
{
Ok(_) => {
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
sleep(Duration::from_secs(1)).await;
if let Some(window) = app_handle.get_window("main") {
let _ = window.show();
let _ = set_shadow(&window, true);
if !winhelp::is_win11() {
let _ = apply_blur(&window, None);
}
}
});
}
Err(err) => log::error!(target: "app", "{err}"),
} }
}
#[cfg(target_os = "macos")] let builder = tauri::window::WindowBuilder::new(
crate::log_if_err!(builder.decorations(true).inner_size(800.0, 642.0).build()); app_handle,
"main".to_string(),
tauri::WindowUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.fullscreen(false)
.min_inner_size(600.0, 520.0);
#[cfg(target_os = "linux")] #[cfg(target_os = "windows")]
crate::log_if_err!(builder {
.decorations(false) use crate::utils::winhelp;
.transparent(true) use std::time::Duration;
.inner_size(800.0, 636.0) use tokio::time::sleep;
.build()); use window_shadows::set_shadow;
use window_vibrancy::apply_blur;
match builder
.decorations(false)
.transparent(true)
.inner_size(800.0, 636.0)
.build()
{
Ok(_) => {
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
sleep(Duration::from_secs(1)).await;
if let Some(window) = app_handle.get_window("main") {
let _ = window.show();
let _ = set_shadow(&window, true);
if !winhelp::is_win11() {
let _ = apply_blur(&window, None);
}
}
});
}
Err(err) => log::error!(target: "app", "{err}"),
}
}
#[cfg(target_os = "macos")]
crate::log_if_err!(builder.decorations(true).inner_size(800.0, 642.0).build());
#[cfg(target_os = "linux")]
crate::log_if_err!(builder
.decorations(false)
.transparent(true)
.inner_size(800.0, 636.0)
.build());
} }

View File

@ -13,32 +13,32 @@ const SERVER_PORT: u16 = 11233;
/// check whether there is already exists /// check whether there is already exists
pub fn check_singleton() -> Result<(), ()> { pub fn check_singleton() -> Result<(), ()> {
let verge = Verge::new(); let verge = Verge::new();
let port = verge.app_singleton_port.unwrap_or(SERVER_PORT); let port = verge.app_singleton_port.unwrap_or(SERVER_PORT);
if !local_port_available(port) { if !local_port_available(port) {
tauri::async_runtime::block_on(async { tauri::async_runtime::block_on(async {
let url = format!("http://127.0.0.1:{}/commands/visible", port); let url = format!("http://127.0.0.1:{}/commands/visible", port);
reqwest::get(url).await.unwrap(); reqwest::get(url).await.unwrap();
Err(()) Err(())
}) })
} else { } else {
Ok(()) Ok(())
} }
} }
/// The embed server only be used to implement singleton process /// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later /// maybe it can be used as pac server later
pub fn embed_server(app_handle: &AppHandle, port: Option<u16>) { pub fn embed_server(app_handle: &AppHandle, port: Option<u16>) {
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
let port = port.unwrap_or(SERVER_PORT); let port = port.unwrap_or(SERVER_PORT);
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let commands = warp::path!("commands" / "visible").map(move || { let commands = warp::path!("commands" / "visible").map(move || {
resolve::create_window(&app_handle); resolve::create_window(&app_handle);
return format!("ok"); return format!("ok");
});
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
}); });
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
});
} }

View File

@ -1,69 +1,69 @@
#![cfg(target_os = "windows")] #![cfg(target_os = "windows")]
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![allow(non_camel_case_types)] #![allow(non_camel_case_types)]
//! //!
//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs //! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs
//! //!
use windows_sys::Win32::{ use windows_sys::Win32::{
Foundation::*, Foundation::*,
System::{LibraryLoader::*, SystemInformation::*}, System::{LibraryLoader::*, SystemInformation::*},
}; };
fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> { fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {
assert_eq!(library.chars().last(), Some('\0')); assert_eq!(library.chars().last(), Some('\0'));
assert_eq!(function.chars().last(), Some('\0')); assert_eq!(function.chars().last(), Some('\0'));
let module = unsafe { LoadLibraryA(library.as_ptr()) }; let module = unsafe { LoadLibraryA(library.as_ptr()) };
if module == 0 { if module == 0 {
return None; return None;
} }
Some(unsafe { GetProcAddress(module, function.as_ptr()) }) Some(unsafe { GetProcAddress(module, function.as_ptr()) })
} }
macro_rules! get_function { macro_rules! get_function {
($lib:expr, $func:ident) => { ($lib:expr, $func:ident) => {
get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe { get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe {
std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f) std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f)
}) })
}; };
} }
/// Returns a tuple of (major, minor, buildnumber) /// Returns a tuple of (major, minor, buildnumber)
fn get_windows_ver() -> Option<(u32, u32, u32)> { fn get_windows_ver() -> Option<(u32, u32, u32)> {
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32; type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32;
let handle = get_function!("ntdll.dll", RtlGetVersion); let handle = get_function!("ntdll.dll", RtlGetVersion);
if let Some(rtl_get_version) = handle { if let Some(rtl_get_version) = handle {
unsafe { unsafe {
let mut vi = OSVERSIONINFOW { let mut vi = OSVERSIONINFOW {
dwOSVersionInfoSize: 0, dwOSVersionInfoSize: 0,
dwMajorVersion: 0, dwMajorVersion: 0,
dwMinorVersion: 0, dwMinorVersion: 0,
dwBuildNumber: 0, dwBuildNumber: 0,
dwPlatformId: 0, dwPlatformId: 0,
szCSDVersion: [0; 128], szCSDVersion: [0; 128],
}; };
let status = (rtl_get_version)(&mut vi as _); let status = (rtl_get_version)(&mut vi as _);
if status >= 0 { if status >= 0 {
Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber)) Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber))
} else { } else {
None None
} }
} }
} else { } else {
None None
} }
} }
pub fn is_win11() -> bool { pub fn is_win11() -> bool {
let v = get_windows_ver().unwrap_or_default(); let v = get_windows_ver().unwrap_or_default();
v.2 >= 22000 v.2 >= 22000
} }
#[test] #[test]
fn test_version() { fn test_version() {
dbg!(get_windows_ver().unwrap_or_default()); dbg!(get_windows_ver().unwrap_or_default());
} }

View File

@ -25,13 +25,8 @@
"icons/icon-new.icns", "icons/icon-new.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": [ "resources": ["resources"],
"resources" "externalBin": ["sidecar/clash", "sidecar/clash-meta"],
],
"externalBin": [
"sidecar/clash",
"sidecar/clash-meta"
],
"copyright": "© 2022 zzzgydi All Rights Reserved", "copyright": "© 2022 zzzgydi All Rights Reserved",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "A Clash GUI based on tauri.", "shortDescription": "A Clash GUI based on tauri.",
@ -51,10 +46,7 @@
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "", "timestampUrl": "",
"wix": { "wix": {
"language": [ "language": ["zh-CN", "en-US"]
"zh-CN",
"en-US"
]
} }
} }
}, },
@ -86,4 +78,4 @@
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';" "csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
} }
} }
} }