feat(system tray): support switch rule/global/direct/script mode in system tray

This commit is contained in:
limsanity 2022-07-13 00:43:27 +08:00
parent 8637a9823e
commit fbb17a0ba5
6 changed files with 1411 additions and 1264 deletions

View File

@ -178,8 +178,12 @@ pub fn get_clash_info(core: State<'_, Core>) -> CmdResult<ClashInfo> {
/// after putting the change to the clash core /// after putting the change to the clash core
/// 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, core: State<'_, Core>) -> CmdResult { pub fn patch_clash_config(
wrap_err!(core.patch_clash(payload)) payload: Mapping,
app_handle: tauri::AppHandle,
core: State<'_, Core>,
) -> CmdResult {
wrap_err!(core.patch_clash(payload, &app_handle))
} }
/// get the verge config /// get the verge config

View File

@ -16,6 +16,9 @@ pub struct ClashInfo {
/// clash secret /// clash secret
pub secret: Option<String>, pub secret: Option<String>,
/// mode
pub mode: Option<String>,
} }
impl ClashInfo { impl ClashInfo {
@ -26,6 +29,7 @@ impl ClashInfo {
let key_port_2 = Value::from("mixed-port"); let key_port_2 = Value::from("mixed-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 key_mode = Value::from("mode");
let port = match config.get(&key_port_1) { let port = match config.get(&key_port_1) {
Some(value) => match value { Some(value) => match value {
@ -72,11 +76,20 @@ impl ClashInfo {
_ => None, _ => None,
}; };
let mode = match config.get(&key_mode) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
_ => None,
},
_ => None,
};
ClashInfo { ClashInfo {
status: "init".into(), status: "init".into(),
port, port,
server, server,
secret, secret,
mode,
} }
} }
} }
@ -113,20 +126,26 @@ impl Clash {
/// patch update the clash config /// patch update the clash config
/// if the port is changed then return true /// if the port is changed then return true
pub fn patch_config(&mut self, patch: Mapping) -> Result<bool> { pub fn patch_config(&mut self, patch: Mapping) -> Result<(bool, bool)> {
let port_key = Value::from("mixed-port"); let port_key = Value::from("mixed-port");
let server_key = Value::from("external-controller"); let server_key = Value::from("external-controller");
let secret_key = Value::from("secret"); let secret_key = Value::from("secret");
let mode_key = Value::from("mode");
let mut change_port = false; let mut change_port = false;
let mut change_info = false; let mut change_info = false;
let mut change_mode = false;
for (key, value) in patch.into_iter() { for (key, value) in patch.into_iter() {
if key == port_key { if key == port_key {
change_port = true; change_port = true;
} }
if key == port_key || key == server_key || key == secret_key { if key == mode_key {
change_mode = true;
}
if key == port_key || key == server_key || key == secret_key || key == mode_key {
change_info = true; change_info = true;
} }
@ -139,7 +158,7 @@ impl Clash {
self.save_config()?; self.save_config()?;
Ok(change_port) Ok((change_port, change_mode))
} }
/// revise the `tun` and `dns` config /// revise the `tun` and `dns` config

View File

@ -7,6 +7,7 @@ use crate::utils::{dirs, help};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use serde_yaml::Value;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tauri::{AppHandle, Manager, Window}; use tauri::{AppHandle, Manager, Window};
@ -168,15 +169,15 @@ impl Core {
/// Patch Clash /// Patch Clash
/// handle the clash config changed /// handle the clash config changed
pub fn patch_clash(&self, patch: Mapping) -> Result<()> { pub fn patch_clash(&self, patch: Mapping, app_handle: &AppHandle) -> Result<()> {
let (changed, port) = { let ((changed_port, changed_mode), port) = {
let mut clash = self.clash.lock(); let mut clash = self.clash.lock();
(clash.patch_config(patch)?, clash.info.port.clone()) (clash.patch_config(patch)?, clash.info.port.clone())
}; };
// todo: port check // todo: port check
if changed { if changed_port {
let mut service = self.service.lock(); let mut service = self.service.lock();
service.restart()?; service.restart()?;
drop(service); drop(service);
@ -189,6 +190,10 @@ impl Core {
sysopt.init_sysproxy(port, &verge); sysopt.init_sysproxy(port, &verge);
} }
if changed_mode {
self.update_systray(app_handle)?;
}
Ok(()) Ok(())
} }
@ -259,17 +264,28 @@ impl Core {
/// update the system tray state /// update the system tray state
pub fn update_systray(&self, app_handle: &AppHandle) -> Result<()> { pub fn update_systray(&self, app_handle: &AppHandle) -> Result<()> {
let clash = self.clash.lock();
let info = clash.info.clone();
let mode = info.mode.as_ref();
let verge = self.verge.lock(); let verge = self.verge.lock();
let tray = app_handle.tray_handle(); let tray = app_handle.tray_handle();
let system_proxy = verge.enable_system_proxy.as_ref(); let system_proxy = verge.enable_system_proxy.as_ref();
let tun_mode = verge.enable_tun_mode.as_ref(); let tun_mode = verge.enable_tun_mode.as_ref();
tray tray.get_item("rule_mode")
.get_item("system_proxy") .set_selected((*mode.unwrap()).eq("rule"))?;
tray.get_item("global_mode")
.set_selected((*mode.unwrap()).eq("global"))?;
tray.get_item("direct_mode")
.set_selected((*mode.unwrap()).eq("direct"))?;
tray.get_item("script_mode")
.set_selected((*mode.unwrap()).eq("script"))?;
tray.get_item("system_proxy")
.set_selected(*system_proxy.unwrap_or(&false))?; .set_selected(*system_proxy.unwrap_or(&false))?;
tray tray.get_item("tun_mode")
.get_item("tun_mode")
.set_selected(*tun_mode.unwrap_or(&false))?; .set_selected(*tun_mode.unwrap_or(&false))?;
// update verge config // update verge config
@ -403,6 +419,31 @@ impl Core {
Ok(()) Ok(())
} }
// update rule/global/direct/script mode
pub fn update_mode(&self, app_handle: &AppHandle, mode: &str) -> Result<()> {
let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), Value::from(mode));
self.patch_clash(mapping, app_handle);
let (config, info) = {
let clash = self.clash.lock();
let config = clash.config.clone();
let info = clash.info.clone();
(config, info)
};
let notice = {
let window = self.window.lock();
Notice::from(window.clone())
};
let service = self.service.lock();
service.patch_config(info, config, notice);
Ok(())
}
} }
impl Core { impl Core {

View File

@ -1,3 +1,4 @@
use super::Clash;
use super::{notice::Notice, ClashInfo}; use super::{notice::Notice, ClashInfo};
use crate::log_if_err; use crate::log_if_err;
use crate::utils::{config, dirs}; use crate::utils::{config, dirs};
@ -170,7 +171,68 @@ impl Service {
match builder.send().await { match builder.send().await {
Ok(resp) => { Ok(resp) => {
if resp.status() != 204 { if resp.status() != 204 {
log::error!("failed to activate clash with status \"{}\"", resp.status()); log::error!(
"failed to activate clash with status \"{}\"",
resp.status()
);
}
notice.refresh_clash();
// do not retry
break;
}
Err(err) => log::error!("failed to activate for `{err}`"),
}
}
Err(err) => log::error!("failed to activate for `{err}`"),
}
sleep(Duration::from_millis(500)).await;
}
});
Ok(())
}
/// patch clash config
pub fn patch_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> {
if !self.service_mode && self.sidecar.is_none() {
bail!("did not start sidecar");
}
if info.server.is_none() {
if info.port.is_none() {
bail!("failed to parse config.yaml file");
} else {
bail!("failed to parse the server");
}
}
let server = info.server.unwrap();
let server = format!("http://{server}/configs");
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
if let Some(secret) = info.secret.as_ref() {
let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
headers.insert("Authorization", secret);
}
tauri::async_runtime::spawn(async move {
// retry 5 times
for _ in 0..5 {
match reqwest::ClientBuilder::new().no_proxy().build() {
Ok(client) => {
let builder = client.patch(&server).headers(headers.clone()).json(&config);
match builder.send().await {
Ok(resp) => {
if resp.status() != 204 {
log::error!(
"failed to activate clash with status \"{}\"",
resp.status()
);
} }
notice.refresh_clash(); notice.refresh_clash();

View File

@ -11,6 +11,7 @@ use crate::{
core::Verge, core::Verge,
utils::{resolve, server}, utils::{resolve, server},
}; };
use serde_yaml::{Mapping, Value};
use tauri::{ use tauri::{
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
}; };
@ -30,6 +31,10 @@ fn main() -> std::io::Result<()> {
let tray_menu = SystemTrayMenu::new() let tray_menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", "Show")) .add_item(CustomMenuItem::new("open_window", "Show"))
.add_item(CustomMenuItem::new("rule_mode", "Rule Mode"))
.add_item(CustomMenuItem::new("global_mode", "Global Mode"))
.add_item(CustomMenuItem::new("direct_mode", "Direct Mode"))
.add_item(CustomMenuItem::new("script_mode", "Script Mode"))
.add_item(CustomMenuItem::new("system_proxy", "System Proxy")) .add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode")) .add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash")) .add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
@ -45,6 +50,22 @@ fn main() -> std::io::Result<()> {
"open_window" => { "open_window" => {
resolve::create_window(app_handle); resolve::create_window(app_handle);
} }
"rule_mode" => {
let core = app_handle.state::<core::Core>();
crate::log_if_err!(core.update_mode(app_handle, "rule"));
}
"global_mode" => {
let core = app_handle.state::<core::Core>();
crate::log_if_err!(core.update_mode(app_handle, "global"));
}
"direct_mode" => {
let core = app_handle.state::<core::Core>();
crate::log_if_err!(core.update_mode(app_handle, "direct"));
}
"script_mode" => {
let core = app_handle.state::<core::Core>();
crate::log_if_err!(core.update_mode(app_handle, "script"));
}
"system_proxy" => { "system_proxy" => {
let core = app_handle.state::<core::Core>(); let core = app_handle.state::<core::Core>();