mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-21 17:52:25 +08:00
refactor: backend (#7102)
* refactor(tray): move tray log opening to utils helper * refactor(enhance): move owned config through pipeline * refactor(enhance): move sequence values in use_seq * refactor(enhance): move values while sorting fields * refactor(enhance): move values when lowercasing owned config * refactor(backend): move owned config values in utilities * refactor(tray): borrow hotkey accelerators * refactor(core): borrow proxy config strings * fix(backend): preserve config patch semantics * refactor(enhance): clarify config transform names
This commit is contained in:
@@ -32,26 +32,6 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
open::that(url.as_str()).stringify_err()
|
||||
}
|
||||
|
||||
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
|
||||
/// 打开 Verge 最新日志
|
||||
#[tauri::command]
|
||||
pub async fn open_app_log() -> CmdResult<()> {
|
||||
let log_path = dirs::app_latest_log().stringify_err()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let log_path = crate::utils::help::snapshot_path(&log_path).stringify_err()?;
|
||||
open::that(log_path).stringify_err()
|
||||
}
|
||||
|
||||
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
|
||||
/// 打开 Clash 最新日志
|
||||
#[tauri::command]
|
||||
pub async fn open_core_log() -> CmdResult<()> {
|
||||
let log_path = dirs::clash_latest_log().stringify_err()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let log_path = crate::utils::help::snapshot_path(&log_path).stringify_err()?;
|
||||
open::that(log_path).stringify_err()
|
||||
}
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: AppHandle) {
|
||||
|
||||
@@ -7,6 +7,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
};
|
||||
@@ -267,11 +268,11 @@ impl IClashTemp {
|
||||
let val_str = val_str.trim();
|
||||
|
||||
let val = match val_str.starts_with(':') {
|
||||
true => format!("127.0.0.1{val_str}"),
|
||||
false => val_str.to_owned(),
|
||||
true => Cow::Owned(format!("127.0.0.1{val_str}")),
|
||||
false => Cow::Borrowed(val_str),
|
||||
};
|
||||
|
||||
SocketAddr::from_str(val.as_str()).ok().map(|s| s.to_string())
|
||||
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
|
||||
@@ -37,23 +37,22 @@ impl IRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
let patch_tun = patch.get("tun");
|
||||
if let Some(patch_tun_value) = patch_tun {
|
||||
let mut tun = config
|
||||
.get("tun")
|
||||
.and_then(|val| val.as_mapping())
|
||||
.cloned()
|
||||
.unwrap_or_else(Mapping::new);
|
||||
let Some(patch_tun) = patch.get("tun") else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(patch_tun_mapping) = patch_tun_value.as_mapping() {
|
||||
for key in use_keys(patch_tun_mapping) {
|
||||
if let Some(value) = patch_tun_mapping.get(key.as_str()) {
|
||||
tun.insert(Value::from(key.as_str()), value.clone());
|
||||
}
|
||||
let tun_key = Value::from("tun");
|
||||
if !matches!(config.get(&tun_key), Some(Value::Mapping(_))) {
|
||||
config.insert(tun_key.clone(), Value::Mapping(Mapping::new()));
|
||||
}
|
||||
|
||||
if let (Some(patch_tun_mapping), Some(Value::Mapping(tun))) = (patch_tun.as_mapping(), config.get_mut(&tun_key))
|
||||
{
|
||||
for key in use_keys(patch_tun_mapping) {
|
||||
if let Some(value) = patch_tun_mapping.get(key.as_str()) {
|
||||
tun.insert(Value::from(key.as_str()), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
config.insert("tun".into(), Value::from(tun));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,23 +60,16 @@ static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
async fn get_bypass() -> String {
|
||||
let use_default = Config::verge().await.latest_arc().use_default_bypass.unwrap_or(true);
|
||||
let res = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
verge.system_proxy_bypass.clone()
|
||||
};
|
||||
let custom_bypass = match res {
|
||||
Some(bypass) => bypass,
|
||||
None => "".into(),
|
||||
};
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let use_default = verge.use_default_bypass.unwrap_or(true);
|
||||
let custom_bypass = verge.system_proxy_bypass.as_deref().unwrap_or("");
|
||||
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.into()
|
||||
} else if use_default {
|
||||
format!("{DEFAULT_BYPASS},{custom_bypass}").into()
|
||||
} else {
|
||||
custom_bypass
|
||||
custom_bypass.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,18 +134,19 @@ impl Sysopt {
|
||||
None => Config::clash().await.latest_arc().get_mixed_port(),
|
||||
};
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
|
||||
verge.enable_system_proxy.unwrap_or_default(),
|
||||
verge.proxy_auto_config.unwrap_or_default(),
|
||||
verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")),
|
||||
verge.enable_proxy_guard.unwrap_or_default(),
|
||||
);
|
||||
// 先 await, 避免持有锁导致的 Send 问题
|
||||
let bypass = get_bypass().await;
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
|
||||
verge.enable_system_proxy.unwrap_or_default(),
|
||||
verge.proxy_auto_config.unwrap_or_default(),
|
||||
verge.proxy_host.as_deref().unwrap_or("127.0.0.1"),
|
||||
verge.enable_proxy_guard.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let (sys, auto, guard_type) = {
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
sys.host = proxy_host.clone().into();
|
||||
sys.host = proxy_host.into();
|
||||
sys.port = port;
|
||||
sys.bypass = bypass.into();
|
||||
auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac");
|
||||
|
||||
@@ -6,8 +6,11 @@ use crate::process::AsyncHandler;
|
||||
use crate::singleton;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode,
|
||||
utils::dirs::find_target_icons,
|
||||
Type, cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
utils::{dirs::find_target_icons, help},
|
||||
};
|
||||
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
||||
use clash_verge_logging::logging_error;
|
||||
@@ -399,7 +402,7 @@ impl Tray {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
|
||||
hotkeys
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
@@ -412,13 +415,13 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
|
||||
if key.to_uppercase().contains("NUMPAD") {
|
||||
None
|
||||
} else {
|
||||
Some((func.into(), key.into()))
|
||||
Some((func, key))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<std::collections::HashMap<String, String>>()
|
||||
.collect::<HashMap<&str, &str>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -655,7 +658,7 @@ async fn create_tray_menu(
|
||||
MenuIds::DASHBOARD,
|
||||
&texts.dashboard,
|
||||
true,
|
||||
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
||||
hotkeys.get("open_or_close_dashboard").copied(),
|
||||
)?;
|
||||
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
@@ -664,7 +667,7 @@ async fn create_tray_menu(
|
||||
&texts.rule_mode,
|
||||
true,
|
||||
current_proxy_mode == "rule",
|
||||
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
|
||||
hotkeys.get("clash_mode_rule").copied(),
|
||||
)?;
|
||||
|
||||
let global_mode = &CheckMenuItem::with_id(
|
||||
@@ -673,7 +676,7 @@ async fn create_tray_menu(
|
||||
&texts.global_mode,
|
||||
true,
|
||||
current_proxy_mode == "global",
|
||||
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
|
||||
hotkeys.get("clash_mode_global").copied(),
|
||||
)?;
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
@@ -682,7 +685,7 @@ async fn create_tray_menu(
|
||||
&texts.direct_mode,
|
||||
true,
|
||||
current_proxy_mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
hotkeys.get("clash_mode_direct").copied(),
|
||||
)?;
|
||||
|
||||
let outbound_modes = if show_outbound_modes_inline {
|
||||
@@ -730,7 +733,7 @@ async fn create_tray_menu(
|
||||
&texts.system_proxy,
|
||||
true,
|
||||
system_proxy_enabled,
|
||||
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
|
||||
hotkeys.get("toggle_system_proxy").copied(),
|
||||
)?;
|
||||
|
||||
let tun_mode = &CheckMenuItem::with_id(
|
||||
@@ -739,7 +742,7 @@ async fn create_tray_menu(
|
||||
&texts.tun_mode,
|
||||
tun_mode_available,
|
||||
tun_mode_enabled,
|
||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||
hotkeys.get("toggle_tun_mode").copied(),
|
||||
)?;
|
||||
|
||||
let close_all_connections = &MenuItem::with_id(
|
||||
@@ -756,7 +759,7 @@ async fn create_tray_menu(
|
||||
&texts.lightweight_mode,
|
||||
true,
|
||||
is_lightweight_mode,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
hotkeys.get("entry_lightweight_mode").copied(),
|
||||
)?;
|
||||
|
||||
let copy_env = &MenuItem::with_id(app_handle, MenuIds::COPY_ENV, &texts.copy_env, true, None::<&str>)?;
|
||||
@@ -811,7 +814,7 @@ async fn create_tray_menu(
|
||||
],
|
||||
)?;
|
||||
|
||||
let quit_accelerator = hotkeys.get("quit").map(|s| s.as_str());
|
||||
let quit_accelerator = hotkeys.get("quit").copied();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let quit_accelerator = quit_accelerator.or(Some("Cmd+Q"));
|
||||
@@ -953,10 +956,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
let _ = cmd::open_logs_dir().await;
|
||||
}
|
||||
MenuIds::APP_LOG => {
|
||||
let _ = cmd::open_app_log().await;
|
||||
let _ = help::open_app_latest_log();
|
||||
}
|
||||
MenuIds::CORE_LOG => {
|
||||
let _ = cmd::open_core_log().await;
|
||||
let _ = help::open_core_latest_log();
|
||||
}
|
||||
MenuIds::RESTART_CLASH => feat::restart_clash_core().await,
|
||||
MenuIds::RESTART_APP => feat::restart_app().await,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 12] = [
|
||||
"mode",
|
||||
@@ -19,46 +18,62 @@ pub const HANDLE_FIELDS: [&str; 12] = [
|
||||
|
||||
pub const DEFAULT_FIELDS: [&str; 5] = ["proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"];
|
||||
|
||||
fn lowercase_key(key: &str) -> Value {
|
||||
let mut key = String::from(key);
|
||||
key.make_ascii_lowercase();
|
||||
Value::from(key.as_str())
|
||||
}
|
||||
|
||||
pub fn use_lowercase(config: &Mapping) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
let mut key_str = String::from(key_str);
|
||||
key_str.make_ascii_lowercase();
|
||||
ret.insert(Value::from(key_str.as_str()), value.clone());
|
||||
lowercased.insert(lowercase_key(key_str), value.clone());
|
||||
}
|
||||
}
|
||||
ret
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_sort(config: Mapping) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
pub fn use_lowercase_owned(config: Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
lowercased.insert(lowercase_key(key_str), value);
|
||||
}
|
||||
}
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_sort(mut config: Mapping) -> Mapping {
|
||||
let mut sorted = Mapping::new();
|
||||
HANDLE_FIELDS.into_iter().for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
if let Some(value) = config.remove(&key) {
|
||||
sorted.insert(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect();
|
||||
|
||||
let config_keys: HashSet<&str> = config.keys().filter_map(|e| e.as_str()).collect();
|
||||
|
||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
let mut default_field_values = Mapping::new();
|
||||
for (key, value) in config {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
if DEFAULT_FIELDS.contains(&key_str) {
|
||||
default_field_values.insert(key, value);
|
||||
} else if !HANDLE_FIELDS.contains(&key_str) {
|
||||
sorted.insert(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
DEFAULT_FIELDS.into_iter().for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
if let Some(value) = config.get(&key) {
|
||||
ret.insert(key, value.clone());
|
||||
if let Some(value) = default_field_values.remove(&key) {
|
||||
sorted.insert(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
ret
|
||||
sorted
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -301,7 +301,7 @@ async fn process_global_items(
|
||||
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
config = use_merge(&merge, config.to_owned());
|
||||
config = use_merge(&merge, config);
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
@@ -333,20 +333,20 @@ async fn process_profile_items(
|
||||
profile_name: &String,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Rules(rules) = rules_item.data {
|
||||
config = use_seq(rules, config.to_owned(), "rules");
|
||||
config = use_seq(rules, config, "rules");
|
||||
}
|
||||
|
||||
if let ChainType::Proxies(proxies) = proxies_item.data {
|
||||
config = use_seq(proxies, config.to_owned(), "proxies");
|
||||
config = use_seq(proxies, config, "proxies");
|
||||
}
|
||||
|
||||
if let ChainType::Groups(groups) = groups_item.data {
|
||||
config = use_seq(groups, config.to_owned(), "proxy-groups");
|
||||
config = use_seq(groups, config, "proxy-groups");
|
||||
}
|
||||
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
config = use_merge(&merge, config.to_owned());
|
||||
config = use_merge(&merge, config);
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::process::AsyncHandler;
|
||||
|
||||
use super::use_lowercase;
|
||||
use super::field::{use_lowercase, use_lowercase_owned};
|
||||
use anyhow::{Error, Result};
|
||||
use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction};
|
||||
use clash_verge_logging::{Type, logging_error};
|
||||
@@ -126,7 +126,7 @@ fn use_script_sync(script: String, config: &Mapping, name: &String) -> Result<(M
|
||||
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
||||
|
||||
match res {
|
||||
Ok(config) => Ok((use_lowercase(&config), outputs.lock().to_vec())),
|
||||
Ok(config) => Ok((use_lowercase_owned(config), outputs.lock().to_vec())),
|
||||
Err(err) => {
|
||||
outputs
|
||||
.lock()
|
||||
|
||||
@@ -49,13 +49,13 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut new_seq = Sequence::new();
|
||||
new_seq.extend(prepend);
|
||||
let mut updated_items = Sequence::new();
|
||||
updated_items.extend(prepend);
|
||||
|
||||
if let Some(Value::Sequence(origin)) = config.get(field) {
|
||||
if let Some(Value::Sequence(existing_items)) = config.remove(field) {
|
||||
// Filter out deleted items
|
||||
let filtered: Sequence = origin
|
||||
.iter()
|
||||
let kept_items: Sequence = existing_items
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
if let Value::String(s) = item {
|
||||
!delete.contains(s)
|
||||
@@ -69,25 +69,34 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
new_seq.extend(filtered);
|
||||
updated_items.extend(kept_items);
|
||||
}
|
||||
|
||||
new_seq.extend(append);
|
||||
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
|
||||
updated_items.extend(append);
|
||||
config.insert(Value::String(field.into()), Value::Sequence(updated_items));
|
||||
|
||||
// If this is proxies field, we also need to filter proxy-groups
|
||||
if field == "proxies"
|
||||
&& let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups")
|
||||
{
|
||||
let mut new_groups = Sequence::new();
|
||||
let mut appended_to_selector = false;
|
||||
for group in groups {
|
||||
if let Value::Mapping(group_map) = group {
|
||||
let mut proxies_seq = group_map.get("proxies").and_then(Value::as_sequence).map(|proxies| {
|
||||
if field != "proxies" {
|
||||
return config;
|
||||
}
|
||||
|
||||
let Some(proxy_groups_value) = config.remove("proxy-groups") else {
|
||||
return config;
|
||||
};
|
||||
|
||||
let Value::Sequence(proxy_groups) = proxy_groups_value else {
|
||||
config.insert(Value::String("proxy-groups".into()), proxy_groups_value);
|
||||
return config;
|
||||
};
|
||||
|
||||
let mut updated_groups = Sequence::new();
|
||||
let mut appended_to_selector = false;
|
||||
for group in proxy_groups {
|
||||
if let Value::Mapping(mut group_map) = group {
|
||||
let mut group_proxies = match group_map.remove("proxies") {
|
||||
Some(Value::Sequence(proxies)) => Some(
|
||||
proxies
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|p| {
|
||||
if let Value::String(name) = p {
|
||||
!delete.contains(name)
|
||||
@@ -95,42 +104,46 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Sequence>()
|
||||
});
|
||||
|
||||
if !appended_to_selector && !added_proxy_names.is_empty() && is_selector_group(group_map) {
|
||||
let base_seq = proxies_seq.unwrap_or_else(Sequence::new);
|
||||
let mut seq = Sequence::new();
|
||||
let mut existing = HashSet::new();
|
||||
for name in &added_proxy_names {
|
||||
if existing.insert(name.clone()) {
|
||||
seq.push(Value::String(name.clone()));
|
||||
}
|
||||
}
|
||||
for value in base_seq {
|
||||
if let Value::String(name) = &value
|
||||
&& !existing.insert(name.to_owned())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
seq.push(value);
|
||||
}
|
||||
proxies_seq = Some(seq);
|
||||
appended_to_selector = true;
|
||||
.collect::<Sequence>(),
|
||||
),
|
||||
Some(value) => {
|
||||
group_map.insert(Value::String("proxies".into()), value);
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(seq) = proxies_seq {
|
||||
group_map.insert(Value::String("proxies".into()), Value::Sequence(seq));
|
||||
if !appended_to_selector && !added_proxy_names.is_empty() && is_selector_group(&group_map) {
|
||||
let base_group_proxies = group_proxies.unwrap_or_else(Sequence::new);
|
||||
let mut merged_proxies = Sequence::new();
|
||||
let mut seen_proxy_names = HashSet::new();
|
||||
for name in &added_proxy_names {
|
||||
if seen_proxy_names.insert(name.clone()) {
|
||||
merged_proxies.push(Value::String(name.clone()));
|
||||
}
|
||||
}
|
||||
new_groups.push(Value::Mapping(group_map.to_owned()));
|
||||
} else {
|
||||
new_groups.push(group.to_owned());
|
||||
for value in base_group_proxies {
|
||||
if let Value::String(name) = &value
|
||||
&& !seen_proxy_names.insert(name.to_owned())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged_proxies.push(value);
|
||||
}
|
||||
group_proxies = Some(merged_proxies);
|
||||
appended_to_selector = true;
|
||||
}
|
||||
|
||||
if let Some(group_proxies) = group_proxies {
|
||||
group_map.insert(Value::String("proxies".into()), Value::Sequence(group_proxies));
|
||||
}
|
||||
updated_groups.push(Value::Mapping(group_map));
|
||||
} else {
|
||||
updated_groups.push(group);
|
||||
}
|
||||
config.insert(Value::String("proxy-groups".into()), Value::Sequence(new_groups));
|
||||
}
|
||||
config.insert(Value::String("proxy-groups".into()), Value::Sequence(updated_groups));
|
||||
|
||||
config
|
||||
}
|
||||
@@ -288,4 +301,27 @@ proxy-groups:
|
||||
let names: Vec<&str> = group2_proxies.iter().filter_map(Value::as_str).collect();
|
||||
assert_eq!(names, vec!["proxy1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_preserve_non_sequence_proxy_groups() {
|
||||
let config_str = r#"
|
||||
proxies:
|
||||
- name: "proxy1"
|
||||
type: "ss"
|
||||
proxy-groups: "invalid"
|
||||
"#;
|
||||
let mut config: Mapping = serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML");
|
||||
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
delete: vec!["proxy1".to_string()],
|
||||
};
|
||||
|
||||
config = use_seq(seq, config, "proxies");
|
||||
|
||||
assert_eq!(config.get("proxy-groups").and_then(Value::as_str), Some("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +141,6 @@ mod app_init {
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_web_url,
|
||||
cmd::open_core_dir,
|
||||
cmd::open_app_log,
|
||||
cmd::open_core_log,
|
||||
cmd::get_portable_flag,
|
||||
cmd::get_network_interfaces,
|
||||
cmd::get_system_hostname,
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use nanoid::nanoid;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::path::Path;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
@@ -35,10 +35,10 @@ pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
val.apply_merge()
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
Ok(val
|
||||
.as_mapping()
|
||||
.ok_or_else(|| anyhow!("failed to transform to yaml mapping \"{}\"", path.display()))?
|
||||
.to_owned())
|
||||
match val {
|
||||
Value::Mapping(map) => Ok(map),
|
||||
_ => Err(anyhow!("failed to transform to yaml mapping \"{}\"", path.display())),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("YAML syntax error in {}: {}", path.display(), err);
|
||||
@@ -66,10 +66,9 @@ pub async fn save_yaml<T: Serialize + Sync>(path: &PathBuf, data: &T, prefix: Op
|
||||
None => data_str,
|
||||
};
|
||||
|
||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||
tokio::fs::write(path, yaml_str.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to save file \"{path_str}\""))?;
|
||||
.with_context(|| format!("failed to save file \"{}\"", path.display()))?;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -203,6 +202,22 @@ pub fn open_file(path: PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open_latest_log(path: PathBuf) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let path = snapshot_path(&path)?;
|
||||
open_file(path)
|
||||
}
|
||||
|
||||
pub fn open_app_latest_log() -> Result<()> {
|
||||
let path = crate::utils::dirs::app_latest_log()?;
|
||||
open_latest_log(path)
|
||||
}
|
||||
|
||||
pub fn open_core_latest_log() -> Result<()> {
|
||||
let path = crate::utils::dirs::clash_latest_log()?;
|
||||
open_latest_log(path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_elevator() -> String {
|
||||
use std::process::Command;
|
||||
|
||||
Reference in New Issue
Block a user