mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-25 05:02:07 +08:00
296 lines
10 KiB
Rust
296 lines
10 KiB
Rust
use crate::config::IVerge;
|
|
use std::collections::BTreeSet;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub(super) enum Effect {
|
|
RestartCore,
|
|
ClashConfig,
|
|
Autostart,
|
|
SystemProxy,
|
|
TrayIcon,
|
|
Hotkey,
|
|
TrayMenu,
|
|
TrayTooltip,
|
|
TrayClick,
|
|
Lightweight,
|
|
Language,
|
|
LogLevel,
|
|
LogFile,
|
|
}
|
|
|
|
pub(super) type Effects = BTreeSet<Effect>;
|
|
|
|
macro_rules! select_field {
|
|
($effects:ident, $field:ident, NoEffect) => { let _ = $field; };
|
|
($effects:ident, $field:ident, [$($effect:ident),*]) => {
|
|
if $field.is_some() { $effects.extend([$(Effect::$effect),*]); }
|
|
};
|
|
($effects:ident, $field:ident, Tun) => {
|
|
select_field!($effects, $field, [ClashConfig, TrayMenu, TrayTooltip, TrayIcon]);
|
|
#[cfg(target_os = "linux")]
|
|
if *$field == Some(true) { $effects.insert(Effect::RestartCore); }
|
|
};
|
|
}
|
|
|
|
macro_rules! verge_registry {
|
|
($( $(#[$cfg:meta])* $field:ident => $rule:tt; )*) => {
|
|
pub(super) fn verge_effects(patch: &IVerge) -> Effects {
|
|
// No rest pattern: even fields omitted by serde must be classified at compile time.
|
|
let IVerge { $( $(#[$cfg])* $field, )* } = patch;
|
|
let mut effects = Effects::new();
|
|
$( $(#[$cfg])* { select_field!(effects, $field, $rule); } )*
|
|
effects
|
|
}
|
|
|
|
#[cfg(test)]
|
|
const VERGE_FIELDS: &[&str] = &[ $( $(#[$cfg])* stringify!($field), )* ];
|
|
};
|
|
}
|
|
|
|
verge_registry! {
|
|
app_log_level => [LogLevel];
|
|
app_log_max_size => [LogFile];
|
|
app_log_max_count => [LogFile];
|
|
language => [Language, TrayMenu, TrayTooltip];
|
|
theme_mode => NoEffect;
|
|
tray_event => [TrayClick];
|
|
env_type => NoEffect;
|
|
start_page => NoEffect;
|
|
startup_script => NoEffect;
|
|
traffic_graph => NoEffect;
|
|
enable_memory_usage => NoEffect;
|
|
enable_group_icon => NoEffect;
|
|
pause_render_traffic_stats_on_blur => NoEffect;
|
|
common_tray_icon => [TrayIcon];
|
|
#[cfg(target_os = "macos")]
|
|
tray_icon => [TrayIcon];
|
|
menu_icon => NoEffect;
|
|
menu_order => NoEffect;
|
|
proxy_group_tools_position => NoEffect;
|
|
notice_position => NoEffect;
|
|
collapse_navbar => NoEffect;
|
|
sysproxy_tray_icon => [TrayIcon];
|
|
tun_tray_icon => [TrayIcon];
|
|
enable_tun_mode => Tun;
|
|
enable_auto_launch => [Autostart];
|
|
enable_silent_start => NoEffect;
|
|
enable_system_proxy => [SystemProxy, TrayMenu, TrayTooltip, TrayIcon];
|
|
enable_proxy_guard => [SystemProxy];
|
|
enable_bypass_check => NoEffect;
|
|
enable_dns_settings => NoEffect;
|
|
profile_dns_settings => NoEffect;
|
|
use_default_bypass => NoEffect;
|
|
system_proxy_bypass => [SystemProxy];
|
|
proxy_guard_duration => [SystemProxy];
|
|
proxy_auto_config => [SystemProxy];
|
|
pac_file_content => [SystemProxy];
|
|
proxy_host => NoEffect;
|
|
theme_setting => NoEffect;
|
|
web_ui_list => NoEffect;
|
|
clash_core => NoEffect;
|
|
hotkeys => [Hotkey, TrayMenu];
|
|
enable_global_hotkey => NoEffect;
|
|
home_cards => NoEffect;
|
|
auto_close_connection => NoEffect;
|
|
auto_check_update => NoEffect;
|
|
default_latency_test => NoEffect;
|
|
default_latency_timeout => NoEffect;
|
|
enable_auto_delay_detection => NoEffect;
|
|
auto_delay_detection_interval_minutes => NoEffect;
|
|
enable_builtin_enhanced => NoEffect;
|
|
proxy_layout_column => NoEffect;
|
|
test_list => NoEffect;
|
|
auto_log_clean => NoEffect;
|
|
enable_auto_backup_schedule => NoEffect;
|
|
auto_backup_interval_hours => NoEffect;
|
|
auto_backup_on_change => NoEffect;
|
|
#[cfg(not(target_os = "windows"))]
|
|
verge_redir_port => [RestartCore];
|
|
#[cfg(not(target_os = "windows"))]
|
|
verge_redir_enabled => [RestartCore];
|
|
#[cfg(target_os = "linux")]
|
|
verge_tproxy_port => [RestartCore];
|
|
#[cfg(target_os = "linux")]
|
|
verge_tproxy_enabled => [RestartCore];
|
|
verge_mixed_port => [RestartCore];
|
|
verge_socks_port => [RestartCore];
|
|
verge_socks_enabled => [RestartCore];
|
|
verge_port => [RestartCore];
|
|
verge_http_enabled => [RestartCore];
|
|
webdav_url => NoEffect;
|
|
webdav_username => NoEffect;
|
|
webdav_password => NoEffect;
|
|
#[cfg(target_os = "macos")]
|
|
enable_tray_speed => [TrayIcon];
|
|
tray_proxy_groups_display_mode => [TrayMenu];
|
|
tray_inline_outbound_modes => [TrayMenu];
|
|
enable_auto_light_weight_mode => [Lightweight];
|
|
auto_light_weight_minutes => NoEffect;
|
|
enable_hover_jump_navigator => NoEffect;
|
|
hover_jump_navigator_delay => NoEffect;
|
|
enable_external_controller => [RestartCore];
|
|
}
|
|
|
|
pub(super) fn clash_effects(patch: &serde_yaml_ng::Mapping) -> Effects {
|
|
use clash_verge_logging::{Type, logging};
|
|
let mut effects = Effects::new();
|
|
for key in patch.keys() {
|
|
effects.extend(match key.as_str() {
|
|
Some("secret" | "external-controller") => &[Effect::RestartCore][..],
|
|
Some("allow-lan") => &[Effect::ClashConfig][..],
|
|
Some("mode") => &[Effect::ClashConfig, Effect::TrayMenu, Effect::TrayIcon][..],
|
|
_ => {
|
|
logging!(
|
|
warn,
|
|
Type::Config,
|
|
"Unregistered Clash configuration key {key:?}; reloading configuration"
|
|
);
|
|
&[Effect::ClashConfig][..]
|
|
}
|
|
});
|
|
}
|
|
// These branches were exclusive: restart supersedes reload, and allow-lan precedes mode.
|
|
if effects.contains(&Effect::RestartCore) {
|
|
effects.retain(|effect| *effect == Effect::RestartCore);
|
|
} else if patch.contains_key("allow-lan") {
|
|
effects.retain(|effect| *effect == Effect::ClashConfig);
|
|
}
|
|
if patch.is_empty() {
|
|
effects.insert(Effect::ClashConfig);
|
|
}
|
|
effects
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn every_serialized_verge_field_is_registered() -> Result<(), serde_json::Error> {
|
|
let fields: std::collections::BTreeMap<String, serde_json::Value> =
|
|
serde_json::from_value(serde_json::to_value(IVerge::default())?)?;
|
|
for field in fields.keys() {
|
|
assert!(VERGE_FIELDS.contains(&field.as_str()), "unregistered field: {field}");
|
|
}
|
|
let unique: BTreeSet<_> = VERGE_FIELDS.iter().collect();
|
|
assert_eq!(unique.len(), VERGE_FIELDS.len());
|
|
Ok(())
|
|
}
|
|
|
|
fn verge_patch(mutate: impl FnOnce(&mut IVerge)) -> IVerge {
|
|
let mut patch = IVerge::default();
|
|
mutate(&mut patch);
|
|
patch
|
|
}
|
|
|
|
#[test]
|
|
fn representative_verge_patches_map_to_frozen_effects() {
|
|
assert_eq!(verge_effects(&IVerge::default()), Effects::new());
|
|
|
|
let cases: [(IVerge, &[Effect]); 7] = [
|
|
(
|
|
verge_patch(|p| p.verge_mixed_port = Some(27899)),
|
|
&[Effect::RestartCore],
|
|
),
|
|
(
|
|
verge_patch(|p| p.language = Some("zh".into())),
|
|
&[Effect::TrayMenu, Effect::TrayTooltip, Effect::Language],
|
|
),
|
|
(
|
|
verge_patch(|p| p.hotkeys = Some(vec!["CmdOrCtrl+K".into()])),
|
|
&[Effect::Hotkey, Effect::TrayMenu],
|
|
),
|
|
(
|
|
verge_patch(|p| p.enable_system_proxy = Some(true)),
|
|
&[
|
|
Effect::SystemProxy,
|
|
Effect::TrayIcon,
|
|
Effect::TrayMenu,
|
|
Effect::TrayTooltip,
|
|
],
|
|
),
|
|
(
|
|
verge_patch(|p| p.system_proxy_bypass = Some("localhost".into())),
|
|
&[Effect::SystemProxy],
|
|
),
|
|
(
|
|
verge_patch(|p| p.tray_event = Some("show_main_window".into())),
|
|
&[Effect::TrayClick],
|
|
),
|
|
(verge_patch(|p| p.theme_mode = Some("dark".into())), &[]),
|
|
];
|
|
for (patch, expected) in cases {
|
|
assert_eq!(
|
|
verge_effects(&patch),
|
|
expected.iter().copied().collect::<Effects>(),
|
|
"patch mapping drifted for {expected:?}"
|
|
);
|
|
}
|
|
|
|
let tun = verge_patch(|p| p.enable_tun_mode = Some(true));
|
|
#[cfg(target_os = "linux")]
|
|
assert_eq!(
|
|
verge_effects(&tun),
|
|
[
|
|
Effect::RestartCore,
|
|
Effect::ClashConfig,
|
|
Effect::TrayIcon,
|
|
Effect::TrayMenu,
|
|
Effect::TrayTooltip,
|
|
]
|
|
.into_iter()
|
|
.collect::<Effects>()
|
|
);
|
|
#[cfg(not(target_os = "linux"))]
|
|
assert_eq!(
|
|
verge_effects(&tun),
|
|
[
|
|
Effect::ClashConfig,
|
|
Effect::TrayIcon,
|
|
Effect::TrayMenu,
|
|
Effect::TrayTooltip,
|
|
]
|
|
.into_iter()
|
|
.collect::<Effects>()
|
|
);
|
|
}
|
|
|
|
fn clash_mapping(pairs: &[(&str, &str)]) -> serde_yaml_ng::Mapping {
|
|
let mut mapping = serde_yaml_ng::Mapping::new();
|
|
for (key, value) in pairs {
|
|
mapping.insert((*key).into(), (*value).into());
|
|
}
|
|
mapping
|
|
}
|
|
|
|
#[test]
|
|
fn clash_keys_map_to_frozen_effects() {
|
|
let cases: [(serde_yaml_ng::Mapping, &[Effect]); 7] = [
|
|
(clash_mapping(&[]), &[Effect::ClashConfig]),
|
|
(clash_mapping(&[("secret", "s")]), &[Effect::RestartCore]),
|
|
(
|
|
clash_mapping(&[("mode", "rule")]),
|
|
&[Effect::ClashConfig, Effect::TrayIcon, Effect::TrayMenu],
|
|
),
|
|
(clash_mapping(&[("allow-lan", "true")]), &[Effect::ClashConfig]),
|
|
(clash_mapping(&[("not-a-known-key", "1")]), &[Effect::ClashConfig]),
|
|
// Restart supersedes reload; allow-lan precedes mode.
|
|
(
|
|
clash_mapping(&[("secret", "s"), ("mode", "rule")]),
|
|
&[Effect::RestartCore],
|
|
),
|
|
(
|
|
clash_mapping(&[("allow-lan", "true"), ("mode", "rule")]),
|
|
&[Effect::ClashConfig],
|
|
),
|
|
];
|
|
for (patch, expected) in cases {
|
|
assert_eq!(
|
|
clash_effects(&patch),
|
|
expected.iter().copied().collect::<Effects>(),
|
|
"clash mapping drifted for {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|