Compare commits

..
Author SHA1 Message Date
renovate[bot] 2a61b783bf chore(deps): update cargo dependencies 2026-10-11 16:08:28 +00:00
155 changed files with 3557 additions and 3874 deletions
+3 -3
View File
@@ -4,11 +4,11 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "24.22.0",
"pnpmVersion": "12.14.0"
"version": "24.21.0",
"pnpmVersion": "12.10.1"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.1",
"version": "1.99.0",
"profile": "default"
},
"ghcr.io/devcontainers/features/git:1": {},
+10 -10
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -154,7 +154,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.99.0'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -185,7 +185,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -206,7 +206,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -257,7 +257,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.99.0'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -282,7 +282,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -347,7 +347,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -426,7 +426,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -454,7 +454,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -524,7 +524,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6.1.0
name: Install pnpm
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+3 -3
View File
@@ -74,7 +74,7 @@ jobs:
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@1.99.1
uses: dtolnay/rust-toolchain@1.99.0
- name: Rust Cache
uses: Swatinem/rust-cache@v2
@@ -103,7 +103,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -129,7 +129,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
+14 -6
View File
@@ -6,7 +6,6 @@ on:
permissions:
contents: read
pull-requests: read
env:
HUSKY: 0
@@ -52,7 +51,7 @@ jobs:
- uses: actions/setup-node@v7
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: '24.22.0'
node-version: '24.21.0'
cache: 'pnpm'
- name: Restore pnpm cache
@@ -100,7 +99,14 @@ jobs:
id: check_inputs
uses: dorny/paths-filter@v4
with:
filters: scripts/contract-inputs.json
filters: |
contract:
- 'src/services/contract.ts'
- 'scripts/gen-contract.mjs'
- 'src-tauri/src/core/notify/**'
- 'src-tauri/src/bin/**'
- 'src-tauri/src/lib.rs'
- '.github/workflows/frontend-check.yml'
- name: Skip if contract inputs unchanged
if: steps.check_inputs.outputs.contract != 'true'
@@ -138,7 +144,7 @@ jobs:
- uses: actions/setup-node@v7
if: steps.check_inputs.outputs.contract == 'true'
with:
node-version: '24.22.0'
node-version: '24.21.0'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
@@ -153,6 +159,8 @@ jobs:
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
done
- name: Check generated contract without changing the checkout
- name: Regenerate contract and require empty diff
if: steps.check_inputs.outputs.contract == 'true'
run: pnpm gen:contract --check
run: |
pnpm gen:contract
git diff --exit-code -- src/services/contract.ts
+12 -12
View File
@@ -204,7 +204,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.99.0'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -230,7 +230,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -244,7 +244,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -311,7 +311,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.99.0'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -331,7 +331,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- name: Install pnpm
uses: pnpm/action-setup@v6
@@ -389,7 +389,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -446,7 +446,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.99.0'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -466,7 +466,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -487,7 +487,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -547,7 +547,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -573,7 +573,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -638,7 +638,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -22,13 +22,13 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.99.0'
targets: x86_64-pc-windows-msvc
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- name: Install pnpm
uses: pnpm/action-setup@v6
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.21.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
-1
View File
@@ -5,5 +5,4 @@ if ! command -v "cargo-make" >/dev/null 2>&1; then
cargo install --force cargo-make
fi
node scripts/check-contract-changes.mjs --push "$1" || exit $?
cargo make pre-push
+3 -3
View File
@@ -1,3 +1,3 @@
nodejs 24.22.0
pnpm 12.14.0
rust 1.99.1
nodejs 24.21.0
pnpm 12.10.1
rust 1.99.0
Generated
+100 -277
View File
@@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "adler2"
version = "2.0.1"
@@ -1119,7 +1113,7 @@ dependencies = [
"getrandom 0.4.3",
"libc",
"log",
"minisign-verify 0.3.0",
"minisign-verify",
"nanoid",
"network-interface",
"objc2",
@@ -1131,18 +1125,16 @@ dependencies = [
"regex",
"reqwest 0.13.5",
"reqwest_dav",
"rfd",
"rfd 0.17.2",
"runas",
"rustls",
"scopeguard",
"serde",
"serde_json",
"serde_yaml_ng",
"sha2 0.11.1",
"sha2 0.11.0",
"smartstring",
"socket2",
"specta",
"specta-typescript",
"sysproxy",
"tauri",
"tauri-build",
@@ -1163,7 +1155,6 @@ dependencies = [
"tauri-plugin-updater",
"tauri-plugin-wdio-webdriver",
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tokio-rustls",
"tokio-stream",
@@ -1175,7 +1166,7 @@ dependencies = [
"windows-service",
"windows-sys 0.61.2",
"winreg 0.56.0",
"zip 9.0.4",
"zip 9.0.1",
]
[[package]]
@@ -1222,7 +1213,6 @@ dependencies = [
"rust_iso3166",
"serde",
"serde_json",
"specta",
"tokio",
]
@@ -1247,7 +1237,7 @@ dependencies = [
"once_cell",
"serde",
"serde_json",
"sha2 0.11.1",
"sha2 0.11.0",
"strum",
"strum_macros",
"sysproxy",
@@ -1462,12 +1452,9 @@ dependencies = [
[[package]]
name = "constant_time_eq"
version = "0.6.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9"
dependencies = [
"rustversion",
]
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cookie"
@@ -3954,9 +3941,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
[[package]]
name = "libc"
version = "0.2.191"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "libdbus-sys"
@@ -4051,11 +4038,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
version = "0.22.0"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb"
checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
dependencies = [
"sha2 0.11.1",
"sha2 0.11.0",
]
[[package]]
@@ -4170,12 +4157,6 @@ dependencies = [
"unicase",
]
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "minisign-verify"
version = "0.3.0"
@@ -5346,9 +5327,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppmd-rust"
version = "1.5.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
[[package]]
name = "ppv-lite86"
@@ -5912,6 +5893,29 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "rfd"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [
"block2",
"dispatch2",
"js-sys",
"libc",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"percent-encoding",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -6070,9 +6074,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.46"
version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"aws-lc-rs",
"log",
@@ -6600,9 +6604,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.11.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -6625,7 +6629,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
dependencies = [
"libc",
"sigchld",
"windows-sys 0.60.2",
]
@@ -6645,17 +6648,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "sigchld"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
dependencies = [
"libc",
"os_pipe",
"signal-hook 0.3.18",
]
[[package]]
name = "signal-hook"
version = "0.1.17"
@@ -6666,16 +6658,6 @@ dependencies = [
"signal-hook-registry",
]
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -6817,58 +6799,6 @@ dependencies = [
"system-deps 6.2.2",
]
[[package]]
name = "specta"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
dependencies = [
"paste",
"rustc_version 0.4.1",
"serde_json",
"specta-macros",
]
[[package]]
name = "specta-macros"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "specta-serde"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
dependencies = [
"specta",
"specta-macros",
]
[[package]]
name = "specta-typescript"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
dependencies = [
"specta",
]
[[package]]
name = "specta-util"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
dependencies = [
"specta",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -6940,9 +6870,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
dependencies = [
"base64 0.21.7",
"serde",
@@ -7189,9 +7119,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tauri"
version = "2.12.1"
version = "2.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9"
checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea"
dependencies = [
"anyhow",
"bytes",
@@ -7224,7 +7154,6 @@ dependencies = [
"serde_json",
"serde_repr",
"serialize-to-javascript",
"specta",
"swift-rs",
"tauri-build",
"tauri-macros",
@@ -7237,16 +7166,16 @@ dependencies = [
"tray-icon",
"url",
"webkit2gtk",
"webview2-com 0.39.1",
"webview2-com",
"window-vibrancy",
"windows 0.62.2",
]
[[package]]
name = "tauri-build"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc"
dependencies = [
"anyhow",
"cargo_toml",
@@ -7265,11 +7194,11 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c"
dependencies = [
"base64 0.22.1",
"base64 0.23.1",
"brotli",
"ico",
"json-patch",
@@ -7292,9 +7221,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -7342,7 +7271,6 @@ dependencies = [
"libc",
"parking_lot",
"serde",
"specta",
"sysinfo",
"tauri",
"tauri-plugin-clipboard-manager",
@@ -7350,9 +7278,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.4.1"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
checksum = "640d0789c9db02265a800fded60520df5a3baa4a1b5f40715b83d58842c24fcb"
dependencies = [
"arboard",
"log",
@@ -7413,13 +7341,13 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
checksum = "3dca325d6e66e9170f2e1b9631722e4e544bcbc51781575d266abdaba697bd51"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"rfd 0.16.0",
"serde",
"serde_json",
"tauri",
@@ -7431,9 +7359,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
checksum = "213fae2d85d4b76b06de5915d1862db9fa74541171f9d2c15da6ac9bd6e788c6"
dependencies = [
"anyhow",
"dunce",
@@ -7455,9 +7383,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2"
checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d"
dependencies = [
"global-hotkey",
"log",
@@ -7470,9 +7398,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.8.0"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1"
checksum = "46f1afc4200ba2e8b596c378d5756d0056febf8755f1563633d98f6bd7fffb2f"
dependencies = [
"bytes",
"cookie_store",
@@ -7489,13 +7417,13 @@ dependencies = [
"thiserror 2.0.20",
"tokio",
"url",
"urlpattern 0.3.0",
"urlpattern",
]
[[package]]
name = "tauri-plugin-mihomo"
version = "0.6.0"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?rev=7a03d16f4d39b1bb885b7babe66b9e153e34b615#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
dependencies = [
"arc-swap",
"clashmap",
@@ -7519,9 +7447,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.5.1"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
checksum = "8cf3cce3ea492b3a8a7f431a4e5dd5d31dc41b2a4a243faa660cbe8095a577bd"
dependencies = [
"log",
"notify-rust",
@@ -7534,13 +7462,14 @@ dependencies = [
"thiserror 2.0.20",
"time",
"url",
"zbus",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.7.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
checksum = "85dead674ee41937d042ee855d785f07ee67206f19402200c12b9255964d5ac8"
dependencies = [
"dunce",
"glob",
@@ -7570,11 +7499,12 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
dependencies = [
"encoding_rs",
"libc",
"log",
"open",
"os_pipe",
@@ -7587,13 +7517,14 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.20",
"tokio",
"windows-sys 0.60.2",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.13.1"
version = "2.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18"
checksum = "855a336eb389d2c1502244fd48fa2d8cb487a430f691bbaed025195f99a04489"
dependencies = [
"base64 0.22.1",
"dirs 7.0.0",
@@ -7601,8 +7532,9 @@ dependencies = [
"futures-util",
"http",
"infer",
"libc",
"log",
"minisign-verify 0.2.5",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.5",
@@ -7624,9 +7556,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-wdio-webdriver"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045"
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
dependencies = [
"async-trait",
"axum",
@@ -7650,9 +7582,9 @@ dependencies = [
"tracing",
"uuid",
"webkit2gtk",
"webview2-com 0.38.2",
"windows 0.61.3",
"windows-core 0.61.2",
"webview2-com",
"windows 0.62.2",
"windows-core 0.62.2",
]
[[package]]
@@ -7691,7 +7623,7 @@ dependencies = [
"thiserror 2.0.20",
"url",
"webkit2gtk",
"webview2-com 0.39.1",
"webview2-com",
"windows 0.62.2",
]
@@ -7717,42 +7649,11 @@ dependencies = [
"tracing",
"url",
"webkit2gtk",
"webview2-com 0.39.1",
"webview2-com",
"windows 0.62.2",
"wry",
]
[[package]]
name = "tauri-specta"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
dependencies = [
"heck 0.5.0",
"serde",
"serde_json",
"specta",
"specta-serde",
"specta-typescript",
"specta-util",
"tauri",
"tauri-specta-macros",
"thiserror 2.0.20",
]
[[package]]
name = "tauri-specta-macros"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
dependencies = [
"darling",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tauri-utils"
version = "2.10.1"
@@ -7786,7 +7687,7 @@ dependencies = [
"thiserror 2.0.20",
"toml 1.1.6+spec-1.1.0",
"url",
"urlpattern 0.6.0",
"urlpattern",
"uuid",
"walkdir",
]
@@ -7905,7 +7806,7 @@ dependencies = [
"regex",
"semver 0.11.0",
"sha2 0.9.9",
"signal-hook 0.1.17",
"signal-hook",
"terminfo",
"termios",
"thiserror 1.0.69",
@@ -8076,9 +7977,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.53.3"
version = "1.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
dependencies = [
"bytes",
"libc",
@@ -8595,47 +8496,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-ucd-ident"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
@@ -8695,18 +8555,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlpattern"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
dependencies = [
"regex",
"serde",
"unic-ucd-ident",
"url",
]
[[package]]
name = "urlpattern"
version = "0.6.0"
@@ -9098,20 +8946,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [
"webview2-com-macros",
"webview2-com-sys 0.38.2",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
]
[[package]]
name = "webview2-com"
version = "0.39.1"
@@ -9119,7 +8953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
dependencies = [
"webview2-com-macros",
"webview2-com-sys 0.39.1",
"webview2-com-sys",
"windows 0.62.2",
"windows-core 0.62.2",
]
@@ -9135,17 +8969,6 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "webview2-com-sys"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
"thiserror 2.0.20",
"windows 0.61.3",
"windows-core 0.61.2",
]
[[package]]
name = "webview2-com-sys"
version = "0.39.1"
@@ -9885,7 +9708,7 @@ dependencies = [
"url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com 0.39.1",
"webview2-com",
"windows 0.62.2",
"windows-core 0.62.2",
"windows-version",
@@ -10153,9 +9976,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.0.4"
version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
dependencies = [
"aes",
"bzip2",
@@ -10205,27 +10028,27 @@ dependencies = [
[[package]]
name = "zstd"
version = "0.14.3"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "8.2.0"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.1.2+zstd.1.5.7"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
+3 -3
View File
@@ -61,13 +61,13 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44"
tauri = { version = "2.12.1" }
tauri-plugin-clipboard-manager = "2.4.1"
tauri = { version = "2.12.3" }
tauri-plugin-clipboard-manager = "2.4.2"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.53.3", features = [
tokio = { version = "1.53.2", features = [
"rt-multi-thread",
"macros",
"time",
+1 -9
View File
@@ -3,14 +3,7 @@
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
- 修复 首页 网站测速失败后卡片一直显示测试中
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
**🐧 Linux**
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
</details>
@@ -23,7 +16,6 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
**🖥️ Windows**
+2
View File
@@ -70,6 +70,8 @@ tray:
global: عام
profiles: الملفات التعريفية
proxies: وكلاء
testLatency: اختبار التأخير
testingLatency: جارٍ اختبار التأخير…
systemProxy: وكيل النظام
tunMode: وضع TUN
closeAllConnections: إغلاق كل الاتصالات
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Global
profiles: Profile
proxies: Proxy
testLatency: Latenz testen
testingLatency: Latenz wird getestet…
systemProxy: Systemproxy
tunMode: TUN-Modus
closeAllConnections: Alle Verbindungen schließen
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Global
profiles: Profiles
proxies: Proxies
testLatency: Test Latency
testingLatency: Testing latency…
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Global
profiles: Perfiles
proxies: Proxies
testLatency: Probar latencia
testingLatency: Probando latencia…
systemProxy: Proxy del sistema
tunMode: Modo TUN
closeAllConnections: Cerrar todas las conexiones
+2
View File
@@ -70,6 +70,8 @@ tray:
global: سراسری
profiles: پروفایل‌ها
proxies: پروکسی‌ها
testLatency: آزمایش تأخیر
testingLatency: در حال آزمایش تأخیر…
systemProxy: پروکسی سیستم
tunMode: حالت TUN
closeAllConnections: بستن همه اتصال‌ها
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Global
profiles: Profil
proxies: Proksi
testLatency: Uji Latensi
testingLatency: Menguji latensi…
systemProxy: Proksi Sistem
tunMode: Mode TUN
closeAllConnections: Tutup Semua Koneksi
+2
View File
@@ -70,6 +70,8 @@ tray:
global: グローバル
profiles: プロファイル
proxies: プロキシ
testLatency: 遅延テスト
testingLatency: 遅延をテスト中…
systemProxy: システムプロキシ
tunMode: TUN モード
closeAllConnections: すべての接続を閉じる
+2
View File
@@ -70,6 +70,8 @@ tray:
global: 글로벌
profiles: 프로필
proxies: 프록시
testLatency: 지연 테스트
testingLatency: 지연 테스트 중…
systemProxy: 시스템 프록시
tunMode: TUN 모드
closeAllConnections: 모든 연결 닫기
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Global
profiles: Perfis
proxies: Proxies
testLatency: Test Latency
testingLatency: Testing latency…
systemProxy: Proxy do sistema
tunMode: Modo TUN
closeAllConnections: Fechar todas as conexões
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Глобальный
profiles: Профили
proxies: Прокси
testLatency: Проверить задержку
testingLatency: Проверка задержки…
systemProxy: Системный прокси
tunMode: Режим TUN
closeAllConnections: Закрыть все соединения
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Küresel
profiles: Profiller
proxies: Vekil'ler
testLatency: Gecikmeyi Test Et
testingLatency: Gecikme test ediliyor…
systemProxy: Sistem Vekil'i
tunMode: TUN Modu
closeAllConnections: Tüm Bağlantıları Kapat
+2
View File
@@ -70,6 +70,8 @@ tray:
global: Глобаль
profiles: Профильләр
proxies: Проксилар
testLatency: Кичектерүне тикшерү
testingLatency: Кичектерү тикшерелә…
systemProxy: Системалы прокси
tunMode: TUN режимы
closeAllConnections: Барлык тоташуларны ябу
+2
View File
@@ -70,6 +70,8 @@ tray:
global: 全局
profiles: 订阅
proxies: 代理
testLatency: 测试延迟
testingLatency: 正在测试延迟…
systemProxy: 系统代理
tunMode: TUN 模式
closeAllConnections: 关闭所有连接
+2
View File
@@ -70,6 +70,8 @@ tray:
global: 全域
profiles: 訂閱
proxies: 代理
testLatency: 測試延遲
testingLatency: 正在測試延遲…
systemProxy: 系統代理
tunMode: 虛擬網路介面卡模式
closeAllConnections: 關閉所有連線
@@ -4,7 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
clash-verge-logging = { workspace = true }
chrono = { workspace = true }
log = { workspace = true }
+1 -1
View File
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use super::utils::{country_code_to_emoji, get_local_date_string};
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnlockItem {
pub name: String,
pub status: String,
+31 -23
View File
@@ -1,15 +1,3 @@
//! Linux-only termination signal bridge.
//!
//! Upstream Tauri delivers session-end and termination events to the app on
//! Windows and macOS, but has no equivalent on Linux: a desktop session may
//! end without any window or run event reaching the process. Linux builds
//! keep this crate to listen for SIGTERM, SIGINT and SIGHUP and forward them
//! to the app quit flow, so system proxy and TUN cleanup still runs before
//! the process dies. Non-Linux builds compile this crate to nothing and must
//! not depend on it.
#![cfg(target_os = "linux")]
use std::sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
@@ -17,13 +5,20 @@ use std::sync::{
use clash_verge_logging::{Type, logging};
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
/// Serializes signal-triggered cleanups: a signal arriving while a quit
/// attempt is still in flight is dropped instead of overlapping it.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ShutdownOutcome {
Committed,
Canceled,
}
pub(crate) struct ShutdownLatch {
is_cleaning_up: AtomicBool,
}
@@ -41,18 +36,17 @@ impl ShutdownLatch {
.is_ok()
}
fn finish(&self) {
self.is_cleaning_up.store(false, Ordering::Release);
fn finish(&self, outcome: ShutdownOutcome) {
if matches!(outcome, ShutdownOutcome::Canceled) {
self.is_cleaning_up.store(false, Ordering::Release);
}
}
}
/// Spawns the Linux signal loop on a dedicated runtime so it survives the
/// teardown of any app-owned runtime. The handler runs once per accepted
/// signal; the quit flow it invokes owns the exit-or-cancel decision.
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
{
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
Ok(rt) => Some(rt),
@@ -67,22 +61,36 @@ where
}
});
#[cfg(unix)]
unix::register(f);
#[cfg(windows)]
windows::register(f);
}
#[cfg(test)]
mod tests {
use super::ShutdownLatch;
use super::{ShutdownLatch, ShutdownOutcome};
#[test]
fn finished_attempt_releases_latch_for_the_next_signal() {
fn canceled_shutdown_releases_latch_and_permits_next_attempt() {
let latch = ShutdownLatch::new();
assert!(latch.try_begin());
assert!(!latch.try_begin());
latch.finish();
latch.finish(ShutdownOutcome::Canceled);
assert!(latch.try_begin());
}
#[test]
fn committed_shutdown_retains_latch_and_suppresses_repeats() {
let latch = ShutdownLatch::new();
assert!(latch.try_begin());
latch.finish(ShutdownOutcome::Committed);
assert!(!latch.try_begin());
}
}
+3 -4
View File
@@ -1,12 +1,12 @@
use clash_verge_logging::{Type, logging};
use tokio::signal::unix::{SignalKind, signal};
use crate::{RUNTIME, SHUTDOWN_LATCH};
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
{
if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move {
@@ -60,8 +60,7 @@ where
}
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
f().await;
SHUTDOWN_LATCH.finish();
SHUTDOWN_LATCH.finish(f().await);
}
});
} else {
+83
View File
@@ -0,0 +1,83 @@
use clash_verge_logging::{Type, logging};
use tokio::signal::windows;
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
{
if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move {
let mut ctrl_c = match windows::ctrl_c() {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
return;
}
};
let mut ctrl_close = match windows::ctrl_close() {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
return;
}
};
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
return;
}
};
let mut ctrl_logoff = match windows::ctrl_logoff() {
Ok(s) => s,
Err(e) => {
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
return;
}
};
loop {
let signal_name;
tokio::select! {
_ = ctrl_c.recv() => {
signal_name = "Ctrl+C";
}
_ = ctrl_close.recv() => {
signal_name = "Ctrl+Close";
}
_ = ctrl_shutdown.recv() => {
signal_name = "Ctrl+Shutdown";
}
_ = ctrl_logoff.recv() => {
signal_name = "Ctrl+Logoff";
}
}
if !SHUTDOWN_LATCH.try_begin() {
logging!(
info,
Type::SystemSignal,
"Already shutting down, ignoring repeated signal: {}",
signal_name
);
continue;
}
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
SHUTDOWN_LATCH.finish(f().await);
}
});
} else {
logging!(
error,
Type::SystemSignal,
"register shutdown signal failed, RUNTIME is not available"
);
}
}
@@ -5,7 +5,6 @@ edition = "2024"
rust-version = "1.91"
[dependencies]
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
@@ -15,7 +14,7 @@ serde = { workspace = true, features = ["derive"] }
sysinfo = { version = "0.39.6", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.191"
libc = "0.2.190"
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
@@ -1,46 +1,53 @@
use parking_lot::RwLock;
use serde::Serialize;
use tauri::{AppHandle, Runtime, State, command};
use tauri_plugin_clipboard_manager::ClipboardExt as _;
use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
use crate::Platform;
#[derive(Serialize, specta::Type)]
#[derive(Serialize)]
pub struct SystemInfo {
pub system_name: String,
pub system_version: String,
pub system_kernel_version: String,
pub system_arch: String,
pub app_version: String,
pub app_core_mode: String,
pub app_is_admin: bool,
}
impl From<&Platform> for SystemInfo {
fn from(platform: &Platform) -> Self {
impl From<Platform> for SystemInfo {
fn from(platform: Platform) -> Self {
Self {
system_name: platform.sysinfo.system_name.clone(),
system_version: platform.sysinfo.system_version.clone(),
system_name: platform.sysinfo.system_name,
system_version: platform.sysinfo.system_version,
system_kernel_version: platform.sysinfo.system_kernel_version,
system_arch: platform.sysinfo.system_arch,
app_version: platform.appinfo.app_version,
app_core_mode: platform.appinfo.app_core_mode,
app_is_admin: platform.appinfo.app_is_admin,
}
}
}
#[command]
#[specta::specta]
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, String> {
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, Error> {
let platform = state.inner().read();
Ok(SystemInfo::from(&*platform))
Ok(SystemInfo::from(platform.clone()))
}
/// 获取应用的运行时间(毫秒)
#[command]
#[specta::specta]
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, String> {
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
}
#[command]
#[specta::specta]
pub fn export_diagnostic_info<R: Runtime>(
app_handle: AppHandle<R>,
state: State<'_, RwLock<Platform>>,
) -> Result<(), String> {
) -> Result<(), Error> {
let info = state.inner().read().to_string();
let clipboard = app_handle.clipboard();
clipboard.write_text(info).map_err(|error| error.to_string())
clipboard.write_text(info)
}
-115
View File
@@ -12,92 +12,6 @@ import pluginUnusedImports from 'eslint-plugin-unused-imports'
import globals from 'globals'
import tseslint from 'typescript-eslint'
const ipcBoundaryRules = ({
allowCommands = false,
allowInvoke = false,
allowEvents = false,
} = {}) => {
const paths = [
...(!allowInvoke
? [
{
name: '@tauri-apps/api/core',
importNames: ['invoke'],
allowTypeImports: true,
message: 'Invoke backend commands through services/cmds.',
},
]
: []),
...(!allowEvents
? [
{
name: '@tauri-apps/api/event',
importNames: ['listen', 'emit', 'emitTo', 'once'],
allowTypeImports: true,
message:
'The event bus owns Tauri event subscriptions and emissions.',
},
]
: []),
]
const patterns = allowCommands
? []
: [
{
regex: '(^|/)bindings(\\.ts)?$',
importNames: ['commands'],
allowTypeImports: true,
message:
'Only services/cmds may import generated command implementations.',
},
]
const modules = [
...(!allowInvoke ? ['@tauri-apps/api/core'] : []),
...(!allowEvents ? ['@tauri-apps/api/event'] : []),
]
const syntax = modules.flatMap((name) => [
{
selector: `ImportExpression[source.value='${name}']`,
message:
'Use the owned IPC service boundary instead of dynamically importing Tauri APIs.',
},
{
selector: `CallExpression[callee.name='require'][arguments.0.value='${name}']`,
message:
'Use the owned IPC service boundary instead of requiring Tauri APIs.',
},
{
selector: `TSImportEqualsDeclaration[moduleReference.expression.value='${name}']`,
message: 'Use the owned IPC service boundary for Tauri APIs.',
},
])
if (!allowCommands) {
syntax.push(
{
selector: 'ImportExpression[source.value=/bindings(\\.ts)?$/]',
message:
'Only services/cmds may load generated command implementations.',
},
{
selector:
"CallExpression[callee.name='require'][arguments.0.value=/bindings(\\.ts)?$/]",
message:
'Only services/cmds may load generated command implementations.',
},
{
selector:
'TSImportEqualsDeclaration[moduleReference.expression.value=/bindings(\\.ts)?$/]',
message:
'Only services/cmds may load generated command implementations.',
},
)
}
return {
'no-restricted-imports': ['error', { paths, patterns }],
'no-restricted-syntax': ['error', ...syntax],
}
}
export default defineConfig([
pluginESx.configs['flat/restrict-to-es2022'],
{
@@ -248,33 +162,4 @@ export default defineConfig([
},
},
},
{
files: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}'],
rules: ipcBoundaryRules(),
},
{
files: ['src/services/cmds.ts'],
rules: ipcBoundaryRules({ allowCommands: true }),
},
{
files: ['src/services/bindings.ts'],
rules: ipcBoundaryRules({ allowCommands: true, allowInvoke: true }),
},
{
files: ['src/services/bus/**/*.{ts,tsx}'],
rules: ipcBoundaryRules({ allowEvents: true }),
},
{
files: [
'src/components/proxy/proxy-chain.tsx',
'src/components/proxy/proxy-groups-chain.tsx',
'src/pages/proxies.tsx',
],
rules: {
'@typescript-eslint/no-floating-promises': [
'error',
{ ignoreVoid: false },
],
},
},
])
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["src/polyfills/*.js!", "scripts/check-contract-changes.mjs!"],
"entry": ["src/polyfills/*.js!"],
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
"include": [
"files",
@@ -18,7 +18,7 @@
"ignoreDependencies": ["lint-staged"],
"ignoreIssues": {
"src/types/generated/**": ["exports", "types"],
"src/services/bindings.ts": ["types"]
"src/services/contract.ts": ["exports", "types"]
},
"cycles": {
"dynamicImports": true
+6 -6
View File
@@ -75,11 +75,11 @@
"monaco-yaml": "^5.5.1",
"nanoid": "^6.0.1",
"qrcode.react": "^4.2.0",
"react": "19.3.1",
"react-dom": "19.3.1",
"react": "19.3.0",
"react-dom": "19.3.0",
"react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0",
"react-i18next": "17.0.19",
"react-i18next": "17.0.15",
"react-markdown": "10.1.0",
"react-router": "^8.4.0",
"rehype-raw": "^7.0.0",
@@ -123,7 +123,7 @@
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.41.0",
"knip": "6.39.0",
"lint-staged": "^17.5.1",
"sass": "^1.104.1",
"tar": "^7.5.22",
@@ -133,7 +133,7 @@
"vite": "^8.3.0",
"vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1",
"webdriverio": "10.0.2",
"webdriverio": "10.0.0",
"ws": "8.22.0"
},
"lint-staged": {
@@ -146,5 +146,5 @@
]
},
"type": "module",
"packageManager": "pnpm@12.14.0"
"packageManager": "pnpm@12.10.1"
}
+309 -313
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -45,17 +45,6 @@
"matchManagers": ["npm"],
"groupName": "npm dependencies"
},
{
"description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync",
"matchPackageNames": [
"@tauri-apps/**",
"@wdio/tauri-service",
"tauri",
"tauri-*"
],
"groupName": "tauri",
"addLabels": ["tauri"]
},
{
"description": "Group all GitHub Actions updates into a single PR",
"matchManagers": ["github-actions"],
+1 -1
View File
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.99.1"
channel = "1.99.0"
components = ["rustfmt", "clippy"]
+75 -25
View File
@@ -1,34 +1,84 @@
import fs from 'node:fs'
import path from 'node:path'
import { ESLint } from 'eslint'
/**
* Architecture grep gates for the frontend contract flow. Select gates via
* argv:
*
* node scripts/check-architecture.mjs listen emit invoke
*
* - listen/emit: the Tauri event API may only be used under src/services/bus/**
* (the event bus). Detected as a bare `listen(`/`emit(` call (member calls
* like `this.emit(` are unrelated class methods) or as importing the name
* from '@tauri-apps/api/event'.
* - invoke: `invoke(` calls may exist only under src/services/**.
*/
const ROOT = path.resolve(import.meta.dirname, '..')
const gates = process.argv.slice(2)
if (
!gates.length ||
gates.some((gate) => !['listen', 'emit', 'invoke'].includes(gate))
) {
const SRC = path.join(ROOT, 'src')
const tauriEventImport = (name) =>
new RegExp(
`import\\s*\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*'@tauri-apps/api/event'`,
)
const GATES = {
listen: {
patterns: [/(?<![.\w])listen\(/, tauriEventImport('listen')],
allowed: (file) => file.startsWith('src/services/bus/'),
description: 'Tauri listen outside the event bus (src/services/bus)',
},
emit: {
patterns: [/(?<![.\w])emit\(/, tauriEventImport('emit')],
allowed: (file) => file.startsWith('src/services/bus/'),
description: 'Tauri emit outside the event bus (src/services/bus)',
},
invoke: {
patterns: [/\binvoke\(/],
allowed: (file) => file.startsWith('src/services/'),
description: 'invoke( outside the services layer (src/services)',
},
}
const walk = (dir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true })
return entries.flatMap((entry) => {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) return walk(full)
return /\.(ts|tsx|js|jsx|mts|mjs)$/.test(entry.name) ? [full] : []
})
}
const selected = process.argv.slice(2)
if (selected.length === 0) {
console.error('usage: check-architecture.mjs <gate>... (listen|emit|invoke)')
process.exit(2)
}
// All IPC boundaries use the lint configuration, including when callers request a legacy gate.
const eslint = new ESLint({ cwd: ROOT, allowInlineConfig: false })
const results = await eslint.lintFiles(['src'])
const boundaryRules = new Set([
'no-restricted-imports',
'no-restricted-syntax',
'@typescript-eslint/no-floating-promises',
])
const violations = results.flatMap((result) =>
result.messages
.filter((message) => message.fatal || boundaryRules.has(message.ruleId))
.map((message) => ({ file: result.filePath, ...message })),
)
for (const violation of violations) {
console.error(
`[arch] ${path.relative(ROOT, violation.file)}:${violation.line}: ${violation.message}`,
)
let violations = 0
for (const gate of selected) {
const rule = GATES[gate]
if (!rule) {
console.error(`unknown gate: ${gate}`)
process.exit(2)
}
for (const file of walk(SRC)) {
const relative = path.relative(ROOT, file).replaceAll('\\', '/')
if (rule.allowed(relative)) continue
const lines = fs.readFileSync(file, 'utf8').split('\n')
lines.forEach((line, index) => {
if (rule.patterns.some((pattern) => pattern.test(line))) {
violations += 1
console.error(
`[arch] ${rule.description}: ${relative}:${index + 1}: ${line.trim()}`,
)
}
})
}
}
if (violations.length) process.exitCode = 1
else console.log('[arch] no violations')
if (violations > 0) {
console.error(`[arch] ${violations} violation(s) found`)
process.exit(1)
}
console.log('[arch] no violations')
-69
View File
@@ -1,69 +0,0 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
const ROOT = path.resolve(import.meta.dirname, '..')
const git = (...args) =>
execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim()
const { contract } = JSON.parse(
fs.readFileSync(path.join(ROOT, 'scripts/contract-inputs.json'), 'utf8'),
)
const inputs = contract.map((pattern) => `:(glob)${pattern}`)
const args = process.argv.slice(2)
if (args.length !== 2 || args[0] !== '--push') {
console.error('usage: check-contract-changes.mjs --push <remote>')
process.exit(2)
}
const hasChanges = (...range) =>
git('diff', '--name-only', ...range, '--', ...inputs) !== ''
const assertCleanInputs = (...range) => {
if (
hasChanges(...range) ||
git('ls-files', '--others', '--exclude-standard', '--', ...inputs)
) {
throw new Error(
'Contract inputs differ from the pushed commit. Commit those changes before pushing.',
)
}
}
let relevant = false
const head = git('rev-parse', 'HEAD')
for (const line of fs
.readFileSync(0, 'utf8')
.trim()
.split('\n')
.filter(Boolean)) {
const fields = line.trim().split(/\s+/)
if (
fields.length !== 4 ||
![fields[1], fields[3]].every((sha) => /^[a-f0-9]{40,64}$/.test(sha))
) {
throw new Error('Invalid pre-push ref update')
}
const [, local, , remote] = fields
if (/^0+$/.test(local) || local === remote) continue
// A new remote branch has no upstream; use its fork point with the remote default branch.
const base = /^0+$/.test(remote)
? git('merge-base', local, `refs/remotes/${args[1]}/HEAD`)
: remote
if (!hasChanges(base, local)) continue
const commit = git('rev-parse', `${local}^{commit}`)
if (commit !== head)
throw new Error(
'Check out the pushed commit before validating its changed IPC contract.',
)
assertCleanInputs('HEAD')
relevant = true
}
if (relevant) {
execFileSync(
process.execPath,
[path.join(ROOT, 'scripts/gen-contract.mjs'), '--check'],
{
cwd: ROOT,
stdio: 'inherit',
},
)
} else {
console.log('[contract] inputs unchanged; skipping Rust generation')
}
-23
View File
@@ -1,23 +0,0 @@
{
"contract": [
"src-tauri/**/*.rs",
"crates/**/*.rs",
"Cargo.toml",
"Cargo.lock",
"src-tauri/Cargo.toml",
"crates/**/Cargo.toml",
"rust-toolchain.toml",
"src-tauri/tauri.conf*.json",
"src/services/bindings.ts",
"scripts/gen-contract.mjs",
"scripts/check-contract-changes.mjs",
"biome.json",
"biome.jsonc",
"package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"scripts/contract-inputs.json",
".github/workflows/frontend-check.yml",
".husky/pre-push"
]
}
+83 -47
View File
@@ -1,55 +1,91 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
/**
* Regenerates src/services/contract.ts from the backend wire contract.
* Source of truth: `cargo run -p clash-verge --bin frontend-contract` inside
* src-tauri, which serializes the same enum variants and event payloads the
* app emits at runtime. CI reruns this and fails on a non-empty diff.
*/
const ROOT = path.resolve(import.meta.dirname, '..')
const BINDINGS = path.join(ROOT, 'src/services/bindings.ts')
const args = process.argv.slice(2)
if (args.length > 1 || (args.length === 1 && args[0] !== '--check')) {
console.error('usage: gen-contract.mjs [--check]')
process.exit(2)
}
const check = args[0] === '--check'
const temporary = check
? fs.mkdtempSync(path.join(os.tmpdir(), 'verge-contract-'))
: null
const output = temporary ? path.join(temporary, 'bindings.ts') : BINDINGS
try {
const OUT = path.join(ROOT, 'src/services/contract.ts')
const contract = JSON.parse(
execFileSync(
'cargo',
[
'run',
'-p',
'clash-verge',
'--example',
'frontend-contract',
'--locked',
'-q',
...(check ? ['--', output] : []),
],
{ cwd: ROOT, stdio: 'inherit' },
)
const formatted = execFileSync(
process.execPath,
[
path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
'format',
'--stdin-file-path',
BINDINGS,
],
{ cwd: ROOT, input: fs.readFileSync(output) },
)
if (check) {
if (!formatted.equals(fs.readFileSync(BINDINGS))) {
console.error(
'IPC bindings are out of date. Run pnpm gen:contract and commit the generated file.',
)
process.exitCode = 1
}
} else {
fs.writeFileSync(BINDINGS, formatted)
}
} finally {
if (temporary) fs.rmSync(temporary, { recursive: true, force: true })
['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '-q'],
{
cwd: path.join(ROOT, 'src-tauri'),
encoding: 'utf8',
maxBuffer: 16 * 1024 * 1024,
},
),
)
if (contract.version !== 1) {
throw new Error(`unsupported wire contract version: ${contract.version}`)
}
/** TS type per event, derived from the serialized sample payload's JSON shape. */
const payloadType = (name, sample) => {
if (sample === null) return 'null'
if (typeof sample === 'string') return 'string'
if (Array.isArray(sample)) {
if (name === 'verge://notice-message') return '[NoticeStatus, string]'
throw new Error(`unexpected array payload for ${name}`)
}
if (typeof sample === 'object') {
const keys = Object.keys(sample)
if (keys.length === 1 && keys[0] === 'uid') return '{ uid: string }'
if (name === 'verge://run-state-changed') return 'RunState'
throw new Error(`unexpected object payload for ${name}: ${keys.join(',')}`)
}
throw new Error(`unexpected payload for ${name}`)
}
const needsRunState = contract.events.some(
(e) => e.name === 'verge://run-state-changed',
)
if (!needsRunState)
throw new Error('run-state-changed event missing from contract')
const lines = []
lines.push(
'// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.',
)
lines.push('// Regen: pnpm gen:contract')
lines.push('')
lines.push("import type { RunState } from './cmds'")
lines.push('')
lines.push(
'/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */',
)
lines.push('export type NoticeStatus =')
for (const status of contract.noticeStatuses) {
lines.push(` | ${JSON.stringify(status)}`)
}
lines.push('')
lines.push('/** Event name → payload type for every backend-emitted event. */')
lines.push('export interface VergeEventPayloads {')
for (const event of contract.events) {
const key = JSON.stringify(event.name)
lines.push(` ${key}: ${payloadType(event.name, event.sample)}`)
}
lines.push('}')
lines.push('')
const content = `${lines.join('\n')}\n`
fs.writeFileSync(OUT, content)
// Match the repo's biome style so CI parity (`git diff --exit-code`) is stable.
execFileSync(
process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
['exec', 'biome', 'format', '--write', path.relative(ROOT, OUT)],
{ cwd: ROOT, stdio: 'ignore' },
)
console.log(
`[gen-contract] wrote ${path.relative(ROOT, OUT)}: ${contract.events.length} events, ${contract.noticeStatuses.length} statuses`,
)
+17 -23
View File
@@ -29,12 +29,13 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.7.1", features = [] }
tauri-build = { version = "2.7.2", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
clash-verge-media-unlock = { workspace = true }
clash-verge-logging = { workspace = true }
clash-verge-signal = { workspace = true }
clash-verge-i18n = { workspace = true }
clash-verge-limiter = { workspace = true }
tauri-plugin-clash-verge-sysinfo = { workspace = true }
@@ -59,7 +60,7 @@ bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
open = "5.4.4"
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0"
dunce = "1.0.5"
nanoid = "0.5"
@@ -72,55 +73,48 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
"guard",
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.4.0"
tauri-plugin-opener = "2.7.0"
tauri-plugin-dialog = "2.8.1"
tauri-plugin-fs = "2.6.0"
tauri-plugin-shell = "2.5.0"
tauri-plugin-opener = "2.7.1"
tauri-plugin-dialog = "2.8.2"
tauri-plugin-fs = "2.7.0"
tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.1"
tauri-plugin-window-state = "2.5.0"
zip = "9.0.4"
zip = "9.0.1"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
base64 = "0.23.1"
minisign-verify = "0.3.0"
sha2 = "0.11.1"
sha2 = "0.11"
getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.5.1"
tauri-plugin-notification = "2.6.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.0"
tauri-plugin-http = "2.8.2"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.2", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
"client",
] }
arc-swap = "1.9.2"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
rustls = { version = "0.23.45", features = ["ring"] }
webpki-roots = "1.0.9"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] }
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies]
libc = "0.2.191"
[target.'cfg(target_os = "linux")'.dependencies]
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
clash-verge-signal = { workspace = true }
libc = "0.2.190"
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
objc2 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [
"NSString",
@@ -153,8 +147,8 @@ windows-sys.workspace = true
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.7.0"
tauri-plugin-global-shortcut = "2.4.0"
tauri-plugin-updater = "2.13.1"
tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.13.3"
[dev-dependencies]
criterion = { workspace = true }
-14
View File
@@ -1,14 +0,0 @@
// Dev-only target: Tauri ships everything under src/bin/ to /usr/bin in deb/rpm
// bundles (PR #8131), so this generator must stay an example, never a bin.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args_os().skip(1);
let output = args
.next()
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
if args.next().is_some() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
}
app_lib::ipc::builder().export(specta_typescript::Typescript::default(), output)?;
Ok(())
}
+8
View File
@@ -0,0 +1,8 @@
//! Prints the frontend wire contract as JSON to stdout. Zero new dependencies:
//! the payload is derived from the same serializers the app uses at runtime.
//! Consumed by scripts/gen-contract.mjs; CI regenerates and diffs the committed
//! TypeScript artifact.
fn main() {
println!("{}", app_lib::frontend_wire_contract());
}
+6 -24
View File
@@ -9,14 +9,12 @@ use smartstring::alias::String;
use tauri::{AppHandle, Manager as _, ipc::Channel};
#[tauri::command]
#[specta::specta]
pub async fn open_app_dir() -> CmdResult<()> {
let app_dir = dirs::app_home_dir().stringify_err()?;
open::that(app_dir).stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn open_core_dir() -> CmdResult<()> {
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
@@ -24,14 +22,12 @@ pub async fn open_core_dir() -> CmdResult<()> {
}
#[tauri::command]
#[specta::specta]
pub async fn open_logs_dir() -> CmdResult<()> {
let log_dir = dirs::app_logs_dir().stringify_err()?;
open::that(log_dir).stringify_err()
}
#[tauri::command]
#[specta::specta]
pub fn open_devtools(app_handle: AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") {
if !window.is_devtools_open() {
@@ -43,26 +39,22 @@ pub fn open_devtools(app_handle: AppHandle) {
}
#[tauri::command]
#[specta::specta]
pub async fn exit_app() {
feat::quit().await;
}
#[tauri::command]
#[specta::specta]
pub async fn restart_app() -> CmdResult<()> {
feat::restart_app().await;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn install_update(
app_handle: AppHandle,
version: std::string::String,
version: String,
on_event: Channel<DownloadEvent>,
) -> CmdResult<bool> {
let version: String = version.into();
SilentUpdater::global()
.install_update(&app_handle, &version, |event| {
let _ = on_event.send(event);
@@ -72,32 +64,22 @@ pub async fn install_update(
}
#[tauri::command]
#[specta::specta]
pub fn cancel_update_download() {
SilentUpdater::global().cancel_download();
}
#[tauri::command]
#[specta::specta]
pub fn get_app_dir() -> CmdResult<std::string::String> {
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
Ok(app_home_dir)
}
#[tauri::command]
#[specta::specta]
pub async fn download_icon_cache(
url: std::string::String,
name: std::string::String,
) -> CmdResult<std::string::String> {
let url: String = url.into();
let name: String = name.into();
feat::download_icon_cache(url, name).await.map(Into::into)
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
feat::download_icon_cache(url, name).await
}
#[tauri::command]
#[specta::specta]
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
let path: String = path.into();
feat::copy_icon_file(path, icon_info).await.map(Into::into)
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
feat::copy_icon_file(path, icon_info).await
}
+5 -16
View File
@@ -4,44 +4,33 @@ use feat::LocalBackupFile;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
pub async fn create_local_backup() -> CmdResult<()> {
feat::create_local_backup().await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
feat::delete_local_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
feat::restore_local_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
}
#[tauri::command]
#[specta::specta]
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
let source: String = source.into();
feat::import_local_backup(source).await.map(Into::into).stringify_err()
pub async fn import_local_backup(source: String) -> CmdResult<String> {
feat::import_local_backup(source).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
let destination: String = destination.into();
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination).await.stringify_err()
}
+24 -36
View File
@@ -12,33 +12,30 @@ use crate::{
},
};
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
#[tauri::command]
#[specta::specta]
pub async fn copy_clash_env() -> CmdResult {
feat::copy_clash_env().await.stringify_err()
feat::copy_clash_env().await;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.data_arc().get_client_info())
}
#[tauri::command]
#[specta::specta]
pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
feat::patch_clash(&payload.0)
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
feat::patch_clash(&payload)
.await
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
}
#[tauri::command]
#[specta::specta]
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
let payload: String = payload.into();
pub async fn patch_clash_mode(payload: String) -> CmdResult {
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
@@ -46,15 +43,12 @@ pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
#[tauri::command]
#[specta::specta]
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
Ok(Config::clash().await.data_arc().get_mode())
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
Ok(Config::clash().await.data_arc().get_mode().map(Into::into))
}
#[tauri::command]
#[specta::specta]
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core.into();
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
@@ -86,7 +80,6 @@ pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Opt
}
#[tauri::command]
#[specta::specta]
pub async fn restart_core() -> CmdResult {
logging_error!(Type::Core, profiles_save_file_safe().await);
let result = CoreManager::global()
@@ -101,7 +94,6 @@ pub async fn restart_core() -> CmdResult {
/// Replaces the managed core binary atomically instead of letting mihomo overwrite itself.
#[tauri::command]
#[specta::specta]
pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeReport> {
let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?;
if report.upgraded {
@@ -111,9 +103,7 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
}
#[tauri::command]
#[specta::specta]
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
let url: String = url.into();
pub async fn test_delay(url: String) -> CmdResult<u32> {
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
@@ -125,14 +115,13 @@ pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
}
#[tauri::command]
#[specta::specta]
pub async fn save_dns_config(dns_config: crate::ipc::YamlMapping) -> CmdResult {
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config.0).stringify_err()?;
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?;
fs::write(&dns_path, yaml_str).await.stringify_err()?;
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
@@ -140,27 +129,22 @@ pub async fn save_dns_config(dns_config: crate::ipc::YamlMapping) -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub fn take_dns_override_notice() -> bool {
crate::config::dns::take_dns_override_notice()
}
#[tauri::command]
#[specta::specta]
pub async fn set_dns_override(
profile_uid: std::string::String,
profile_uid: String,
enabled: bool,
confirmation: Option<std::string::String>,
confirmation: Option<String>,
) -> CmdResult<feat::DnsOverrideOutcome> {
let profile_uid: String = profile_uid.into();
let confirmation = confirmation.map(String::from);
feat::set_dns_override(profile_uid, enabled, confirmation)
.await
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
}
#[tauri::command]
#[specta::specta]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
if apply {
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
@@ -179,7 +163,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
);
})?;
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
logging!(
error,
Type::Config,
@@ -188,6 +172,13 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
);
})?;
let mut patch = serde_yaml_ng::Mapping::new();
patch.insert("dns".into(), patch_config.into());
Config::runtime().await.edit_draft(|d| {
d.patch_config(&patch);
});
CoreManager::global().update_config_checked().await.map_err(|error| {
logging!(error, Type::Config, "failed to apply the config with DNS: {error:#}");
proxy_aware_error(&error)
@@ -211,22 +202,20 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
match fs::read_to_string(&dns_path).await {
Ok(content) => Ok(Some(content)),
Ok(content) => Ok(Some(content.into())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(CommandFailure::plain(error)),
}
}
#[tauri::command]
#[specta::specta]
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
let app_dir = dirs::app_home_dir().stringify_err()?;
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
@@ -243,7 +232,6 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
}
#[tauri::command]
#[specta::specta]
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs)
-1
View File
@@ -3,7 +3,6 @@ use crate::core::lightweight;
use super::CmdResult;
#[tauri::command]
#[specta::specta]
pub async fn entry_lightweight_mode() -> CmdResult {
lightweight::entry_lightweight_mode().await;
Ok(())
-2
View File
@@ -5,13 +5,11 @@ use crate::{
};
#[tauri::command]
#[specta::specta]
pub async fn probe_listener(request: ListenerProbe) -> CmdResult<ListenerProbeOutcome> {
feat::probe_listener(request).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult<SaveProxyPortsOutcome> {
feat::save_proxy_ports(settings)
.await
@@ -4,7 +4,6 @@ use std::time::Duration;
use tauri::{command, ipc::Channel};
#[command]
#[specta::specta]
pub fn get_unlock_items() -> Vec<UnlockItem> {
clash_verge_media_unlock::default_unlock_items()
}
@@ -27,7 +26,6 @@ async fn create_client() -> Result<Client, String> {
}
#[command]
#[specta::specta]
pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<UnlockItem>, String> {
let client = create_client().await?;
Ok(clash_verge_media_unlock::check_media_unlock(&client, |item| {
@@ -37,7 +35,6 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
}
#[command]
#[specta::specta]
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
let client = create_client().await?;
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
+1 -1
View File
@@ -4,7 +4,7 @@ use smartstring::alias::String;
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
/// Structured command failure returned to the frontend.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct CommandFailure {
/// Stable frontend identifier, when classified.
#[serde(skip_serializing_if = "Option::is_none")]
+20 -34
View File
@@ -3,12 +3,13 @@ use crate::cmd::StringifyErr as _;
use crate::core::{proxy_control, sysopt::Sysopt};
use gethostname::gethostname;
use network_interface::NetworkInterface;
use serde_yaml_ng::Mapping;
use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_clash_verge_sysinfo;
#[tauri::command]
#[specta::specta]
pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
Sysopt::global().wait_idle().await;
// With no network service there is no proxy configured anywhere, which reads as disabled.
let sys_proxy = match Sysproxy::get_system_proxy() {
@@ -16,40 +17,40 @@ pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
other => other.stringify_err()?,
};
let Sysproxy {
host,
bypass,
port,
enable,
ref host,
ref bypass,
ref port,
ref enable,
} = sys_proxy;
Ok(SystemProxy {
enable,
server: format!("{host}:{port}"),
bypass,
})
let mut map = Mapping::new();
map.insert("enable".into(), (*enable).into());
map.insert("server".into(), format!("{}:{}", host, port).into());
map.insert("bypass".into(), bypass.as_str().into());
Ok(map)
}
#[tauri::command]
#[specta::specta]
pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
Sysopt::global().wait_idle().await;
let auto_proxy = match Autoproxy::get_auto_proxy() {
Err(error) if proxy_control::is_missing_network_service(&error) => Autoproxy::default(),
other => other.stringify_err()?,
};
let Autoproxy { enable, url } = auto_proxy;
let Autoproxy { ref enable, ref url } = auto_proxy;
Ok(AutoProxy { enable, url })
let mut map = Mapping::new();
map.insert("enable".into(), (*enable).into());
map.insert("url".into(), url.as_str().into());
Ok(map)
}
#[tauri::command]
#[specta::specta]
pub fn get_embedded_server_port() -> CmdResult<u16> {
crate::utils::server::embedded_server_port().stringify_err()
}
#[tauri::command]
#[specta::specta]
pub fn get_system_hostname() -> String {
match gethostname().into_string() {
Ok(name) => name,
@@ -61,14 +62,12 @@ pub fn get_system_hostname() -> String {
}
#[tauri::command]
#[specta::specta]
pub fn get_network_interfaces() -> Vec<String> {
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
}
#[tauri::command]
#[specta::specta]
pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfaceView>> {
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
let names = get_network_interfaces();
@@ -78,22 +77,9 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfa
for interface in interfaces {
if names.contains(&interface.name) {
result.push(interface.into());
result.push(interface);
}
}
Ok(result)
}
#[derive(serde::Serialize, specta::Type)]
pub struct SystemProxy {
pub enable: bool,
pub server: String,
pub bypass: String,
}
#[derive(serde::Serialize, specta::Type)]
pub struct AutoProxy {
pub enable: bool,
pub url: String,
}
+21 -117
View File
@@ -33,7 +33,6 @@ fn profile_import_error(err: &anyhow::Error) -> std::string::String {
}
#[tauri::command]
#[specta::specta]
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
let draft = Config::profiles().await;
let data = draft.data_arc();
@@ -41,7 +40,6 @@ pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
}
#[tauri::command]
#[specta::specta]
pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
match feat::enhance_profiles().await {
Ok(outcome) if outcome.is_valid() => {
@@ -67,7 +65,6 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
#[specta::specta]
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
Ok(it) => it,
@@ -97,10 +94,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
}
#[tauri::command]
#[specta::specta]
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
let active_id: String = active_id.into();
let over_id: String = over_id.into();
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
@@ -120,16 +114,16 @@ pub async fn reorder_profile(active_id: std::string::String, over_id: std::strin
}
#[tauri::command]
#[specta::specta]
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
let item: PrfItem = item.into();
let file_data = file_data.map(String::from);
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
profiles_save_file_safe()
.await
.with_error_code("PROFILE_CREATE_FAILED")?;
logging_error!(Type::Timer, Timer::global().refresh().await);
if let Some(uid) = &item.uid {
handle::Handle::notify_profile_changed(uid);
}
Ok(())
}
Err(err) => Err(coded_error("PROFILE_CREATE_FAILED", err)),
@@ -137,9 +131,7 @@ pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::
}
#[tauri::command]
#[specta::specta]
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
let index: String = index.into();
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(&index, option.as_ref(), true).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -150,9 +142,7 @@ pub async fn update_profile(index: std::string::String, option: Option<PrfOption
}
#[tauri::command]
#[specta::specta]
pub async fn delete_profile(index: std::string::String) -> CmdResult {
let index: String = index.into();
pub async fn delete_profile(index: String) -> CmdResult {
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = Config::profiles().await;
@@ -282,9 +272,7 @@ async fn handle_validation_failure(
) -> CmdResult<ValidationOutcome> {
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
discard_and_restore(current_profile).await?;
if !matches!(outcome, ValidationOutcome::Busy) {
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
}
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
Ok(outcome)
}
@@ -313,6 +301,9 @@ async fn perform_config_update(
current_value: Option<&String>,
current_profile: Option<&String>,
) -> CmdResult<ValidationOutcome> {
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
match update_result {
@@ -322,82 +313,9 @@ async fn perform_config_update(
}
}
#[derive(Default, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct ProfileSelection {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
}
#[derive(Default, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct ProfileCreate {
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
pub option: Option<PrfOption>,
}
impl From<ProfileCreate> for PrfItem {
fn from(item: ProfileCreate) -> Self {
Self {
itype: item.itype,
name: item.name,
desc: item.desc,
url: item.url,
option: item.option,
..Self::default()
}
}
}
#[derive(Default, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct ProfilePatch {
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
pub selected: Option<Vec<crate::config::PrfSelected>>,
pub extra: Option<crate::config::PrfExtra>,
pub updated: Option<usize>,
pub option: Option<PrfOption>,
}
impl From<ProfilePatch> for PrfItem {
fn from(patch: ProfilePatch) -> Self {
Self {
itype: patch.itype,
name: patch.name,
desc: patch.desc,
file: patch.file,
url: patch.url,
selected: patch.selected,
extra: patch.extra,
updated: patch.updated,
option: patch.option,
..Self::default()
}
}
}
#[tauri::command]
#[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))]
#[specta::specta]
pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<ValidationOutcome> {
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationOutcome> {
if CURRENT_SWITCHING_PROFILE
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
@@ -405,22 +323,13 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(ValidationOutcome::Busy);
}
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = IProfiles {
current: profiles.current,
items: None,
};
let target_profile = profiles.current.as_ref();
let previous_profile = Config::profiles().await.data_arc().current.clone();
Config::profiles().await.edit_draft(|d| {
d.patch_config(&profiles);
});
Config::profiles().await.edit_draft(|d| d.patch_config(&profiles));
perform_config_update(target_profile, previous_profile.as_ref())
.await
@@ -428,17 +337,15 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
}
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<ValidationOutcome> {
let profiles = ProfileSelection {
let profiles = IProfiles {
current: Some(profile_index),
items: None,
};
patch_profiles_config(profiles).await
}
#[tauri::command]
#[specta::specta]
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
let profile: PrfItem = profile.into();
let index: String = index.into();
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
@@ -478,8 +385,7 @@ pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) ->
}
#[tauri::command]
#[specta::specta]
pub async fn view_profile(index: std::string::String) -> CmdResult {
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let file = profiles_ref
@@ -503,8 +409,7 @@ pub async fn view_profile(index: std::string::String) -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
@@ -524,7 +429,7 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
.join(file.as_str());
match tokio::fs::try_exists(&path).await {
Ok(true) => {}
Ok(false) => return Ok(std::string::String::new()),
Ok(false) => return Ok(String::new()),
Err(err) => {
return Err(coded_error(
"PROFILE_READ_FAILED",
@@ -535,12 +440,11 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
}
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
Ok(data.into())
Ok(data)
}
#[tauri::command]
#[specta::specta]
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time)
+14 -6
View File
@@ -18,7 +18,6 @@ use std::{
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
#[specta::specta]
pub async fn record_selected_node(group_name: String, node: String) -> CmdResult<()> {
crate::config::profiles::record_selected_node(&group_name, &node)
.await
@@ -26,7 +25,6 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
}
#[tauri::command]
#[specta::specta]
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
crate::config::profiles::forget_selected_node(&group_name)
.await
@@ -106,27 +104,37 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
}
#[tauri::command]
#[specta::specta]
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
proxy_view(None).await
}
pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) -> CmdResult<ProxyViewV1> {
let runtime = Config::runtime().await;
let latest_runtime = runtime.latest_arc();
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
let mihomo = Handle::mihomo();
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),);
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async {
match provider_timeout {
Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers())
.await
.ok()
.and_then(Result::ok),
None => mihomo.get_proxy_providers().await.ok(),
}
});
let proxies = proxies.stringify_err()?;
Ok(ProxyViewBuilder::build(ProxyViewInput {
runtime_group_order,
group_scopes,
proxies,
providers: providers.ok(),
providers,
}))
}
#[tauri::command]
#[specta::specta]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
if TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+36 -41
View File
@@ -1,57 +1,44 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
use anyhow::{Context as _, anyhow};
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::HashMap;
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>> {
Ok(Config::runtime()
.await
.latest_arc()
.config
.clone()
.map(crate::ipc::YamlMapping))
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().await.latest_arc().config.clone())
}
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
let config = runtime.config.as_ref();
config
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
.and_then(|config| yaml_emitter::to_mihomo_config_string(config).context("failed to convert config to yaml"))
.and_then(|config| {
yaml_emitter::to_mihomo_config_string(config)
.context("failed to convert config to yaml")
.map(|s| s.into())
})
.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
Ok(RuntimeLogs(Config::runtime().await.latest_arc().chain_logs.clone()))
}
#[derive(serde::Serialize, specta::Type)]
#[serde(transparent)]
pub struct RuntimeLogs(
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
HashMap<String, Vec<(String, String)>>,
);
#[tauri::command]
#[specta::specta]
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
crate::enhance::take_discarded_keys_notice().map(Into::into)
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
}
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_proxy_chain_config(
proxy_chain_exit_node: std::string::String,
) -> CmdResult<std::string::String> {
pub fn take_discarded_keys_notice() -> Option<String> {
crate::enhance::take_discarded_keys_notice()
}
#[tauri::command]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
@@ -72,7 +59,7 @@ pub async fn get_runtime_proxy_chain_config(
false
}
}) {
proxies_chain.push(proxy);
proxies_chain.push(proxy.to_owned());
proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str());
}
@@ -81,17 +68,18 @@ pub async fn get_runtime_proxy_chain_config(
.find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name)
&& !proxies_chain.is_empty()
{
proxies_chain.push(entry_proxy);
proxies_chain.push(entry_proxy.to_owned());
}
proxies_chain.reverse();
let mut config: HashMap<String, Vec<&serde_yaml_ng::Value>> = HashMap::new();
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
config.insert("proxies".into(), proxies_chain);
yaml_emitter::to_mihomo_config_string(&config)
.context("YAML generation failed")
.map(|s| s.into())
.stringify_err()
} else {
Err("failed to get proxies or proxy-groups".into())
@@ -99,13 +87,20 @@ pub async fn get_runtime_proxy_chain_config(
}
#[tauri::command]
#[specta::specta]
pub async fn update_proxy_chain_config_in_runtime(
proxy_chain_config: Option<crate::ipc::YamlValue>,
) -> CmdResult<crate::core::validate::ValidationOutcome> {
let proxy_chain_config = proxy_chain_config.map(|value| value.0);
CoreManager::global()
pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<serde_yaml_ng::Value>) -> CmdResult<()> {
match CoreManager::global()
.update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config))
.await
.stringify_err()
{
Ok(outcome) if outcome.is_valid() => {}
Ok(outcome) => logging!(
warn,
Type::Core,
"Failed to apply runtime proxy chain config: {}",
outcome
),
Err(err) => logging!(error, Type::Core, "Failed to apply runtime proxy chain config: {err:#}"),
}
Ok(())
}
+1 -7
View File
@@ -16,13 +16,7 @@ use tokio::fs;
/// 保存profiles的配置
#[tauri::command]
#[specta::specta]
pub async fn save_profile_file(
index: std::string::String,
file_data: Option<std::string::String>,
) -> CmdResult<ValidationOutcome> {
let index: String = index.into();
let file_data = file_data.map(String::from);
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
let file_data = match file_data {
Some(d) => d,
None => return Ok(ValidationOutcome::Valid),
+2 -13
View File
@@ -8,11 +8,11 @@ use crate::{
},
};
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ServiceInstallOutcome {
Installed,
#[cfg_attr(not(any(windows, test)), allow(dead_code))]
#[cfg(any(windows, test))]
Sidecar {
reason: String,
},
@@ -82,13 +82,11 @@ where
}
#[tauri::command]
#[specta::specta]
pub async fn install_service() -> CmdResult<ServiceInstallOutcome> {
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
}
#[tauri::command]
#[specta::specta]
pub async fn uninstall_service() -> CmdResult {
CoreManager::global()
.uninstall_service_and_start_sidecar()
@@ -97,19 +95,16 @@ pub async fn uninstall_service() -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub async fn reinstall_service() -> CmdResult<ServiceInstallOutcome> {
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
}
#[tauri::command]
#[specta::specta]
pub async fn repair_service() -> CmdResult<ServiceInstallOutcome> {
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
}
#[tauri::command]
#[specta::specta]
pub async fn continue_with_sidecar() -> CmdResult {
crate::core::CoreManager::global()
.continue_with_sidecar(None)
@@ -118,37 +113,31 @@ pub async fn continue_with_sidecar() -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub fn get_sidecar_failure() -> crate::core::notification::SidecarFailureSnapshot {
crate::core::notification::sidecar_failure_snapshot()
}
#[tauri::command]
#[specta::specta]
pub fn take_service_fallback_notice() -> Option<crate::core::service::ServiceFallbackNotice> {
crate::core::service::take_service_fallback_notice()
}
#[tauri::command]
#[specta::specta]
pub fn get_core_startup_error() -> Option<crate::core::manager::CoreFailure> {
crate::core::CoreManager::global().get_startup_error()
}
#[tauri::command]
#[specta::specta]
pub fn take_service_repair_notice() -> bool {
crate::core::service::take_service_repair_notice()
}
#[tauri::command]
#[specta::specta]
pub fn take_service_owner_notice() -> Option<String> {
crate::core::service::take_service_owner_notice()
}
#[tauri::command]
#[specta::specta]
pub fn sync_runtime_providers() {
if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
-2
View File
@@ -5,13 +5,11 @@ use crate::core::{
/// Returns one coherent core/service snapshot instead of independently refreshed state.
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_state() -> Result<RunStateView, String> {
Ok(RUN_STATE.settled().await.to_view())
}
#[tauri::command]
#[specta::specta]
pub async fn get_pending_failures() -> Vec<PendingFailure> {
notification::pending_failures()
}
-1
View File
@@ -22,7 +22,6 @@ mod platform {
}
#[tauri::command]
#[specta::specta]
pub async fn invoke_uwp_tool() -> CmdResult {
platform::invoke_uwp_tool()
}
-2
View File
@@ -9,14 +9,12 @@ use clash_verge_draft::SharedDraft;
/// 获取Verge配置
#[tauri::command]
#[specta::specta]
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
feat::fetch_verge_config().await.stringify_err()
}
/// 修改Verge配置
#[tauri::command]
#[specta::specta]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
let operation = system_proxy_operation(&payload);
let result = match operation {
+6 -22
View File
@@ -4,18 +4,11 @@ use crate::{
config::{Config, IVerge},
core, feat,
};
use reqwest_dav::list_cmd::ListFile;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
pub async fn save_webdav_config(
url: std::string::String,
username: std::string::String,
password: std::string::String,
) -> CmdResult<()> {
let url: String = url.into();
let username: String = username.into();
let password: String = password.into();
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
@@ -32,31 +25,22 @@ pub async fn save_webdav_config(
}
#[tauri::command]
#[specta::specta]
pub async fn create_webdav_backup() -> CmdResult<()> {
feat::create_backup_and_upload_webdav().await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
feat::list_wevdav_backup()
.await
.map(|files| files.into_iter().map(Into::into).collect())
.stringify_err()
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
feat::list_wevdav_backup().await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
feat::delete_webdav_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
feat::restore_webdav_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
+6 -6
View File
@@ -308,7 +308,7 @@ impl IClashTemp {
}
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ClashInfo {
/// clash core port
pub mixed_port: u16,
@@ -364,14 +364,14 @@ fn test_clash_info() {
assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097"));
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashExternalControllerCors {
pub allow_origins: Option<Vec<String>>,
pub allow_private_network: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClash {
pub mixed_port: Option<u16>,
@@ -387,7 +387,7 @@ pub struct IClash {
pub external_controller_cors: Option<IClashExternalControllerCors>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashTUN {
pub enable: Option<bool>,
@@ -397,7 +397,7 @@ pub struct IClashTUN {
pub dns_hijack: Option<Vec<String>>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashDNS {
pub enable: Option<bool>,
@@ -414,7 +414,7 @@ pub struct IClashDNS {
pub nameserver_policy: Option<Vec<String>>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashFallbackFilter {
pub geoip: Option<bool>,
+1 -2
View File
@@ -106,11 +106,10 @@ impl DnsOverrideState {
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, specta::Type)]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ProfileDnsSettings {
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub confirmation: Option<String>,
}
+16 -23
View File
@@ -27,32 +27,30 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
Some(url.to_string().into())
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
#[serde(default)]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
#[specta(type = Option<std::string::String>)]
pub uid: Option<String>,
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<Vec<PrfSelected>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<PrfExtra>,
@@ -62,24 +60,19 @@ pub struct PrfItem {
pub option: Option<PrfOption>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub home: Option<String>,
#[serde(skip)]
#[specta(type = Option<std::string::String>)]
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(default)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct PrfSelected {
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, specta::Type)]
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
pub struct PrfExtra {
pub upload: u64,
pub download: u64,
@@ -87,11 +80,9 @@ pub struct PrfExtra {
pub expire: u64,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(default)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -114,15 +105,15 @@ pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_auto_update: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub merge: Option<String>,
#[specta(type = Option<std::string::String>)]
pub script: Option<String>,
#[specta(type = Option<std::string::String>)]
pub rules: Option<String>,
#[specta(type = Option<std::string::String>)]
pub proxies: Option<String>,
#[specta(type = Option<std::string::String>)]
pub groups: Option<String>,
}
@@ -229,6 +220,7 @@ impl PrfItem {
desc: Some(desc),
file: Some(file),
url: None,
latency_test_urls: None,
selected: None,
extra: None,
option: Some(PrfOption {
@@ -408,6 +400,7 @@ impl PrfItem {
desc: desc.cloned(),
file: Some(file),
url: Some(url.as_str().into()),
latency_test_urls: None,
selected: None,
extra,
option: Some(PrfOption {
+2 -3
View File
@@ -48,10 +48,8 @@ const SELECTED_NODES_SETTLE_INTERVAL: Duration = Duration::from_secs(1);
const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IProfiles {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
pub items: Option<Vec<PrfItem>>,
@@ -250,6 +248,7 @@ impl IProfiles {
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, latency_test_urls);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
+14 -45
View File
@@ -11,30 +11,26 @@ use serde::{Deserialize, Serialize};
use smartstring::alias::String;
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVerge {
/// silent | error | warn | info | debug | trace
#[specta(type = Option<std::string::String>)]
pub app_log_level: Option<String>,
/// Maximum app log size in KiB.
pub app_log_max_size: Option<u64>,
pub app_log_max_count: Option<usize>,
#[specta(type = Option<std::string::String>)]
pub language: Option<String>,
/// `light` or `dark` or `system`
#[specta(type = Option<std::string::String>)]
pub theme_mode: Option<String>,
#[specta(type = Option<std::string::String>)]
pub tray_event: Option<String>,
#[specta(type = Option<std::string::String>)]
pub env_type: Option<String>,
#[specta(type = Option<std::string::String>)]
pub start_page: Option<String>,
#[specta(type = Option<std::string::String>)]
pub startup_script: Option<String>,
pub traffic_graph: Option<bool>,
@@ -52,23 +48,18 @@ pub struct IVerge {
#[cfg(target_os = "macos")]
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub tray_icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub menu_icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<Vec<std::string::String>>)]
pub menu_order: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub proxy_group_tools_position: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub notice_position: Option<String>,
pub collapse_navbar: Option<bool>,
@@ -93,43 +84,39 @@ pub struct IVerge {
pub enable_dns_settings: Option<bool>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
#[specta(type = std::collections::BTreeMap<std::string::String, super::dns::ProfileDnsSettings>)]
pub profile_dns_settings: std::collections::BTreeMap<String, super::dns::ProfileDnsSettings>,
pub use_default_bypass: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
pub proxy_auto_config: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub pac_file_content: Option<String>,
#[specta(type = Option<std::string::String>)]
pub proxy_host: Option<String>,
pub theme_setting: Option<IVergeTheme>,
#[specta(type = Option<Vec<std::string::String>>)]
pub web_ui_list: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub clash_core: Option<String>,
/// format: {func},{key}
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<Vec<std::string::String>>)]
pub hotkeys: Option<Vec<String>>,
pub enable_global_hotkey: Option<bool>,
#[specta(type = Option<crate::ipc::JsonValue>)]
pub home_cards: Option<serde_json::Value>,
pub auto_close_connection: Option<bool>,
pub auto_check_update: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub default_latency_test: Option<String>,
pub default_latency_timeout: Option<i16>,
@@ -184,7 +171,6 @@ pub struct IVerge {
skip_serializing_if = "Option::is_none",
default
)]
#[specta(type = Option<std::string::String>)]
pub webdav_url: Option<String>,
#[serde(
@@ -193,7 +179,6 @@ pub struct IVerge {
skip_serializing_if = "Option::is_none",
default
)]
#[specta(type = Option<std::string::String>)]
pub webdav_username: Option<String>,
#[serde(
@@ -202,14 +187,12 @@ pub struct IVerge {
skip_serializing_if = "Option::is_none",
default
)]
#[specta(type = Option<std::string::String>)]
pub webdav_password: Option<String>,
#[cfg(target_os = "macos")]
pub enable_tray_speed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub tray_proxy_groups_display_mode: Option<String>,
pub tray_inline_outbound_modes: Option<bool>,
@@ -226,41 +209,27 @@ pub struct IVerge {
pub enable_external_controller: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVergeTestItem {
#[specta(type = Option<std::string::String>)]
pub uid: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub icon: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVergeTheme {
#[specta(type = Option<std::string::String>)]
pub primary_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub secondary_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub primary_text: Option<String>,
#[specta(type = Option<std::string::String>)]
pub secondary_text: Option<String>,
#[specta(type = Option<std::string::String>)]
pub info_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub error_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub warning_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub success_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub font_family: Option<String>,
#[specta(type = Option<std::string::String>)]
pub css_injection: Option<String>,
}
+6 -6
View File
@@ -38,7 +38,7 @@ pub(crate) fn proxy_listener_keys() -> impl Iterator<Item = &'static str> {
PROXY_LISTENERS.iter().map(|(key, _, _)| *key)
}
#[derive(Debug, Deserialize, specta::Type)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyPortSettings {
pub mixed_port: u16,
@@ -48,27 +48,27 @@ pub struct ProxyPortSettings {
pub tproxy: ToggleableProxyPort,
}
#[derive(Clone, Copy, Debug, Deserialize, specta::Type)]
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct ToggleableProxyPort {
pub enabled: bool,
pub port: u16,
}
#[derive(Debug, Deserialize, specta::Type)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListenerProbe {
pub address: String,
pub transports: Vec<ListenerTransport>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, specta::Type)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ListenerTransport {
Tcp,
Udp,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ListenerProbeOutcome {
Available,
@@ -77,7 +77,7 @@ pub enum ListenerProbeOutcome {
Indeterminate { message: String },
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum SaveProxyPortsOutcome {
Saved,
+2 -2
View File
@@ -57,7 +57,7 @@ fn claim_core_readiness_generation(state: &AtomicU64, captured_generation: u64)
.is_ok()
}
#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq, specta::Type)]
#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq)]
pub enum RunningMode {
Service,
Sidecar,
@@ -75,7 +75,7 @@ impl fmt::Display for RunningMode {
}
/// Why the Core is not running, kept until a Core starts again.
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, specta::Type)]
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)]
#[serde(tag = "kind", content = "detail", rename_all = "camelCase")]
pub enum CoreFailure {
StartFailed(String),
+4 -1
View File
@@ -52,7 +52,10 @@ impl Handle {
/// Sent on every transition, so the frontend does not have to poll to notice that the Core
/// stopped or that the Service came back.
pub fn notify_run_state(state: &crate::core::runstate::RunStateView) {
Self::send_event(FrontendEvent::RunStateChanged { state: state.clone() });
let Ok(state) = serde_json::to_value(state) else {
return;
};
Self::send_event(FrontendEvent::RunStateChanged { state });
}
pub fn notify_profile_changed(profile_id: &String) {
+2 -2
View File
@@ -3,8 +3,9 @@ pub use announce::{Refresh, announce};
pub mod desktop;
pub mod handle;
pub mod notification;
pub use notification::frontend_wire_contract;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum NoticeStatus {
#[serde(rename = "info")]
Info,
@@ -82,7 +83,6 @@ impl NoticeStatus {
/// Every variant in golden order. The wire-contract export and the golden
/// test both derive their variant list from here; expected strings stay
/// hand-written in the test so renames cannot slip through.
#[cfg(test)]
pub const ALL: [Self; 34] = [
Self::Info,
Self::ImportSubUrlOk,
+58 -61
View File
@@ -1,7 +1,6 @@
use super::NoticeStatus;
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
#[cfg(test)]
use serde_json::json;
use smartstring::alias::String;
use std::sync::Arc;
@@ -32,7 +31,7 @@ pub enum FrontendEvent<'a> {
uid: &'a String,
},
RunStateChanged {
state: crate::core::runstate::RunStateView,
state: serde_json::Value,
},
PendingFailuresChanged,
#[cfg(target_os = "linux")]
@@ -43,7 +42,7 @@ pub enum FrontendEvent<'a> {
/// Operation associated with a pending failure.
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum FailedOperation {
SystemProxyEnable,
@@ -73,14 +72,12 @@ impl FailedOperation {
}
/// Latest unresolved failure for one stable code.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingFailure {
/// Stable code used as the table key.
#[specta(type = std::string::String)]
pub code: String,
/// Full diagnostic context chain.
#[specta(type = std::string::String)]
pub detail: String,
pub operation: FailedOperation,
/// Monotonic identity for repeated failures under the same code.
@@ -244,10 +241,9 @@ pub fn what_was_asked() -> FailedOperation {
static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::default);
/// A removal has a revision too, so a delayed read cannot restore a resolved failure.
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct SidecarFailureSnapshot {
pub revision: u64,
#[specta(type = Option<std::string::String>)]
pub detail: Option<String>,
}
@@ -331,7 +327,24 @@ impl NotificationSystem {
}
fn serialize_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
serialize_frontend_event(event)
match event {
FrontendEvent::RefreshClash => ("verge://refresh-clash-config", Ok(json!("yes"))),
FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))),
FrontendEvent::RefreshProfiles => ("verge://refresh-profiles", Ok(json!("yes"))),
FrontendEvent::RefreshProxyConfig => ("verge://refresh-proxy-config", Ok(serde_json::Value::Null)),
FrontendEvent::NoticeMessage { status, message } => (
"verge://notice-message",
serde_json::to_value((status, message.as_ref())),
),
FrontendEvent::ProfileChanged { current_profile_id } => ("profile-changed", Ok(json!(current_profile_id))),
FrontendEvent::TimerUpdated { profile_index } => ("verge://timer-updated", Ok(json!(profile_index))),
FrontendEvent::ProfileUpdateStarted { uid } => ("profile-update-started", Ok(json!({ "uid": uid }))),
FrontendEvent::ProfileUpdateCompleted { uid } => ("profile-update-completed", Ok(json!({ "uid": uid }))),
FrontendEvent::RunStateChanged { state } => ("verge://run-state-changed", Ok(state)),
FrontendEvent::PendingFailuresChanged => ("verge://pending-failures-changed", Ok(serde_json::Value::Null)),
#[cfg(target_os = "linux")]
FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)),
}
}
pub(crate) fn send_event(app_handle: AppHandle, event: FrontendEvent) {
@@ -368,21 +381,20 @@ mod wire_tests {
}
/// One sample per `FrontendEvent` variant, paired with its wire name and payload.
#[cfg(test)]
/// Shared by the wire golden test and the contract export so the two cannot drift.
fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, serde_json::Value)> {
let state = crate::core::runstate::RunState {
health: Default::default(),
pending: None,
sidecar_allowed: false,
mode: crate::core::manager::RunningMode::NotRunning,
is_admin: false,
op_in_flight: false,
}
.to_view();
vec![
(FrontendEvent::RefreshClash, "verge://refresh-clash-config", json!(null)),
(FrontendEvent::RefreshVerge, "verge://refresh-verge-config", json!(null)),
(FrontendEvent::RefreshProfiles, "verge://refresh-profiles", json!(null)),
(
FrontendEvent::RefreshClash,
"verge://refresh-clash-config",
json!("yes"),
),
(
FrontendEvent::RefreshVerge,
"verge://refresh-verge-config",
json!("yes"),
),
(FrontendEvent::RefreshProfiles, "verge://refresh-profiles", json!("yes")),
(
FrontendEvent::RefreshProxyConfig,
"verge://refresh-proxy-config",
@@ -402,9 +414,11 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
json!("profile"),
),
(
FrontendEvent::RunStateChanged { state: state.clone() },
FrontendEvent::RunStateChanged {
state: json!({"running": true}),
},
"verge://run-state-changed",
serde_json::to_value(state).unwrap_or(serde_json::Value::Null),
json!({"running": true}),
),
(
FrontendEvent::PendingFailuresChanged,
@@ -431,41 +445,24 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
]
}
macro_rules! frontend_events {
($( $field:ident: $payload:ty = $name:literal, $pattern:pat => $value:expr; )*) => {
#[derive(serde::Serialize, specta::Type)]
pub struct VergeEventPayloads {
$(#[serde(rename = $name)] pub $field: $payload,)*
}
fn serialize_frontend_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
match event {
$($pattern => {
let payload: $payload = $value;
($name, serde_json::to_value(payload))
},)*
#[cfg(target_os = "linux")]
FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)),
}
}
};
}
frontend_events! {
refresh_clash: () = "verge://refresh-clash-config", FrontendEvent::RefreshClash => ();
refresh_verge: () = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => ();
refresh_profiles: () = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => ();
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
}
#[derive(serde::Serialize, specta::Type)]
pub struct ProfileUpdatePayload {
pub uid: std::string::String,
/// Machine-readable form of the frontend wire contract: every event name with a
/// serialized sample payload, plus every notice status string. The sample
/// payload's JSON shape (string / null / array / object) is what the frontend
/// generator maps to TypeScript types. `ThemeChanged` is a Tauri built-in
/// linux-only window event, not part of this contract.
pub fn frontend_wire_contract() -> serde_json::Value {
let uid = String::from("sample");
let events: Vec<_> = sample_frontend_events(&uid)
.into_iter()
.map(|(event, _, _)| {
let (name, payload) = NotificationSystem::serialize_event(event);
json!({ "name": name, "sample": payload.unwrap_or(serde_json::Value::Null) })
})
.collect();
let statuses: Vec<_> = NoticeStatus::ALL
.iter()
// A unit variant with a string rename cannot fail to serialize.
.filter_map(|status| serde_json::to_value(status).ok())
.collect();
json!({ "version": 1, "events": events, "noticeStatuses": statuses })
}
+88 -21
View File
@@ -1,8 +1,8 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use serde::Serialize;
use tauri_plugin_mihomo::models::{
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
};
pub struct ProxyViewInput {
@@ -20,7 +20,7 @@ pub struct GroupScope {
pub struct ProxyViewBuilder;
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyViewV1 {
pub schema_version: u8,
@@ -34,21 +34,21 @@ pub struct ProxyViewV1 {
pub providers: Vec<ProxyProviderView>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProxyViewOrderSource {
Runtime,
Fallback,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProxyViewProviderState {
Ready,
Unavailable,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct ProxyCapabilities {
pub udp: bool,
pub xudp: bool,
@@ -57,13 +57,13 @@ pub struct ProxyCapabilities {
pub smux: bool,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyGroupView {
pub name: String,
#[serde(rename = "type")]
#[specta(type = String)]
pub proxy_type: ProxyType,
pub alive: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub now: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -74,23 +74,23 @@ pub struct ProxyGroupView {
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_url: Option<String>,
#[specta(type = Vec<crate::ipc::DelayHistory>)]
pub history: Vec<DelayHistory>,
pub extra: HashMap<String, Extra>,
#[serde(flatten)]
pub capabilities: ProxyCapabilities,
pub members: Vec<ProxyMemberRef>,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyNodeView {
pub record_id: String,
pub name: String,
#[serde(rename = "type")]
#[specta(type = String)]
pub proxy_type: ProxyType,
#[specta(type = Vec<crate::ipc::DelayHistory>)]
pub alive: bool,
pub history: Vec<DelayHistory>,
pub extra: HashMap<String, Extra>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -104,7 +104,7 @@ pub struct ProxyNodeView {
pub source: ProxyNodeSource,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum ProxyNodeSource {
Core {
@@ -119,7 +119,7 @@ pub enum ProxyNodeSource {
},
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum ProxyMemberRef {
Group {
@@ -136,7 +136,7 @@ pub enum ProxyMemberRef {
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum ProxyMemberUnresolvedReason {
#[serde(rename = "missing")]
Missing,
@@ -146,7 +146,7 @@ pub enum ProxyMemberUnresolvedReason {
ProviderUnavailable,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyProviderView {
pub name: String,
@@ -158,7 +158,7 @@ pub struct ProxyProviderView {
pub proxy_record_ids: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum ProxyProviderVehicleType {
#[serde(rename = "HTTP")]
Http,
@@ -168,7 +168,7 @@ pub enum ProxyProviderVehicleType {
Inline,
}
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct ProxySubscriptionInfo {
pub upload: i64,
pub download: i64,
@@ -256,7 +256,7 @@ impl ProxyViewBuilder {
} else {
ProxyViewProviderState::Unavailable
};
let (mut core_groups, core_nodes) = partition_core(proxies);
let (mut core_groups, core_nodes) = partition_core(proxies, providers.is_some());
let (mut records, core_node_ids) = build_core_records(core_nodes);
let (providers, provider_candidates) = build_provider_records(providers, &mut records);
let resolver = MemberResolver {
@@ -289,14 +289,14 @@ impl ProxyViewBuilder {
}
}
fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
fn partition_core(proxies: Proxies, providers_available: bool) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
let mut groups = BTreeMap::new();
let mut nodes = BTreeMap::new();
for (name, proxy) in proxies.proxies {
if proxy.all.is_some() {
groups.insert(name, proxy);
} else {
} else if !providers_available || proxy.provider_name.is_empty() {
nodes.insert(name, proxy);
}
}
@@ -402,7 +402,9 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
icon,
now,
test_url,
alive,
history,
extra,
udp,
xudp,
tfo,
@@ -429,12 +431,14 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
ProxyGroupView {
name,
proxy_type,
alive,
now,
fixed,
hidden,
icon,
test_url,
history,
extra,
capabilities: ProxyCapabilities {
udp,
xudp,
@@ -452,7 +456,9 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
hidden,
icon,
test_url,
alive,
history,
extra,
udp,
xudp,
tfo,
@@ -466,7 +472,9 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
record_id,
name,
proxy_type,
alive,
history,
extra,
id,
hidden,
icon,
@@ -520,3 +528,62 @@ fn build_standalone(core_node_ids: &BTreeMap<String, String>) -> Vec<String> {
);
standalone
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn provider_echo_in_proxies_does_not_override_group_membership() -> anyhow::Result<()> {
let view = ProxyViewBuilder::build(ProxyViewInput {
runtime_group_order: vec!["A".into(), "B".into()],
group_scopes: ["A", "B"]
.into_iter()
.map(|name| {
(
name.into(),
GroupScope {
providers: vec![name.into()],
exclude_types: vec![],
},
)
})
.collect(),
proxies: serde_json::from_value(json!({"proxies": {
"A": {"all": ["node"], "type": "Selector"},
"B": {"all": ["node"], "type": "Selector"},
"node": {"name": "node", "provider-name": "A", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}
}}))?,
providers: Some(serde_json::from_value(json!({"providers": {
"A": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}]},
"B": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 123}]}]}
}}))?),
});
for (group, expected) in view.groups.iter().zip([32, 123]) {
let ProxyMemberRef::Node { record_id, .. } = &group.members[0] else {
anyhow::bail!("Expected node")
};
let record = &view.records[record_id];
assert_eq!(record.history.last().map(|entry| entry.delay), Some(expected));
assert!(
matches!(&record.source, ProxyNodeSource::Provider { provider_name, .. } if provider_name == &group.name)
);
}
Ok(())
}
#[test]
fn unavailable_provider_keeps_core_echo_interactable() -> anyhow::Result<()> {
let view = ProxyViewBuilder::build(ProxyViewInput {
runtime_group_order: vec!["A".into()],
group_scopes: BTreeMap::new(),
proxies: serde_json::from_value(json!({"proxies": {
"A": {"all": ["node"], "type": "Selector"},
"node": {"name": "node", "provider-name": "provider"}
}}))?,
providers: None,
});
assert!(matches!(&view.groups[0].members[0], ProxyMemberRef::Node { .. }));
Ok(())
}
}
+11 -21
View File
@@ -14,13 +14,13 @@ pub enum ServiceHealth {
}
impl ServiceHealth {
const fn kind(&self) -> ServiceHealthKind {
const fn kind(&self) -> &'static str {
match self {
Self::Unknown => ServiceHealthKind::Unknown,
Self::Ready => ServiceHealthKind::Ready,
Self::NotInstalled => ServiceHealthKind::NotInstalled,
Self::VersionMismatch => ServiceHealthKind::VersionMismatch,
Self::Unavailable(_) => ServiceHealthKind::Unavailable,
Self::Unknown => "unknown",
Self::Ready => "ready",
Self::NotInstalled => "notInstalled",
Self::VersionMismatch => "versionMismatch",
Self::Unavailable(_) => "unavailable",
}
}
@@ -32,7 +32,7 @@ impl ServiceHealth {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum PendingAction {
Install,
@@ -118,11 +118,11 @@ impl RunState {
}
}
#[derive(Debug, Clone, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunStateView {
pub mode: RunningMode,
pub service: ServiceHealthKind,
pub service: &'static str,
pub service_unavailable_reason: Option<String>,
pub pending_action: Option<PendingAction>,
pub sidecar_allowed: bool,
@@ -232,7 +232,7 @@ mod tests {
let view = run_state.to_view();
assert_eq!(view.service, ServiceHealthKind::Unavailable);
assert_eq!(view.service, "unavailable");
assert_eq!(view.service_unavailable_reason.as_deref(), Some("socket refused"));
assert_eq!(view.pending_action, Some(PendingAction::Reinstall));
assert!(view.tun_capable, "elevation alone makes TUN possible");
@@ -244,7 +244,7 @@ mod tests {
fn a_healthy_view_reports_no_unavailable_reason() {
let view = state(ServiceHealth::Ready, false, false).to_view();
assert_eq!(view.service, ServiceHealthKind::Ready);
assert_eq!(view.service, "ready");
assert_eq!(view.service_unavailable_reason, None);
assert!(view.service_usable);
assert!(!view.service_needs_attention);
@@ -350,13 +350,3 @@ mod tests {
assert!(settled.tun_should_be_disabled(true));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum ServiceHealthKind {
Unknown,
Ready,
NotInstalled,
VersionMismatch,
Unavailable,
}
+1 -1
View File
@@ -44,7 +44,7 @@ static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false);
static PENDING_SERVICE_OWNER_NOTICE: Mutex<Option<String>> = Mutex::new(None);
/// Why the Service was unavailable when the core fell back to Sidecar.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "kind", content = "reason", rename_all = "camelCase")]
pub enum ServiceFallbackNotice {
Unavailable,
+156
View File
@@ -0,0 +1,156 @@
use super::Tray;
use crate::{
Type, cmd,
config::Config,
core::{
handle,
notify::{Refresh, announce},
proxy_view::{ProxyMemberRef, ProxyNodeSource},
},
logging,
};
const TRAY_DELAY_TEST_CONCURRENCY: usize = 10;
const TRAY_DEFAULT_LATENCY_URL: &str = "http://cp.cloudflare.com/generate_204";
fn is_latency_test_url(raw: &str) -> bool {
let raw = raw.trim();
raw.split_once("://")
.is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https"))
&& tauri::Url::parse(raw).is_ok_and(|url| url.has_host() && matches!(url.scheme(), "http" | "https"))
}
pub(super) fn test_url(
group: &crate::core::proxy_view::ProxyGroupView,
verge: &crate::config::IVerge,
profiles: &crate::config::IProfiles,
) -> String {
let custom_url = profiles
.current
.as_ref()
.and_then(|uid| profiles.get_item(uid).ok())
.and_then(|profile| profile.latency_test_urls.as_ref())
.and_then(|urls| urls.get(group.name.as_str()))
.map(|url| url.as_str())
.filter(|url| !url.trim().is_empty());
let group_url = group.test_url.as_deref().filter(|url| !url.trim().is_empty());
let default_url = verge
.default_latency_test
.as_deref()
.filter(|url| !url.trim().is_empty());
custom_url
.or(group_url)
.or(default_url)
.filter(|&url| is_latency_test_url(url))
.unwrap_or(TRAY_DEFAULT_LATENCY_URL)
.trim()
.to_owned()
}
static RUNNING: parking_lot::Mutex<std::collections::BTreeSet<String>> =
parking_lot::Mutex::new(std::collections::BTreeSet::new());
pub(super) fn is_running(group_name: &str) -> bool {
RUNNING.lock().contains(group_name)
}
pub(super) async fn test_proxy_group_delay(group_name: &str) {
if !RUNNING.lock().insert(group_name.to_owned()) {
return;
}
Tray::global().refresh_latency_item(group_name);
let _running = scopeguard::guard(group_name, |name| {
RUNNING.lock().remove(name);
Tray::global().refresh_latency_item(name);
});
use futures::{StreamExt as _, stream};
let view = match cmd::proxy::get_proxy_view().await {
Ok(view) => view,
Err(err) => {
logging!(warn, Type::Tray, "Failed to read proxies for tray latency test: {err}");
return;
}
};
let verge = Config::verge().await.latest_arc();
let group = view
.groups
.iter()
.chain(view.global.iter())
.find(|group| group.name.as_str() == group_name);
let Some(group) = group else {
logging!(warn, Type::Tray, "Tray latency test group not found: {group_name}");
return;
};
let profiles = Config::profiles().await.latest_arc();
let url = test_url(group, &verge, &profiles);
let timeout = verge
.default_latency_timeout
.filter(|timeout| *timeout > 0)
.unwrap_or(10000) as u32;
let mut targets: Vec<(std::string::String, Option<std::string::String>)> = Vec::with_capacity(group.members.len());
for member in &group.members {
match member {
ProxyMemberRef::Node { name, record_id } => match view.records.get(record_id).map(|node| &node.source) {
Some(ProxyNodeSource::Provider {
provider_name,
proxy_name,
}) => targets.push((proxy_name.clone(), Some(provider_name.clone()))),
_ => targets.push((name.clone(), None)),
},
ProxyMemberRef::Group { name } => targets.push((name.clone(), None)),
ProxyMemberRef::Unresolved { .. } => {}
}
}
let mihomo = handle::Handle::mihomo();
stream::iter(targets.into_iter().map(move |(member, provider)| {
let url = url.clone();
async move {
let result = match provider {
Some(provider_name) => {
mihomo
.healthcheck_node_in_provider(&provider_name, &member, &url, timeout)
.await
}
None => mihomo.delay_proxy_by_name(&member, &url, timeout).await,
};
if let Err(err) = result {
logging!(debug, Type::Tray, "Tray latency test failed: {member} - {err}");
}
}
}))
.buffer_unordered(TRAY_DELAY_TEST_CONCURRENCY)
.for_each(|()| std::future::ready(()))
.await;
if let Err(err) = cmd::proxy::sync_tray_proxy_selection().await {
logging!(warn, Type::Tray, "Failed to refresh tray after latency test: {err}");
}
announce(Refresh::Proxies);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn latency_urls_require_a_valid_http_host_and_port() {
for invalid in [
"http://?",
"http://host:invalid",
"http://bad host/",
"ftp://localhost",
"http:localhost",
] {
assert!(!is_latency_test_url(invalid), "{invalid}");
}
for valid in ["http://localhost/", " https://example.com/ ", "HTTP://localhost:8080/"] {
assert!(is_latency_test_url(valid), "{valid}");
}
}
}
+1
View File
@@ -32,6 +32,7 @@ define_menu! {
outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes",
profiles => PROFILES, "tray_profiles", "tray.profiles",
proxies => PROXIES, "tray_proxies", "tray.proxies",
test_latency => TEST_LATENCY, "tray_test_latency", "tray.testLatency",
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
+388 -183
View File
@@ -1,5 +1,6 @@
use crate::config::{IProfilePreview, IProfiles, IVerge};
use crate::config::{IProfilePreview, IVerge};
use crate::core::lightweight;
use crate::core::proxy_view::{ProxyGroupView, ProxyMemberRef, ProxyNodeSource, ProxyViewProviderState, ProxyViewV1};
use crate::core::tray::menu_def::TrayAction;
use crate::process::AsyncHandler;
use crate::singleton;
@@ -14,7 +15,6 @@ use crate::{
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
use clash_verge_logging::logging_error;
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
use tauri_plugin_mihomo::models::Proxies;
use tokio::fs;
use super::handle;
@@ -28,6 +28,7 @@ use tauri::{
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
};
mod latency;
mod menu_def;
#[cfg(target_os = "macos")]
mod speed_task;
@@ -41,10 +42,7 @@ const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
pub const TRAY_ID: &str = "clash-verge-rev-tray";
#[derive(Clone, Copy)]
struct TrayMenuOptions<'a> {
mode: Option<&'a str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
struct TrayMenuOptions {
is_lightweight_mode: bool,
include_proxy_groups: bool,
}
@@ -58,8 +56,25 @@ enum IconKind {
Tun,
}
struct LatencyMenuItem {
item: MenuItem<Wry>,
running: bool,
proxies: Vec<ProxyLatencyItem>,
provider_state: ProxyViewProviderState,
}
struct ProxyLatencyItem {
item: CheckMenuItem<Wry>,
name: std::string::String,
provider_name: Option<std::string::String>,
text: std::string::String,
}
pub struct Tray {
limiter: SystemLimiter,
proxy_hover_limiter: SystemLimiter,
menu_update: tokio::sync::Mutex<()>,
latency_items: parking_lot::Mutex<HashMap<std::string::String, LatencyMenuItem>>,
#[cfg(target_os = "macos")]
speed_controller: speed_task::TraySpeedController,
}
@@ -133,6 +148,9 @@ impl Default for Tray {
fn default() -> Self {
Self {
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
proxy_hover_limiter: Limiter::new(Duration::from_secs(1), SystemClock),
menu_update: tokio::sync::Mutex::new(()),
latency_items: parking_lot::Mutex::new(HashMap::new()),
#[cfg(target_os = "macos")]
speed_controller: speed_task::TraySpeedController::new(),
}
@@ -198,14 +216,16 @@ impl Tray {
}
async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> {
let _update = self.menu_update.lock().await;
let Some(tray) = app_handle.tray_by_id(TRAY_ID) else {
logging!(warn, Type::Tray, "Failed to update tray menu: tray not found");
return Ok(());
};
let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.unwrap_or(false);
let tun_mode = verge.enable_tun_mode.unwrap_or(false);
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let tun_mode_available = crate::core::runstate::RUN_STATE.state().tun_capable();
let mode = {
Config::clash()
.await
@@ -216,78 +236,138 @@ impl Tray {
.unwrap_or("rule")
.to_owned()
};
let profiles_config = Config::profiles().await;
let profiles_arc = profiles_config.latest_arc();
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode();
let fetch_proxy_groups =
include_proxy_groups && verge.tray_proxy_groups_display_mode.as_deref() != Some("disable");
// TODO: should update tray menu again when it was timeout error
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
let proxy_nodes_data =
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
.await
.map_or(None, |res| res.ok());
let runtime = Config::runtime().await.latest_arc();
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
config
.get("proxy-groups")
.and_then(|groups| groups.as_sequence())
.map(|groups| {
groups
.iter()
.filter_map(|group| group.get("name"))
.filter_map(|name| name.as_str())
.enumerate()
.map(|(index, name)| (name.into(), index))
.collect::<HashMap<String, usize>>()
})
.unwrap_or_default()
});
(proxy_nodes_data, runtime_proxy_groups_order)
} else {
(None, None)
};
let verge = if fetch_proxy_groups {
Config::verge().await.latest_arc()
} else {
verge
};
let profiles = Config::profiles().await.latest_arc();
let (tx, rx) = tokio::sync::oneshot::channel();
let dispatch_handle = app_handle.clone();
app_handle
.run_on_main_thread(move || {
let _ = tx.send((|| -> Result<()> {
let menu = build_tray_menu(
&dispatch_handle,
TrayMenuOptions {
mode: Some(mode.as_str()),
system_proxy_enabled: system_proxy,
tun_mode_enabled: tun_mode,
is_lightweight_mode,
include_proxy_groups,
},
&verge,
&profiles,
runtime_proxy_groups_order,
proxy_nodes_data,
)?;
logging_error!(Type::Tray, tray.set_menu(Some(menu)));
Ok(())
})());
})
.map_err(|e| anyhow::anyhow!("Failed to dispatch tray menu update: {e}"))?;
rx.await
.map_err(|_| anyhow::anyhow!("Tray menu update returned no result"))??;
let (menu, latency_items) = create_tray_menu(
app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
tun_mode_available,
profiles_preview,
TrayMenuOptions {
is_lightweight_mode,
include_proxy_groups,
},
)
.await?;
tray.set_menu(Some(menu))?;
let groups: Vec<_> = latency_items.keys().cloned().collect();
*self.latency_items.lock() = latency_items;
for group in groups {
self.refresh_latency_item(&group);
}
logging!(debug, Type::Tray, "托盘菜单更新成功");
Ok(())
}
async fn refresh_proxy_items(&'static self) {
let Ok(update) = self.menu_update.try_lock() else {
return;
};
if self.latency_items.lock().is_empty() || handle::Handle::global().is_exiting() {
return;
}
let Ok(Ok(view)) = tokio::time::timeout(
Duration::from_secs(1),
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
)
.await
else {
return;
};
if self
.latency_items
.lock()
.values()
.any(|entry| entry.provider_state != view.provider_state)
{
// Provider fallback changes member identities; rebuild through the existing queue.
drop(update);
logging_error!(Type::Tray, cmd::proxy::sync_tray_proxy_selection().await);
return;
}
let verge = Config::verge().await.latest_arc();
let profiles = Config::profiles().await.latest_arc();
logging_error!(
Type::Tray,
handle::Handle::app_handle().run_on_main_thread(move || {
// Keep rebuilding serialized until these handles have received their snapshot.
let _update = update;
let mut items = self.latency_items.lock();
for (group_name, entry) in items.iter_mut() {
let Some(group) = find_group(&view, group_name) else {
continue;
};
let url = latency::test_url(group, &verge, &profiles);
let mut members = HashMap::with_capacity(group.members.len());
for member in &group.members {
members
.entry((member_name(member), member_provider(&view, member)))
.or_insert(member);
}
for proxy in &mut entry.proxies {
let Some(member) = members.get(&(proxy.name.as_str(), proxy.provider_name.as_deref())) else {
continue;
};
let text = proxy_item_text(&view, member, &url, &verge);
let checked = group.now.as_ref() == Some(&proxy.name);
if text != proxy.text {
match proxy.item.set_text(&text) {
Ok(()) => proxy.text = text,
Err(err) => logging!(warn, Type::Tray, "Failed to refresh proxy latency: {err}"),
}
}
match proxy.item.is_checked() {
Ok(current) if current != checked => {
logging_error!(Type::Tray, proxy.item.set_checked(checked))
}
Err(err) => logging!(warn, Type::Tray, "Failed to read proxy selection: {err}"),
_ => {}
}
}
}
})
);
}
fn refresh_latency_item(&self, group_name: &str) {
let group_name = group_name.to_owned();
logging_error!(
Type::Tray,
handle::Handle::app_handle().run_on_main_thread(move || {
Self::global().refresh_latency_item_on_main_thread(&group_name);
})
);
}
fn refresh_latency_item_on_main_thread(&self, group_name: &str) {
let running = latency::is_running(group_name);
let item = {
let mut items = self.latency_items.lock();
items.get_mut(group_name).and_then(|entry| {
if entry.running == running {
return None;
}
entry.running = running;
Some(entry.item.clone())
})
};
if let Some(item) = item {
let label = if running {
clash_verge_i18n::t!("tray.testingLatency")
} else {
clash_verge_i18n::t!("tray.testLatency")
};
logging_error!(Type::Tray, item.set_text(label));
logging_error!(Type::Tray, item.set_enabled(!running));
}
}
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
@@ -503,96 +583,175 @@ fn create_profile_menu_item(
.collect()
}
const UNTESTED_DELAY_TEXT: &str = "-ms";
const TIMEOUT_DELAY_TEXT: &str = "T/O";
const TRAY_GROUP_DEPTH_LIMIT: usize = 8;
const fn member_name(member: &ProxyMemberRef) -> &str {
match member {
ProxyMemberRef::Node { name, .. }
| ProxyMemberRef::Group { name }
| ProxyMemberRef::Unresolved { name, .. } => name.as_str(),
}
}
fn find_group<'a>(view: &'a ProxyViewV1, name: &str) -> Option<&'a ProxyGroupView> {
view.groups
.iter()
.chain(view.global.iter())
.find(|group| group.name.as_str() == name)
}
/// The delay recorded for `url`, falling back to the node's latest measurement under any URL, so a
/// node the core tested on its own still shows a value.
fn node_delay(view: &ProxyViewV1, record_id: &str, url: &str) -> Option<u16> {
view.records
.get(record_id)
.map(|node| node.extra.get(url).map_or(&node.history, |extra| &extra.history))
.and_then(|history| history.last())
.map(|history| history.delay)
}
/// A group row shows the delay of the node the group currently selects. The core never writes a
/// history for the group itself, so `now` is followed until the chain reaches a node.
fn selected_member_delay(view: &ProxyViewV1, group_name: &str, url: &str) -> Option<u16> {
let mut current = group_name;
for _ in 0..TRAY_GROUP_DEPTH_LIMIT {
let Some(group) = find_group(view, current) else { break };
let Some(now) = group.now.as_deref().filter(|now| !now.is_empty() && *now != current) else {
break;
};
let Some(member) = group.members.iter().find(|member| member_name(member) == now) else {
break;
};
match member {
ProxyMemberRef::Node { record_id, .. } => return node_delay(view, record_id, url),
ProxyMemberRef::Group { name } => current = name.as_str(),
ProxyMemberRef::Unresolved { .. } => break,
}
}
find_group(view, group_name)
.map(|group| group.extra.get(url).map_or(&group.history, |extra| &extra.history))
.and_then(|history| history.last())
.map(|history| history.delay)
}
fn member_provider<'a>(view: &'a ProxyViewV1, member: &ProxyMemberRef) -> Option<&'a str> {
let ProxyMemberRef::Node { record_id, .. } = member else {
return None;
};
match &view.records.get(record_id)?.source {
ProxyNodeSource::Provider { provider_name, .. } => Some(provider_name),
ProxyNodeSource::Core { .. } => None,
}
}
fn proxy_item_text(view: &ProxyViewV1, member: &ProxyMemberRef, url: &str, verge: &IVerge) -> std::string::String {
let timeout = verge
.default_latency_timeout
.filter(|timeout| *timeout > 0)
.unwrap_or(10000);
let delay = match member {
ProxyMemberRef::Node { record_id, .. } => node_delay(view, record_id, url),
ProxyMemberRef::Group { name } => selected_member_delay(view, name, url),
ProxyMemberRef::Unresolved { .. } => None,
};
let delay_text = delay.map_or_else(
|| UNTESTED_DELAY_TEXT.to_owned(),
|delay| {
if delay == 0 || u32::from(delay) >= timeout as u32 {
TIMEOUT_DELAY_TEXT.to_owned()
} else {
format!("{delay}ms")
}
},
);
format!("{} | {delay_text}", member_name(member))
}
fn create_subcreate_proxy_menu_item(
app_handle: &AppHandle,
proxy_mode: &str,
proxy_group_order_map: Option<HashMap<String, usize>>,
proxy_nodes_data: Option<Proxies>,
view: Option<ProxyViewV1>,
latency_items: &mut HashMap<std::string::String, LatencyMenuItem>,
texts: &MenuTexts,
verge: &IVerge,
profiles: &crate::config::IProfiles,
) -> Vec<Submenu<Wry>> {
let proxy_submenus: Vec<Submenu<Wry>> = {
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
if let Some(proxy_nodes_data) = proxy_nodes_data {
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
let should_show = match proxy_mode {
"global" => group_name == "GLOBAL",
_ => group_name != "GLOBAL",
} && !group_data.hidden.unwrap_or_default();
if !should_show {
continue;
}
let Some(all_proxies) = group_data.all.as_ref() else {
continue;
};
let now_proxy = group_data.now.as_deref().unwrap_or_default();
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
.iter()
.filter_map(|proxy_str| {
let is_selected = *proxy_str == now_proxy;
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
let delay_text = proxy_nodes_data
.proxies
.get(proxy_str)
.and_then(|h| h.history.last())
.map(|h| match h.delay {
0 => "-ms".into(),
delay if delay >= 10000 => "-ms".into(),
_ => format!("{}ms", h.delay),
})
.unwrap_or_else(|| "-ms".into());
let display_text = format!("{} | {}", proxy_str, delay_text);
CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>)
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e))
.ok()
})
.collect();
if group_items.is_empty() {
continue;
}
let group_display_name = group_name.to_string();
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
if let Ok(submenu) = Submenu::with_id_and_items(
app_handle,
format!("proxy_group_{}", group_name),
group_display_name,
true,
&group_items_refs,
) {
let insertion_index = submenus.len();
submenus.push((group_name.into(), insertion_index, submenu));
} else {
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
}
let Some(view) = view else { return Vec::new() };
view.groups
.iter()
.chain(view.global.iter())
.filter_map(|group| {
if group.hidden.unwrap_or_default() || (proxy_mode == "global") != (group.name == "GLOBAL") {
return None;
}
}
if let Some(order_map) = proxy_group_order_map.as_ref() {
submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| {
match (order_map.get(name_a), order_map.get(name_b)) {
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => original_index_a.cmp(original_index_b),
}
});
}
submenus.into_iter().map(|(_, _, submenu)| submenu).collect()
};
proxy_submenus
let url = latency::test_url(group, verge, profiles);
let proxies: Vec<ProxyLatencyItem> = group
.members
.iter()
.filter_map(|member| {
let name = member_name(member);
let text = proxy_item_text(&view, member, &url, verge);
let checked = group.now.as_deref() == Some(name);
CheckMenuItem::with_id(
app_handle,
format!("proxy_{}_{}", group.name, name),
&text,
true,
checked,
None::<&str>,
)
.map(|item| ProxyLatencyItem {
item,
name: name.to_owned(),
provider_name: member_provider(&view, member).map(str::to_owned),
text,
})
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {e}"))
.ok()
})
.collect();
let group_items: Vec<_> = proxies.iter().map(|proxy| proxy.item.clone()).collect();
if group_items.is_empty() {
return None;
}
let running = latency::is_running(&group.name);
let test_item = MenuItem::with_id(
app_handle,
format!("{}_{}", MenuIds::TEST_LATENCY, group.name),
if running {
clash_verge_i18n::t!("tray.testingLatency")
} else {
texts.test_latency.clone()
},
!running,
None::<&str>,
)
.ok()?;
latency_items.insert(
group.name.clone(),
LatencyMenuItem {
item: test_item.clone(),
running,
proxies,
provider_state: view.provider_state,
},
);
let separator = PredefinedMenuItem::separator(app_handle).ok()?;
let mut items: Vec<&dyn IsMenuItem<Wry>> = vec![&test_item, &separator];
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>));
Submenu::with_id_and_items(
app_handle,
format!("proxy_group_{}", group.name),
&group.name,
true,
&items,
)
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy group submenu: {e}"))
.ok()
})
.collect()
}
fn create_proxy_menu_item(
@@ -631,16 +790,39 @@ fn create_proxy_menu_item(
Ok((proxies_submenu, inline_proxy_items))
}
fn build_tray_menu(
async fn create_tray_menu(
app_handle: &AppHandle,
options: TrayMenuOptions<'_>,
verge_settings: &IVerge,
profiles_config: &IProfiles,
proxy_group_order_map: Option<HashMap<String, usize>>,
proxy_nodes_data: Option<Proxies>,
) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = options.mode.unwrap_or("");
let tun_mode_available = crate::core::runstate::RUN_STATE.state().tun_capable();
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
tun_mode_available: bool,
profiles_preview: Vec<IProfilePreview<'_>>,
options: TrayMenuOptions,
) -> Result<(tauri::menu::Menu<Wry>, HashMap<std::string::String, LatencyMenuItem>)> {
let current_proxy_mode = mode.unwrap_or("");
let mut verge_settings = Config::verge().await.latest_arc();
let fetch_proxy_groups =
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
// Proxy data must remain optional so a stopped Core cannot roll back unrelated settings.
let proxy_view = if fetch_proxy_groups {
tokio::time::timeout(
Duration::from_millis(1000),
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
)
.await
.ok()
.and_then(Result::ok)
} else {
None
};
if fetch_proxy_groups {
verge_settings = Config::verge().await.latest_arc();
}
let profiles_config = Config::profiles().await.latest_arc();
let tray_proxy_groups_display_mode = verge_settings
.tray_proxy_groups_display_mode
@@ -654,8 +836,7 @@ fn build_tray_menu(
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
let profile_menu_items: Vec<CheckMenuItem<Wry>> =
create_profile_menu_item(app_handle, profiles_config.profiles_preview().unwrap_or_default())?;
let profile_menu_items: Vec<CheckMenuItem<Wry>> = create_profile_menu_item(app_handle, profiles_preview)?;
let texts = MenuTexts::new();
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
@@ -728,9 +909,17 @@ fn build_tray_menu(
&profile_menu_items_refs,
)?;
let mut latency_items = HashMap::new();
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
let proxy_sub_menus =
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
let proxy_sub_menus = create_subcreate_proxy_menu_item(
app_handle,
current_proxy_mode,
proxy_view,
&mut latency_items,
&texts,
&verge_settings,
&profiles_config,
);
match tray_proxy_groups_display_mode {
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
@@ -746,7 +935,7 @@ fn build_tray_menu(
MenuIds::SYSTEM_PROXY,
&texts.system_proxy,
true,
options.system_proxy_enabled,
system_proxy_enabled,
hotkeys.get("toggle_system_proxy").copied(),
)?;
@@ -755,7 +944,7 @@ fn build_tray_menu(
MenuIds::TUN_MODE,
&texts.tun_mode,
tun_mode_available,
options.tun_mode_enabled,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").copied(),
)?;
@@ -874,10 +1063,25 @@ fn build_tray_menu(
]);
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
Ok(menu)
Ok((menu, latency_items))
}
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
let refresh_proxies = match tray_event {
TrayIconEvent::Enter { .. } => Tray::global().proxy_hover_limiter.check(),
TrayIconEvent::Click {
button: MouseButton::Left | MouseButton::Right,
button_state: MouseButtonState::Down,
..
} => true,
_ => false,
};
if refresh_proxies {
AsyncHandler::spawn(|| async {
Tray::global().refresh_proxy_items().await;
});
}
if matches!(
tray_event,
TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. }
@@ -956,15 +1160,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
logging!(error, Type::Tray, "Failed to close all connections from tray: {err}");
}
}
MenuIds::COPY_ENV => {
if let Err(err) = feat::copy_clash_env().await {
logging!(
error,
Type::Tray,
"Failed to copy proxy environment variables from tray: {err:#}"
);
}
}
MenuIds::COPY_ENV => feat::copy_clash_env().await,
MenuIds::CONF_DIR => {
let _ = cmd::open_app_dir().await;
}
@@ -999,6 +1195,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
};
feat::toggle_proxy_profile(profile_index.into()).await;
}
id if id.starts_with(MenuIds::TEST_LATENCY) => {
let group_name = id
.strip_prefix(MenuIds::TEST_LATENCY)
.and_then(|rest| rest.strip_prefix('_'));
if let Some(group_name) = group_name {
logging!(info, Type::Tray, "Tray menu: test group latency {}", group_name);
latency::test_proxy_group_delay(group_name).await;
}
}
id if id.starts_with("proxy_") => {
let rest = match id.strip_prefix("proxy_") {
Some(r) => r,
+2 -2
View File
@@ -34,7 +34,7 @@ impl SilentUpdater {
}
}
#[derive(Serialize, Deserialize, specta::Type)]
#[derive(Serialize, Deserialize)]
struct UpdateCacheMeta {
version: String,
downloaded_at: String,
@@ -214,7 +214,7 @@ fn nsis_language_id(app_language: &str) -> &'static str {
}
/// Same wire shape as the updater plugin's JS `DownloadEvent`.
#[derive(Clone, Serialize, specta::Type)]
#[derive(Clone, Serialize)]
#[serde(tag = "event", content = "data")]
pub enum DownloadEvent {
#[serde(rename_all = "camelCase")]
+5 -11
View File
@@ -51,7 +51,7 @@ pub struct CoreConfigValidator {
is_processing: AtomicBool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum ValidationErrorKind {
FileMissing,
@@ -94,7 +94,7 @@ impl ValidationErrorKind {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum ValidationSkipReason {
Exiting,
@@ -110,18 +110,12 @@ impl fmt::Display for ValidationSkipReason {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ValidationOutcome {
Valid,
Invalid {
kind: ValidationErrorKind,
#[specta(type = std::string::String)]
message: String,
},
Skipped {
reason: ValidationSkipReason,
},
Invalid { kind: ValidationErrorKind, message: String },
Skipped { reason: ValidationSkipReason },
Busy,
}
+1 -4
View File
@@ -16,13 +16,10 @@ use smartstring::alias::String;
use std::path::PathBuf;
use tokio::fs;
#[derive(Debug, Serialize, specta::Type)]
#[derive(Debug, Serialize)]
pub struct LocalBackupFile {
#[specta(type = std::string::String)]
pub filename: String,
#[specta(type = std::string::String)]
pub path: String,
#[specta(type = std::string::String)]
pub last_modified: String,
pub content_length: u64,
}
+13 -3
View File
@@ -34,10 +34,12 @@ static STAGING_GENERATION: AtomicU64 = AtomicU64::new(0);
/// `.rollback` and `.old` are fixed paths, so two upgrades must not overlap.
static UPGRADE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
#[derive(Debug, serde::Serialize, specta::Type)]
#[derive(Debug, serde::Serialize)]
pub struct CoreUpgradeReport {
/// False when the managed core was already at the latest version.
pub upgraded: bool,
pub from: std::string::String,
pub to: std::string::String,
}
pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
@@ -60,7 +62,11 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
logging!(debug, Type::Core, "core upgrade: latest version resolved via {proxy:?}");
if !force && installed == latest {
return Ok(CoreUpgradeReport { upgraded: false });
return Ok(CoreUpgradeReport {
upgraded: false,
from: installed,
to: latest,
});
}
let package = download_package(proxy, alpha, &latest).await?;
@@ -144,7 +150,11 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
#[cfg(windows)]
let _ = std::fs::remove_file(target.with_extension("old"));
Ok(CoreUpgradeReport { upgraded: true })
Ok(CoreUpgradeReport {
upgraded: true,
from: installed,
to: latest,
})
}
/// Whether this upgrade handed bytes to the Service's approved core directory.
+2 -5
View File
@@ -15,14 +15,11 @@ use crate::{
utils::dirs,
};
#[derive(Serialize, specta::Type)]
#[derive(Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DnsOverrideOutcome {
Applied,
ConfirmationRequired {
#[specta(type = std::string::String)]
source: String,
},
ConfirmationRequired { source: String },
}
pub async fn set_dns_override(
+1 -4
View File
@@ -8,13 +8,10 @@ use std::path::{Component, Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct IconInfo {
#[specta(type = std::string::String)]
name: String,
#[specta(type = std::string::String)]
previous_t: String,
#[specta(type = std::string::String)]
current_t: String,
}
+6 -6
View File
@@ -11,7 +11,6 @@ use crate::{
window_manager::WindowManager,
},
};
use anyhow::Context as _;
use clash_verge_logging::{Type, logging};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt as _;
@@ -96,7 +95,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) -> bool {
}
}
pub async fn copy_clash_env() -> anyhow::Result<()> {
pub async fn copy_clash_env() {
let env_ip = env::var("CLASH_VERGE_REV_IP").ok();
let verge_cfg = Config::verge().await.latest_arc();
let ip = env_ip
@@ -134,11 +133,12 @@ pub async fn copy_clash_env() -> anyhow::Result<()> {
}
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
_ => {
anyhow::bail!("Invalid environment variable format: {env_type}");
logging!(error, Type::ProxyMode, "copy_clash_env: Invalid env type! {env_type}");
return;
}
};
clipboard
.write_text(export_text)
.context("Failed to write proxy environment variables to clipboard")
if let Err(err) = clipboard.write_text(&export_text) {
logging!(error, Type::ProxyMode, "Failed to write to clipboard: {err}");
}
}
+3 -1
View File
@@ -130,7 +130,7 @@ pub async fn open_or_close_dashboard() {
logging!(info, Type::Window, "Window toggle result: {result:?}");
}
pub async fn quit() {
pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
logging!(debug, Type::System, "启动退出流程");
// 设置退出标志
handle::Handle::global().set_is_exiting();
@@ -165,11 +165,13 @@ pub async fn quit() {
NoticeStatus::AppQuitCoreStopFailed,
cleanup_result.stop_error.unwrap_or_default(),
);
return clash_verge_signal::ShutdownOutcome::Canceled;
}
utils::server::shutdown_embedded_server();
let app_handle = handle::Handle::app_handle();
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
clash_verge_signal::ShutdownOutcome::Committed
}
pub async fn clean_async() -> CleanupResult {
-234
View File
@@ -1,234 +0,0 @@
use crate::cmd;
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
tauri_specta::Builder::new()
.types(&platform_types())
.commands(tauri_specta::collect_commands![
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info::<tauri::Wry>,
cmd::probe_listener,
cmd::save_proxy_ports,
cmd::get_sys_proxy,
cmd::get_auto_proxy,
cmd::get_embedded_server_port,
cmd::open_app_dir,
cmd::open_logs_dir,
cmd::open_core_dir,
cmd::get_network_interfaces,
cmd::get_system_hostname,
cmd::restart_app,
cmd::install_update,
cmd::cancel_update_download,
cmd::restart_core,
cmd::upgrade_clash_core,
cmd::get_runtime_state,
cmd::get_pending_failures,
cmd::get_sidecar_failure,
cmd::entry_lightweight_mode,
cmd::install_service,
cmd::uninstall_service,
cmd::reinstall_service,
cmd::repair_service,
cmd::continue_with_sidecar,
cmd::sync_runtime_providers,
cmd::get_clash_info,
cmd::patch_clash_config,
cmd::patch_clash_mode,
cmd::get_clash_mode,
cmd::change_clash_core,
cmd::get_runtime_config,
cmd::get_proxy_view,
cmd::get_runtime_yaml,
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node,
cmd::forget_selected_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::set_dns_override,
cmd::take_dns_override_notice,
cmd::take_service_fallback_notice,
cmd::get_core_startup_error,
cmd::take_service_repair_notice,
cmd::take_service_owner_notice,
cmd::take_discarded_keys_notice,
cmd::get_dns_config_content,
cmd::validate_dns_config,
cmd::get_clash_logs,
cmd::get_verge_config,
cmd::patch_verge_config,
cmd::test_delay,
cmd::get_app_dir,
cmd::copy_icon_file,
cmd::download_icon_cache,
cmd::open_devtools,
cmd::exit_app,
cmd::get_network_interfaces_info,
cmd::get_profiles,
cmd::enhance_profiles,
cmd::patch_profiles_config,
cmd::view_profile,
cmd::patch_profile,
cmd::create_profile,
cmd::import_profile,
cmd::reorder_profile,
cmd::update_profile,
cmd::delete_profile,
cmd::read_profile_file,
cmd::save_profile_file,
cmd::get_next_update_time,
cmd::create_local_backup,
cmd::list_local_backup,
cmd::delete_local_backup,
cmd::restore_local_backup,
cmd::import_local_backup,
cmd::export_local_backup,
cmd::create_webdav_backup,
cmd::save_webdav_config,
cmd::list_webdav_backup,
cmd::delete_webdav_backup,
cmd::restore_webdav_backup,
cmd::get_unlock_items,
cmd::check_media_unlock,
cmd::check_media_unlock_item,
])
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
.dangerously_cast_bigints_to_number()
.typ::<crate::core::notify::notification::VergeEventPayloads>()
}
// Tauri transports YAML values as JSON; keep YAML storage out of the IPC type graph.
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(transparent)]
pub struct YamlMapping(#[specta(type = std::collections::HashMap<String, JsonValue>)] pub serde_yaml_ng::Mapping);
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(transparent)]
pub struct YamlValue(#[specta(type = JsonValue)] pub serde_yaml_ng::Value);
#[derive(serde::Serialize, specta::Type)]
pub struct WebDavFile {
pub href: String,
#[specta(type = String)]
pub last_modified: chrono::DateTime<chrono::Utc>,
}
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
Self {
href: file.href,
last_modified: file.last_modified,
}
}
}
#[derive(serde::Serialize, specta::Type)]
pub struct NetworkInterfaceView {
pub name: String,
pub addr: Vec<InterfaceAddress>,
pub mac_addr: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
pub enum InterfaceAddress {
V4(IpAddress),
V6(IpAddress),
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
pub struct IpAddress {
pub ip: String,
}
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
fn from(interface: network_interface::NetworkInterface) -> Self {
Self {
name: interface.name,
addr: interface
.addr
.into_iter()
.map(|address| match address {
network_interface::Addr::V4(address) => InterfaceAddress::V4(IpAddress {
ip: address.ip.to_string(),
}),
network_interface::Addr::V6(address) => InterfaceAddress::V6(IpAddress {
ip: address.ip.to_string(),
}),
})
.collect(),
mac_addr: interface.mac_addr,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
pub struct DelayHistory {
pub time: String,
pub delay: u16,
}
fn platform_types() -> specta::Types {
use specta::datatype::{DataType, Fields};
let mut types = specta::Types::default().register::<crate::config::IVerge>();
<PlatformVergeFields as specta::Type>::definition(&mut types);
let platform = types
.into_unsorted_iter()
.find(|ty| ty.name == "PlatformVergeFields")
.and_then(|ty| ty.ty.clone());
let Some(DataType::Struct(platform)) = platform else {
unreachable!()
};
let Fields::Named(platform) = platform.fields else {
unreachable!()
};
types.iter_mut(|ty| {
if ty.name != "IVerge" {
return;
}
let Some(DataType::Struct(config)) = &mut ty.ty else {
return;
};
let Fields::Named(fields) = &mut config.fields else {
return;
};
fields
.fields
.retain(|(name, _)| !platform.fields.iter().any(|(platform_name, _)| name == platform_name));
fields.fields.extend(platform.fields.iter().cloned());
});
types
}
// These fields exist only on some hosts; IPC generation covers every desktop target.
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
struct PlatformVergeFields {
#[specta(optional)]
tray_icon: Option<String>,
#[specta(optional)]
verge_redir_port: Option<u16>,
#[specta(optional)]
verge_redir_enabled: Option<bool>,
#[specta(optional)]
verge_tproxy_port: Option<u16>,
#[specta(optional)]
verge_tproxy_enabled: Option<bool>,
#[specta(optional)]
enable_tray_speed: Option<bool>,
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(untagged)]
pub enum JsonValue {
Null(()),
Bool(bool),
Number(f64),
String(String),
// Specta resolves fields in nested functions, where Self is unavailable.
Array(#[specta(type = Vec<JsonValue>)] Vec<Self>),
Object(#[specta(type = std::collections::HashMap<String, JsonValue>)] std::collections::HashMap<String, Self>),
}
+97 -6
View File
@@ -7,7 +7,6 @@ mod constants;
mod core;
mod enhance;
mod feat;
pub mod ipc;
mod process;
pub mod utils;
@@ -27,6 +26,9 @@ use tauri_plugin_deep_link::DeepLinkExt as _;
pub static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
// Re-exported for src/bin/frontend-contract.rs; the wire contract stays owned
// by core::notify.
pub use crate::core::notify::frontend_wire_contract;
/// Application initialization helper functions
mod app_init {
use super::*;
@@ -125,10 +127,100 @@ mod app_init {
}
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
{
let builder = crate::ipc::builder();
move |invoke| builder.invoke_handler()(invoke)
}
tauri::generate_handler![
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info,
cmd::probe_listener,
cmd::save_proxy_ports,
cmd::get_sys_proxy,
cmd::get_auto_proxy,
cmd::get_embedded_server_port,
cmd::open_app_dir,
cmd::open_logs_dir,
cmd::open_core_dir,
cmd::get_network_interfaces,
cmd::get_system_hostname,
cmd::restart_app,
cmd::install_update,
cmd::cancel_update_download,
cmd::restart_core,
cmd::upgrade_clash_core,
cmd::get_runtime_state,
cmd::get_pending_failures,
cmd::get_sidecar_failure,
cmd::entry_lightweight_mode,
cmd::install_service,
cmd::uninstall_service,
cmd::reinstall_service,
cmd::repair_service,
cmd::continue_with_sidecar,
cmd::sync_runtime_providers,
cmd::get_clash_info,
cmd::patch_clash_config,
cmd::patch_clash_mode,
cmd::get_clash_mode,
cmd::change_clash_core,
cmd::get_runtime_config,
cmd::get_proxy_view,
cmd::get_runtime_yaml,
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node,
cmd::forget_selected_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::set_dns_override,
cmd::take_dns_override_notice,
cmd::take_service_fallback_notice,
cmd::get_core_startup_error,
cmd::take_service_repair_notice,
cmd::take_service_owner_notice,
cmd::take_discarded_keys_notice,
cmd::get_dns_config_content,
cmd::validate_dns_config,
cmd::get_clash_logs,
cmd::get_verge_config,
cmd::patch_verge_config,
cmd::test_delay,
cmd::get_app_dir,
cmd::copy_icon_file,
cmd::download_icon_cache,
cmd::open_devtools,
cmd::exit_app,
cmd::get_network_interfaces_info,
cmd::get_profiles,
cmd::enhance_profiles,
cmd::patch_profiles_config,
cmd::view_profile,
cmd::patch_profile,
cmd::create_profile,
cmd::import_profile,
cmd::reorder_profile,
cmd::update_profile,
cmd::delete_profile,
cmd::read_profile_file,
cmd::save_profile_file,
cmd::get_next_update_time,
cmd::create_local_backup,
cmd::list_local_backup,
cmd::delete_local_backup,
cmd::restore_local_backup,
cmd::import_local_backup,
cmd::export_local_backup,
cmd::create_webdav_backup,
cmd::save_webdav_config,
cmd::list_webdav_backup,
cmd::delete_webdav_backup,
cmd::restore_webdav_backup,
cmd::get_unlock_items,
cmd::check_media_unlock,
cmd::check_media_unlock_item,
]
}
}
@@ -230,7 +322,6 @@ pub fn run() -> std::process::ExitCode {
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
resolve::resolve_setup_async();
resolve::resolve_setup_sync();
#[cfg(target_os = "linux")]
resolve::init_signal();
})) {
log_setup_panic("window-core", panic);
+1 -1
View File
@@ -16,7 +16,7 @@ fn main() -> ExitCode {
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
fn main() -> ExitCode {
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
let worker_limit = std::cmp::min(default_parallelism, 4);
let worker_limit = std::cmp::min(default_parallelism, 8);
let blocking_limit = 2 * worker_limit;
#[allow(clippy::unwrap_used)]
+1 -2
View File
@@ -20,6 +20,7 @@ use crate::{
utils::{init, server, window_manager::WindowManager},
};
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_signal;
#[cfg(target_os = "macos")]
pub mod dns;
@@ -188,8 +189,6 @@ async fn init_silent_updater() {
logging!(info, Type::Setup, "Silent updater initialized");
}
// Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
#[cfg(target_os = "linux")]
pub(crate) fn init_signal() {
logging!(debug, Type::Setup, "Initializing signal handlers...");
clash_verge_signal::register(feat::quit);
+52 -1
View File
@@ -39,6 +39,8 @@ static INSTANCE_LOCK: OnceLock<std::fs::File> = OnceLock::new();
const PAC_INITIAL_AVAILABLE: bool = false;
static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE);
static COMMANDS_READY: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "verge-dev")]
static DEV_QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SingletonDisposition {
@@ -162,12 +164,55 @@ fn instance_auth(token: String) -> impl warp::Filter<Extract = (), Error = warp:
.untuple_one()
}
#[cfg(feature = "verge-dev")]
fn dev_quit_route<F>(
token: String,
commands_ready: &'static AtomicBool,
quit_requested: &'static AtomicBool,
trigger: F,
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone
where
F: Fn() + Clone + Send + Sync + 'static,
{
instance_auth(token)
.and(warp::path!("commands" / "dev" / "quit"))
.and(warp::post())
.and_then(move || {
let trigger = trigger.clone();
async move {
if !commands_ready.load(Ordering::Acquire) {
return Ok::<_, warp::Rejection>(warp::reply::with_status(
String::from("starting"),
warp::http::StatusCode::SERVICE_UNAVAILABLE,
));
}
if quit_requested
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
trigger();
}
Ok(warp::reply::with_status(
String::from("accepted"),
warp::http::StatusCode::ACCEPTED,
))
}
})
}
#[cfg(feature = "verge-dev")]
fn release_dev_quit_latch(quit_requested: &AtomicBool, outcome: clash_verge_signal::ShutdownOutcome) {
if matches!(outcome, clash_verge_signal::ShutdownOutcome::Canceled) {
quit_requested.store(false, Ordering::Release);
}
}
fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx)));
#[cfg(feature = "verge-dev")]
let auth = instance_auth(token);
let auth = instance_auth(token.clone());
#[cfg(not(feature = "verge-dev"))]
let auth = instance_auth(token);
@@ -233,6 +278,12 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
});
let commands = auth.clone().and(visible).or(auth.and(scheme)).or(pac);
#[cfg(feature = "verge-dev")]
let commands = commands.or(dev_quit_route(token, &COMMANDS_READY, &DEV_QUIT_REQUESTED, || {
AsyncHandler::spawn(|| async {
release_dev_quit_latch(&DEV_QUIT_REQUESTED, crate::feat::quit().await);
});
}));
AsyncHandler::spawn(move || async move {
warp::serve(commands)
.incoming(listener)
+11 -33
View File
@@ -27,12 +27,8 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0;
const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0;
const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0;
/// (button_height, mono_advance, attributes).
type CachedAttrs = (f64, f64, Retained<NSDictionary<NSString, AnyObject>>);
thread_local! {
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
static CACHED_ATTRS: RefCell<Option<CachedAttrs>> = const { RefCell::new(None) };
}
fn format_tray_speed(up: u64, down: u64) -> String {
@@ -78,26 +74,8 @@ fn create_attributed_string(
}
}
/// Attributes and per-glyph advance, rebuilt only when the button height changes.
fn cached_attributes(button_height: f64) -> (f64, Retained<NSDictionary<NSString, AnyObject>>) {
CACHED_ATTRS.with(|cache| {
if let Some((height, advance, attrs)) = cache.borrow().as_ref()
&& *height == button_height
{
return (*advance, attrs.clone());
}
let attrs = build_attributes(button_height);
let probe = NSString::from_str("0");
let advance = create_attributed_string(&probe, Some(&attrs)).size().width;
*cache.borrow_mut() = Some((button_height, advance, attrs.clone()));
(advance, attrs)
})
}
/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text.
fn arithmetic_width(text: &str, advance: f64) -> f64 {
let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64;
glyphs * advance
fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 {
(attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH)
}
fn sync_click_target_frame(button: &NSStatusBarButton) {
@@ -111,7 +89,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) {
}
/// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not.
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) {
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) {
let Some(mtm) = MainThreadMarker::new() else {
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
return;
@@ -119,19 +97,17 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, sh
let Some(button) = status_item.button(mtm) else {
return;
};
let ns_string = NSString::from_str(text);
let attr_str = if show_speed {
let (advance, attrs) = cached_attributes(button.bounds().size.height);
let attrs = build_attributes(button.bounds().size.height);
let attrs: &NSDictionary<NSString, AnyObject> = &attrs;
let attr_str = create_attributed_string(&ns_string, Some(attrs));
let width = arithmetic_width(text, advance);
let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH);
let attr_str = create_attributed_string(text, Some(attrs));
let length = status_item_length_for_speed(&attr_str);
if status_item.length() != length {
status_item.setLength(length);
}
attr_str
} else {
create_attributed_string(&ns_string, None)
create_attributed_string(text, None)
};
button.setAttributedTitle(&attr_str);
sync_click_target_frame(&button);
@@ -145,7 +121,8 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64
return;
}
*last = speed_text;
apply_status_item_attributed_title(status_item, last.as_str(), true);
let ns_string = NSString::from_str(last.as_str());
apply_status_item_attributed_title(status_item, &ns_string, true);
});
}
@@ -153,6 +130,7 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) {
LAST_DISPLAY_STR.with(|last| {
last.borrow_mut().clear();
});
let empty = NSString::from_str("");
status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH);
apply_status_item_attributed_title(status_item, "", false);
apply_status_item_attributed_title(status_item, &empty, false);
}
+27 -4
View File
@@ -47,7 +47,10 @@ import {
useCoreDataStatus,
useProxiesData,
} from '@/providers/app-data-context'
import { syncTrayProxySelection } from '@/services/cmds'
import delayManager from '@/services/delay'
import { mutate } from '@/services/mutate'
import { profileTestUrls } from '@/services/proxy-test-url'
import {
findCurrentGroupMember,
getRecord,
@@ -420,7 +423,7 @@ interface PersistentProxySelectProps
value: string
selectedName: string
groupName: string
fixed?: string | null
fixed?: string
selectedDelay: number
options: ProxyOption[]
onChange: (value: string) => void
@@ -675,6 +678,20 @@ export const CurrentProxyCard = () => {
[proxyView],
)
useEffect(() => {
if (!selectedGroup) return
const customUrl = currentProfile
? profileTestUrls(currentProfile)[selectedGroup.name]
: undefined
delayManager.setUrl(
selectedGroup.name,
customUrl?.trim() ||
selectedGroup.testUrl?.trim() ||
verge?.default_latency_test?.trim() ||
'http://cp.cloudflare.com/generate_204',
)
}, [selectedGroup, currentProfile, verge?.default_latency_test])
const unsortedProxyOptions = useMemo(
() => optionsForGroup(selectedGroup),
[optionsForGroup, selectedGroup],
@@ -768,8 +785,8 @@ export const CurrentProxyCard = () => {
// The profile selection is the durable source across core restarts and run modes.
handleSelectChange(
selectedGroup.name,
previousProxy!,
selectedGroup.fixed!,
previousProxy,
selectedGroup.fixed,
)({
target: { value: nextName },
})
@@ -907,6 +924,12 @@ export const CurrentProxyCard = () => {
}
refreshProxy()
mutate(() => syncTrayProxySelection(), {
id: 'sync-tray-selection',
errorNotice: false,
}).catch((error) => {
console.error('[CurrentProxyCard] Failed to sync tray latency:', error)
})
})
const proxyOptions = useMemo(
@@ -917,7 +940,7 @@ export const CurrentProxyCard = () => {
unsortedProxyOptions,
sortType,
delays.of,
defaultLatencyTimeout!,
defaultLatencyTimeout,
),
[
delays,
+25 -9
View File
@@ -56,7 +56,26 @@ const openProfileHome = (url: string) => {
void openExternalUrl(url).catch(showNotice.error)
}
type ProfileItem = IProfileItem
// 使用类型定义,而不是导入
interface ProfileExtra {
upload: number
download: number
total: number
expire: number
}
interface ProfileItem {
uid: string
type?: 'local' | 'remote' | 'merge' | 'script'
name?: string
desc?: string
file?: string
url?: string
updated?: number
extra?: ProfileExtra
home?: string
option?: any
}
interface HomeProfileCardProps {
current: ProfileItem | null | undefined
@@ -279,13 +298,10 @@ export const HomeProfileCard = ({
setUpdating(true)
try {
await mutate(
() => updateProfile(current.uid!, current.option ?? undefined),
{
id: `update-profile:${current.uid}`,
errorNotice: false,
},
)
await mutate(() => updateProfile(current.uid, current.option), {
id: `update-profile:${current.uid}`,
errorNotice: false,
})
onProfileUpdated?.()
// 刷新首页数据
@@ -329,7 +345,7 @@ export const HomeProfileCard = ({
flex: 1,
},
}}
title={current.name!}
title={current.name}
>
<span>{current.name}</span>
<LaunchOutlined
+1 -1
View File
@@ -201,7 +201,7 @@ export const ProxyTunCard: FC = () => {
onClick={() => handleTabChange('tun')}
icon={TroubleshootRounded}
label={t('settings.sections.system.toggles.tunMode')}
hasIndicator={enable_tun_mode! && isTunModeAvailable}
hasIndicator={enable_tun_mode && isTunModeAvailable}
/>
</Stack>
+1 -1
View File
@@ -164,7 +164,7 @@ export const TestCard = () => {
}}
>
{testList.map((item, index) => (
<SortableItem key={item.uid} id={item.uid!} index={index}>
<SortableItem key={item.uid} id={item.uid} index={index}>
<TestItem
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
+18 -18
View File
@@ -120,7 +120,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
)
const setLoading = useCallback(
(loading: boolean) => {
setProfileLoading([itemData.uid!], loading)
setProfileLoading([itemData.uid], loading)
},
[itemData.uid, setProfileLoading],
)
@@ -140,7 +140,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
debugLog(`强制刷新定时器任务`)
}
const nextUpdate = await getNextUpdateTime(itemData.uid!)
const nextUpdate = await getNextUpdateTime(itemData.uid)
debugLog(`获取到下次更新时间结果:`, nextUpdate)
if (nextUpdate) {
@@ -245,7 +245,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const hasHome = !!itemData.home // only subscription url has home page
const { upload = 0, download = 0, total = 0 } = extra ?? {}
const from = parseUrl(itemData.url!)
const from = parseUrl(itemData.url)
const description = itemData.desc
const expire = parseExpire(extra?.expire)
const progress = Math.min(
@@ -253,7 +253,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
100,
)
const loading = loadingCache.has(itemData.uid!)
const loading = loadingCache.has(itemData.uid)
const [, forceRefresh] = useReducer((value: number) => value + 1, 0)
useEffect(() => {
@@ -263,7 +263,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const handler = () => {
const now = Date.now()
const lastUpdate = updated! * 1000
const lastUpdate = updated * 1000
if (now - lastUpdate >= 24 * 36e5) return
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4
@@ -293,7 +293,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const [confirmOpen, setConfirmOpen] = useState(false)
const [qrOpen, setQrOpen] = useState(false)
const loadProfileDocument = useCallback(() => readProfileFile(uid!), [uid])
const loadProfileDocument = useCallback(() => readProfileFile(uid), [uid])
const loadMergeDocument = useCallback(
() => readProfileFile(option?.merge ?? ''),
[option?.merge],
@@ -370,7 +370,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const onOpenFile = useLockFn(async () => {
setAnchorEl(null)
try {
await mutate(() => viewProfile(itemData.uid!), {
await mutate(() => viewProfile(itemData.uid), {
id: 'view-profile',
errorNotice: false,
})
@@ -402,7 +402,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
try {
const payload = Object.keys(option).length > 0 ? option : undefined
await mutate(() => updateProfile(itemData.uid!, payload), {
await mutate(() => updateProfile(itemData.uid, payload), {
id: `update-profile:${itemData.uid}`,
errorNotice: false,
})
@@ -593,7 +593,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const handleSaveProfileDocument = useLockFn(async () => {
const currentValue = profileDocument.value
const result = await mutate(() => saveProfileFile(uid!, currentValue), {
const result = await mutate(() => saveProfileFile(uid, currentValue), {
id: `save-profile-file:${uid}`,
errorNotice: false,
})
@@ -728,7 +728,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
variant="h6"
component="h2"
noWrap
title={name!}
title={name}
>
{name}
</Typography>
@@ -794,7 +794,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
title={
showNextUpdate
? t('profiles.components.profileItem.tooltips.showLast')
: `${t('shared.labels.updateTime')}: ${parseExpire(updated!)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
: `${t('shared.labels.updateTime')}: ${parseExpire(updated)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
}
sx={{
fontSize: 14,
@@ -812,8 +812,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
>
{showNextUpdate
? nextUpdateTime
: updated! > 0
? dayjs(updated! * 1000).fromNow()
: updated > 0
? dayjs(updated * 1000).fromNow()
: ''}
</Typography>
</Box>
@@ -831,7 +831,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
) : (
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: 'flex-end' }}>
<span title={t('shared.labels.updateTime')}>
{parseExpire(updated!)}
{parseExpire(updated)}
</span>
</Box>
)}
@@ -896,7 +896,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
<RulesEditorViewer
groupsUid={option?.groups ?? ''}
mergeUid={option?.merge ?? ''}
profileUid={uid!}
profileUid={uid}
property={option?.rules ?? ''}
open={true}
onSave={onSave}
@@ -905,7 +905,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
)}
{proxiesOpen && (
<ProxiesEditorViewer
profileUid={uid!}
profileUid={uid}
property={option?.proxies ?? ''}
open={true}
onSave={onSave}
@@ -916,7 +916,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
<GroupsEditorViewer
mergeUid={option?.merge ?? ''}
proxiesUid={option?.proxies ?? ''}
profileUid={uid!}
profileUid={uid}
property={option?.groups ?? ''}
open={true}
onSave={onSave}
@@ -976,7 +976,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
{qrOpen && itemData.url && (
<QrViewer
open={true}
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name!)}`}
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name)}`}
onClose={() => setQrOpen(false)}
/>
)}

Some files were not shown because too many files have changed in this diff Show More