Parcourir la source

Merge branch 'refactor'

GyDi il y a 2 ans
Parent
commit
3a37075e71
41 fichiers modifiés avec 2003 ajouts et 2023 suppressions
  1. 22 22
      src-tauri/Cargo.lock
  2. 90 197
      src-tauri/src/cmds.rs
  3. 94 61
      src-tauri/src/config/clash.rs
  4. 111 0
      src-tauri/src/config/config.rs
  5. 127 0
      src-tauri/src/config/draft.rs
  6. 15 69
      src-tauri/src/config/mod.rs
  7. 16 14
      src-tauri/src/config/prfitem.rs
  8. 69 86
      src-tauri/src/config/profiles.rs
  9. 18 0
      src-tauri/src/config/runtime.rs
  10. 45 16
      src-tauri/src/config/verge.rs
  11. 64 0
      src-tauri/src/core/clash_api.rs
  12. 246 0
      src-tauri/src/core/core.rs
  13. 38 26
      src-tauri/src/core/handle.rs
  14. 34 28
      src-tauri/src/core/hotkey.rs
  15. 36 0
      src-tauri/src/core/logger.rs
  16. 9 347
      src-tauri/src/core/mod.rs
  17. 0 537
      src-tauri/src/core/service.rs
  18. 106 101
      src-tauri/src/core/sysopt.rs
  19. 81 69
      src-tauri/src/core/timer.rs
  20. 15 16
      src-tauri/src/core/tray.rs
  21. 173 0
      src-tauri/src/core/win_service.rs
  22. 0 32
      src-tauri/src/data/mod.rs
  23. 0 0
      src-tauri/src/enhance/field.rs
  24. 0 0
      src-tauri/src/enhance/merge.rs
  25. 68 0
      src-tauri/src/enhance/mod.rs
  26. 0 0
      src-tauri/src/enhance/script.rs
  27. 0 0
      src-tauri/src/enhance/tun.rs
  28. 274 69
      src-tauri/src/feat.rs
  29. 12 32
      src-tauri/src/main.rs
  30. 0 59
      src-tauri/src/utils/config.rs
  31. 56 43
      src-tauri/src/utils/dirs.rs
  32. 75 26
      src-tauri/src/utils/help.rs
  33. 39 43
      src-tauri/src/utils/init.rs
  34. 1 2
      src-tauri/src/utils/mod.rs
  35. 34 39
      src-tauri/src/utils/resolve.rs
  36. 16 16
      src-tauri/src/utils/server.rs
  37. 1 35
      src-tauri/src/utils/tmpl.rs
  38. 5 5
      src/components/profile/enhanced.tsx
  39. 2 2
      src/components/setting/mods/clash-field-viewer.tsx
  40. 7 7
      src/pages/profiles.tsx
  41. 4 24
      src/services/cmds.ts

+ 22 - 22
src-tauri/Cargo.lock

