use crate::config::{IProfilePreview, IVerge}; use crate::core::lightweight; use crate::core::tray::menu_def::TrayAction; use crate::process::AsyncHandler; use crate::singleton; use crate::utils::window_manager::WindowManager; use crate::{ Type, cmd, config::Config, core::lightweight::is_in_lightweight_mode, feat, logging, utils::{dirs::find_target_icons, help}, }; use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter}; use clash_verge_logging::logging_error; use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent}; use tauri_plugin_mihomo::models::Proxies; use tokio::fs; use super::handle; use anyhow::Result; use smartstring::alias::String; use std::borrow::Cow; use std::collections::HashMap; use std::time::Duration; use tauri::{ AppHandle, Wry, menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, }; mod menu_def; #[cfg(target_os = "macos")] mod speed_task; use menu_def::{MenuIds, MenuTexts}; // TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑) type ProxyMenuItem = (Option>, Vec>>); const TRAY_CLICK_DEBOUNCE_MS: u64 = 300; pub const TRAY_ID: &str = "clash-verge-rev-tray"; #[derive(Clone, Copy)] struct TrayMenuOptions { is_lightweight_mode: bool, include_proxy_groups: bool, } #[derive(Clone)] struct TrayState {} enum IconKind { Common, SysProxy, Tun, } pub struct Tray { limiter: SystemLimiter, #[cfg(target_os = "macos")] speed_controller: speed_task::TraySpeedController, } impl TrayState { async fn get_tray_icon(verge: &IVerge) -> (bool, Cow<'_, [u8]>) { let tun_mode = verge.enable_tun_mode.unwrap_or(false) && crate::core::runstate::RUN_STATE.state().tun_capable(); let system_mode = verge.enable_system_proxy.unwrap_or(false); let kind = if tun_mode { IconKind::Tun } else if system_mode { IconKind::SysProxy } else { IconKind::Common }; Self::load_icon(verge, kind).await } async fn load_icon(verge: &IVerge, kind: IconKind) -> (bool, Cow<'_, [u8]>) { let (custom_enabled, icon_name) = match kind { IconKind::Common => (verge.common_tray_icon.unwrap_or(false), "common"), IconKind::SysProxy => (verge.sysproxy_tray_icon.unwrap_or(false), "sysproxy"), IconKind::Tun => (verge.tun_tray_icon.unwrap_or(false), "tun"), }; if custom_enabled && let Ok(Some(path)) = find_target_icons(icon_name) && let Ok(data) = fs::read(path).await { return (true, Cow::Owned(data)); } Self::default_icon(verge, kind) } #[allow(clippy::missing_const_for_fn)] fn default_icon(verge: &IVerge, kind: IconKind) -> (bool, Cow<'_, [u8]>) { #[cfg(target_os = "macos")] { let is_mono = verge.tray_icon.as_deref().unwrap_or("monochrome") == "monochrome"; if is_mono { return ( false, match kind { IconKind::Common => Cow::Borrowed(include_bytes!("../../../icons/tray-icon-mono.ico")), IconKind::SysProxy => { Cow::Borrowed(include_bytes!("../../../icons/tray-icon-sys-mono-new.ico")) } IconKind::Tun => Cow::Borrowed(include_bytes!("../../../icons/tray-icon-tun-mono-new.ico")), }, ); } } #[cfg(not(target_os = "macos"))] let _ = verge; ( false, match kind { IconKind::Common => Cow::Borrowed(include_bytes!("../../../icons/tray-icon.ico")), IconKind::SysProxy => Cow::Borrowed(include_bytes!("../../../icons/tray-icon-sys.ico")), IconKind::Tun => Cow::Borrowed(include_bytes!("../../../icons/tray-icon-tun.ico")), }, ) } } impl Default for Tray { #[allow(clippy::unwrap_used)] fn default() -> Self { Self { limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock), #[cfg(target_os = "macos")] speed_controller: speed_task::TraySpeedController::new(), } } } singleton!(Tray, TRAY); impl Tray { fn new() -> Self { Self::default() } pub async fn init(&self) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化"); return Ok(()); } let app_handle = handle::Handle::app_handle(); match self.create_tray_from_handle(app_handle).await { Ok(_) => { logging!(info, Type::Tray, "System tray created successfully"); } Err(e) => { logging!( warn, Type::Tray, "System tray creation failed: {e}, Application will continue running without tray icon", ); } } Ok(()) } pub async fn update_click_behavior(&self) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘点击行为更新"); return Ok(()); } let app_handle = handle::Handle::app_handle(); let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event = TrayAction::from(tray_event.as_deref().unwrap_or("main_window")); let tray = app_handle .tray_by_id(TRAY_ID) .ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?; match tray_event { TrayAction::TrayMenu => tray.set_show_menu_on_left_click(true)?, _ => tray.set_show_menu_on_left_click(false)?, } Ok(()) } pub async fn update_menu(&self) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新"); return Ok(()); } let app_handle = handle::Handle::app_handle(); self.update_menu_internal(app_handle, true).await } async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> { let Some(tray) = app_handle.tray_by_id(TRAY_ID) else { logging!(warn, Type::Tray, "Failed to update tray menu: tray not found"); return Ok(()); }; let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let tun_mode_available = crate::core::runstate::RUN_STATE.state().tun_capable(); let mode = { Config::clash() .await .latest_arc() .0 .get("mode") .map(|val| val.as_str().unwrap_or("rule")) .unwrap_or("rule") .to_owned() }; let profiles_config = Config::profiles().await; let profiles_arc = profiles_config.latest_arc(); let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default(); let is_lightweight_mode = is_in_lightweight_mode(); logging_error!( Type::Tray, tray.set_menu(Some( create_tray_menu( app_handle, Some(mode.as_str()), *system_proxy, *tun_mode, tun_mode_available, profiles_preview, TrayMenuOptions { is_lightweight_mode, include_proxy_groups, }, ) .await?, )) ); logging!(debug, Type::Tray, "托盘菜单更新成功"); Ok(()) } pub async fn update_icon(&self, verge: &IVerge) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); } let app_handle = handle::Handle::app_handle(); let Some(tray) = app_handle.tray_by_id(TRAY_ID) else { logging!(warn, Type::Tray, "Failed to update tray icon: tray not found"); return Ok(()); }; let (_is_custom_icon, icon_bytes) = TrayState::get_tray_icon(verge).await; let template = { #[cfg(target_os = "macos")] { verge.tray_icon.as_ref().is_none_or(|v| v == "monochrome") } #[cfg(not(target_os = "macos"))] { false } }; let icon = Some(tauri::image::Image::from_bytes(&icon_bytes)?); logging_error!(Type::Tray, tray.set_icon_with_as_template(icon, template)); Ok(()) } pub async fn update_tooltip(&self) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘提示更新"); return Ok(()); } let app_handle = handle::Handle::app_handle(); let verge = Config::verge().await.latest_arc(); let system_proxy = verge.enable_system_proxy.unwrap_or(false); let tun_mode = verge.enable_tun_mode.unwrap_or(false) && crate::core::runstate::RUN_STATE.state().tun_capable(); let switch_str = |flag: bool| { if flag { "on" } else { "off" } }; let mut current_profile_name = "None".into(); { let profiles = Config::profiles().await; let profiles = profiles.latest_arc(); if let Some(current_profile_uid) = profiles.current.as_ref() && let Ok(profile) = profiles.get_item(current_profile_uid) { current_profile_name = match &profile.name { Some(profile_name) => profile_name.to_string(), None => current_profile_name, }; } } let sys_proxy_text = clash_verge_i18n::t!("tray.tooltip.systemProxy"); let tun_text = clash_verge_i18n::t!("tray.tooltip.tun"); let profile_text = clash_verge_i18n::t!("tray.tooltip.profile"); let v = env!("CARGO_PKG_VERSION"); let reassembled_version = v.split_once('+').map_or_else( || v.into(), |(main, rest)| format!("{main}+{}", rest.split('.').next().unwrap_or("")), ); let tooltip = format!( "Clash Verge {}\n{}: {}\n{}: {}\n{}: {}", reassembled_version, sys_proxy_text, switch_str(system_proxy), tun_text, switch_str(tun_mode), profile_text, current_profile_name ); let Some(tray) = app_handle.tray_by_id(TRAY_ID) else { logging!(warn, Type::Tray, "Failed to update tray tooltip: tray not found"); return Ok(()); }; logging_error!(Type::Tray, tray.set_tooltip(Some(&tooltip))); Ok(()) } pub async fn update_part(&self) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新"); return Ok(()); } let verge = Config::verge().await.data_arc(); let app_handle = handle::Handle::app_handle(); self.update_menu_internal(app_handle, false).await?; AsyncHandler::spawn(|| async { logging_error!(Type::Tray, Self::global().update_menu().await); }); self.update_icon(&verge).await?; #[cfg(target_os = "macos")] self.update_speed_task(verge.enable_tray_speed.unwrap_or(false)); self.update_tooltip().await?; Ok(()) } pub async fn update_menu_and_icon(&self) { logging_error!(Type::Tray, self.update_menu().await); let verge = Config::verge().await.data_arc(); logging_error!(Type::Tray, self.update_icon(&verge).await); } async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建"); return Ok(()); } logging!(info, Type::Tray, "正在从AppHandle创建系统托盘"); let verge = Config::verge().await.data_arc(); let icon_bytes = TrayState::get_tray_icon(&verge).await.1; let icon = tauri::image::Image::from_bytes(&icon_bytes)?; #[cfg(target_os = "linux")] let builder = TrayIconBuilder::with_id(TRAY_ID).icon(icon).icon_as_template(false); #[cfg(any(target_os = "macos", target_os = "windows"))] let show_menu_on_left_click = verge.tray_event.as_ref().is_some_and(|v| v == "tray_menu"); #[cfg(not(target_os = "linux"))] let mut builder = TrayIconBuilder::with_id(TRAY_ID).icon(icon).icon_as_template(false); #[cfg(target_os = "macos")] { let is_monochrome = verge.tray_icon.as_ref().is_none_or(|v| v == "monochrome"); builder = builder.icon_as_template(is_monochrome); } #[cfg(any(target_os = "macos", target_os = "windows"))] { if !show_menu_on_left_click { builder = builder.show_menu_on_left_click(false); } } let tray = builder.build(app_handle)?; tray.on_tray_icon_event(on_tray_icon_event); tray.on_menu_event(on_menu_event); Ok(()) } fn should_handle_tray_click(&self) -> bool { let allow = self.limiter.check(); if !allow { logging!(debug, Type::Tray, "tray click rate limited"); } allow } #[cfg(target_os = "macos")] pub fn update_speed_task(&self, enable_tray_speed: bool) { self.speed_controller.update_task(enable_tray_speed); } } fn create_hotkeys(hotkeys: &Option>) -> HashMap<&str, &str> { hotkeys .as_ref() .map(|h| { h.iter() .filter_map(|item| { let mut parts = item.split(','); match (parts.next(), parts.next()) { (Some(func), Some(key)) => { // 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`) let uppercase_key = key.to_uppercase(); let unsupported = uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD")); if unsupported { None } else { Some((func, key)) } } _ => None, } }) .collect::>() }) .unwrap_or_default() } fn create_profile_menu_item( app_handle: &AppHandle, profiles_preview: Vec>, ) -> Result>> { profiles_preview .into_iter() .map(|profile| { CheckMenuItem::with_id( app_handle, format!("profiles_{}", profile.uid), profile.name, true, profile.is_current, None::<&str>, ) .map_err(|e| e.into()) }) .collect() } fn create_subcreate_proxy_menu_item( app_handle: &AppHandle, proxy_mode: &str, proxy_group_order_map: Option>, proxy_nodes_data: Option, ) -> Vec> { let proxy_submenus: Vec> = { let mut submenus: Vec<(String, usize, Submenu)> = Vec::new(); // TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息 if let Some(proxy_nodes_data) = proxy_nodes_data { for (group_name, group_data) in proxy_nodes_data.proxies.iter() { let should_show = match proxy_mode { "global" => group_name == "GLOBAL", _ => group_name != "GLOBAL", } && !group_data.hidden.unwrap_or_default(); if !should_show { continue; } let Some(all_proxies) = group_data.all.as_ref() else { continue; }; let now_proxy = group_data.now.as_deref().unwrap_or_default(); let group_items: Vec> = all_proxies .iter() .filter_map(|proxy_str| { let is_selected = *proxy_str == now_proxy; let item_id = format!("proxy_{}_{}", group_name, proxy_str); let delay_text = proxy_nodes_data .proxies .get(proxy_str) .and_then(|h| h.history.last()) .map(|h| match h.delay { 0 => "-ms".into(), delay if delay >= 10000 => "-ms".into(), _ => format!("{}ms", h.delay), }) .unwrap_or_else(|| "-ms".into()); let display_text = format!("{} | {}", proxy_str, delay_text); CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>) .map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)) .ok() }) .collect(); if group_items.is_empty() { continue; } let group_display_name = group_name.to_string(); let group_items_refs: Vec<&dyn IsMenuItem> = group_items.iter().map(|item| item as &dyn IsMenuItem).collect(); if let Ok(submenu) = Submenu::with_id_and_items( app_handle, format!("proxy_group_{}", group_name), group_display_name, true, &group_items_refs, ) { let insertion_index = submenus.len(); submenus.push((group_name.into(), insertion_index, submenu)); } else { logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name); } } } if let Some(order_map) = proxy_group_order_map.as_ref() { submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| { match (order_map.get(name_a), order_map.get(name_b)) { (Some(index_a), Some(index_b)) => index_a.cmp(index_b), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => original_index_a.cmp(original_index_b), } }); } submenus.into_iter().map(|(_, _, submenu)| submenu).collect() }; proxy_submenus } fn create_proxy_menu_item( app_handle: &AppHandle, show_proxy_groups_inline: bool, proxy_submenus: Vec>, proxies_text: &str, ) -> Result { let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline { ( None, proxy_submenus .into_iter() .map(|submenu| Box::new(submenu) as Box>) .collect(), ) } else if !proxy_submenus.is_empty() { let proxy_submenu_refs: Vec<&dyn IsMenuItem> = proxy_submenus .iter() .map(|submenu| submenu as &dyn IsMenuItem) .collect(); ( Some(Submenu::with_id_and_items( app_handle, MenuIds::PROXIES, proxies_text, true, &proxy_submenu_refs, )?), Vec::new(), ) } else { (None, Vec::new()) }; Ok((proxies_submenu, inline_proxy_items)) } async fn create_tray_menu( app_handle: &AppHandle, mode: Option<&str>, system_proxy_enabled: bool, tun_mode_enabled: bool, tun_mode_available: bool, profiles_preview: Vec>, options: TrayMenuOptions, ) -> Result> { let current_proxy_mode = mode.unwrap_or(""); let mut verge_settings = Config::verge().await.latest_arc(); let fetch_proxy_groups = options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable"); // TODO: should update tray menu again when it was timeout error let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups { let proxy_nodes_data = tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies()) .await .map_or(None, |res| res.ok()); let runtime = Config::runtime().await.latest_arc(); let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| { config .get("proxy-groups") .and_then(|groups| groups.as_sequence()) .map(|groups| { groups .iter() .filter_map(|group| group.get("name")) .filter_map(|name| name.as_str()) .enumerate() .map(|(index, name)| (name.into(), index)) .collect::>() }) .unwrap_or_default() }); (proxy_nodes_data, runtime_proxy_groups_order) } else { (None, None) }; if fetch_proxy_groups { verge_settings = Config::verge().await.latest_arc(); } let tray_proxy_groups_display_mode = verge_settings .tray_proxy_groups_display_mode .as_deref() .unwrap_or("default"); let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable"; let proxy_group_order_map = runtime_proxy_groups_order; let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false); let version = env!("CARGO_PKG_VERSION"); let hotkeys = create_hotkeys(&verge_settings.hotkeys); let profile_menu_items: Vec> = create_profile_menu_item(app_handle, profiles_preview)?; let texts = MenuTexts::new(); let profile_menu_items_refs: Vec<&dyn IsMenuItem> = profile_menu_items .iter() .map(|item| item as &dyn IsMenuItem) .collect(); let open_window = &MenuItem::with_id( app_handle, MenuIds::DASHBOARD, &texts.dashboard, true, hotkeys.get("open_or_close_dashboard").copied(), )?; let rule_mode = &CheckMenuItem::with_id( app_handle, MenuIds::RULE_MODE, &texts.rule_mode, true, current_proxy_mode == "rule", hotkeys.get("clash_mode_rule").copied(), )?; let global_mode = &CheckMenuItem::with_id( app_handle, MenuIds::GLOBAL_MODE, &texts.global_mode, true, current_proxy_mode == "global", hotkeys.get("clash_mode_global").copied(), )?; let direct_mode = &CheckMenuItem::with_id( app_handle, MenuIds::DIRECT_MODE, &texts.direct_mode, true, current_proxy_mode == "direct", hotkeys.get("clash_mode_direct").copied(), )?; let outbound_modes = if show_outbound_modes_inline { None } else { let current_mode_text = match current_proxy_mode { "global" => clash_verge_i18n::t!("tray.global"), "direct" => clash_verge_i18n::t!("tray.direct"), _ => clash_verge_i18n::t!("tray.rule"), }; let outbound_modes_label = format!("{} ({})", texts.outbound_modes, current_mode_text); Some(Submenu::with_id_and_items( app_handle, MenuIds::OUTBOUND_MODES, outbound_modes_label.as_str(), true, &[ rule_mode as &dyn IsMenuItem, global_mode as &dyn IsMenuItem, direct_mode as &dyn IsMenuItem, ], )?) }; let profiles = &Submenu::with_id_and_items( app_handle, MenuIds::PROFILES, &texts.profiles, true, &profile_menu_items_refs, )?; let (proxies_menu, inline_proxy_items) = if include_proxy_groups { let proxy_sub_menus = create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data); match tray_proxy_groups_display_mode { "default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?, "inline" => create_proxy_menu_item(app_handle, true, proxy_sub_menus, &texts.proxies)?, _ => (None, Vec::new()), } } else { (None, Vec::new()) }; let system_proxy = &CheckMenuItem::with_id( app_handle, MenuIds::SYSTEM_PROXY, &texts.system_proxy, true, system_proxy_enabled, hotkeys.get("toggle_system_proxy").copied(), )?; let tun_mode = &CheckMenuItem::with_id( app_handle, MenuIds::TUN_MODE, &texts.tun_mode, tun_mode_available, tun_mode_enabled, hotkeys.get("toggle_tun_mode").copied(), )?; let close_all_connections = &MenuItem::with_id( app_handle, MenuIds::CLOSE_ALL_CONNECTIONS, &texts.close_all_connections, true, None::<&str>, )?; let lightweight_mode = &CheckMenuItem::with_id( app_handle, MenuIds::LIGHTWEIGHT_MODE, &texts.lightweight_mode, true, options.is_lightweight_mode, hotkeys.get("entry_lightweight_mode").copied(), )?; let copy_env = &MenuItem::with_id(app_handle, MenuIds::COPY_ENV, &texts.copy_env, true, None::<&str>)?; let open_app_dir = &MenuItem::with_id(app_handle, MenuIds::CONF_DIR, &texts.conf_dir, true, None::<&str>)?; let open_core_dir = &MenuItem::with_id(app_handle, MenuIds::CORE_DIR, &texts.core_dir, true, None::<&str>)?; let open_logs_dir = &MenuItem::with_id(app_handle, MenuIds::LOGS_DIR, &texts.logs_dir, true, None::<&str>)?; let open_app_log = &MenuItem::with_id(app_handle, MenuIds::APP_LOG, &texts.app_log, true, None::<&str>)?; let open_core_log = &MenuItem::with_id(app_handle, MenuIds::CORE_LOG, &texts.core_log, true, None::<&str>)?; let open_dir = &Submenu::with_id_and_items( app_handle, MenuIds::OPEN_DIR, &texts.open_dir, true, &[open_app_dir, open_core_dir, open_logs_dir, open_app_log, open_core_log], )?; let restart_clash = &MenuItem::with_id( app_handle, MenuIds::RESTART_CLASH, &texts.restart_clash, true, None::<&str>, )?; let restart_app = &MenuItem::with_id(app_handle, MenuIds::RESTART_APP, &texts.restart_app, true, None::<&str>)?; let app_version = &MenuItem::with_id( app_handle, MenuIds::VERGE_VERSION, format!("{} {version}", texts.verge_version), true, None::<&str>, )?; let more = &Submenu::with_id_and_items( app_handle, MenuIds::MORE, &texts.more, true, &[ copy_env as &dyn IsMenuItem, close_all_connections, restart_clash, restart_app, app_version, ], )?; let quit_accelerator = hotkeys.get("quit").copied(); #[cfg(target_os = "macos")] let quit_accelerator = quit_accelerator.or(Some("Cmd+Q")); let quit = &MenuItem::with_id(app_handle, MenuIds::EXIT, &texts.exit, true, quit_accelerator)?; let separator = &PredefinedMenuItem::separator(app_handle)?; let mut menu_items: Vec<&dyn IsMenuItem> = vec![open_window, separator]; if show_outbound_modes_inline { menu_items.extend_from_slice(&[ rule_mode as &dyn IsMenuItem, global_mode as &dyn IsMenuItem, direct_mode as &dyn IsMenuItem, ]); } else if let Some(ref outbound_modes) = outbound_modes { menu_items.push(outbound_modes); } menu_items.extend_from_slice(&[separator, profiles]); match tray_proxy_groups_display_mode { "default" => { menu_items.extend(proxies_menu.iter().map(|item| item as &dyn IsMenuItem<_>)); } "inline" if !inline_proxy_items.is_empty() => { menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref())); } _ => {} } menu_items.extend_from_slice(&[ separator, system_proxy as &dyn IsMenuItem, tun_mode as &dyn IsMenuItem, separator, lightweight_mode as &dyn IsMenuItem, open_dir as &dyn IsMenuItem, more as &dyn IsMenuItem, separator, quit as &dyn IsMenuItem, ]); let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?; Ok(menu) } fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) { if matches!( tray_event, TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. } ) { return; } if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Down, .. } = tray_event { #[allow(clippy::use_self)] if !Tray::global().should_handle_tray_click() { return; } AsyncHandler::spawn(|| async move { let verge = Config::verge().await.data_arc(); let verge_tray_event = verge.tray_event.clone().unwrap_or_else(|| "main_window".into()); let verge_tray_action = TrayAction::from(verge_tray_event.as_str()); logging!(debug, Type::Tray, "tray event: {verge_tray_action:?}"); match verge_tray_action { TrayAction::SystemProxy => { let _ = feat::toggle_system_proxy().await; } TrayAction::TunMode => { let _ = feat::toggle_tun_mode(None).await; } TrayAction::MainWindow => { if !lightweight::exit_lightweight_mode().await { WindowManager::show_main_window().await; }; } // tray_menu 模式下菜单由系统原生展示(见 set_show_menu_on_left_click), // 左键点击事件无需额外处理 TrayAction::TrayMenu => {} TrayAction::Unknown => { logging!(warn, Type::Tray, "invalid tray event: {}", verge_tray_event); } }; }); } } fn on_menu_event(_: &AppHandle, event: MenuEvent) { if !Tray::global().should_handle_tray_click() { return; } if event.id.as_ref().is_empty() { return; } AsyncHandler::spawn(|| async move { match event.id.as_ref() { mode @ (MenuIds::RULE_MODE | MenuIds::GLOBAL_MODE | MenuIds::DIRECT_MODE) => { if let Some(final_mode) = mode.strip_circumfix("tray_", "_mode") { logging!(info, Type::ProxyMode, "Switch Proxy Mode To: {}", final_mode); let _ = feat::change_clash_mode(final_mode.into()).await; } } MenuIds::DASHBOARD => { logging!(info, Type::Tray, "托盘菜单点击: 打开窗口"); if !lightweight::exit_lightweight_mode().await { WindowManager::show_main_window().await; }; } MenuIds::SYSTEM_PROXY => { let _ = feat::toggle_system_proxy().await; } MenuIds::TUN_MODE => { feat::toggle_tun_mode(None).await; } MenuIds::CLOSE_ALL_CONNECTIONS => { if let Err(err) = handle::Handle::mihomo().close_all_connections().await { logging!(error, Type::Tray, "Failed to close all connections from tray: {err}"); } } MenuIds::COPY_ENV => { if let Err(err) = feat::copy_clash_env().await { logging!( error, Type::Tray, "Failed to copy proxy environment variables from tray: {err:#}" ); } } MenuIds::CONF_DIR => { let _ = cmd::open_app_dir().await; } MenuIds::CORE_DIR => { let _ = cmd::open_core_dir().await; } MenuIds::LOGS_DIR => { let _ = cmd::open_logs_dir().await; } MenuIds::APP_LOG => { let _ = help::open_app_latest_log(); } MenuIds::CORE_LOG => { let _ = help::open_core_latest_log().await; } MenuIds::RESTART_CLASH => feat::restart_clash_core().await, MenuIds::RESTART_APP => feat::restart_app().await, MenuIds::LIGHTWEIGHT_MODE => { if !is_in_lightweight_mode() { lightweight::entry_lightweight_mode().await; } else { lightweight::exit_lightweight_mode().await; } } MenuIds::EXIT => { feat::quit().await; } id if id.starts_with("profiles_") => { let profile_index = match id.strip_prefix("profiles_") { Some(index_str) => index_str, None => return, }; feat::toggle_proxy_profile(profile_index.into()).await; } id if id.starts_with("proxy_") => { let rest = match id.strip_prefix("proxy_") { Some(r) => r, None => return, }; let (group_name, proxy_name) = match rest.split_once('_') { Some((g, p)) => (g, p), None => return, }; feat::switch_proxy_node(group_name, proxy_name).await; } _ => { logging!(debug, Type::Tray, "Unhandled tray menu event: {:?}", event.id); } } }); }