@@ -488,9 +488,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.22"
+version = "0.4.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
 dependencies = [
  "iana-time-zone",
  "js-sys",
@@ -985,9 +985,9 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.10.5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
  "block-buffer 0.10.3",
  "crypto-common",
@@ -1860,9 +1860,9 @@ dependencies = [
 
 [[package]]
 name = "image"
-version = "0.24.4"
+version = "0.24.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
+checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
 dependencies = [
  "bytemuck",
  "byteorder",
@@ -3235,9 +3235,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.12"
+version = "0.11.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
+checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
 dependencies = [
  "base64",
  "bytes",
@@ -3670,7 +3670,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
- "digest 0.10.5",
+ "digest 0.10.6",
 ]
 
 [[package]]
@@ -3681,7 +3681,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
- "digest 0.10.5",
+ "digest 0.10.6",
 ]
 
 [[package]]
@@ -3705,7 +3705,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
- "digest 0.10.5",
+ "digest 0.10.6",
 ]
 
 [[package]]
@@ -3955,9 +3955,9 @@ dependencies = [
 
 [[package]]
 name = "tao"
-version = "0.15.3"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42c460173627564bde252ca5ebf346ba5b37c5cee1a445782bacc8e9b8d38b5e"
+checksum = "a0382fcc02b87420a0d024ff6ec2dbfccdccafb46de8c03fb07dbf2f36810a42"
 dependencies = [
  "bitflags",
  "cairo-rs",
@@ -3995,7 +3995,7 @@ dependencies = [
  "scopeguard",
  "serde",
  "unicode-segmentation",
- "uuid 1.2.1",
+ "uuid 1.2.2",
  "windows 0.39.0",
  "windows-implement",
  "x11-dl",
@@ -4060,7 +4060,7 @@ dependencies = [
  "time 0.3.17",
  "tokio",
  "url",
- "uuid 1.2.1",
+ "uuid 1.2.2",
  "webkit2gtk",
  "webview2-com",
  "windows 0.39.0",
@@ -4105,7 +4105,7 @@ dependencies = [
  "tauri-utils",
  "thiserror",
  "time 0.3.17",
- "uuid 1.2.1",
+ "uuid 1.2.2",
  "walkdir",
 ]
 
@@ -4138,7 +4138,7 @@ dependencies = [
  "serde_json",
  "tauri-utils",
  "thiserror",
- "uuid 1.2.1",
+ "uuid 1.2.2",
  "webview2-com",
  "windows 0.39.0",
 ]
@@ -4156,7 +4156,7 @@ dependencies = [
  "raw-window-handle",
  "tauri-runtime",
  "tauri-utils",
- "uuid 1.2.1",
+ "uuid 1.2.2",
  "webkit2gtk",
  "webview2-com",
  "windows 0.39.0",
@@ -4708,9 +4708,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
 
 [[package]]
 name = "uuid"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
+checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
 dependencies = [
  "getrandom 0.2.8",
 ]
@@ -5358,9 +5358,9 @@ dependencies = [
 
 [[package]]
 name = "wry"
-version = "0.22.0"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "923d297b203eae65b095af16c02978b7932be1968012b4da7138390edf34dea5"
+checksum = "e0fd80bb2bd8e8eae26d59c5164e70233f29f7593cb886a958024a4fd8b5cd21"
 dependencies = [
  "base64",
  "block",

+ 90 - 197
src-tauri/src/cmds.rs

@@ -1,258 +1,179 @@
 use crate::{
-    core::Core,
-    data::{ClashInfo, Data, PrfItem, PrfOption, Profiles, Verge},
+    config::*,
+    core::*,
+    feat,
     utils::{dirs, help},
 };
-use crate::{log_if_err, ret_err, wrap_err};
-use anyhow::Result;
+use crate::{ret_err, wrap_err};
+use anyhow::{Context, Result};
 use serde_yaml::Mapping;
 use std::collections::{HashMap, VecDeque};
 use sysproxy::Sysproxy;
 
 type CmdResult<T = ()> = Result<T, String>;
 
-/// get all profiles from `profiles.yaml`
 #[tauri::command]
-pub fn get_profiles() -> CmdResult<Profiles> {
-    let global = Data::global();
-    let profiles = global.profiles.lock();
-    Ok(profiles.clone())
+pub fn get_profiles() -> CmdResult<IProfiles> {
+    Ok(Config::profiles().data().clone())
 }
 
-/// manually exec enhanced profile
 #[tauri::command]
-pub fn enhance_profiles() -> CmdResult {
-    let core = Core::global();
-    wrap_err!(core.activate())
+pub async fn enhance_profiles() -> CmdResult {
+    wrap_err!(CoreManager::global().update_config().await)?;
+    handle::Handle::refresh_clash();
+    Ok(())
 }
 
-/// import the profile from url
-/// and save to `profiles.yaml`
+#[deprecated]
 #[tauri::command]
 pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
     let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
-
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    wrap_err!(profiles.append_item(item))
+    wrap_err!(Config::profiles().data().append_item(item))
 }
 
-/// new a profile
-/// append a temp profile item file to the `profiles` dir
-/// view the temp profile file by using vscode or other editor
 #[tauri::command]
 pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
     let item = wrap_err!(PrfItem::from(item, file_data).await)?;
-
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    wrap_err!(profiles.append_item(item))
+    wrap_err!(Config::profiles().data().append_item(item))
 }
 
-/// Update the profile
 #[tauri::command]
 pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
-    let core = Core::global();
-    wrap_err!(core.update_profile_item(index, option).await)
+    wrap_err!(feat::update_profile(index, option).await)
 }
 
-/// change the current profile
 #[tauri::command]
-pub fn select_profile(index: String) -> CmdResult {
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    wrap_err!(profiles.put_current(index))?;
-    drop(profiles);
-
-    let core = Core::global();
-    wrap_err!(core.activate())
-}
+pub async fn delete_profile(index: String) -> CmdResult {
+    let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
+    if should_update {
+        wrap_err!(CoreManager::global().update_config().await)?;
+        handle::Handle::refresh_clash();
+    }
 
-/// change the profile chain
-#[tauri::command]
-pub fn change_profile_chain(chain: Option<Vec<String>>) -> CmdResult {
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    wrap_err!(profiles.put_chain(chain))?;
-    drop(profiles);
-
-    let core = Core::global();
-    wrap_err!(core.activate())
+    Ok(())
 }
 
-/// change the profile valid fields
+/// 修改profiles的
 #[tauri::command]
-pub fn change_profile_valid(valid: Option<Vec<String>>) -> CmdResult {
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    wrap_err!(profiles.put_valid(valid))?;
-    drop(profiles);
-
-    let core = Core::global();
-    wrap_err!(core.activate())
-}
+pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
+    wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
 
-/// delete profile item
-#[tauri::command]
-pub fn delete_profile(index: String) -> CmdResult {
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    if wrap_err!(profiles.delete_item(index))? {
-        drop(profiles);
-
-        let core = Core::global();
-        log_if_err!(core.activate());
+    match CoreManager::global().update_config().await {
+        Ok(_) => {
+            handle::Handle::refresh_clash();
+            Config::profiles().apply();
+            wrap_err!(Config::profiles().data().save_file())?;
+            Ok(())
+        }
+        Err(err) => {
+            Config::profiles().discard();
+            log::error!(target: "app", "{err}");
+            Err(format!("{err}"))
+        }
     }
-    Ok(())
 }
 
-/// patch the profile config
+/// 修改某个profile item的
 #[tauri::command]
 pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
-    let global = Data::global();
-    let mut profiles = global.profiles.lock();
-    wrap_err!(profiles.patch_item(index, profile))?;
-    drop(profiles);
-
-    // update cron task
-    let core = Core::global();
-    let mut timer = core.timer.lock();
-    wrap_err!(timer.refresh())
+    wrap_err!(Config::profiles().data().patch_item(index, profile))?;
+    wrap_err!(timer::Timer::global().refresh())
 }
 
-/// run vscode command to edit the profile
 #[tauri::command]
 pub fn view_profile(index: String) -> CmdResult {
-    let global = Data::global();
-    let profiles = global.profiles.lock();
-    let item = wrap_err!(profiles.get_item(&index))?;
-
-    let file = item.file.clone();
-    if file.is_none() {
-        ret_err!("file is null");
-    }
-
-    let path = dirs::app_profiles_dir().join(file.unwrap());
+    let file = {
+        wrap_err!(Config::profiles().latest().get_item(&index))?
+            .file
+            .clone()
+            .ok_or("the file field is null")
+    }?;
+
+    let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
     if !path.exists() {
-        ret_err!("file not found");
+        ret_err!("the file not found");
     }
 
     wrap_err!(help::open_file(path))
 }
 
-/// read the profile item file data
 #[tauri::command]
 pub fn read_profile_file(index: String) -> CmdResult<String> {
-    let global = Data::global();
-    let profiles = global.profiles.lock();
+    let profiles = Config::profiles();
+    let profiles = profiles.latest();
     let item = wrap_err!(profiles.get_item(&index))?;
     let data = wrap_err!(item.read_file())?;
     Ok(data)
 }
 
-/// save the profile item file data
 #[tauri::command]
 pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
     if file_data.is_none() {
         return Ok(());
     }
 
-    let global = Data::global();
-    let profiles = global.profiles.lock();
+    let profiles = Config::profiles();
+    let profiles = profiles.latest();
     let item = wrap_err!(profiles.get_item(&index))?;
     wrap_err!(item.save_file(file_data.unwrap()))
 }
 
-/// get the clash core info from the state
-/// the caller can also get the infomation by clash's api
 #[tauri::command]
-pub fn get_clash_info() -> CmdResult<ClashInfo> {
-    let global = Data::global();
-    let clash = global.clash.lock();
-    Ok(clash.info.clone())
+pub fn get_clash_info() -> CmdResult<ClashInfoN> {
+    wrap_err!(Config::clash().latest().get_info())
 }
 
-/// get the runtime clash config mapping
 #[tauri::command]
 pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
-    let core = Core::global();
-    let rt = core.runtime.lock();
-    Ok(rt.config.clone())
+    Ok(Config::runtime().latest().config.clone())
 }
 
-/// get the runtime clash config yaml string
 #[tauri::command]
-pub fn get_runtime_yaml() -> CmdResult<Option<String>> {
-    let core = Core::global();
-    let rt = core.runtime.lock();
-    Ok(rt.config_yaml.clone())
+pub fn get_runtime_yaml() -> CmdResult<String> {
+    let runtime = Config::runtime();
+    let runtime = runtime.latest();
+    let config = runtime.config.as_ref();
+    wrap_err!(config
+        .ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
+        .and_then(
+            |config| serde_yaml::to_string(config).context("failed to convert config to yaml")
+        ))
 }
 
-/// get the runtime config exists keys
 #[tauri::command]
 pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
-    let core = Core::global();
-    let rt = core.runtime.lock();
-    Ok(rt.exists_keys.clone())
+    Ok(Config::runtime().latest().exists_keys.clone())
 }
 
-/// get the runtime enhanced chain log
 #[tauri::command]
 pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
-    let core = Core::global();
-    let rt = core.runtime.lock();
-    Ok(rt.chain_logs.clone())
+    Ok(Config::runtime().latest().chain_logs.clone())
 }
 
-/// update the clash core config
-/// after putting the change to the clash core
-/// then we should save the latest config
 #[tauri::command]
-pub fn patch_clash_config(payload: Mapping) -> CmdResult {
-    let core = Core::global();
-    wrap_err!(core.patch_clash(payload))
+pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
+    wrap_err!(feat::patch_clash(payload).await)
 }
 
 #[tauri::command]
-pub fn get_verge_config() -> CmdResult<Verge> {
-    let global = Data::global();
-    let verge = global.verge.lock();
-    Ok(verge.clone())
+pub fn get_verge_config() -> CmdResult<IVerge> {
+    Ok(Config::verge().data().clone())
 }
 
-/// patch the verge config
-/// this command only save the config and not responsible for other things
 #[tauri::command]
-pub fn patch_verge_config(payload: Verge) -> CmdResult {
-    let core = Core::global();
-    wrap_err!(core.patch_verge(payload))
+pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
+    wrap_err!(feat::patch_verge(payload).await)
 }
 
 #[tauri::command]
-pub fn update_hotkeys(hotkeys: Vec<String>) -> CmdResult {
-    let core = Core::global();
-    let mut hotkey = core.hotkey.lock();
-    wrap_err!(hotkey.update(hotkeys))
-}
-
-/// change clash core
-#[tauri::command]
-pub fn change_clash_core(clash_core: Option<String>) -> CmdResult {
-    let core = Core::global();
-    wrap_err!(core.change_core(clash_core))
+pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
+    wrap_err!(CoreManager::global().change_core(clash_core).await)
 }
 
 /// restart the sidecar
 #[tauri::command]
-pub fn restart_sidecar() -> CmdResult {
-    let core = Core::global();
-    wrap_err!(core.restart_clash())
-}
-
-/// kill all sidecars when update app
-#[tauri::command]
-pub fn kill_sidecar() {
-    tauri::api::process::kill_children();
+pub async fn restart_sidecar() -> CmdResult {
+    wrap_err!(CoreManager::global().run_core().await)
 }
 
 /// get the system proxy
@@ -273,64 +194,44 @@ pub fn get_sys_proxy() -> CmdResult<Mapping> {
 
 #[tauri::command]
 pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
-    let core = Core::global();
-    let service = core.service.lock();
-    Ok(service.get_logs())
+    Ok(logger::Logger::global().get_log())
 }
 
-/// open app config dir
 #[tauri::command]
 pub fn open_app_dir() -> CmdResult<()> {
-    let app_dir = dirs::app_home_dir();
+    let app_dir = wrap_err!(dirs::app_home_dir())?;
     wrap_err!(open::that(app_dir))
 }
 
-/// open logs dir
 #[tauri::command]
 pub fn open_logs_dir() -> CmdResult<()> {
-    let log_dir = dirs::app_logs_dir();
+    let log_dir = wrap_err!(dirs::app_logs_dir())?;
     wrap_err!(open::that(log_dir))
 }
 
-/// open url
 #[tauri::command]
 pub fn open_web_url(url: String) -> CmdResult<()> {
     wrap_err!(open::that(url))
 }
 
-/// service mode
 #[cfg(windows)]
 pub mod service {
     use super::*;
-    use crate::core::win_service::JsonResponse;
+    use crate::core::win_service;
 
     #[tauri::command]
-    pub async fn start_service() -> CmdResult<()> {
-        wrap_err!(crate::core::Service::start_service().await)
+    pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
+        wrap_err!(win_service::check_service().await)
     }
 
     #[tauri::command]
-    pub async fn stop_service() -> CmdResult<()> {
-        wrap_err!(crate::core::Service::stop_service().await)
+    pub async fn install_service() -> CmdResult {
+        wrap_err!(win_service::install_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]
-    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)
+    pub async fn uninstall_service() -> CmdResult {
+        wrap_err!(win_service::uninstall_service().await)
     }
 }
 
@@ -339,23 +240,15 @@ pub mod service {
     use super::*;
 
     #[tauri::command]
-    pub async fn start_service() -> CmdResult<()> {
-        Ok(())
-    }
-    #[tauri::command]
-    pub async fn stop_service() -> CmdResult<()> {
-        Ok(())
-    }
-    #[tauri::command]
-    pub async fn check_service() -> CmdResult<()> {
+    pub async fn check_service() -> CmdResult {
         Ok(())
     }
     #[tauri::command]
-    pub async fn install_service() -> CmdResult<()> {
+    pub async fn install_service() -> CmdResult {
         Ok(())
     }
     #[tauri::command]
-    pub async fn uninstall_service() -> CmdResult<()> {
+    pub async fn uninstall_service() -> CmdResult {
         Ok(())
     }
 }

+ 94 - 61
src-tauri/src/data/clash.rs → src-tauri/src/config/clash.rs

@@ -1,27 +1,70 @@
-use crate::utils::{config, dirs};
+use crate::utils::{dirs, help};
 use anyhow::Result;
 use serde::{Deserialize, Serialize};
 use serde_yaml::{Mapping, Value};
 
+#[derive(Default, Debug, Clone)]
+pub struct IClashTemp(pub Mapping);
+
+impl IClashTemp {
+    pub fn new() -> Self {
+        match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
+            Ok(map) => Self(map),
+            Err(err) => {
+                log::error!(target: "app", "{err}");
+                Self::template()
+            }
+        }
+    }
+
+    pub fn template() -> Self {
+        let mut map = Mapping::new();
+
+        map.insert("mixed-port".into(), 7890.into());
+        map.insert("log-level".into(), "info".into());
+        map.insert("allow-lan".into(), false.into());
+        map.insert("mode".into(), "rule".into());
+        map.insert("external-controller".into(), "127.0.0.1:9090".into());
+        map.insert("secret".into(), "".into());
+
+        Self(map)
+    }
+
+    pub fn patch_config(&mut self, patch: Mapping) {
+        for (key, value) in patch.into_iter() {
+            self.0.insert(key, value);
+        }
+    }
+
+    pub fn save_config(&self) -> Result<()> {
+        help::save_yaml(
+            &dirs::clash_path()?,
+            &self.0,
+            Some("# Generated by Clash Verge"),
+        )
+    }
+
+    pub fn get_info(&self) -> Result<ClashInfoN> {
+        Ok(ClashInfoN::from(&self.0))
+    }
+}
+
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct ClashInfo {
+pub struct ClashInfoN {
     /// clash sidecar status
     pub status: String,
-
     /// clash core port
     pub port: Option<String>,
-
     /// same as `external-controller`
     pub server: Option<String>,
-
     /// clash secret
     pub secret: Option<String>,
 }
 
-impl ClashInfo {
+impl ClashInfoN {
     /// parse the clash's config.yaml
     /// get some information
-    pub fn from(config: &Mapping) -> ClashInfo {
+    pub fn from(config: &Mapping) -> ClashInfoN {
         let key_port_1 = Value::from("mixed-port");
         let key_port_2 = Value::from("port");
         let key_server = Value::from("external-controller");
@@ -97,7 +140,7 @@ impl ClashInfo {
             _ => None,
         };
 
-        ClashInfo {
+        ClashInfoN {
             status: format!("{status}"),
             port,
             server,
@@ -106,62 +149,52 @@ impl ClashInfo {
     }
 }
 
-#[derive(Debug)]
-pub struct Clash {
-    /// maintain the clash config
-    pub config: Mapping,
-
-    /// some info
-    pub info: ClashInfo,
+#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+pub struct IClash {
+    pub mixed_port: Option<u16>,
+    pub allow_lan: Option<bool>,
+    pub log_level: Option<String>,
+    pub ipv6: Option<bool>,
+    pub mode: Option<String>,
+    pub external_controller: Option<String>,
+    pub secret: Option<String>,
+    pub dns: Option<IClashDNS>,
+    pub tun: Option<IClashTUN>,
+    pub interface_name: Option<String>,
 }
 
-impl Clash {
-    pub fn new() -> Clash {
-        let config = Clash::read_config();
-        let info = ClashInfo::from(&config);
-
-        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 {
-            self.info = ClashInfo::from(&self.config);
-        }
+#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+pub struct IClashTUN {
+    pub enable: Option<bool>,
+    pub stack: Option<String>,
+    pub auto_route: Option<bool>,
+    pub auto_detect_interface: Option<bool>,
+    pub dns_hijack: Option<Vec<String>>,
+}
 
-        self.save_config()
-    }
+#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+pub struct IClashDNS {
+    pub enable: Option<bool>,
+    pub listen: Option<String>,
+    pub default_nameserver: Option<Vec<String>>,
+    pub enhanced_mode: Option<String>,
+    pub fake_ip_range: Option<String>,
+    pub use_hosts: Option<bool>,
+    pub fake_ip_filter: Option<Vec<String>>,
+    pub nameserver: Option<Vec<String>>,
+    pub fallback: Option<Vec<String>>,
+    pub fallback_filter: Option<IClashFallbackFilter>,
+    pub nameserver_policy: Option<Vec<String>>,
 }
 
-impl Default for Clash {
-    fn default() -> Self {
-        Clash::new()
-    }
+#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+pub struct IClashFallbackFilter {
+    pub geoip: Option<bool>,
+    pub geoip_code: Option<String>,
+    pub ipcidr: Option<Vec<String>>,
+    pub domain: Option<Vec<String>>,
 }

+ 111 - 0
src-tauri/src/config/config.rs

@@ -0,0 +1,111 @@
+use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
+use crate::{
+    enhance,
+    utils::{dirs, help},
+};
+use anyhow::{anyhow, Result};
+use once_cell::sync::OnceCell;
+use std::{env::temp_dir, path::PathBuf};
+
+pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
+pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
+
+pub struct Config {
+    clash_config: Draft<IClashTemp>,
+    verge_config: Draft<IVerge>,
+    profiles_config: Draft<IProfiles>,
+    runtime_config: Draft<IRuntime>,
+}
+
+impl Config {
+    pub fn global() -> &'static Config {
+        static CONFIG: OnceCell<Config> = OnceCell::new();
+
+        CONFIG.get_or_init(|| Config {
+            clash_config: Draft::from(IClashTemp::new()),
+            verge_config: Draft::from(IVerge::new()),
+            profiles_config: Draft::from(IProfiles::new()),
+            runtime_config: Draft::from(IRuntime::new()),
+        })
+    }
+
+    pub fn clash() -> Draft<IClashTemp> {
+        Self::global().clash_config.clone()
+    }
+
+    pub fn verge() -> Draft<IVerge> {
+        Self::global().verge_config.clone()
+    }
+
+    pub fn profiles() -> Draft<IProfiles> {
+        Self::global().profiles_config.clone()
+    }
+
+    pub fn runtime() -> Draft<IRuntime> {
+        Self::global().runtime_config.clone()
+    }
+
+    /// 初始化配置
+    pub fn init_config() -> Result<()> {
+        crate::log_err!(Self::generate());
+        if let Err(err) = Self::generate_file(ConfigType::Run) {
+            log::error!(target: "app", "{err}");
+
+            let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
+            // 如果不存在就将默认的clash文件拿过来
+            if !runtime_path.exists() {
+                help::save_yaml(
+                    &runtime_path,
+                    &Config::clash().latest().0,
+                    Some("# Clash Verge Runtime"),
+                )?;
+            }
+        }
+        Ok(())
+    }
+
+    /// 将配置丢到对应的文件中
+    pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
+        let path = match typ {
+            ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
+            ConfigType::Check => temp_dir().join(CHECK_CONFIG),
+        };
+
+        let runtime = Config::runtime();
+        let runtime = runtime.latest();
+        let config = runtime
+            .config
+            .as_ref()
+            .ok_or(anyhow!("failed to get runtime config"))?;
+
+        help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
+        Ok(path)
+    }
+
+    /// 生成配置存好
+    pub fn generate() -> Result<()> {
+        let clash_config = { Config::clash().latest().clone() };
+
+        let tun_mode = { Config::verge().latest().enable_tun_mode.clone() };
+        let tun_mode = tun_mode.unwrap_or(false);
+
+        let pa = { Config::profiles().latest().gen_activate()? };
+
+        let (config, exists_keys, logs) =
+            enhance::enhance_config(clash_config.0, pa.current, pa.chain, pa.valid, tun_mode);
+
+        *Config::runtime().draft() = IRuntime {
+            config: Some(config),
+            exists_keys,
+            chain_logs: logs,
+        };
+
+        Ok(())
+    }
+}
+
+#[derive(Debug)]
+pub enum ConfigType {
+    Run,
+    Check,
+}

+ 127 - 0
src-tauri/src/config/draft.rs

@@ -0,0 +1,127 @@
+use super::{IClashTemp, IProfiles, IRuntime, IVerge};
+use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
+use std::sync::Arc;
+
+#[derive(Debug, Clone)]
+pub struct Draft<T: Clone + ToOwned> {
+    inner: Arc<Mutex<(T, Option<T>)>>,
+}
+
+macro_rules! draft_define {
+    ($id: ident) => {
+        impl Draft<$id> {
+            #[allow(unused)]
+            pub fn data(&self) -> MappedMutexGuard<$id> {
+                MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
+            }
+
+            pub fn latest(&self) -> MappedMutexGuard<$id> {
+                MutexGuard::map(self.inner.lock(), |inner| {
+                    if inner.1.is_none() {
+                        &mut inner.0
+                    } else {
+                        inner.1.as_mut().unwrap()
+                    }
+                })
+            }
+
+            pub fn draft(&self) -> MappedMutexGuard<$id> {
+                MutexGuard::map(self.inner.lock(), |mut inner| {
+                    if inner.1.is_none() {
+                        inner.1 = Some(inner.0.clone());
+                    }
+
+                    inner.1.as_mut().unwrap()
+                })
+            }
+
+            pub fn apply(&self) -> Option<$id> {
+                let mut inner = self.inner.lock();
+
+                match inner.1.take() {
+                    Some(draft) => {
+                        let old_value = inner.0.to_owned();
+                        inner.0 = draft.to_owned();
+                        Some(old_value)
+                    }
+                    None => None,
+                }
+            }
+
+            pub fn discard(&self) -> Option<$id> {
+                let mut inner = self.inner.lock();
+                inner.1.take()
+            }
+        }
+
+        impl From<$id> for Draft<$id> {
+            fn from(data: $id) -> Self {
+                Draft {
+                    inner: Arc::new(Mutex::new((data, None))),
+                }
+            }
+        }
+    };
+}
+
+// draft_define!(IClash);
+draft_define!(IClashTemp);
+draft_define!(IProfiles);
+draft_define!(IRuntime);
+draft_define!(IVerge);
+
+#[test]
+fn test_draft() {
+    let verge = IVerge {
+        enable_auto_launch: Some(true),
+        enable_tun_mode: Some(false),
+        ..IVerge::default()
+    };
+
+    let draft = Draft::from(verge);
+
+    assert_eq!(draft.data().enable_auto_launch, Some(true));
+    assert_eq!(draft.data().enable_tun_mode, Some(false));
+
+    assert_eq!(draft.draft().enable_auto_launch, Some(true));
+    assert_eq!(draft.draft().enable_tun_mode, Some(false));
+
+    let mut d = draft.draft();
+    d.enable_auto_launch = Some(false);
+    d.enable_tun_mode = Some(true);
+    drop(d);
+
+    assert_eq!(draft.data().enable_auto_launch, Some(true));
+    assert_eq!(draft.data().enable_tun_mode, Some(false));
+
+    assert_eq!(draft.draft().enable_auto_launch, Some(false));
+    assert_eq!(draft.draft().enable_tun_mode, Some(true));
+
+    assert_eq!(draft.latest().enable_auto_launch, Some(false));
+    assert_eq!(draft.latest().enable_tun_mode, Some(true));
+
+    assert!(draft.apply().is_some());
+    assert!(draft.apply().is_none());
+
+    assert_eq!(draft.data().enable_auto_launch, Some(false));
+    assert_eq!(draft.data().enable_tun_mode, Some(true));
+
+    assert_eq!(draft.draft().enable_auto_launch, Some(false));
+    assert_eq!(draft.draft().enable_tun_mode, Some(true));
+
+    let mut d = draft.draft();
+    d.enable_auto_launch = Some(true);
+    drop(d);
+
+    assert_eq!(draft.data().enable_auto_launch, Some(false));
+
+    assert_eq!(draft.draft().enable_auto_launch, Some(true));
+
+    assert!(draft.discard().is_some());
+
+    assert_eq!(draft.data().enable_auto_launch, Some(false));
+
+    assert!(draft.discard().is_none());
+
+    assert_eq!(draft.draft().enable_auto_launch, Some(false));
+}

+ 15 - 69
src-tauri/src/config/mod.rs

@@ -1,69 +1,15 @@
-mod field;
-mod merge;
-mod script;
-mod tun;
-
-pub(self) use self::field::*;
-use self::merge::*;
-use self::script::*;
-use self::tun::*;
-use crate::data::ChainItem;
-use crate::data::ChainType;
-use serde_yaml::Mapping;
-use std::collections::HashMap;
-use std::collections::HashSet;
-
-type ResultLog = Vec<(String, String)>;
-
-pub fn enhance_config(
-    clash_config: Mapping,
-    profile_config: Mapping,
-    chain: Vec<ChainItem>,
-    valid: Vec<String>,
-    tun_mode: bool,
-) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
-    let mut config = profile_config;
-    let mut result_map = HashMap::new();
-    let mut exists_keys = use_keys(&config);
-
-    let valid = use_valid_fields(valid);
-
-    chain.into_iter().for_each(|item| match item.data {
-        ChainType::Merge(merge) => {
-            exists_keys.extend(use_keys(&merge));
-            config = use_merge(merge, config.to_owned());
-            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())),
-            }
-
-            result_map.insert(item.uid, logs);
-        }
-    });
-
-    config = use_filter(config, &valid);
-
-    for (key, value) in clash_config.into_iter() {
-        config.insert(key, value);
-    }
-
-    let clash_fields = use_clash_fields();
-    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)
-}
+mod clash;
+mod config;
+mod draft;
+mod prfitem;
+mod profiles;
+mod runtime;
+mod verge;
+
+pub use self::clash::*;
+pub use self::config::*;
+pub use self::draft::*;
+pub use self::prfitem::*;
+pub use self::profiles::*;
+pub use self::runtime::*;
+pub use self::verge::*;

+ 16 - 14
src-tauri/src/data/prfitem.rs → src-tauri/src/config/prfitem.rs

@@ -1,4 +1,4 @@
-use crate::utils::{config, dirs, help, tmpl};
+use crate::utils::{dirs, help, tmpl};
 use anyhow::{bail, Context, Result};
 use reqwest::StatusCode;
 use serde::{Deserialize, Serialize};
@@ -6,6 +6,8 @@ use serde_yaml::Mapping;
 use std::fs;
 use sysproxy::Sysproxy;
 
+use super::Config;
+
 #[derive(Debug, Clone, Deserialize, Serialize)]
 pub struct PrfItem {
     pub uid: Option<String>,
@@ -29,7 +31,7 @@ pub struct PrfItem {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub url: Option<String>,
 
-    /// selected infomation
+    /// selected information
     #[serde(skip_serializing_if = "Option::is_none")]
     pub selected: Option<Vec<PrfSelected>>,
 
@@ -170,7 +172,7 @@ impl PrfItem {
             selected: None,
             extra: None,
             option: None,
-            updated: Some(help::get_now()),
+            updated: Some(chrono::Local::now().timestamp() as usize),
             file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
         })
     }
@@ -192,9 +194,9 @@ impl PrfItem {
 
         // 使用软件自己的代理
         if self_proxy {
-            let data = super::Data::global();
-            let port = data.clash.lock().info.port.clone();
+            let port = Config::clash().data().get_info()?.port;
             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) {
@@ -299,7 +301,7 @@ impl PrfItem {
             selected: None,
             extra,
             option,
-            updated: Some(help::get_now()),
+            updated: Some(chrono::Local::now().timestamp() as usize),
             file_data: Some(data),
         })
     }
@@ -320,13 +322,13 @@ impl PrfItem {
             selected: None,
             extra: None,
             option: None,
-            updated: Some(help::get_now()),
+            updated: Some(chrono::Local::now().timestamp() as usize),
             file_data: Some(tmpl::ITEM_MERGE.into()),
         })
     }
 
     /// ## Script type (enhance)
-    /// create the enhanced item by using javascript(browserjs)
+    /// create the enhanced item by using javascript quick.js
     pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
         let uid = help::get_uid("s");
         let file = format!("{uid}.js"); // js ext
@@ -341,7 +343,7 @@ impl PrfItem {
             selected: None,
             extra: None,
             option: None,
-            updated: Some(help::get_now()),
+            updated: Some(chrono::Local::now().timestamp() as usize),
             file_data: Some(tmpl::ITEM_SCRIPT.into()),
         })
     }
@@ -353,7 +355,7 @@ impl PrfItem {
         }
 
         let file = self.file.clone().unwrap();
-        let path = dirs::app_profiles_dir().join(file);
+        let path = dirs::app_profiles_dir()?.join(file);
         fs::read_to_string(path).context("failed to read the file")
     }
 
@@ -364,7 +366,7 @@ impl PrfItem {
         }
 
         let file = self.file.clone().unwrap();
-        let path = dirs::app_profiles_dir().join(file);
+        let path = dirs::app_profiles_dir()?.join(file);
         fs::write(path, data.as_bytes()).context("failed to save the file")
     }
 
@@ -373,7 +375,7 @@ impl PrfItem {
         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);
+        let path = dirs::app_profiles_dir().ok()?.join(file);
 
         if !path.exists() {
             return None;
@@ -382,11 +384,11 @@ impl PrfItem {
         match itype {
             "script" => Some(ChainItem {
                 uid,
-                data: ChainType::Script(fs::read_to_string(path).unwrap_or("".into())),
+                data: ChainType::Script(fs::read_to_string(path).ok()?),
             }),
             "merge" => Some(ChainItem {
                 uid,
-                data: ChainType::Merge(config::read_merge_mapping(path)),
+                data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
             }),
             _ => None,
         }

+ 69 - 86
src-tauri/src/data/profiles.rs → src-tauri/src/config/profiles.rs

@@ -1,30 +1,24 @@
-use super::prfitem::PrfItem;
-use super::ChainItem;
-use crate::utils::{config, dirs, help};
+use super::{prfitem::PrfItem, ChainItem};
+use crate::utils::{dirs, help};
 use anyhow::{bail, Context, Result};
 use serde::{Deserialize, Serialize};
 use serde_yaml::Mapping;
-use std::collections::HashMap;
 use std::{fs, io::Write};
 
-///
-/// ## Profiles Config
-///
 /// Define the `profiles.yaml` schema
-///
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct Profiles {
+pub struct IProfiles {
     /// same as PrfConfig.current
-    current: Option<String>,
+    pub current: Option<String>,
 
     /// same as PrfConfig.chain
-    chain: Option<Vec<String>>,
+    pub chain: Option<Vec<String>>,
 
     /// record valid fields for clash
-    valid: Option<Vec<String>>,
+    pub valid: Option<Vec<String>>,
 
     /// profile list
-    items: Option<Vec<PrfItem>>,
+    pub items: Option<Vec<PrfItem>>,
 }
 
 macro_rules! patch {
@@ -35,72 +29,74 @@ macro_rules! patch {
     };
 }
 
-impl Profiles {
+impl IProfiles {
     pub fn new() -> Self {
-        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
-        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"));
+        match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
+            Ok(mut profiles) => {
+                if profiles.items.is_none() {
+                    profiles.items = Some(vec![]);
                 }
+                // compatible 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
             }
-        });
+            Err(err) => {
+                log::error!(target: "app", "{err}");
+                Self::template()
+            }
+        }
+    }
 
-        profiles
+    pub fn template() -> Self {
+        Self {
+            valid: Some(vec!["dns".into()]),
+            items: Some(vec![]),
+            ..Self::default()
+        }
     }
 
-    /// save the config to the file
     pub fn save_file(&self) -> Result<()> {
-        config::save_yaml(
-            dirs::profiles_path(),
+        help::save_yaml(
+            &dirs::profiles_path()?,
             self,
-            Some("# Profiles Config for Clash Verge\n\n"),
+            Some("# Profiles Config for Clash Verge"),
         )
     }
 
-    /// 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<()> {
+    /// 只修改current,valid和chain
+    pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
         if self.items.is_none() {
             self.items = Some(vec![]);
         }
 
-        let items = self.items.as_ref().unwrap();
-        let some_uid = Some(uid.clone());
+        if let Some(current) = patch.current {
+            let items = self.items.as_ref().unwrap();
+            let some_uid = Some(current);
+
+            if items.iter().any(|e| e.uid == some_uid) {
+                self.current = some_uid;
+            }
+        }
 
-        if items.iter().find(|&each| each.uid == some_uid).is_some() {
-            self.current = some_uid;
-            return self.save_file();
+        if let Some(chain) = patch.chain {
+            self.chain = Some(chain);
         }
 
-        bail!("invalid uid \"{uid}\"");
-    }
+        if let Some(valid) = patch.valid {
+            self.valid = Some(valid);
+        }
 
-    /// just change the `chain`
-    pub fn put_chain(&mut self, chain: Option<Vec<String>>) -> Result<()> {
-        self.chain = chain;
-        self.save_file()
+        Ok(())
     }
 
-    /// just change the `field`
-    pub fn put_valid(&mut self, valid: Option<Vec<String>>) -> Result<()> {
-        self.valid = valid;
-        self.save_file()
+    pub fn get_current(&self) -> Option<String> {
+        self.current.clone()
     }
 
     /// get items ref
@@ -110,8 +106,7 @@ impl Profiles {
 
     /// 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();
+        if let Some(items) = self.items.as_ref() {
             let some_uid = Some(uid.clone());
 
             for each in items.iter() {
@@ -140,7 +135,7 @@ impl Profiles {
             }
 
             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)
                 .context(format!("failed to create file \"{}\"", file))?
@@ -209,7 +204,7 @@ impl Profiles {
                         // the file must exists
                         each.file = Some(file.clone());
 
-                        let path = dirs::app_profiles_dir().join(&file);
+                        let path = dirs::app_profiles_dir()?.join(&file);
 
                         fs::File::create(path)
                             .context(format!("failed to create file \"{}\"", file))?
@@ -244,10 +239,12 @@ impl Profiles {
 
         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);
-                }
+                let _ = dirs::app_profiles_dir().map(|path| {
+                    let path = path.join(file);
+                    if path.exists() {
+                        let _ = fs::remove_file(path);
+                    }
+                });
             });
         }
 
@@ -272,22 +269,18 @@ impl Profiles {
             return Ok(config);
         }
 
-        let current = self.current.clone().unwrap();
+        let current = self.current.as_ref().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),
+            if item.uid.as_ref() == Some(current) {
+                let file_path = match item.file.as_ref() {
+                    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()));
+                return Ok(help::read_merge_mapping(&file_path)?);
             }
         }
-        bail!("failed to find current profile \"uid:{current}\"");
+        bail!("failed to find the current profile \"uid:{current}\"");
     }
 
     /// generate the data for activate clash config
@@ -317,13 +310,3 @@ pub struct PrfActivate {
     pub chain: Vec<ChainItem>,
     pub valid: Vec<String>,
 }
-
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct RuntimeResult {
-    pub config: Option<Mapping>,
-    pub config_yaml: Option<String>,
-    // 记录在配置中(包括merge和script生成的)出现过的keys
-    // 这些keys不一定都生效
-    pub exists_keys: Vec<String>,
-    pub chain_logs: HashMap<String, Vec<(String, String)>>,
-}

+ 18 - 0
src-tauri/src/config/runtime.rs

@@ -0,0 +1,18 @@
+use serde::{Deserialize, Serialize};
+use serde_yaml::Mapping;
+use std::collections::HashMap;
+
+#[derive(Default, Debug, Clone, Deserialize, Serialize)]
+pub struct IRuntime {
+    pub config: Option<Mapping>,
+    // 记录在配置中(包括merge和script生成的)出现过的keys
+    // 这些keys不一定都生效
+    pub exists_keys: Vec<String>,
+    pub chain_logs: HashMap<String, Vec<(String, String)>>,
+}
+
+impl IRuntime {
+    pub fn new() -> Self {
+        Self::default()
+    }
+}

+ 45 - 16
src-tauri/src/data/verge.rs → src-tauri/src/config/verge.rs

@@ -1,12 +1,11 @@
-use crate::utils::{config, dirs};
+use crate::utils::{dirs, help};
 use anyhow::Result;
 use serde::{Deserialize, Serialize};
 
 /// ### `verge.yaml` schema
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct Verge {
-    /// app listening port
-    /// for app singleton
+pub struct IVerge {
+    /// app listening port for app singleton
     pub app_singleton_port: Option<u16>,
 
     // i18n
@@ -48,7 +47,7 @@ pub struct Verge {
     pub proxy_guard_duration: Option<u64>,
 
     /// theme setting
-    pub theme_setting: Option<VergeTheme>,
+    pub theme_setting: Option<IVergeTheme>,
 
     /// web ui list
     pub web_ui_list: Option<Vec<String>>,
@@ -69,7 +68,7 @@ pub struct Verge {
 }
 
 #[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct VergeTheme {
+pub struct IVergeTheme {
     pub primary_color: Option<String>,
     pub secondary_color: Option<String>,
     pub primary_text: Option<String>,
@@ -84,23 +83,42 @@ pub struct VergeTheme {
     pub css_injection: Option<String>,
 }
 
-impl Verge {
+impl IVerge {
     pub fn new() -> Self {
-        config::read_yaml::<Verge>(dirs::verge_path())
+        match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
+            Ok(config) => config,
+            Err(err) => {
+                log::error!(target: "app", "{err}");
+                Self::template()
+            }
+        }
+    }
+
+    pub fn template() -> Self {
+        Self {
+            clash_core: Some("clash".into()),
+            language: Some("en".into()),
+            theme_mode: Some("system".into()),
+            theme_blur: Some(false),
+            traffic_graph: Some(true),
+            enable_auto_launch: Some(false),
+            enable_silent_start: Some(false),
+            enable_system_proxy: Some(false),
+            enable_proxy_guard: Some(false),
+            proxy_guard_duration: Some(30),
+            auto_close_connection: Some(true),
+            ..Self::default()
+        }
     }
 
-    /// Save Verge App Config
+    /// Save IVerge App Config
     pub fn save_file(&self) -> Result<()> {
-        config::save_yaml(
-            dirs::verge_path(),
-            self,
-            Some("# The Config for Clash Verge App\n\n"),
-        )
+        help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
     }
 
     /// patch verge config
     /// only save to file
-    pub fn patch_config(&mut self, patch: Verge) -> Result<()> {
+    pub fn patch_config(&mut self, patch: IVerge) {
         macro_rules! patch {
             ($key: tt) => {
                 if patch.$key.is_some() {
@@ -130,7 +148,18 @@ impl Verge {
 
         patch!(auto_close_connection);
         patch!(default_latency_test);
+    }
+
+    /// 在初始化前尝试拿到单例端口的值
+    pub fn get_singleton_port() -> u16 {
+        #[cfg(not(feature = "verge-dev"))]
+        const SERVER_PORT: u16 = 33331;
+        #[cfg(feature = "verge-dev")]
+        const SERVER_PORT: u16 = 11233;
 
-        self.save_file()
+        match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
+            Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
+            Err(_) => SERVER_PORT, // 这里就不log错误了
+        }
     }
 }

+ 64 - 0
src-tauri/src/core/clash_api.rs

@@ -0,0 +1,64 @@
+use crate::config::Config;
+use anyhow::{bail, Result};
+use reqwest::header::HeaderMap;
+use serde_yaml::Mapping;
+use std::collections::HashMap;
+
+/// PUT /configs
+/// path 是绝对路径
+pub async fn put_configs(path: &str) -> Result<()> {
+    let (url, headers) = clash_client_info()?;
+    let url = format!("{url}/configs");
+
+    let mut data = HashMap::new();
+    data.insert("path", path);
+
+    let client = reqwest::ClientBuilder::new().no_proxy().build()?;
+    let builder = client.put(&url).headers(headers).json(&data);
+    let response = builder.send().await?;
+
+    match response.status().as_u16() {
+        204 => Ok(()),
+        status @ _ => {
+            bail!("failed to put configs with status \"{status}\"")
+        }
+    }
+}
+
+/// PATCH /configs
+pub async fn patch_configs(config: &Mapping) -> Result<()> {
+    let (url, headers) = clash_client_info()?;
+    let url = format!("{url}/configs");
+
+    let client = reqwest::ClientBuilder::new().no_proxy().build()?;
+    let builder = client.patch(&url).headers(headers.clone()).json(config);
+    builder.send().await?;
+    Ok(())
+}
+
+/// 根据clash info获取clash服务地址和请求头
+fn clash_client_info() -> Result<(String, HeaderMap)> {
+    let info = { Config::clash().data().get_info()? };
+
+    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();
+    let server = format!("http://{server}");
+
+    let mut headers = HeaderMap::new();
+    headers.insert("Content-Type", "application/json".parse()?);
+
+    if let Some(secret) = info.secret.as_ref() {
+        let secret = format!("Bearer {}", secret.clone()).parse()?;
+        headers.insert("Authorization", secret);
+    }
+
+    Ok((server, headers))
+}

+ 246 - 0
src-tauri/src/core/core.rs

@@ -0,0 +1,246 @@
+use super::{clash_api, logger::Logger};
+use crate::log_err;
+use crate::{config::*, utils::dirs};
+use anyhow::{bail, Context, Result};
+use once_cell::sync::OnceCell;
+use parking_lot::Mutex;
+use std::{fs, io::Write, sync::Arc, time::Duration};
+use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
+use tauri::api::process::{Command, CommandChild, CommandEvent};
+use tokio::time::sleep;
+
+#[derive(Debug)]
+pub struct CoreManager {
+    sidecar: Arc<Mutex<Option<CommandChild>>>,
+
+    #[allow(unused)]
+    use_service_mode: Arc<Mutex<bool>>,
+}
+
+impl CoreManager {
+    pub fn global() -> &'static CoreManager {
+        static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
+
+        CORE_MANAGER.get_or_init(|| CoreManager {
+            sidecar: Arc::new(Mutex::new(None)),
+            use_service_mode: Arc::new(Mutex::new(false)),
+        })
+    }
+
+    pub fn init(&self) -> Result<()> {
+        // kill old clash process
+        let _ = dirs::clash_pid_path()
+            .and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
+            .and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
+            .map(|pid| {
+                let mut system = System::new();
+                system.refresh_all();
+                system.process(Pid::from_u32(pid)).map(|proc| {
+                    if proc.name().contains("clash") {
+                        proc.kill();
+                    }
+                });
+            });
+
+        tauri::async_runtime::spawn(async {
+            // 启动clash
+            log_err!(Self::global().run_core().await);
+        });
+
+        Ok(())
+    }
+
+    /// 检查配置是否正确
+    pub fn check_config(&self) -> Result<()> {
+        let config_path = Config::generate_file(ConfigType::Check)?;
+        let config_path = dirs::path_to_str(&config_path)?;
+
+        let clash_core = { Config::verge().latest().clash_core.clone() };
+        let clash_core = clash_core.unwrap_or("clash".into());
+
+        let output = Command::new_sidecar(clash_core)?
+            .args(["-t", "-f", config_path])
+            .output()?;
+
+        if !output.status.success() {
+            Logger::global().set_log(output.stdout.clone());
+            bail!("{}", output.stdout); // 过滤掉终端颜色值
+        }
+
+        Ok(())
+    }
+
+    /// 启动核心
+    pub async fn run_core(&self) -> Result<()> {
+        let config_path = Config::generate_file(ConfigType::Run)?;
+
+        #[cfg(target_os = "windows")]
+        {
+            use super::win_service;
+
+            // 服务模式
+            let enable = {
+                let enable = Config::verge().data().enable_service_mode.clone();
+                enable.unwrap_or(false)
+            };
+
+            *self.use_service_mode.lock() = enable;
+
+            if enable {
+                // 服务模式启动失败就直接运行sidecar
+                match {
+                    win_service::check_service().await?;
+                    win_service::run_core_by_service().await
+                } {
+                    Ok(_) => return Ok(()),
+                    Err(err) => {
+                        // 修改这个值,免得stop出错
+                        *self.use_service_mode.lock() = false;
+
+                        log::error!(target: "app", "{err}");
+                    }
+                }
+            }
+        }
+
+        let mut sidecar = self.sidecar.lock();
+
+        if let Some(child) = sidecar.take() {
+            let _ = child.kill();
+        }
+
+        let app_dir = dirs::app_home_dir()?;
+        let app_dir = dirs::path_to_str(&app_dir)?;
+
+        let clash_core = { Config::verge().latest().clash_core.clone() };
+        let clash_core = clash_core.unwrap_or("clash".into());
+
+        let config_path = dirs::path_to_str(&config_path)?;
+
+        // fix #212
+        let args = match clash_core.as_str() {
+            "clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
+            _ => vec!["-d", app_dir, "-f", config_path],
+        };
+
+        let cmd = Command::new_sidecar(clash_core)?;
+        let (mut rx, cmd_child) = cmd.args(args).spawn()?;
+
+        // 将pid写入文件中
+        crate::log_err!({
+            let pid = cmd_child.pid();
+            let path = dirs::clash_pid_path()?;
+            fs::File::create(path)
+                .context("failed to create the pid file")?
+                .write(format!("{pid}").as_bytes())
+                .context("failed to write pid to the file")?;
+            <Result<()>>::Ok(())
+        });
+
+        *sidecar = Some(cmd_child);
+
+        tauri::async_runtime::spawn(async move {
+            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);
+                        Logger::global().set_log(line);
+                    }
+                    CommandEvent::Stderr(err) => {
+                        log::error!(target: "app" ,"[clash]: {err}");
+                        Logger::global().set_log(err);
+                    }
+                    CommandEvent::Error(err) => {
+                        log::error!(target: "app" ,"[clash]: {err}");
+                        Logger::global().set_log(err);
+                    }
+                    CommandEvent::Terminated(_) => {
+                        log::info!(target: "app" ,"clash core terminated");
+                        break;
+                    }
+                    _ => {}
+                }
+            }
+        });
+
+        Ok(())
+    }
+
+    /// 停止核心运行
+    pub fn stop_core(&self) -> Result<()> {
+        #[cfg(target_os = "windows")]
+        if *self.use_service_mode.lock() {
+            tauri::async_runtime::block_on(async move {
+                log_err!(super::win_service::stop_core_by_service().await);
+            });
+            return Ok(());
+        }
+
+        let mut sidecar = self.sidecar.lock();
+        if let Some(child) = sidecar.take() {
+            let _ = child.kill();
+        }
+        Ok(())
+    }
+
+    /// 切换核心
+    pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
+        let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
+
+        if &clash_core != "clash" && &clash_core != "clash-meta" {
+            bail!("invalid clash core name \"{clash_core}\"");
+        }
+
+        Config::verge().draft().clash_core = Some(clash_core);
+
+        // 清掉旧日志
+        Logger::global().clear_log();
+
+        match self.run_core().await {
+            Ok(_) => {
+                Config::verge().apply();
+                Config::runtime().apply();
+                log_err!(Config::verge().latest().save_file());
+                Ok(())
+            }
+            Err(err) => {
+                Config::verge().discard();
+                Config::runtime().discard();
+                Err(err)
+            }
+        }
+    }
+
+    /// 更新proxies那些
+    /// 如果涉及端口和外部控制则需要重启
+    pub async fn update_config(&self) -> Result<()> {
+        // 更新配置
+        Config::generate()?;
+
+        // 检查配置是否正常
+        self.check_config()?;
+
+        // 更新运行时配置
+        let path = Config::generate_file(ConfigType::Run)?;
+        let path = dirs::path_to_str(&path)?;
+
+        // 发送请求 发送5次
+        for i in 0..5 {
+            match clash_api::put_configs(path).await {
+                Ok(_) => break,
+                Err(err) => {
+                    if i < 4 {
+                        log::error!(target: "app", "{err}");
+                    } else {
+                        bail!(err);
+                    }
+                }
+            }
+            sleep(Duration::from_millis(250)).await;
+        }
+
+        Ok(())
+    }
+}

+ 38 - 26
src-tauri/src/core/handle.rs

@@ -1,65 +1,77 @@
 use super::tray::Tray;
-use crate::log_if_err;
+use crate::log_err;
 use anyhow::{bail, Result};
+use once_cell::sync::OnceCell;
+use parking_lot::Mutex;
+use std::sync::Arc;
 use tauri::{AppHandle, Manager, Window};
 
 #[derive(Debug, Default, Clone)]
 pub struct Handle {
-    pub app_handle: Option<AppHandle>,
+    pub app_handle: Arc<Mutex<Option<AppHandle>>>,
 }
 
 impl Handle {
-    pub fn set_inner(&mut self, app_handle: AppHandle) {
-        self.app_handle = Some(app_handle);
+    pub fn global() -> &'static Handle {
+        static HANDLE: OnceCell<Handle> = OnceCell::new();
+
+        HANDLE.get_or_init(|| Handle {
+            app_handle: Arc::new(Mutex::new(None)),
+        })
+    }
+
+    pub fn init(&self, app_handle: AppHandle) {
+        *self.app_handle.lock() = Some(app_handle);
     }
 
     pub fn get_window(&self) -> Option<Window> {
         self.app_handle
+            .lock()
             .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_clash() {
+        if let Some(window) = Self::global().get_window() {
+            log_err!(window.emit("verge://refresh-clash-config", "yes"));
         }
     }
 
-    pub fn refresh_verge(&self) {
-        if let Some(window) = self.get_window() {
-            log_if_err!(window.emit("verge://refresh-verge-config", "yes"));
+    pub fn refresh_verge() {
+        if let Some(window) = Self::global().get_window() {
+            log_err!(window.emit("verge://refresh-verge-config", "yes"));
         }
     }
 
     #[allow(unused)]
-    pub fn refresh_profiles(&self) {
-        if let Some(window) = self.get_window() {
-            log_if_err!(window.emit("verge://refresh-profiles-config", "yes"));
+    pub fn refresh_profiles() {
+        if let Some(window) = Self::global().get_window() {
+            log_err!(window.emit("verge://refresh-profiles-config", "yes"));
         }
     }
 
-    pub fn notice_message(&self, status: String, msg: String) {
-        if let Some(window) = self.get_window() {
-            log_if_err!(window.emit("verge://notice-message", (status, msg)));
+    pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
+        if let Some(window) = Self::global().get_window() {
+            log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
         }
     }
 
-    pub fn update_systray(&self) -> Result<()> {
-        if self.app_handle.is_none() {
-            bail!("update_systray unhandle error");
+    pub fn update_systray() -> Result<()> {
+        let app_handle = Self::global().app_handle.lock();
+        if app_handle.is_none() {
+            bail!("update_systray unhandled error");
         }
-        let app_handle = self.app_handle.as_ref().unwrap();
-        Tray::update_systray(app_handle)?;
+        Tray::update_systray(app_handle.as_ref().unwrap())?;
         Ok(())
     }
 
     /// update the system tray state
-    pub fn update_systray_part(&self) -> Result<()> {
-        if self.app_handle.is_none() {
-            bail!("update_systray unhandle error");
+    pub fn update_systray_part() -> Result<()> {
+        let app_handle = Self::global().app_handle.lock();
+        if app_handle.is_none() {
+            bail!("update_systray unhandled error");
         }
-        let app_handle = self.app_handle.as_ref().unwrap();
-        Tray::update_part(app_handle)?;
+        Tray::update_part(app_handle.as_ref().unwrap())?;
         Ok(())
     }
 }

+ 34 - 28
src-tauri/src/core/hotkey.rs

@@ -1,52 +1,58 @@
-use crate::{data::*, feat, log_if_err};
+use crate::{config::Config, feat, log_err};
 use anyhow::{bail, Result};
-use std::collections::HashMap;
+use once_cell::sync::OnceCell;
+use parking_lot::Mutex;
+use std::{collections::HashMap, sync::Arc};
 use tauri::{AppHandle, GlobalShortcutManager};
 
 pub struct Hotkey {
-    current: Vec<String>, // 保存当前的热键设置
-    manager: Option<AppHandle>,
+    current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
+
+    app_handle: Arc<Mutex<Option<AppHandle>>>,
 }
 
 impl Hotkey {
-    pub fn new() -> Hotkey {
-        Hotkey {
-            current: Vec::new(),
-            manager: None,
-        }
+    pub fn global() -> &'static Hotkey {
+        static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
+
+        HOTKEY.get_or_init(|| Hotkey {
+            current: Arc::new(Mutex::new(Vec::new())),
+            app_handle: Arc::new(Mutex::new(None)),
+        })
     }
 
-    pub fn init(&mut self, app_handle: AppHandle) -> Result<()> {
-        self.manager = Some(app_handle);
-        let data = Data::global();
-        let verge = data.verge.lock();
+    pub fn init(&self, app_handle: AppHandle) -> Result<()> {
+        *self.app_handle.lock() = Some(app_handle);
+
+        let verge = Config::verge();
 
-        if let Some(hotkeys) = verge.hotkeys.as_ref() {
+        if let Some(hotkeys) = verge.latest().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()));
+                    log_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();
+            *self.current.lock() = hotkeys.clone();
         }
 
         Ok(())
     }
 
     fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
-        if self.manager.is_none() {
+        let app_handle = self.app_handle.lock();
+        if app_handle.is_none() {
             bail!("failed to get hotkey manager");
         }
-        Ok(self.manager.as_ref().unwrap().global_shortcut_manager())
+        Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
     }
 
-    fn register(&mut self, hotkey: &str, func: &str) -> Result<()> {
+    fn register(&self, hotkey: &str, func: &str) -> Result<()> {
         let mut manager = self.get_manager()?;
 
         if manager.is_registered(hotkey)? {
@@ -54,10 +60,10 @@ impl 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"),
+            "clash_mode_rule" => || feat::change_clash_mode("rule".into()),
+            "clash_mode_global" => || feat::change_clash_mode("global".into()),
+            "clash_mode_direct" => || feat::change_clash_mode("direct".into()),
+            "clash_mode_script" => || feat::change_clash_mode("script".into()),
             "toggle_system_proxy" => || feat::toggle_system_proxy(),
             "enable_system_proxy" => || feat::enable_system_proxy(),
             "disable_system_proxy" => || feat::disable_system_proxy(),
@@ -73,14 +79,14 @@ impl Hotkey {
         Ok(())
     }
 
-    fn unregister(&mut self, hotkey: &str) -> Result<()> {
+    fn unregister(&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();
+    pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
+        let mut current = self.current.lock();
         let old_map = Self::get_map_from_vec(&current);
         let new_map = Self::get_map_from_vec(&new_hotkeys);
 
@@ -91,10 +97,10 @@ impl Hotkey {
         });
 
         add.iter().for_each(|(key, func)| {
-            log_if_err!(self.register(key, func));
+            log_err!(self.register(key, func));
         });
 
-        self.current = new_hotkeys;
+        *current = new_hotkeys;
         Ok(())
     }
 

+ 36 - 0
src-tauri/src/core/logger.rs

@@ -0,0 +1,36 @@
+use once_cell::sync::OnceCell;
+use parking_lot::Mutex;
+use std::{collections::VecDeque, sync::Arc};
+
+const LOGS_QUEUE_LEN: usize = 100;
+
+pub struct Logger {
+    log_data: Arc<Mutex<VecDeque<String>>>,
+}
+
+impl Logger {
+    pub fn global() -> &'static Logger {
+        static LOGGER: OnceCell<Logger> = OnceCell::new();
+
+        LOGGER.get_or_init(|| Logger {
+            log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
+        })
+    }
+
+    pub fn get_log(&self) -> VecDeque<String> {
+        self.log_data.lock().clone()
+    }
+
+    pub fn set_log(&self, text: String) {
+        let mut logs = self.log_data.lock();
+        if logs.len() > LOGS_QUEUE_LEN {
+            logs.pop_front();
+        }
+        logs.push_back(text);
+    }
+
+    pub fn clear_log(&self) {
+        let mut logs = self.log_data.lock();
+        logs.clear();
+    }
+}

+ 9 - 347
src-tauri/src/core/mod.rs

@@ -1,349 +1,11 @@
-use self::handle::Handle;
-use self::hotkey::Hotkey;
-use self::sysopt::Sysopt;
-use self::timer::Timer;
-use crate::config::enhance_config;
-use crate::data::*;
-use crate::log_if_err;
-use anyhow::{bail, Result};
-use once_cell::sync::OnceCell;
-use parking_lot::Mutex;
-use serde_yaml::{Mapping, Value};
-use std::sync::Arc;
-
-mod handle;
-mod hotkey;
-mod service;
-mod sysopt;
-mod timer;
+pub mod clash_api;
+mod core;
+pub mod handle;
+pub mod hotkey;
+pub mod logger;
+pub mod sysopt;
+pub mod timer;
 pub mod tray;
+pub mod win_service;
 
-pub use self::service::*;
-
-#[derive(Clone)]
-pub struct Core {
-    pub service: Arc<Mutex<Service>>,
-    pub sysopt: Arc<Mutex<Sysopt>>,
-    pub timer: Arc<Mutex<Timer>>,
-    pub hotkey: Arc<Mutex<Hotkey>>,
-    pub runtime: Arc<Mutex<RuntimeResult>>,
-    pub handle: Arc<Mutex<Handle>>,
-}
-
-impl Core {
-    pub fn global() -> &'static Core {
-        static CORE: OnceCell<Core> = OnceCell::new();
-
-        CORE.get_or_init(|| Core {
-            service: Arc::new(Mutex::new(Service::new())),
-            sysopt: Arc::new(Mutex::new(Sysopt::new())),
-            timer: Arc::new(Mutex::new(Timer::new())),
-            hotkey: Arc::new(Mutex::new(Hotkey::new())),
-            runtime: Arc::new(Mutex::new(RuntimeResult::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();
-        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()
-    }
-
-    /// Patch Clash
-    /// 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 global = Data::global();
-            let mut clash = global.clash.lock();
-            clash.patch_config(patch_cloned)?;
-            clash.info.port.is_some()
-        };
-
-        // todo: port check
-        if (mixed_port.is_some() && valid_port) || external.is_some() || secret.is_some() {
-            let mut service = self.service.lock();
-            service.restart()?;
-            drop(service);
-
-            self.activate()?;
-
-            let mut sysopt = self.sysopt.lock();
-            sysopt.init_sysproxy()?;
-        }
-
-        if clash_mode.is_some() {
-            let handle = self.handle.lock();
-            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();
-                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);
-            self.activate()?;
-        }
-
-        Ok(())
-    }
-}
+pub use self::core::*;

+ 0 - 537
src-tauri/src/core/service.rs

@@ -1,537 +0,0 @@
-use crate::data::{ClashInfo, Data};
-use crate::log_if_err;
-use crate::utils::{config, dirs};
-use anyhow::{bail, Result};
-use parking_lot::RwLock;
-use reqwest::header::HeaderMap;
-use serde_yaml::Mapping;
-use std::fs;
-use std::io::Write;
-use std::sync::Arc;
-use std::{
-    collections::{HashMap, VecDeque},
-    time::Duration,
-};
-use tauri::api::process::{Command, CommandChild, CommandEvent};
-use tokio::time::sleep;
-
-const LOGS_QUEUE_LEN: usize = 100;
-
-#[derive(Debug)]
-pub struct Service {
-    sidecar: Option<CommandChild>,
-
-    logs: Arc<RwLock<VecDeque<String>>>,
-
-    #[allow(unused)]
-    use_service_mode: bool,
-}
-
-impl Service {
-    pub fn new() -> Service {
-        let queue = VecDeque::with_capacity(LOGS_QUEUE_LEN + 10);
-
-        Service {
-            sidecar: None,
-            logs: Arc::new(RwLock::new(queue)),
-            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 stop(&mut self) -> Result<()> {
-        #[cfg(not(target_os = "windows"))]
-        self.stop_clash_by_sidecar()?;
-
-        #[cfg(target_os = "windows")]
-        {
-            let _ = self.stop_clash_by_sidecar();
-
-            if self.use_service_mode {
-                tauri::async_runtime::block_on(async move {
-                    log_if_err!(Self::stop_clash_by_service().await);
-                });
-            }
-        }
-
-        Ok(())
-    }
-
-    pub fn restart(&mut self) -> Result<()> {
-        self.stop()?;
-        self.start()
-    }
-
-    pub fn get_logs(&self) -> VecDeque<String> {
-        self.logs.read().clone()
-    }
-
-    #[allow(unused)]
-    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 = {
-            let global = Data::global();
-            let verge = global.verge.lock();
-            verge.clash_core.clone().unwrap_or("clash".into())
-        };
-
-        let app_dir = dirs::app_home_dir();
-        let app_dir = app_dir.as_os_str().to_str().unwrap();
-
-        // fix #212
-        let args = match clash_core.as_str() {
-            "clash-meta" => vec!["-m", "-d", app_dir],
-            _ => vec!["-d", app_dir],
-        };
-
-        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);
-            };
-
-            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(())
-    }
-
-    /// stop the clash sidecar
-    fn stop_clash_by_sidecar(&mut self) -> Result<()> {
-        if let Some(sidecar) = self.sidecar.take() {
-            sidecar.kill()?;
-        }
-        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);
-
-            if !service_mode && self.sidecar.is_none() {
-                self.start()?;
-            }
-        }
-
-        #[cfg(not(target_os = "windows"))]
-        if self.sidecar.is_none() {
-            self.start()?;
-        }
-
-        Ok(())
-    }
-
-    /// 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) => {
-        match $i {
-          4 => bail!($e),
-          _ => log::error!(target: "app", $e),
-        }
-      };
-    }
-
-        // retry 5 times
-        for i in 0..5 {
-            let headers = headers.clone();
-            match reqwest::ClientBuilder::new().no_proxy().build() {
-                Ok(client) => {
-                    let builder = client.put(&server).headers(headers).json(&data);
-                    match builder.send().await {
-                        Ok(resp) => match resp.status().as_u16() {
-                            204 => break,
-                            // 配置有问题不重试
-                            400 => bail!("failed to update clash config with status 400"),
-                            status @ _ => {
-                                report_err!(i, "failed to activate clash with status \"{status}\"")
-                            }
-                        },
-                        Err(err) => report_err!(i, "{err}"),
-                    }
-                }
-                Err(err) => report_err!(i, "{err}"),
-            }
-            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)?;
-
-        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();
-        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);
-        }
-
-        Ok((server, headers))
-    }
-
-    /// 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 {
-    fn drop(&mut self) {
-        log_if_err!(self.stop());
-    }
-}
-
-/// ### Service Mode
-///
-#[cfg(target_os = "windows")]
-pub mod win_service {
-    use super::*;
-    use anyhow::Context;
-    use deelevate::{PrivilegeLevel, Token};
-    use runas::Command as RunasCommand;
-    use serde::{Deserialize, Serialize};
-    use std::os::windows::process::CommandExt;
-    use std::{env::current_exe, process::Command as StdCommand};
-
-    const SERVICE_NAME: &str = "clash_verge_service";
-
-    const SERVICE_URL: &str = "http://127.0.0.1:33211";
-
-    #[derive(Debug, Deserialize, Serialize, Clone)]
-    pub struct ResponseBody {
-        pub bin_path: String,
-        pub config_dir: 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
-        /// 该函数应该在协程或者线程中执行,避免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(())
-        }
-    }
-}

+ 106 - 101
src-tauri/src/core/sysopt.rs

@@ -1,23 +1,25 @@
-use crate::{data::*, log_if_err};
+use crate::{config::Config, log_err};
 use anyhow::{anyhow, bail, Result};
 use auto_launch::{AutoLaunch, AutoLaunchBuilder};
+use once_cell::sync::OnceCell;
+use parking_lot::Mutex;
 use std::sync::Arc;
 use sysproxy::Sysproxy;
-use tauri::{async_runtime::Mutex, utils::platform::current_exe};
+use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
 
 pub struct Sysopt {
     /// current system proxy setting
-    cur_sysproxy: Option<Sysproxy>,
+    cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
 
     /// record the original system proxy
     /// recover it when exit
-    old_sysproxy: Option<Sysproxy>,
+    old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
 
     /// helps to auto launch the app
-    auto_launch: Option<AutoLaunch>,
+    auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
 
     /// record whether the guard async is running or not
-    guard_state: Arc<Mutex<bool>>,
+    guard_state: Arc<TokioMutex<bool>>,
 }
 
 #[cfg(target_os = "windows")]
@@ -28,44 +30,49 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
 static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
 
 impl Sysopt {
-    pub fn new() -> Sysopt {
-        Sysopt {
-            cur_sysproxy: None,
-            old_sysproxy: None,
-            auto_launch: None,
-            guard_state: Arc::new(Mutex::new(false)),
-        }
+    pub fn global() -> &'static Sysopt {
+        static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
+
+        SYSOPT.get_or_init(|| Sysopt {
+            cur_sysproxy: Arc::new(Mutex::new(None)),
+            old_sysproxy: Arc::new(Mutex::new(None)),
+            auto_launch: Arc::new(Mutex::new(None)),
+            guard_state: Arc::new(TokioMutex::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();
+    pub fn init_sysproxy(&self) -> Result<()> {
+        let port = { Config::clash().latest().get_info()?.port };
 
         if port.is_none() {
             bail!("clash port is none");
         }
 
-        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 port = port.unwrap().parse::<u16>()?;
-        let host = String::from("127.0.0.1");
 
-        self.cur_sysproxy = Some(Sysproxy {
+        let (enable, bypass) = {
+            let verge = Config::verge();
+            let verge = verge.latest();
+            (
+                verge.enable_system_proxy.clone().unwrap_or(false),
+                verge.system_proxy_bypass.clone(),
+            )
+        };
+
+        let current = Sysproxy {
             enable,
-            host,
+            host: String::from("127.0.0.1"),
             port,
-            bypass,
-        });
+            bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
+        };
 
         if enable {
-            self.old_sysproxy = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
-            self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
+            let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
+            current.set_system_proxy()?;
+
+            *self.old_sysproxy.lock() = old;
+            *self.cur_sysproxy.lock() = Some(current);
         }
 
         // run the system proxy guard
@@ -74,37 +81,46 @@ impl Sysopt {
     }
 
     /// update the system proxy
-    pub fn update_sysproxy(&mut self) -> Result<()> {
-        if self.cur_sysproxy.is_none() || self.old_sysproxy.is_none() {
+    pub fn update_sysproxy(&self) -> Result<()> {
+        let mut cur_sysproxy = self.cur_sysproxy.lock();
+        let old_sysproxy = self.old_sysproxy.lock();
+
+        if cur_sysproxy.is_none() || old_sysproxy.is_none() {
+            drop(cur_sysproxy);
+            drop(old_sysproxy);
             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();
+        let (enable, bypass) = {
+            let verge = Config::verge();
+            let verge = verge.latest();
+            (
+                verge.enable_system_proxy.clone().unwrap_or(false),
+                verge.system_proxy_bypass.clone(),
+            )
+        };
+        let mut sysproxy = cur_sysproxy.take().unwrap();
 
         sysproxy.enable = enable;
-        sysproxy.bypass = bypass;
+        sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
 
-        self.cur_sysproxy = Some(sysproxy);
-        self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?;
+        sysproxy.set_system_proxy()?;
+        *cur_sysproxy = Some(sysproxy);
 
         Ok(())
     }
 
     /// reset the sysproxy
-    pub fn reset_sysproxy(&mut self) -> Result<()> {
-        let cur = self.cur_sysproxy.take();
+    pub fn reset_sysproxy(&self) -> Result<()> {
+        let mut cur_sysproxy = self.cur_sysproxy.lock();
+        let mut old_sysproxy = self.old_sysproxy.lock();
+
+        let cur_sysproxy = cur_sysproxy.take();
 
-        if let Some(mut old) = self.old_sysproxy.take() {
+        if let Some(mut old) = old_sysproxy.take() {
             // 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置
             // 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
-            let port_same = cur.map_or(true, |cur| old.port == cur.port);
+            let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
 
             if old.enable && port_same {
                 old.enable = false;
@@ -114,7 +130,7 @@ impl Sysopt {
             }
 
             old.set_system_proxy()?;
-        } else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur {
+        } else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
             // 没有原代理,就按现在的代理设置disable即可
             log::info!(target: "app", "reset proxy by disabling the current proxy");
             cur.enable = false;
@@ -127,10 +143,9 @@ impl Sysopt {
     }
 
     /// 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);
+    pub fn init_launch(&self) -> Result<()> {
+        let enable = { Config::verge().latest().enable_auto_launch.clone() };
+        let enable = enable.unwrap_or(false);
 
         let app_exe = current_exe()?;
         let app_exe = dunce::canonicalize(app_exe)?;
@@ -167,51 +182,39 @@ impl Sysopt {
             .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()?;
-            }
-        }
+        let _ = auto.disable();
 
-        #[cfg(not(target_os = "macos"))]
-        {
-            match enable {
-                true => auto.enable()?,
-                false => auto.disable()?,
-            };
+        if enable {
+            auto.enable()?;
         }
+        *self.auto_launch.lock() = Some(auto);
 
         Ok(())
     }
 
     /// update the startup
-    pub fn update_launch(&mut self) -> Result<()> {
-        if self.auto_launch.is_none() {
+    pub fn update_launch(&self) -> Result<()> {
+        let auto_launch = self.auto_launch.lock();
+
+        if auto_launch.is_none() {
+            drop(auto_launch);
             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();
+        let enable = { Config::verge().latest().enable_auto_launch.clone() };
+        let enable = enable.unwrap_or(false);
+        let auto_launch = auto_launch.as_ref().unwrap();
 
         match enable {
             true => auto_launch.enable()?,
-            false => crate::log_if_err!(auto_launch.disable()), // 忽略关闭的错误
+            false => log_err!(auto_launch.disable()), // 忽略关闭的错误
         };
 
         Ok(())
@@ -239,14 +242,16 @@ impl Sysopt {
             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);
+                let (enable, guard, guard_duration, bypass) = {
+                    let verge = Config::verge();
+                    let verge = verge.latest();
+                    (
+                        verge.enable_system_proxy.clone().unwrap_or(false),
+                        verge.enable_proxy_guard.clone().unwrap_or(false),
+                        verge.proxy_guard_duration.clone().unwrap_or(10),
+                        verge.system_proxy_bypass.clone(),
+                    )
+                };
 
                 // stop loop
                 if !enable || !guard {
@@ -256,30 +261,30 @@ impl Sysopt {
                 // 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());
+                if let Ok(info) = { Config::clash().latest().get_info() } {
+                    match info.port.unwrap_or("".into()).parse::<u16>() {
+                        Ok(port) => {
+                            let sysproxy = Sysproxy {
+                                enable: true,
+                                host: "127.0.0.1".into(),
+                                port,
+                                bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
+                            };
+
+                            log_err!(sysproxy.set_system_proxy());
+                        }
+                        Err(_) => {
+                            log::error!(target: "app", "failed to parse clash port in guard proxy")
+                        }
                     }
-                    Err(_) => log::error!(target: "app", "failed to parse clash port"),
                 }
             }
 
             let mut state = guard_state.lock().await;
             *state = false;
+            drop(state);
         });
     }
 }

+ 81 - 69
src-tauri/src/core/timer.rs

@@ -1,50 +1,92 @@
-use super::Core;
-use crate::utils::help::get_now;
-use crate::{data::Data, log_if_err};
+use crate::config::Config;
+use crate::feat;
 use anyhow::{Context, Result};
 use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
+use once_cell::sync::OnceCell;
+use parking_lot::Mutex;
 use std::collections::HashMap;
+use std::sync::Arc;
 
 type TaskID = u64;
 
 pub struct Timer {
     /// cron manager
-    delay_timer: DelayTimer,
+    delay_timer: Arc<Mutex<DelayTimer>>,
 
     /// save the current state
-    timer_map: HashMap<String, (TaskID, u64)>,
+    timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
 
     /// increment id
-    timer_count: TaskID,
+    timer_count: Arc<Mutex<TaskID>>,
 }
 
 impl Timer {
-    pub fn new() -> Self {
-        Timer {
-            delay_timer: DelayTimerBuilder::default().build(),
-            timer_map: HashMap::new(),
-            timer_count: 1,
-        }
+    pub fn global() -> &'static Timer {
+        static TIMER: OnceCell<Timer> = OnceCell::new();
+
+        TIMER.get_or_init(|| Timer {
+            delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
+            timer_map: Arc::new(Mutex::new(HashMap::new())),
+            timer_count: Arc::new(Mutex::new(1)),
+        })
+    }
+
+    /// restore timer
+    pub fn init(&self) -> Result<()> {
+        self.refresh()?;
+
+        let cur_timestamp = chrono::Local::now().timestamp();
+
+        let timer_map = self.timer_map.lock();
+        let delay_timer = self.delay_timer.lock();
+
+        Config::profiles().latest().get_items().map(|items| {
+            items
+                .iter()
+                .filter_map(|item| {
+                    // mins to seconds
+                    let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
+                    let updated = item.updated? as i64;
+
+                    if interval > 0 && cur_timestamp - updated >= interval {
+                        Some(item)
+                    } else {
+                        None
+                    }
+                })
+                .for_each(|item| {
+                    if let Some(uid) = item.uid.as_ref() {
+                        if let Some((task_id, _)) = timer_map.get(uid) {
+                            crate::log_err!(delay_timer.advance_task(*task_id));
+                        }
+                    }
+                })
+        });
+
+        Ok(())
     }
 
     /// Correctly update all cron tasks
-    pub fn refresh(&mut self) -> Result<()> {
+    pub fn refresh(&self) -> Result<()> {
         let diff_map = self.gen_diff();
 
+        let mut timer_map = self.timer_map.lock();
+        let mut delay_timer = self.delay_timer.lock();
+
         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));
+                    let _ = timer_map.remove(&uid);
+                    crate::log_err!(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));
+                    let _ = timer_map.insert(uid.clone(), (tid, val));
+                    crate::log_err!(self.add_task(&mut delay_timer, 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));
+                    let _ = timer_map.insert(uid.clone(), (tid, val));
+                    crate::log_err!(delay_timer.remove_task(tid));
+                    crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
                 }
             }
         }
@@ -52,45 +94,11 @@ impl Timer {
         Ok(())
     }
 
-    /// restore timer
-    pub fn restore(&mut self) -> Result<()> {
-        self.refresh()?;
-
-        let cur_timestamp = get_now(); // seconds
-
-        let global = Data::global();
-        let profiles = global.profiles.lock();
-
-        profiles
-            .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));
-                }
-            });
-
-        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() {
+        if let Some(items) = Config::profiles().latest().get_items() {
             for item in items.iter() {
                 if item.option.is_some() {
                     let option = item.option.as_ref().unwrap();
@@ -107,11 +115,13 @@ impl Timer {
     }
 
     /// generate the diff map for refresh
-    fn gen_diff(&mut self) -> HashMap<String, DiffFlag> {
+    fn gen_diff(&self) -> HashMap<String, DiffFlag> {
         let mut diff_map = HashMap::new();
 
+        let timer_map = self.timer_map.lock();
+
         let new_map = self.gen_map();
-        let cur_map = &self.timer_map;
+        let cur_map = &timer_map;
 
         cur_map.iter().for_each(|(uid, (tid, val))| {
             let new_val = new_map.get(uid).unwrap_or(&0);
@@ -123,34 +133,36 @@ impl Timer {
             }
         });
 
-        let mut count = self.timer_count;
+        let mut count = self.timer_count.lock();
 
         new_map.iter().for_each(|(uid, val)| {
             if cur_map.get(uid).is_none() {
-                diff_map.insert(uid.clone(), DiffFlag::Add(count, *val));
+                diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
 
-                count += 1;
+                *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();
-
+    fn add_task(
+        &self,
+        delay_timer: &mut DelayTimer,
+        uid: String,
+        tid: TaskID,
+        minutes: u64,
+    ) -> Result<()> {
         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()))
+            .spawn_async_routine(move || Self::async_task(uid.to_owned()))
             .context("failed to create timer task")?;
 
-        self.delay_timer
+        delay_timer
             .add_task(task)
             .context("failed to add timer task")?;
 
@@ -158,9 +170,9 @@ impl Timer {
     }
 
     /// the task runner
-    async fn async_task(core: Core, uid: String) {
+    async fn async_task(uid: String) {
         log::info!(target: "app", "running timer task `{uid}`");
-        log_if_err!(core.update_profile_item(uid, None).await);
+        crate::log_err!(feat::update_profile(uid, None).await);
     }
 }
 

+ 15 - 16
src-tauri/src/core/tray.rs

@@ -1,5 +1,5 @@
-use crate::{data::Data, feat, utils::resolve};
-use anyhow::{Ok, Result};
+use crate::{config::Config, feat, utils::resolve};
+use anyhow::Result;
 use tauri::{
     api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
     SystemTraySubmenu,
@@ -9,11 +9,7 @@ pub struct Tray {}
 
 impl Tray {
     pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
-        let data = Data::global();
-        let zh = {
-            let verge = data.verge.lock();
-            verge.language == Some("zh".into())
-        };
+        let zh = { Config::verge().latest().language == Some("zh".into()) };
 
         let version = app_handle.package_info().version.to_string();
 
@@ -75,13 +71,15 @@ impl Tray {
     }
 
     pub fn update_part(app_handle: &AppHandle) -> Result<()> {
-        let global = Data::global();
-        let clash = global.clash.lock();
-        let mode = clash
-            .config
-            .get(&serde_yaml::Value::from("mode"))
-            .map(|val| val.as_str().unwrap_or("rule"))
-            .unwrap_or("rule");
+        let mode = {
+            Config::clash()
+                .latest()
+                .0
+                .get("mode")
+                .map(|val| val.as_str().unwrap_or("rule"))
+                .unwrap_or("rule")
+                .to_owned()
+        };
 
         let tray = app_handle.tray_handle();
 
@@ -90,7 +88,8 @@ impl Tray {
         let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
         let _ = tray.get_item("script_mode").set_selected(mode == "script");
 
-        let verge = global.verge.lock();
+        let verge = Config::verge();
+        let verge = verge.latest();
         let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
         let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
 
@@ -105,7 +104,7 @@ impl Tray {
             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);
+                    feat::change_clash_mode(mode.into());
                 }
 
                 "open_window" => resolve::create_window(app_handle),

+ 173 - 0
src-tauri/src/core/win_service.rs

@@ -0,0 +1,173 @@
+#![cfg(target_os = "windows")]
+
+use crate::config::Config;
+use crate::utils::dirs;
+use anyhow::{bail, Context, Result};
+use deelevate::{PrivilegeLevel, Token};
+use runas::Command as RunasCommand;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::os::windows::process::CommandExt;
+use std::time::Duration;
+use std::{env::current_exe, process::Command as StdCommand};
+use tokio::time::sleep;
+
+const SERVICE_URL: &str = "http://127.0.0.1:33211";
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct ResponseBody {
+    pub core_type: Option<String>,
+    pub bin_path: String,
+    pub config_dir: String,
+    pub log_file: String,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct JsonResponse {
+    pub code: u64,
+    pub msg: String,
+    pub data: Option<ResponseBody>,
+}
+
+/// 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
+/// 该函数应该在协程或者线程中执行,避免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(())
+}
+
+/// 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 run_core_by_service() -> Result<()> {
+    let status = check_service().await?;
+
+    if status.code == 0 {
+        stop_core_by_service().await?;
+        sleep(Duration::from_secs(1)).await;
+    }
+
+    let clash_core = { Config::verge().latest().clash_core.clone() };
+    let clash_core = clash_core.unwrap_or("clash".into());
+
+    let clash_bin = format!("{clash_core}.exe");
+    let bin_path = current_exe()?.with_file_name(clash_bin);
+    let bin_path = dirs::path_to_str(&bin_path)?;
+
+    let config_dir = dirs::app_home_dir();
+    let config_dir = dirs::path_to_str(&config_dir)?;
+
+    let log_path = dirs::service_log_file();
+    let log_path = dirs::path_to_str(&log_path)?;
+
+    let mut map = HashMap::new();
+    map.insert("core_type", clash_core.as_str());
+    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_core_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(())
+}

+ 0 - 32
src-tauri/src/data/mod.rs

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

+ 0 - 0
src-tauri/src/config/field.rs → src-tauri/src/enhance/field.rs


+ 0 - 0
src-tauri/src/config/merge.rs → src-tauri/src/enhance/merge.rs


+ 68 - 0
src-tauri/src/enhance/mod.rs

@@ -0,0 +1,68 @@
+mod field;
+mod merge;
+mod script;
+mod tun;
+
+pub(self) use self::field::*;
+use self::merge::*;
+use self::script::*;
+use self::tun::*;
+use crate::config::{ChainItem, ChainType};
+use serde_yaml::Mapping;
+use std::collections::HashMap;
+use std::collections::HashSet;
+
+type ResultLog = Vec<(String, String)>;
+
+pub fn enhance_config(
+    clash_config: Mapping,
+    profile_config: Mapping,
+    chain: Vec<ChainItem>,
+    valid: Vec<String>,
+    tun_mode: bool,
+) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
+    let mut config = profile_config;
+    let mut result_map = HashMap::new();
+    let mut exists_keys = use_keys(&config);
+
+    let valid = use_valid_fields(valid);
+
+    chain.into_iter().for_each(|item| match item.data {
+        ChainType::Merge(merge) => {
+            exists_keys.extend(use_keys(&merge));
+            config = use_merge(merge, config.to_owned());
+            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())),
+            }
+
+            result_map.insert(item.uid, logs);
+        }
+    });
+
+    config = use_filter(config, &valid);
+
+    for (key, value) in clash_config.into_iter() {
+        config.insert(key, value);
+    }
+
+    let clash_fields = use_clash_fields();
+    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)
+}

+ 0 - 0
src-tauri/src/config/script.rs → src-tauri/src/enhance/script.rs


+ 0 - 0
src-tauri/src/config/tun.rs → src-tauri/src/enhance/tun.rs


+ 274 - 69
src-tauri/src/feat.rs

@@ -1,102 +1,307 @@
+//!
+//! feat mod 里的函数主要用于
+//! - hotkey 快捷键
+//! - timer 定时器
+//! - cmds 页面调用
+//!
+use crate::config::*;
 use crate::core::*;
-use crate::data::*;
-use crate::log_if_err;
+use crate::log_err;
+use anyhow::{bail, Result};
+use serde_yaml::{Mapping, Value};
 
 // 重启clash
 pub fn restart_clash_core() {
-    let core = Core::global();
-    let mut service = core.service.lock();
-    log_if_err!(service.restart());
-    drop(service);
-    log_if_err!(core.activate());
+    tauri::async_runtime::spawn(async {
+        match CoreManager::global().run_core().await {
+            Ok(_) => {
+                handle::Handle::refresh_clash();
+                handle::Handle::notice_message("set_config::ok", "ok");
+            }
+            Err(err) => {
+                handle::Handle::notice_message("set_config::error", format!("{err}"));
+                log::error!(target:"app", "{err}");
+            }
+        }
+    });
 }
 
-// 切换模式
-pub fn change_clash_mode(mode: &str) {
-    let core = Core::global();
-    log_if_err!(core.update_mode(mode));
+// 切换模式 rule/global/direct/script mode
+pub fn change_clash_mode(mode: String) {
+    let mut mapping = Mapping::new();
+    mapping.insert(Value::from("mode"), mode.clone().into());
+
+    tauri::async_runtime::spawn(async move {
+        match clash_api::patch_configs(&mapping).await {
+            Ok(_) => {
+                // 更新配置
+                Config::clash().data().patch_config(mapping);
+
+                if let Ok(_) = Config::clash().data().save_config() {
+                    handle::Handle::refresh_clash();
+                    log_err!(handle::Handle::update_systray_part());
+                }
+            }
+            Err(err) => {
+                log::error!(target: "app", "{err}");
+            }
+        }
+    });
 }
 
 // 切换系统代理
 pub fn toggle_system_proxy() {
-    let core = Core::global();
-    let data = Data::global();
-
-    let verge = data.verge.lock();
-    let enable = !verge.enable_system_proxy.clone().unwrap_or(false);
-    drop(verge);
+    let enable = Config::verge().draft().enable_system_proxy.clone();
+    let enable = enable.unwrap_or(false);
 
-    log_if_err!(core.patch_verge(Verge {
-        enable_system_proxy: Some(enable),
-        ..Verge::default()
-    }));
-
-    let handle = core.handle.lock();
-    let _ = handle.refresh_verge();
+    tauri::async_runtime::spawn(async move {
+        match patch_verge(IVerge {
+            enable_system_proxy: Some(!enable),
+            ..IVerge::default()
+        })
+        .await
+        {
+            Ok(_) => handle::Handle::refresh_verge(),
+            Err(err) => log::error!(target: "app", "{err}"),
+        }
+    });
 }
 
 // 打开系统代理
 pub fn enable_system_proxy() {
-    let core = Core::global();
-    log_if_err!(core.patch_verge(Verge {
-        enable_system_proxy: Some(true),
-        ..Verge::default()
-    }));
-
-    let handle = core.handle.lock();
-    let _ = handle.refresh_verge();
+    tauri::async_runtime::spawn(async {
+        match patch_verge(IVerge {
+            enable_system_proxy: Some(true),
+            ..IVerge::default()
+        })
+        .await
+        {
+            Ok(_) => handle::Handle::refresh_verge(),
+            Err(err) => log::error!(target: "app", "{err}"),
+        }
+    });
 }
 
 // 关闭系统代理
 pub fn disable_system_proxy() {
-    let core = Core::global();
-    log_if_err!(core.patch_verge(Verge {
-        enable_system_proxy: Some(false),
-        ..Verge::default()
-    }));
-
-    let handle = core.handle.lock();
-    let _ = handle.refresh_verge();
+    tauri::async_runtime::spawn(async {
+        match patch_verge(IVerge {
+            enable_system_proxy: Some(false),
+            ..IVerge::default()
+        })
+        .await
+        {
+            Ok(_) => handle::Handle::refresh_verge(),
+            Err(err) => log::error!(target: "app", "{err}"),
+        }
+    });
 }
 
 // 切换tun模式
 pub fn toggle_tun_mode() {
-    let core = Core::global();
-    let data = Data::global();
-
-    let verge = data.verge.lock();
-    let enable = !verge.enable_tun_mode.clone().unwrap_or(false);
-    drop(verge);
+    let enable = Config::verge().data().enable_tun_mode.clone();
+    let enable = enable.unwrap_or(false);
 
-    log_if_err!(core.patch_verge(Verge {
-        enable_tun_mode: Some(enable),
-        ..Verge::default()
-    }));
-
-    let handle = core.handle.lock();
-    let _ = handle.refresh_verge();
+    tauri::async_runtime::spawn(async move {
+        match patch_verge(IVerge {
+            enable_tun_mode: Some(!enable),
+            ..IVerge::default()
+        })
+        .await
+        {
+            Ok(_) => handle::Handle::refresh_verge(),
+            Err(err) => log::error!(target: "app", "{err}"),
+        }
+    });
 }
 
 // 打开tun模式
 pub fn enable_tun_mode() {
-    let core = Core::global();
-    log_if_err!(core.patch_verge(Verge {
-        enable_tun_mode: Some(true),
-        ..Verge::default()
-    }));
-
-    let handle = core.handle.lock();
-    let _ = handle.refresh_verge();
+    tauri::async_runtime::spawn(async {
+        match patch_verge(IVerge {
+            enable_tun_mode: Some(true),
+            ..IVerge::default()
+        })
+        .await
+        {
+            Ok(_) => handle::Handle::refresh_verge(),
+            Err(err) => log::error!(target: "app", "{err}"),
+        }
+    });
 }
 
 // 关闭tun模式
 pub fn disable_tun_mode() {
-    let core = Core::global();
-    log_if_err!(core.patch_verge(Verge {
-        enable_tun_mode: Some(false),
-        ..Verge::default()
-    }));
-
-    let handle = core.handle.lock();
-    let _ = handle.refresh_verge();
+    tauri::async_runtime::spawn(async {
+        match patch_verge(IVerge {
+            enable_tun_mode: Some(false),
+            ..IVerge::default()
+        })
+        .await
+        {
+            Ok(_) => handle::Handle::refresh_verge(),
+            Err(err) => log::error!(target: "app", "{err}"),
+        }
+    });
+}
+
+/// 修改clash的配置
+pub async fn patch_clash(patch: Mapping) -> Result<()> {
+    Config::clash().draft().patch_config(patch.clone());
+
+    match {
+        let mixed_port = patch.get("mixed-port");
+        if mixed_port.is_some() {
+            let changed = mixed_port != Config::clash().data().0.get("mixed-port");
+            // 检查端口占用
+            if changed {
+                if let Some(port) = mixed_port.clone().unwrap().as_u64() {
+                    if !port_scanner::local_port_available(port as u16) {
+                        Config::clash().discard();
+                        bail!("the port not available");
+                    }
+                }
+            }
+        };
+
+        // 激活配置
+        if mixed_port.is_some()
+            || patch.get("secret").is_some()
+            || patch.get("external-controller").is_some()
+        {
+            CoreManager::global().run_core().await?;
+        }
+
+        // 更新系统代理
+        if mixed_port.is_some() {
+            log_err!(sysopt::Sysopt::global().init_sysproxy());
+        }
+
+        if patch.get("mode").is_some() {
+            log_err!(handle::Handle::update_systray_part());
+        }
+
+        <Result<()>>::Ok(())
+    } {
+        Ok(()) => {
+            Config::clash().apply();
+            Config::clash().data().save_config()?;
+            Ok(())
+        }
+        Err(err) => {
+            Config::clash().discard();
+            Err(err)
+        }
+    }
+}
+
+/// 修改verge的配置
+/// 一般都是一个个的修改
+pub async fn patch_verge(patch: IVerge) -> Result<()> {
+    Config::verge().draft().patch_config(patch.clone());
+
+    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 language = patch.language;
+
+    match {
+        #[cfg(target_os = "windows")]
+        {
+            todo!()
+        }
+
+        if tun_mode.is_some() {
+            update_core_config().await?;
+        }
+
+        if auto_launch.is_some() {
+            sysopt::Sysopt::global().update_launch()?;
+        }
+        if system_proxy.is_some() || proxy_bypass.is_some() {
+            sysopt::Sysopt::global().update_sysproxy()?;
+            sysopt::Sysopt::global().guard_proxy();
+        }
+
+        if let Some(true) = patch.enable_proxy_guard {
+            sysopt::Sysopt::global().guard_proxy();
+        }
+
+        if let Some(hotkeys) = patch.hotkeys {
+            hotkey::Hotkey::global().update(hotkeys)?;
+        }
+
+        if language.is_some() {
+            handle::Handle::update_systray()?;
+        } else if system_proxy.or(tun_mode).is_some() {
+            handle::Handle::update_systray_part()?;
+        }
+
+        <Result<()>>::Ok(())
+    } {
+        Ok(()) => {
+            Config::verge().apply();
+            Config::verge().data().save_file()?;
+            Ok(())
+        }
+        Err(err) => {
+            Config::verge().discard();
+            Err(err)
+        }
+    }
+}
+
+/// 更新某个profile
+/// 如果更新当前配置就激活配置
+pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
+    let url_opt = {
+        let profiles = Config::profiles();
+        let profiles = profiles.latest();
+        let item = profiles.get_item(&uid)?;
+        let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
+
+        if !is_remote {
+            None // 直接更新
+        } else if item.url.is_none() {
+            bail!("failed to get the profile item url");
+        } else {
+            Some((item.url.clone().unwrap(), item.option.clone()))
+        }
+    };
+
+    let should_update = match url_opt {
+        Some((url, opt)) => {
+            let merged_opt = PrfOption::merge(opt, option);
+            let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
+
+            let profiles = Config::profiles();
+            let mut profiles = profiles.latest();
+            profiles.update_item(uid.clone(), item)?;
+
+            Some(uid) == profiles.get_current()
+        }
+        None => true,
+    };
+
+    if should_update {
+        update_core_config().await?;
+    }
+
+    Ok(())
+}
+
+/// 更新配置
+async fn update_core_config() -> Result<()> {
+    match CoreManager::global().update_config().await {
+        Ok(_) => {
+            handle::Handle::refresh_clash();
+            handle::Handle::notice_message("set_config::ok", "ok");
+            Ok(())
+        }
+        Err(err) => {
+            handle::Handle::notice_message("set_config::error", format!("{err}"));
+            Err(err)
+        }
+    }
 }

+ 12 - 32
src-tauri/src/main.rs

@@ -6,12 +6,12 @@
 mod cmds;
 mod config;
 mod core;
-mod data;
+mod enhance;
 mod feat;
 mod utils;
 
 use crate::utils::{init, resolve, server};
-use tauri::{api, Manager, SystemTray};
+use tauri::{api, SystemTray};
 
 fn main() -> std::io::Result<()> {
     // 单例检测
@@ -20,18 +20,12 @@ fn main() -> std::io::Result<()> {
         return Ok(());
     }
 
-    #[cfg(target_os = "windows")]
-    unsafe {
-        use crate::utils::dirs;
-        dirs::init_portable_flag();
-    }
-
-    crate::log_if_err!(init::init_config());
+    crate::log_err!(init::init_config());
 
     #[allow(unused_mut)]
     let mut builder = tauri::Builder::default()
-        .setup(|app| Ok(resolve::resolve_setup(app)))
         .system_tray(SystemTray::new())
+        .setup(|app| Ok(resolve::resolve_setup(app)))
         .on_system_tray_event(core::tray::Tray::on_system_tray_event)
         .invoke_handler(tauri::generate_handler![
             // common
@@ -39,7 +33,7 @@ fn main() -> std::io::Result<()> {
             cmds::open_app_dir,
             cmds::open_logs_dir,
             cmds::open_web_url,
-            cmds::kill_sidecar,
+            // cmds::kill_sidecar,
             cmds::restart_sidecar,
             // clash
             cmds::get_clash_info,
@@ -53,24 +47,20 @@ fn main() -> std::io::Result<()> {
             // verge
             cmds::get_verge_config,
             cmds::patch_verge_config,
-            cmds::update_hotkeys,
+            // cmds::update_hotkeys,
             // profile
+            cmds::get_profiles,
+            cmds::enhance_profiles,
+            cmds::patch_profiles_config,
             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,
@@ -96,22 +86,10 @@ fn main() -> std::io::Result<()> {
         );
     }
 
-    #[allow(unused_mut)]
-    let mut app = builder
+    let 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();
@@ -123,6 +101,8 @@ fn main() -> std::io::Result<()> {
         }
         #[cfg(target_os = "macos")]
         tauri::RunEvent::WindowEvent { label, event, .. } => {
+            use tauri::Manager;
+
             if label == "main" {
                 match event {
                     tauri::WindowEvent::CloseRequested { api, .. } => {

+ 0 - 59
src-tauri/src/utils/config.rs

@@ -1,59 +0,0 @@
-use anyhow::{Context, Result};
-use serde::{de::DeserializeOwned, Serialize};
-use serde_yaml::{Mapping, Value};
-use std::{fs, path::PathBuf};
-
-/// read data from yaml as struct T
-pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
-    if !path.exists() {
-        log::error!(target: "app", "file not found \"{}\"", path.display());
-        return T::default();
-    }
-
-    let yaml_str = fs::read_to_string(&path).unwrap_or("".into());
-
-    match serde_yaml::from_str::<T>(&yaml_str) {
-        Ok(val) => val,
-        Err(_) => {
-            log::error!(target: "app", "failed to read yaml file \"{}\"", path.display());
-            T::default()
-        }
-    }
-}
-
-/// read mapping from yaml fix #165
-pub fn read_merge_mapping(path: PathBuf) -> Mapping {
-    let map = Mapping::new();
-
-    if !path.exists() {
-        log::error!(target: "app", "file not found \"{}\"", path.display());
-        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());
-            map
-        }
-    }
-}
-
-/// save the data to the file
-/// can set `prefix` string to add some comments
-pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
-    let data_str = serde_yaml::to_string(data)?;
-
-    let yaml_str = match prefix {
-        Some(prefix) => format!("{prefix}{data_str}"),
-        None => data_str,
-    };
-
-    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}\""))
-}

+ 56 - 43
src-tauri/src/utils/dirs.rs

@@ -1,4 +1,4 @@
-use std::env::temp_dir;
+use anyhow::Result;
 use std::path::PathBuf;
 use tauri::{
     api::path::{home_dir, resource_dir},
@@ -13,7 +13,6 @@ static APP_DIR: &str = "clash-verge-dev";
 static CLASH_CONFIG: &str = "config.yaml";
 static VERGE_CONFIG: &str = "verge.yaml";
 static PROFILE_YAML: &str = "profiles.yaml";
-static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
 
 static mut RESOURCE_DIR: Option<PathBuf> = None;
 
@@ -21,49 +20,58 @@ static mut RESOURCE_DIR: Option<PathBuf> = None;
 #[allow(unused)]
 static mut PORTABLE_FLAG: bool = false;
 
-pub static mut APP_VERSION: &str = "v1.1.1";
+pub static mut APP_VERSION: &str = "v1.1.2";
 
 /// initialize portable flag
-#[allow(unused)]
-pub unsafe fn init_portable_flag() {
-    #[cfg(target_os = "windows")]
-    {
-        use tauri::utils::platform::current_exe;
+#[cfg(target_os = "windows")]
+pub unsafe fn init_portable_flag() -> Result<()> {
+    use tauri::utils::platform::current_exe;
 
-        let exe = current_exe().unwrap();
-        let dir = exe.parent().unwrap();
+    let exe = current_exe()?;
+
+    if let Some(dir) = exe.parent() {
         let dir = PathBuf::from(dir).join(".config/PORTABLE");
 
         if dir.exists() {
             PORTABLE_FLAG = true;
         }
     }
+
+    Ok(())
 }
 
 /// get the verge app home dir
-pub fn app_home_dir() -> PathBuf {
+pub fn app_home_dir() -> Result<PathBuf> {
     #[cfg(target_os = "windows")]
     unsafe {
         use tauri::utils::platform::current_exe;
 
         if !PORTABLE_FLAG {
-            home_dir().unwrap().join(".config").join(APP_DIR)
+            Ok(home_dir()
+                .ok_or(anyhow::anyhow!("failed to get app home dir"))?
+                .join(".config")
+                .join(APP_DIR))
         } else {
-            let app_exe = current_exe().unwrap();
-            let app_exe = dunce::canonicalize(app_exe).unwrap();
-            let app_dir = app_exe.parent().unwrap();
-            PathBuf::from(app_dir).join(".config").join(APP_DIR)
+            let app_exe = current_exe()?;
+            let app_exe = dunce::canonicalize(app_exe)?;
+            let app_dir = app_exe
+                .parent()
+                .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
+            Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR))
         }
     }
 
     #[cfg(not(target_os = "windows"))]
-    home_dir().unwrap().join(".config").join(APP_DIR)
+    Ok(home_dir()
+        .ok_or(anyhow::anyhow!("failed to get the app home dir"))?
+        .join(".config")
+        .join(APP_DIR))
 }
 
 /// get the resources dir
-pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
+pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
     let res_dir = resource_dir(package_info, &Env::default())
-        .unwrap()
+        .ok_or(anyhow::anyhow!("failed to get the resource dir"))?
         .join("resources");
 
     unsafe {
@@ -74,51 +82,48 @@ pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
         APP_VERSION = Box::leak(Box::new(ver_str));
     }
 
-    res_dir
+    Ok(res_dir)
 }
 
 /// profiles dir
-pub fn app_profiles_dir() -> PathBuf {
-    app_home_dir().join("profiles")
+pub fn app_profiles_dir() -> Result<PathBuf> {
+    Ok(app_home_dir()?.join("profiles"))
 }
 
 /// logs dir
-pub fn app_logs_dir() -> PathBuf {
-    app_home_dir().join("logs")
+pub fn app_logs_dir() -> Result<PathBuf> {
+    Ok(app_home_dir()?.join("logs"))
 }
 
-pub fn clash_path() -> PathBuf {
-    app_home_dir().join(CLASH_CONFIG)
+pub fn clash_path() -> Result<PathBuf> {
+    Ok(app_home_dir()?.join(CLASH_CONFIG))
 }
 
-pub fn verge_path() -> PathBuf {
-    app_home_dir().join(VERGE_CONFIG)
+pub fn verge_path() -> Result<PathBuf> {
+    Ok(app_home_dir()?.join(VERGE_CONFIG))
 }
 
-pub fn profiles_path() -> PathBuf {
-    app_home_dir().join(PROFILE_YAML)
+pub fn profiles_path() -> Result<PathBuf> {
+    Ok(app_home_dir()?.join(PROFILE_YAML))
 }
 
-pub fn profiles_temp_path() -> PathBuf {
-    #[cfg(not(feature = "debug-yml"))]
-    return temp_dir().join(PROFILE_TEMP);
-
-    #[cfg(feature = "debug-yml")]
-    return app_home_dir().join(PROFILE_TEMP);
+pub fn app_res_dir() -> Result<PathBuf> {
+    unsafe {
+        Ok(RESOURCE_DIR
+            .clone()
+            .ok_or(anyhow::anyhow!("failed to get the resource dir"))?)
+    }
 }
 
-pub fn clash_pid_path() -> PathBuf {
-    unsafe { RESOURCE_DIR.clone().unwrap().join("clash.pid") }
+pub fn clash_pid_path() -> Result<PathBuf> {
+    unsafe { Ok(RESOURCE_DIR.clone().unwrap().join("clash.pid")) }
 }
 
-#[cfg(windows)]
-static SERVICE_PATH: &str = "clash-verge-service.exe";
-
 #[cfg(windows)]
 pub fn service_path() -> PathBuf {
     unsafe {
         let res_dir = RESOURCE_DIR.clone().unwrap();
-        res_dir.join(SERVICE_PATH)
+        res_dir.join("clash-verge-service.exe")
     }
 }
 
@@ -128,7 +133,7 @@ pub fn service_log_file() -> PathBuf {
 
     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").to_string();
     let log_file = format!("{}.log", local_time);
     let log_file = log_dir.join(log_file);
 
@@ -136,3 +141,11 @@ pub fn service_log_file() -> PathBuf {
 
     log_file
 }
+
+pub fn path_to_str(path: &PathBuf) -> Result<&str> {
+    let path_str = path
+        .as_os_str()
+        .to_str()
+        .ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
+    Ok(path_str)
+}

+ 75 - 26
src-tauri/src/utils/help.rs

@@ -1,15 +1,51 @@
-use anyhow::Result;
+use anyhow::{anyhow, bail, Context, Result};
 use nanoid::nanoid;
-use std::path::PathBuf;
-use std::process::Command;
-use std::str::FromStr;
-use std::time::{SystemTime, UNIX_EPOCH};
-
-pub fn get_now() -> usize {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .unwrap()
-        .as_secs() as _
+use serde::{de::DeserializeOwned, Serialize};
+use serde_yaml::{Mapping, Value};
+use std::{fs, path::PathBuf, process::Command, str::FromStr};
+
+/// read data from yaml as struct T
+pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
+    if !path.exists() {
+        bail!("file not found \"{}\"", path.display());
+    }
+
+    let yaml_str = fs::read_to_string(&path)
+        .context(format!("failed to read the file \"{}\"", path.display()))?;
+
+    serde_yaml::from_str::<T>(&yaml_str).context(format!(
+        "failed to read the file with yaml format \"{}\"",
+        path.display()
+    ))
+}
+
+/// read mapping from yaml fix #165
+pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
+    let mut val: Value = read_yaml(path)?;
+    val.apply_merge()
+        .context(format!("failed to apply merge \"{}\"", path.display()))?;
+
+    Ok(val
+        .as_mapping()
+        .ok_or(anyhow!(
+            "failed to transform to yaml mapping \"{}\"",
+            path.display()
+        ))?
+        .to_owned())
+}
+
+/// save the data to the file
+/// can set `prefix` string to add some comments
+pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
+    let data_str = serde_yaml::to_string(data)?;
+
+    let yaml_str = match prefix {
+        Some(prefix) => format!("{prefix}\n\n{data_str}"),
+        None => data_str,
+    };
+
+    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}\""))
 }
 
 const ALPHABET: [char; 62] = [
@@ -70,27 +106,40 @@ pub fn open_file(path: PathBuf) -> Result<()> {
 }
 
 #[macro_export]
-macro_rules! log_if_err {
-  ($result: expr) => {
-    if let Err(err) = $result {
-      log::error!(target: "app", "{err}");
-    }
-  };
+macro_rules! error {
+    ($result: expr) => {
+        log::error!(target: "app", "{}", $result);
+    };
+}
+
+#[macro_export]
+macro_rules! log_err {
+    ($result: expr) => {
+        if let Err(err) = $result {
+            log::error!(target: "app", "{err}");
+        }
+    };
+
+    ($result: expr, $err_str: expr) => {
+        if let Err(_) = $result {
+            log::error!(target: "app", "{}", $err_str);
+        }
+    };
 }
 
 /// wrap the anyhow error
 /// transform the error to String
 #[macro_export]
 macro_rules! wrap_err {
-  ($stat: expr) => {
-    match $stat {
-      Ok(a) => Ok(a),
-      Err(err) => {
-        log::error!(target: "app", "{}", err.to_string());
-        Err(format!("{}", err.to_string()))
-      }
-    }
-  };
+    ($stat: expr) => {
+        match $stat {
+            Ok(a) => Ok(a),
+            Err(err) => {
+                log::error!(target: "app", "{}", err.to_string());
+                Err(format!("{}", err.to_string()))
+            }
+        }
+    };
 }
 
 /// return the string literal error

+ 39 - 43
src-tauri/src/utils/init.rs

@@ -1,4 +1,4 @@
-use crate::utils::{dirs, tmpl};
+use crate::utils::dirs;
 use anyhow::Result;
 use chrono::Local;
 use log::LevelFilter;
@@ -7,27 +7,33 @@ use log4rs::append::file::FileAppender;
 use log4rs::config::{Appender, Config, Logger, Root};
 use log4rs::encode::pattern::PatternEncoder;
 use std::fs;
-use std::io::Write;
 use tauri::PackageInfo;
 
 /// initialize this instance's log file
 fn init_log() -> Result<()> {
-    let log_dir = dirs::app_logs_dir();
+    let log_dir = dirs::app_logs_dir()?;
     if !log_dir.exists() {
         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").to_string();
     let log_file = format!("{}.log", local_time);
     let log_file = log_dir.join(log_file);
 
-    let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}";
-    let stdout = ConsoleAppender::builder()
-        .encoder(Box::new(PatternEncoder::new(time_format)))
-        .build();
-    let tofile = FileAppender::builder()
-        .encoder(Box::new(PatternEncoder::new(time_format)))
-        .build(log_file)?;
+    #[cfg(feature = "verge-dev")]
+    let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {M} {m}{n}";
+    #[cfg(not(feature = "verge-dev"))]
+    let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}";
+
+    let encode = Box::new(PatternEncoder::new(time_format));
+
+    let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
+    let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
+
+    #[cfg(feature = "verge-dev")]
+    let level = LevelFilter::Debug;
+    #[cfg(not(feature = "verge-dev"))]
+    let level = LevelFilter::Info;
 
     let config = Config::builder()
         .appender(Appender::builder().build("stdout", Box::new(stdout)))
@@ -36,9 +42,9 @@ fn init_log() -> Result<()> {
             Logger::builder()
                 .appenders(["file", "stdout"])
                 .additive(false)
-                .build("app", LevelFilter::Info),
+                .build("app", level),
         )
-        .build(Root::builder().appender("stdout").build(LevelFilter::Info))?;
+        .build(Root::builder().appender("stdout").build(level))?;
 
     log4rs::init_config(config)?;
 
@@ -47,44 +53,32 @@ fn init_log() -> Result<()> {
 
 /// Initialize all the files from resources
 pub fn init_config() -> Result<()> {
-    let _ = init_log();
+    #[cfg(target_os = "windows")]
+    unsafe {
+        let _ = dirs::init_portable_flag();
+    }
 
-    let app_dir = dirs::app_home_dir();
-    let profiles_dir = dirs::app_profiles_dir();
+    let _ = init_log();
 
-    if !app_dir.exists() {
-        let _ = fs::create_dir_all(&app_dir);
-    }
-    if !profiles_dir.exists() {
-        let _ = fs::create_dir_all(&profiles_dir);
-    }
+    let _ = dirs::app_home_dir().map(|app_dir| {
+        if !app_dir.exists() {
+            let _ = fs::create_dir_all(&app_dir);
+        }
+    });
 
-    // target path
-    let clash_path = app_dir.join("config.yaml");
-    let verge_path = app_dir.join("verge.yaml");
-    let profile_path = app_dir.join("profiles.yaml");
+    let _ = dirs::app_profiles_dir().map(|profiles_dir| {
+        if !profiles_dir.exists() {
+            let _ = fs::create_dir_all(&profiles_dir);
+        }
+    });
 
-    if !clash_path.exists() {
-        fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?;
-    }
-    if !verge_path.exists() {
-        fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?;
-    }
-    if !profile_path.exists() {
-        fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?;
-    }
     Ok(())
 }
 
 /// initialize app
-pub fn init_resources(package_info: &PackageInfo) {
-    // create app dir
-    let app_dir = dirs::app_home_dir();
-    let res_dir = dirs::app_resources_dir(package_info);
-
-    if !app_dir.exists() {
-        let _ = fs::create_dir_all(&app_dir);
-    }
+pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
+    let app_dir = dirs::app_home_dir()?;
+    let res_dir = dirs::app_resources_dir(package_info)?;
 
     // copy the resource file
     for file in ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"].iter() {
@@ -94,4 +88,6 @@ pub fn init_resources(package_info: &PackageInfo) {
             let _ = fs::copy(src_path, target_path);
         }
     }
+
+    Ok(())
 }

+ 1 - 2
src-tauri/src/utils/mod.rs

@@ -1,8 +1,7 @@
-pub mod config;
 pub mod dirs;
 pub mod help;
 pub mod init;
 pub mod resolve;
 pub mod server;
 pub mod tmpl;
-mod winhelp;
+// mod winhelp;

+ 34 - 39
src-tauri/src/utils/resolve.rs

@@ -1,48 +1,43 @@
-use crate::{
-    core::{tray, Core},
-    data::Data,
-    utils::init,
-    utils::server,
-};
+use crate::config::Config;
+use crate::log_err;
+use crate::{core::*, utils::init, utils::server};
 use tauri::{App, AppHandle, Manager};
 
 /// handle something when start app
-pub fn resolve_setup(app: &App) {
-    let _ = app
-        .tray_handle()
-        .set_menu(tray::Tray::tray_menu(&app.app_handle()));
+pub fn resolve_setup(app: &mut App) {
+    #[cfg(target_os = "macos")]
+    app.set_activation_policy(tauri::ActivationPolicy::Accessory);
 
-    init::init_resources(app.package_info());
+    handle::Handle::global().init(app.app_handle());
 
-    let silent_start = {
-        let global = Data::global();
-        let verge = global.verge.lock();
-        let singleton = verge.app_singleton_port.clone();
+    log_err!(init::init_resources(app.package_info()));
 
-        // setup a simple http server for singleton
-        server::embed_server(&app.app_handle(), singleton);
+    // 启动核心
+    log_err!(Config::init_config());
+    log_err!(CoreManager::global().init());
 
-        verge.enable_silent_start.clone().unwrap_or(false)
-    };
+    // setup a simple http server for singleton
+    server::embed_server(app.app_handle());
 
-    // core should be initialized after init_app fix #122
-    let core = Core::global();
-    core.init(app.app_handle());
+    log_err!(tray::Tray::update_systray(&app.app_handle()));
 
-    if !silent_start {
+    let silent_start = { Config::verge().data().enable_silent_start.clone() };
+    if !silent_start.unwrap_or(false) {
         create_window(&app.app_handle());
     }
+
+    log_err!(sysopt::Sysopt::global().init_launch());
+    log_err!(sysopt::Sysopt::global().init_sysproxy());
+
+    log_err!(handle::Handle::update_systray_part());
+    log_err!(hotkey::Hotkey::global().init(app.app_handle()));
+    log_err!(timer::Timer::global().init());
 }
 
 /// reset system proxy
 pub fn resolve_reset() {
-    let core = Core::global();
-    let mut sysopt = core.sysopt.lock();
-    crate::log_if_err!(sysopt.reset_sysproxy());
-    drop(sysopt);
-
-    let mut service = core.service.lock();
-    crate::log_if_err!(service.stop());
+    log_err!(sysopt::Sysopt::global().reset_sysproxy());
+    log_err!(CoreManager::global().stop_core());
 }
 
 /// create main window
@@ -66,31 +61,31 @@ pub fn create_window(app_handle: &AppHandle) {
 
     #[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)
+            .visible(false)
             .build()
         {
             Ok(_) => {
                 let app_handle = app_handle.clone();
 
+                if let Some(window) = app_handle.get_window("main") {
+                    let _ = set_shadow(&window, true);
+                }
+
                 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);
-                        }
+                        let _ = window.unminimize();
+                        let _ = window.set_focus();
                     }
                 });
             }
@@ -99,10 +94,10 @@ pub fn create_window(app_handle: &AppHandle) {
     }
 
     #[cfg(target_os = "macos")]
-    crate::log_if_err!(builder.decorations(true).inner_size(800.0, 642.0).build());
+    crate::log_err!(builder.decorations(true).inner_size(800.0, 642.0).build());
 
     #[cfg(target_os = "linux")]
-    crate::log_if_err!(builder
+    crate::log_err!(builder
         .decorations(false)
         .transparent(true)
         .inner_size(800.0, 636.0)

+ 16 - 16
src-tauri/src/utils/server.rs

@@ -1,26 +1,27 @@
 extern crate warp;
 
 use super::resolve;
-use crate::data::Verge;
+use crate::config::IVerge;
+use anyhow::{bail, Result};
 use port_scanner::local_port_available;
 use tauri::AppHandle;
 use warp::Filter;
 
-#[cfg(not(feature = "verge-dev"))]
-const SERVER_PORT: u16 = 33331;
-#[cfg(feature = "verge-dev")]
-const SERVER_PORT: u16 = 11233;
-
 /// check whether there is already exists
-pub fn check_singleton() -> Result<(), ()> {
-    let verge = Verge::new();
-    let port = verge.app_singleton_port.unwrap_or(SERVER_PORT);
+pub fn check_singleton() -> Result<()> {
+    let port = IVerge::get_singleton_port();
 
     if !local_port_available(port) {
         tauri::async_runtime::block_on(async {
-            let url = format!("http://127.0.0.1:{}/commands/visible", port);
-            reqwest::get(url).await.unwrap();
-            Err(())
+            let url = format!("http://127.0.0.1:{port}/commands/visible");
+            let resp = reqwest::get(url).await?.text().await?;
+
+            if &resp == "ok" {
+                bail!("app exists");
+            }
+
+            log::error!("failed to setup singleton listen server");
+            Ok(())
         })
     } else {
         Ok(())
@@ -29,14 +30,13 @@ pub fn check_singleton() -> Result<(), ()> {
 
 /// The embed server only be used to implement singleton process
 /// maybe it can be used as pac server later
-pub fn embed_server(app_handle: &AppHandle, port: Option<u16>) {
-    let app_handle = app_handle.clone();
-    let port = port.unwrap_or(SERVER_PORT);
+pub fn embed_server(app_handle: AppHandle) {
+    let port = IVerge::get_singleton_port();
 
     tauri::async_runtime::spawn(async move {
         let commands = warp::path!("commands" / "visible").map(move || {
             resolve::create_window(&app_handle);
-            return format!("ok");
+            format!("ok")
         });
 
         warp::serve(commands).bind(([127, 0, 0, 1], port)).await;

+ 1 - 35
src-tauri/src/utils/tmpl.rs

@@ -1,37 +1,5 @@
 ///! Some config file template
 
-/// template for clash core `config.yaml`
-pub const CLASH_CONFIG: &[u8] = br#"# Default Config For Clash Core
-
-mixed-port: 7890
-log-level: info
-allow-lan: false
-external-controller: 127.0.0.1:9090
-mode: rule
-secret: ""
-"#;
-
-/// template for `profiles.yaml`
-pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge
-
-current: ~
-items: ~
-";
-
-/// template for `verge.yaml`
-pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge
-
-clash_core: clash-meta
-language: en
-theme_mode: light
-theme_blur: false
-traffic_graph: true
-enable_self_startup: false
-enable_system_proxy: false
-enable_proxy_guard: false
-proxy_guard_duration: 10
-";
-
 /// template for new a profile item
 pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
 
@@ -61,9 +29,7 @@ append-proxy-groups:
 
 /// enhanced profile
 pub const ITEM_SCRIPT: &str = "// Define the `main` function
-// The argument to this function is the clash config 
-// or the result of the previous handler
-// so you should return the config after processing
+
 function main(params) {
   return params;
 }

+ 5 - 5
src/components/profile/enhanced.tsx

@@ -7,7 +7,7 @@ import {
   getProfiles,
   deleteProfile,
   enhanceProfiles,
-  changeProfileChain,
+  patchProfilesConfig,
   getRuntimeLogs,
 } from "@/services/cmds";
 import ProfileMore from "./profile-more";
@@ -43,7 +43,7 @@ const EnhancedMode = (props: Props) => {
     if (chain.includes(uid)) return;
 
     const newChain = [...chain, uid];
-    await changeProfileChain(newChain);
+    await patchProfilesConfig({ chain: newChain });
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
     mutateLogs();
   });
@@ -52,7 +52,7 @@ const EnhancedMode = (props: Props) => {
     if (!chain.includes(uid)) return;
 
     const newChain = chain.filter((i) => i !== uid);
-    await changeProfileChain(newChain);
+    await patchProfilesConfig({ chain: newChain });
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
     mutateLogs();
   });
@@ -72,7 +72,7 @@ const EnhancedMode = (props: Props) => {
     if (!chain.includes(uid)) return;
 
     const newChain = [uid].concat(chain.filter((i) => i !== uid));
-    await changeProfileChain(newChain);
+    await patchProfilesConfig({ chain: newChain });
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
     mutateLogs();
   });
@@ -81,7 +81,7 @@ const EnhancedMode = (props: Props) => {
     if (!chain.includes(uid)) return;
 
     const newChain = chain.filter((i) => i !== uid).concat([uid]);
-    await changeProfileChain(newChain);
+    await patchProfilesConfig({ chain: newChain });
     mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
     mutateLogs();
   });

+ 2 - 2
src/components/setting/mods/clash-field-viewer.tsx

@@ -15,9 +15,9 @@ import {
 } from "@mui/material";
 import { InfoRounded } from "@mui/icons-material";
 import {
-  changeProfileValid,
   getProfiles,
   getRuntimeExists,
+  patchProfilesConfig,
 } from "@/services/cmds";
 import { ModalHandler } from "@/hooks/use-modal-handler";
 import {
@@ -91,7 +91,7 @@ const ClashFieldViewer = ({ handler }: Props) => {
     if (curSet.size === oldSet.size && curSet.size === joinSet.size) return;
 
     try {
-      await changeProfileValid([...curSet]);
+      await patchProfilesConfig({ valid: [...curSet] });
       mutateProfile();
       // Notice.success("Refresh clash config", 1000);
     } catch (err: any) {

+ 7 - 7
src/pages/profiles.tsx

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
 import {
   getProfiles,
   patchProfile,
-  selectProfile,
+  patchProfilesConfig,
   importProfile,
 } from "@/services/cmds";
 import { getProxies, updateProxy } from "@/services/api";
@@ -113,7 +113,7 @@ const ProfilePage = () => {
 
         if (!newProfiles.current && remoteItem) {
           const current = remoteItem.uid;
-          selectProfile(current);
+          patchProfilesConfig({ current });
           mutate("getProfiles", { ...newProfiles, current }, true);
           mutate("getRuntimeLogs");
         }
@@ -125,13 +125,13 @@ const ProfilePage = () => {
     }
   };
 
-  const onSelect = useLockFn(async (uid: string, force: boolean) => {
-    if (!force && uid === profiles.current) return;
+  const onSelect = useLockFn(async (current: string, force: boolean) => {
+    if (!force && current === profiles.current) return;
 
     try {
-      await selectProfile(uid);
-      setCurrentProfile(uid);
-      mutate("getProfiles", { ...profiles, current: uid }, true);
+      await patchProfilesConfig({ current });
+      setCurrentProfile(current);
+      mutate("getProfiles", { ...profiles, current: current }, true);
       mutate("getRuntimeLogs");
       // if (force) Notice.success("Refresh clash config", 1000);
     } catch (err: any) {

+ 4 - 24
src/services/cmds.ts

@@ -32,6 +32,10 @@ export async function enhanceProfiles() {
   return invoke<void>("enhance_profiles");
 }
 
+export async function patchProfilesConfig(profiles: CmdType.ProfilesConfig) {
+  return invoke<void>("patch_profiles_config");
+}
+
 export async function createProfile(
   item: Partial<CmdType.ProfileItem>,
   fileData?: string | null
@@ -76,18 +80,6 @@ export async function patchProfile(
   return invoke<void>("patch_profile", { index, profile });
 }
 
-export async function selectProfile(index: string) {
-  return invoke<void>("select_profile", { index });
-}
-
-export async function changeProfileChain(chain?: string[]) {
-  return invoke<void>("change_profile_chain", { chain });
-}
-
-export async function changeProfileValid(valid?: string[]) {
-  return invoke<void>("change_profile_valid", { valid });
-}
-
 export async function getClashInfo() {
   return invoke<CmdType.ClashInfo | null>("get_clash_info");
 }
@@ -136,10 +128,6 @@ export async function restartSidecar() {
   return invoke<void>("restart_sidecar");
 }
 
-export async function killSidecar() {
-  return invoke<any>("kill_sidecar");
-}
-
 export async function openAppDir() {
   return invoke<void>("open_app_dir").catch((err) =>
     Notice.error(err?.message || err.toString(), 1500)
@@ -158,14 +146,6 @@ export async function openWebUrl(url: string) {
 
 /// service mode
 
-export async function startService() {
-  return invoke<void>("start_service");
-}
-
-export async function stopService() {
-  return invoke<void>("stop_service");
-}
-
 export async function checkService() {
   try {
     const result = await invoke<any>("check_service");