Compare commits

..
Author SHA1 Message Date
renovate[bot] b284eda1a6 chore(deps): update tauri 2026-10-26 17:10:27 +00:00
Ekko82 eefd025806 feat(home): add an IP auto-refresh toggle (#8403) 2026-10-26 02:04:55 +00:00
71 changed files with 615 additions and 587 deletions
Generated
+13 -9
View File
@@ -1139,6 +1139,7 @@ dependencies = [
"serde_json",
"serde_yaml_ng",
"sha2 0.11.1",
"smartstring",
"socket2",
"specta",
"specta-typescript",
@@ -1168,7 +1169,6 @@ dependencies = [
"tokio-stream",
"tokio-util",
"tracing",
"tristr",
"warp",
"webpki-roots",
"windows 0.62.2",
@@ -6720,6 +6720,18 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"serde",
"static_assertions",
"version_check",
]
[[package]]
name = "socket2"
version = "0.6.5"
@@ -8493,14 +8505,6 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "tristr"
version = "0.1.0"
dependencies = [
"serde",
"specta",
]
[[package]]
name = "try-lock"
version = "0.2.5"
+3 -2
View File
@@ -61,7 +61,7 @@ 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.4" }
tauri = { version = "2.12.5" }
tauri-plugin-clipboard-manager = "2.5.0"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
@@ -85,8 +85,9 @@ reqwest = { version = "0.13.5", features = [
] }
rust_iso3166 = "0.2.0"
smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" }
tristr = { path = "../tristr", default-features = false, features = ["serde", "specta"] }
serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
+1
View File
@@ -18,6 +18,7 @@
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
</details>
+2 -2
View File
@@ -95,7 +95,7 @@
"@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.3",
"@tauri-apps/cli": "2.12.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.13.6",
"@types/react": "19.3.0",
@@ -103,7 +103,7 @@
"@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.5.0",
"@wdio/tauri-service": "1.5.1",
"adm-zip": "^0.6.1",
"axios": "^1.20.0",
"cli-color": "^2.0.4",
+75 -75
View File
@@ -318,8 +318,8 @@ importers:
specifier: ^10.0.1
version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
'@tauri-apps/cli':
specifier: 2.12.3
version: 2.12.3
specifier: 2.12.4
version: 2.12.4
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -342,8 +342,8 @@ importers:
specifier: ^6.1.1
version: 6.1.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
'@wdio/tauri-service':
specifier: 1.5.0
version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
specifier: 1.5.1
version: 1.5.1(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
adm-zip:
specifier: ^0.6.1
version: 0.6.1
@@ -2124,79 +2124,79 @@ packages:
'@tauri-apps/api@2.12.4':
resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==}
'@tauri-apps/cli-darwin-arm64@2.12.3':
resolution: {integrity: sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==}
'@tauri-apps/cli-darwin-arm64@2.12.4':
resolution: {integrity: sha512-OxqKSckIkoJ6vX2AjifIJpTasFC0Hbdoo7gEiCmAjauABO3U8m2MRsLWe+QTiBwDx1mIrvgA+rvI7tvDnbnHVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.12.3':
resolution: {integrity: sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==}
'@tauri-apps/cli-darwin-x64@2.12.4':
resolution: {integrity: sha512-fz76/77wMMJOaaUN6n3CybRG0P/MTEjjqYk81mGPyhi9m6Ue5T+LWBnJP/AkPooNatBPI2vtulGqomyMJ44lCQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.3':
resolution: {integrity: sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.4':
resolution: {integrity: sha512-K0w8Bof6Lt8QKLAYhRy0yy36ZH5iA/35VuSOvnxyjvCUZ+A3JKsUTlALviH/sVgAlisaPzzmeJSH8XOn4Q2HBA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.12.3':
resolution: {integrity: sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==}
'@tauri-apps/cli-linux-arm64-gnu@2.12.4':
resolution: {integrity: sha512-j5rOnjmRUwWrm1oosf0zBYl4D+fqydgUROFCk0hvKZxvbTT8SOERes0QB7rBV/XWNy+kM6pCKzb3oSm2sRglzg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.12.3':
resolution: {integrity: sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==}
'@tauri-apps/cli-linux-arm64-musl@2.12.4':
resolution: {integrity: sha512-aOue3Fy6Jb7+oWcfAVUsVHfeTybSCt2CZoU06aO4jv4d2Ks/KnaTPwMAK3CLEduGOvBLY1lbBBrIyomqLLH5Sg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.12.3':
resolution: {integrity: sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==}
'@tauri-apps/cli-linux-riscv64-gnu@2.12.4':
resolution: {integrity: sha512-Fx6GShY/RvST+t6LMlTHAwd66+1tEaAAGrnU7cysVfaiRP1BKng5Vl6rVmO6rSLOA1Sp1dRzNYM4f0OWQGb7qA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.12.3':
resolution: {integrity: sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==}
'@tauri-apps/cli-linux-x64-gnu@2.12.4':
resolution: {integrity: sha512-tNzZSa79Z1nRpHxEpQ38lsAFpKlFlYvoXzjTV+lRRn/zSf3wrdUC3wnLcq2XEadchPu4ttK0zivhYzr+XGD19Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.12.3':
resolution: {integrity: sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==}
'@tauri-apps/cli-linux-x64-musl@2.12.4':
resolution: {integrity: sha512-jmZ1pr0xEqUhuEusWtt9Yt+GF6lCnGHUmIL+KIknpbjK/XQ2qkhDBhaxK/JMHM2A8dAacVXCxXJk2EXpwhQUfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.12.3':
resolution: {integrity: sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==}
'@tauri-apps/cli-win32-arm64-msvc@2.12.4':
resolution: {integrity: sha512-fGo8pgEHZxdSYgZ/J98m1usJ7NHKmKbq7k6uT0x1rpOq6Bg2YVzzpaAblWk710f5LHYBJIL5QIFS5IrD+4gmTg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.12.3':
resolution: {integrity: sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==}
'@tauri-apps/cli-win32-ia32-msvc@2.12.4':
resolution: {integrity: sha512-ezl8sQIcjYsye3TUnTH5VcWmWzccZASlQm7hlZt0dYyj1XD0qpVW47u2xYVrIKui0F8uQv/7QGZixdWiClBsbg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.12.3':
resolution: {integrity: sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==}
'@tauri-apps/cli-win32-x64-msvc@2.12.4':
resolution: {integrity: sha512-DJhDeCqjLDNMWYFy7ZS8cfeAHX2RK0RbQMLiA3bMVVkXRDJ4NBYiHriGqsUhsr9iG41aTb+dJGkFPmPw1gWwVw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.12.3':
resolution: {integrity: sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==}
'@tauri-apps/cli@2.12.4':
resolution: {integrity: sha512-JI9bHfudSRJikGOrjbXEm6RjzzvmKuLc+JixB1NE8hNzQkNlCCIC3pTES3uypkR9lxHrsbxzcnSwhI2IxBQphw==}
engines: {node: '>= 10'}
hasBin: true
@@ -2574,20 +2574,20 @@ packages:
resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==}
engines: {node: '>=18.20.0'}
'@wdio/native-core@1.3.0':
resolution: {integrity: sha512-wUwc0QTdvhD0LwH4YcRaN9JWLX/yac8sovddUR5/4sqHMNJr3yaN11bpIfYoD/bEk+1nTLjWuDCFpe5l5wUgLQ==}
'@wdio/native-core@1.3.1':
resolution: {integrity: sha512-fdGk2lEr2vS+bClJXuZEnsG30cLzQv+B9W+dAt/Yte/fE33s/vDBn7hHwFx8aZLTOS0pPExErUxYiZEMXX+04g==}
engines: {node: '>=18.20.0'}
'@wdio/native-spy@1.4.0':
resolution: {integrity: sha512-unsC2Vgu5T9atD478Zj/p2N+rVSK4i/NibIaaEw4rZF/T5u5bmAHChFntEiBEfyMzd62VwbhKhzwwOSKjzrfJA==}
'@wdio/native-spy@1.4.1':
resolution: {integrity: sha512-KGOmgaicHqDkqj+s2bxXsRsb27IzW6carlyTu1xcrJI34yUbC0v14F8mbhuZcewW+mt2xhmC17yITwAbNa8PKw==}
engines: {node: '>=18.20.0'}
'@wdio/native-types@2.6.0':
resolution: {integrity: sha512-MVMVpZTH+UY6nUtLClCzY4BQSIs/nCCHo81i4ClLR+b2NehyBvwccttXDAGPgf0icyHUhd+PgIPXaLgRcm4M9A==}
'@wdio/native-types@2.6.1':
resolution: {integrity: sha512-Rm5chX+MrRhVgJYcYdcX/yyTZaLjxvi0w/fnSgPaDT16gMIntXlk22K2x3hwEdaUonOKepZyhh4jr9ZgRz7UaQ==}
engines: {node: '>=18.20.0'}
'@wdio/native-utils@2.8.0':
resolution: {integrity: sha512-TmIkPAd19FkJ747AdFVEtawbakVV3HHCh8zk3Kl/FLc6B2bbea7hn90jtLxqBZgCVqUuJIBcMNFg/nV/hgBFOw==}
'@wdio/native-utils@2.8.1':
resolution: {integrity: sha512-QbmZjGkLUC2bmUJSzyqsHM11HND1D7tkxHFssUV1Kmx43+oPAeHHVR0NSZu2hMzSNrDZMBAmNYycR+3X8rqWcA==}
engines: {node: '>=18.20.0'}
peerDependencies:
tsx: ^4.22.4
@@ -2611,8 +2611,8 @@ packages:
resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==}
engines: {node: '>=18.20.0'}
'@wdio/tauri-service@1.5.0':
resolution: {integrity: sha512-S3fs8T+jgNOFi4x/X2z69unS/1K3ZB28+owBouhHtVnrMKeBWAKP46IHnp8c8AZXppn6fiPNC4M1jfRZbLKeKw==}
'@wdio/tauri-service@1.5.1':
resolution: {integrity: sha512-nfzJU2x7bxvIXtYo+yvrX4Xwuba8h5hXOlJ0IyHanGg0ELavY/2UfyxygJDTcSGO+pCQMdC9JJEyMvaS50AtZQ==}
engines: {node: '>=18.20.0'}
peerDependencies:
webdriverio: ^9.0.0
@@ -6811,52 +6811,52 @@ snapshots:
'@tauri-apps/api@2.12.4': {}
'@tauri-apps/cli-darwin-arm64@2.12.3':
'@tauri-apps/cli-darwin-arm64@2.12.4':
optional: true
'@tauri-apps/cli-darwin-x64@2.12.3':
'@tauri-apps/cli-darwin-x64@2.12.4':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.3':
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.4':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.12.3':
'@tauri-apps/cli-linux-arm64-gnu@2.12.4':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.12.3':
'@tauri-apps/cli-linux-arm64-musl@2.12.4':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.12.3':
'@tauri-apps/cli-linux-riscv64-gnu@2.12.4':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.12.3':
'@tauri-apps/cli-linux-x64-gnu@2.12.4':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.12.3':
'@tauri-apps/cli-linux-x64-musl@2.12.4':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.12.3':
'@tauri-apps/cli-win32-arm64-msvc@2.12.4':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.12.3':
'@tauri-apps/cli-win32-ia32-msvc@2.12.4':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.12.3':
'@tauri-apps/cli-win32-x64-msvc@2.12.4':
optional: true
'@tauri-apps/cli@2.12.3':
'@tauri-apps/cli@2.12.4':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.12.3
'@tauri-apps/cli-darwin-x64': 2.12.3
'@tauri-apps/cli-linux-arm-gnueabihf': 2.12.3
'@tauri-apps/cli-linux-arm64-gnu': 2.12.3
'@tauri-apps/cli-linux-arm64-musl': 2.12.3
'@tauri-apps/cli-linux-riscv64-gnu': 2.12.3
'@tauri-apps/cli-linux-x64-gnu': 2.12.3
'@tauri-apps/cli-linux-x64-musl': 2.12.3
'@tauri-apps/cli-win32-arm64-msvc': 2.12.3
'@tauri-apps/cli-win32-ia32-msvc': 2.12.3
'@tauri-apps/cli-win32-x64-msvc': 2.12.3
'@tauri-apps/cli-darwin-arm64': 2.12.4
'@tauri-apps/cli-darwin-x64': 2.12.4
'@tauri-apps/cli-linux-arm-gnueabihf': 2.12.4
'@tauri-apps/cli-linux-arm64-gnu': 2.12.4
'@tauri-apps/cli-linux-arm64-musl': 2.12.4
'@tauri-apps/cli-linux-riscv64-gnu': 2.12.4
'@tauri-apps/cli-linux-x64-gnu': 2.12.4
'@tauri-apps/cli-linux-x64-musl': 2.12.4
'@tauri-apps/cli-win32-arm64-msvc': 2.12.4
'@tauri-apps/cli-win32-ia32-msvc': 2.12.4
'@tauri-apps/cli-win32-x64-msvc': 2.12.4
'@tauri-apps/plugin-clipboard-manager@2.5.0':
dependencies:
@@ -7222,7 +7222,7 @@ snapshots:
'@wdio/globals@9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
dependencies:
expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@10.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
webdriverio: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
'@wdio/logger@10.1.0':
@@ -7241,11 +7241,11 @@ snapshots:
safe-regex2: 5.1.1
strip-ansi: 7.2.0
'@wdio/native-core@1.3.0(supports-color@7.2.0)':
'@wdio/native-core@1.3.1(supports-color@7.2.0)':
dependencies:
'@wdio/logger': 9.29.1
'@wdio/native-types': 2.6.0
'@wdio/native-utils': 2.8.0(supports-color@7.2.0)
'@wdio/native-types': 2.6.1
'@wdio/native-utils': 2.8.1(supports-color@7.2.0)
debug: 4.4.3(supports-color@7.2.0)
get-port: 7.2.0
tslib: 2.8.1
@@ -7253,11 +7253,11 @@ snapshots:
- supports-color
- tsx
'@wdio/native-spy@1.4.0': {}
'@wdio/native-spy@1.4.1': {}
'@wdio/native-types@2.6.0': {}
'@wdio/native-types@2.6.1': {}
'@wdio/native-utils@2.8.0(supports-color@7.2.0)':
'@wdio/native-utils@2.8.1(supports-color@7.2.0)':
dependencies:
'@wdio/logger': 9.29.1
debug: 4.4.3(supports-color@7.2.0)
@@ -7291,14 +7291,14 @@ snapshots:
easy-table: 1.2.0
pretty-ms: 9.3.1
'@wdio/tauri-service@1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
'@wdio/tauri-service@1.5.1(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
dependencies:
'@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
'@wdio/logger': 9.29.1
'@wdio/native-core': 1.3.0(supports-color@7.2.0)
'@wdio/native-spy': 1.4.0
'@wdio/native-types': 2.6.0
'@wdio/native-utils': 2.8.0(supports-color@7.2.0)
'@wdio/native-core': 1.3.1(supports-color@7.2.0)
'@wdio/native-spy': 1.4.1
'@wdio/native-types': 2.6.1
'@wdio/native-utils': 2.8.1(supports-color@7.2.0)
'@wdio/spec-reporter': 9.31.2
'@wdio/types': 9.31.2
debug: 4.4.3(supports-color@7.2.0)
@@ -8206,11 +8206,11 @@ snapshots:
expect-type@1.4.0: {}
expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@10.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)):
expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)):
dependencies:
'@vitest/snapshot': 4.1.11
'@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
'@wdio/logger': 10.1.0
'@wdio/logger': 9.29.1
deep-eql: 5.0.2
expect: 30.5.2
jest-matcher-utils: 30.5.2
+4 -4
View File
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.7.3", features = [] }
tauri-build = { version = "2.7.4", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
@@ -52,9 +52,9 @@ anyhow = { workspace = true }
tokio = { workspace = true }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tristr = { workspace = true }
serde_json = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
@@ -109,7 +109,7 @@ 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"] }
tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] }
specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies]
@@ -120,7 +120,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
tauri-plugin-wdio-webdriver = { version = "=1.5.1", optional = true }
objc2 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [
"NSString",
+14 -11
View File
@@ -5,8 +5,8 @@ use crate::{
feat,
utils::dirs,
};
use smartstring::alias::String;
use tauri::{AppHandle, Manager as _, ipc::Channel};
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -59,10 +59,10 @@ pub async fn restart_app() -> CmdResult<()> {
#[specta::specta]
pub async fn install_update(
app_handle: AppHandle,
version: String,
version: std::string::String,
on_event: Channel<DownloadEvent>,
) -> CmdResult<bool> {
let version: String = version;
let version: String = version.into();
SilentUpdater::global()
.install_update(&app_handle, &version, |event| {
let _ = on_event.send(event);
@@ -79,22 +79,25 @@ pub fn cancel_update_download() {
#[tauri::command]
#[specta::specta]
pub fn get_app_dir() -> CmdResult<String> {
pub fn get_app_dir() -> CmdResult<std::string::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: String, name: String) -> CmdResult<String> {
let url: String = url;
let name: String = name;
feat::download_icon_cache(url, name).await
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)
}
#[tauri::command]
#[specta::specta]
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
let path: String = path;
feat::copy_icon_file(path, icon_info).await
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)
}
+11 -11
View File
@@ -1,7 +1,7 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, feat};
use feat::LocalBackupFile;
use tristr::alias::String;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
@@ -17,15 +17,15 @@ pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
#[tauri::command]
#[specta::specta]
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::delete_local_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::restore_local_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
@@ -33,15 +33,15 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
#[tauri::command]
#[specta::specta]
pub async fn import_local_backup(source: String) -> CmdResult<String> {
let source: String = source;
feat::import_local_backup(source).await.stringify_err()
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()
}
#[tauri::command]
#[specta::specta]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
let filename: String = filename;
let destination: String = destination;
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();
feat::export_local_backup(filename, destination).await.stringify_err()
}
+16 -16
View File
@@ -12,8 +12,8 @@ use crate::{
},
};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -37,8 +37,8 @@ pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
let payload: String = payload;
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
let payload: String = payload.into();
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
@@ -47,14 +47,14 @@ pub async fn patch_clash_mode(payload: 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<String>> {
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
Ok(Config::clash().await.data_arc().get_mode())
}
#[tauri::command]
#[specta::specta]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core;
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core.into();
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
@@ -112,8 +112,8 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
#[tauri::command]
#[specta::specta]
pub async fn test_delay(url: String) -> CmdResult<u32> {
let url: String = url;
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
let url: String = url.into();
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
@@ -148,12 +148,12 @@ pub fn take_dns_override_notice() -> bool {
#[tauri::command]
#[specta::specta]
pub async fn set_dns_override(
profile_uid: String,
profile_uid: std::string::String,
enabled: bool,
confirmation: Option<String>,
confirmation: Option<std::string::String>,
) -> CmdResult<feat::DnsOverrideOutcome> {
let profile_uid: String = profile_uid;
let confirmation = confirmation;
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"))
@@ -212,14 +212,14 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::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.into())),
Ok(content) => Ok(Some(content)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(CommandFailure::plain(error)),
}
@@ -244,7 +244,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[specta::specta]
pub async fn get_clash_logs() -> CmdResult<Vec<String>> {
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs.into_iter().collect())
Ok(logs)
}
@@ -2,7 +2,6 @@ pub use clash_verge_media_unlock::UnlockItem;
use reqwest::{Client, Proxy};
use std::time::Duration;
use tauri::{command, ipc::Channel};
use tristr::alias::String;
#[command]
#[specta::specta]
@@ -24,7 +23,7 @@ async fn create_client() -> Result<Client, String> {
.cookie_store(true)
.proxy(proxy)
.build()
.map_err(|error| format!("创建HTTP客户端失败: {error}").into())
.map_err(|error| format!("创建HTTP客户端失败: {error}"))
}
#[command]
@@ -41,7 +40,5 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
#[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
.map_err(Into::into)
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
}
+16 -6
View File
@@ -1,5 +1,5 @@
use anyhow::Result;
use tristr::alias::String;
use smartstring::alias::String;
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
@@ -8,9 +8,9 @@ pub type CmdResult<T = ()> = Result<T, CommandFailure>;
pub struct CommandFailure {
/// Stable frontend identifier, when classified.
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub code: Option<std::string::String>,
/// Human-readable diagnostic detail.
pub detail: String,
pub detail: std::string::String,
/// Requested operation when known by the command.
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<crate::core::notification::FailedOperation>,
@@ -20,8 +20,8 @@ impl CommandFailure {
/// A failure with a stable code.
pub fn coded(code: &str, detail: impl std::fmt::Display) -> Self {
Self {
code: Some(code.into()),
detail: detail.to_string().into(),
code: Some(code.to_owned()),
detail: detail.to_string(),
operation: None,
}
}
@@ -30,7 +30,7 @@ impl CommandFailure {
pub fn plain(detail: impl std::fmt::Display) -> Self {
Self {
code: None,
detail: detail.to_string().into(),
detail: detail.to_string(),
operation: None,
}
}
@@ -50,6 +50,16 @@ impl From<&str> for CommandFailure {
}
}
impl From<std::string::String> for CommandFailure {
fn from(detail: std::string::String) -> Self {
Self {
code: None,
detail,
operation: None,
}
}
}
impl From<String> for CommandFailure {
fn from(detail: String) -> Self {
Self::plain(detail)
+6 -13
View File
@@ -5,7 +5,6 @@ use crate::core::{proxy_control, sysopt::Sysopt};
use gethostname::gethostname;
use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_clash_verge_sysinfo;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -25,8 +24,8 @@ pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
Ok(SystemProxy {
enable,
server: format!("{host}:{port}").into(),
bypass: bypass.into(),
server: format!("{host}:{port}"),
bypass,
})
}
@@ -40,10 +39,7 @@ pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
};
let Autoproxy { enable, url } = auto_proxy;
Ok(AutoProxy {
enable,
url: url.into(),
})
Ok(AutoProxy { enable, url })
}
#[tauri::command]
@@ -56,10 +52,10 @@ pub fn get_embedded_server_port() -> CmdResult<u16> {
#[specta::specta]
pub fn get_system_hostname() -> String {
match gethostname().into_string() {
Ok(name) => name.into(),
Ok(name) => name,
Err(os_string) => {
let fallback = format!("{os_string:?}");
fallback.trim_matches('"').into()
fallback.trim_matches('"').to_string()
}
}
}
@@ -68,9 +64,6 @@ pub fn get_system_hostname() -> String {
#[specta::specta]
pub fn get_network_interfaces() -> Vec<String> {
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
.into_iter()
.map(Into::into)
.collect()
}
#[tauri::command]
@@ -84,7 +77,7 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfa
let mut result = Vec::new();
for interface in interfaces {
if names.iter().any(|name| name == &interface.name) {
if names.contains(&interface.name) {
result.push(interface.into());
}
}
+31 -21
View File
@@ -19,17 +19,17 @@ use crate::{
use clash_verge_draft::{Draft, SharedDraft};
use clash_verge_logging::{Type, logging, logging_error};
use scopeguard::defer;
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use tristr::alias::String;
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
fn profile_import_error(err: &anyhow::Error) -> String {
fn profile_import_error(err: &anyhow::Error) -> std::string::String {
if let Some(cause) = err.chain().find(|cause| cause.to_string().contains("TLS 1.0/1.1")) {
return cause.to_string().into();
return cause.to_string();
}
format!("导入订阅失败: {err:#}").into()
format!("导入订阅失败: {err:#}")
}
#[tauri::command]
@@ -68,7 +68,7 @@ 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: String, option: Option<PrfOption>) -> CmdResult {
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,
Err(e) => {
@@ -98,9 +98,9 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
#[tauri::command]
#[specta::specta]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
let active_id: String = active_id;
let over_id: String = over_id;
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();
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
@@ -121,9 +121,9 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> CmdResult {
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
let item: PrfItem = item.into();
let file_data = file_data;
let file_data = file_data.map(String::from);
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
profiles_save_file_safe()
@@ -138,8 +138,8 @@ pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> C
#[tauri::command]
#[specta::specta]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
let index: String = index;
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
let index: String = index.into();
match feat::update_profile(&index, option.as_ref(), true).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -151,8 +151,8 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
#[tauri::command]
#[specta::specta]
pub async fn delete_profile(index: String) -> CmdResult {
let index: String = index;
pub async fn delete_profile(index: std::string::String) -> CmdResult {
let index: String = index.into();
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = Config::profiles().await;
@@ -325,6 +325,7 @@ 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>,
}
@@ -332,9 +333,13 @@ pub struct ProfileSelection {
#[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>,
}
@@ -356,10 +361,15 @@ impl From<ProfileCreate> for PrfItem {
#[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>,
@@ -426,9 +436,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
#[tauri::command]
#[specta::specta]
pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
let profile: PrfItem = profile.into();
let index: String = index;
let index: String = index.into();
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()
@@ -469,7 +479,7 @@ pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn view_profile(index: String) -> CmdResult {
pub async fn view_profile(index: std::string::String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let file = profiles_ref
@@ -494,7 +504,7 @@ pub async fn view_profile(index: String) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn read_profile_file(index: String) -> CmdResult<String> {
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
@@ -514,7 +524,7 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
.join(file.as_str());
match tokio::fs::try_exists(&path).await {
Ok(true) => {}
Ok(false) => return Ok(String::new()),
Ok(false) => return Ok(std::string::String::new()),
Err(err) => {
return Err(coded_error(
"PROFILE_READ_FAILED",
@@ -525,12 +535,12 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
}
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
Ok(data)
Ok(data.into())
}
#[tauri::command]
#[specta::specta]
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time)
+5 -6
View File
@@ -15,7 +15,6 @@ use std::{
collections::{BTreeMap, HashSet},
sync::atomic::{AtomicBool, Ordering},
};
use tristr::alias::String;
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
@@ -49,7 +48,7 @@ fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
.filter_map(|name| name.as_str())
.filter(|name| !name.is_empty() && *name != "GLOBAL")
.filter(|name| seen.insert(*name))
.map(String::from)
.map(str::to_owned)
.collect()
}
@@ -64,7 +63,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.into_iter()
.flat_map(|providers| providers.keys())
.filter_map(|name| name.as_str())
.map(String::from)
.map(str::to_owned)
.collect::<Vec<_>>();
all_providers.sort();
let flag = |group: &Mapping, key: &str| group.get(key).and_then(|value| value.as_bool()) == Some(true);
@@ -76,7 +75,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.flatten()
.filter_map(|group| group.as_mapping())
.filter_map(|group| {
let name: String = group.get("name")?.as_str()?.into();
let name = group.get("name")?.as_str()?.to_owned();
let providers = if flag(group, "include-all") || flag(group, "include-all-providers") {
all_providers.clone()
} else {
@@ -86,14 +85,14 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.into_iter()
.flatten()
.filter_map(|provider| provider.as_str())
.map(String::from)
.map(str::to_owned)
.collect()
};
let exclude_types = group
.get("exclude-type")
.and_then(|types| types.as_str())
.filter(|types| !types.is_empty())
.map(|types| types.split('|').map(String::from).collect())
.map(|types| types.split('|').map(str::to_owned).collect())
.unwrap_or_default();
Some((
name,
+11 -6
View File
@@ -1,8 +1,8 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
use anyhow::{Context as _, anyhow};
use smartstring::alias::String;
use std::collections::HashMap;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -17,7 +17,7 @@ pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>>
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_yaml() -> CmdResult<String> {
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
@@ -36,17 +36,22 @@ pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
#[derive(serde::Serialize, specta::Type)]
#[serde(transparent)]
pub struct RuntimeLogs(HashMap<String, Vec<(String, String)>>);
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<String> {
crate::enhance::take_discarded_keys_notice()
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
crate::enhance::take_discarded_keys_notice().map(Into::into)
}
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
pub async fn get_runtime_proxy_chain_config(
proxy_chain_exit_node: std::string::String,
) -> CmdResult<std::string::String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
+10 -7
View File
@@ -1,4 +1,4 @@
use super::{CmdResult, CommandFailure};
use super::CmdResult;
use crate::{
cmd::StringifyErr as _,
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
@@ -11,15 +11,18 @@ use crate::{
utils::dirs,
};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
/// 保存profiles的配置
#[tauri::command]
#[specta::specta]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
let index: String = index;
let file_data = file_data;
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);
let file_data = match file_data {
Some(d) => d,
None => return Ok(ValidationOutcome::Valid),
@@ -150,7 +153,7 @@ async fn handle_saved_profile_file(
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {e:#}");
restore_original(file_path, original_content, original_existed).await?;
return Err(CommandFailure::plain(e));
return Err(e.to_string().into());
}
}
@@ -178,7 +181,7 @@ async fn handle_saved_profile_file(
Err(err) => {
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {err:#}");
restore_original(file_path, original_content, original_existed).await?;
Err(CommandFailure::plain(err))
Err(err.to_string().into())
}
}
}
+3 -4
View File
@@ -7,7 +7,6 @@ use crate::{
service::{SERVICE_MANAGER, ServiceStatus, request_runtime_provider_sync},
},
};
use tristr::alias::String;
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
@@ -75,7 +74,7 @@ where
if reasons.is_empty() {
return Err(proxy_aware_coded_error(&error, error_code));
}
let reason: String = reasons.join("\n").into();
let reason = reasons.join("\n");
fallback(reason.clone())
.await
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))?;
@@ -180,7 +179,7 @@ mod tests {
let error = match availability {
Some(availability) => anyhow::Error::new(InstallationVerificationError {
status: InstallationStatus {
service_sha256: std::string::String::new(),
service_sha256: String::new(),
protocol: ProtocolInfo::current(),
cores: vec![CoreInspection {
name: "verge-mihomo.exe".into(),
@@ -204,7 +203,7 @@ mod tests {
assert_eq!(
result,
Ok(ServiceInstallOutcome::Sidecar {
reason: format!("verge-mihomo.exe: {reason}").into()
reason: format!("verge-mihomo.exe: {reason}")
})
);
} else {
-1
View File
@@ -2,7 +2,6 @@ use crate::core::{
notification::{self, PendingFailure},
runstate::{RUN_STATE, RunStateView},
};
use tristr::alias::String;
/// Returns one coherent core/service snapshot instead of independently refreshed state.
#[tauri::command]
+13 -9
View File
@@ -4,14 +4,18 @@ use crate::{
config::{Config, IVerge},
core, feat,
};
use tristr::alias::String;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let url: String = url;
let username: String = username;
let password: String = password;
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();
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
@@ -44,15 +48,15 @@ pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
#[tauri::command]
#[specta::specta]
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::delete_webdav_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::restore_webdav_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
+11 -18
View File
@@ -10,7 +10,6 @@ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr as _,
};
use tristr::alias::String;
#[derive(Default, Debug, Clone)]
pub struct IClashTemp(pub Mapping);
@@ -85,12 +84,12 @@ impl IClashTemp {
#[cfg(unix)]
map.insert(
"external-controller-unix".into(),
Self::guard_external_controller_ipc().as_str().into(),
Self::guard_external_controller_ipc().into(),
);
#[cfg(windows)]
map.insert(
"external-controller-pipe".into(),
Self::guard_external_controller_ipc().as_str().into(),
Self::guard_external_controller_ipc().into(),
);
map.insert("tun".into(), tun_config.into());
cors_map.insert("allow-private-network".into(), true.into());
@@ -135,18 +134,12 @@ impl IClashTemp {
config.insert("mixed-port".into(), mixed_port.into());
config.insert("socks-port".into(), socks_port.into());
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.as_str().into());
config.insert("external-controller".into(), ctrl.into());
#[cfg(unix)]
config.insert(
"external-controller-unix".into(),
external_controller_unix.as_str().into(),
);
config.insert("external-controller-unix".into(), external_controller_unix.into());
#[cfg(windows)]
config.insert(
"external-controller-pipe".into(),
external_controller_pipe.as_str().into(),
);
config.insert("external-controller-pipe".into(), external_controller_pipe.into());
config
}
@@ -173,9 +166,9 @@ impl IClashTemp {
port: Self::guard_port(config),
server: Self::guard_client_ctrl(config),
secret: config.get("secret").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone().into()),
Value::Bool(val_bool) => Some(val_bool.to_string().into()),
Value::Number(val_num) => Some(val_num.to_string().into()),
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
}),
}
@@ -187,7 +180,7 @@ impl IClashTemp {
/// 的严格 `BaseConfig` 反序列化,因此即使核心返回的字段与插件结构体不匹配也能取到。
pub fn get_mode(&self) -> Option<String> {
self.0.get("mode").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone().into()),
Value::String(val_str) => Some(val_str.clone()),
_ => None,
})
}
@@ -283,7 +276,7 @@ impl IClashTemp {
false => Cow::Borrowed(val_str),
};
SocketAddr::from_str(&val).ok().map(|s| s.to_string().into())
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
}
None => None,
})
@@ -297,7 +290,7 @@ impl IClashTemp {
if socket.ip().is_unspecified() {
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
}
socket.to_string().into()
socket.to_string()
}
Err(_) => "127.0.0.1:9097".into(),
}
+3 -3
View File
@@ -20,10 +20,10 @@ use backon::{ExponentialBuilder, Retryable as _};
use clash_verge_draft::Draft;
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::{collections::HashSet, path::PathBuf};
use tokio::sync::{Mutex, MutexGuard, OnceCell};
use tokio::time::sleep;
use tristr::alias::String;
pub(crate) struct Config {
clash_config: Draft<IClashTemp>,
@@ -205,7 +205,7 @@ impl Config {
Self::write_runtime_file(&yaml).await
}
pub(crate) async fn runtime_config_yaml() -> Result<String> {
pub(crate) async fn runtime_config_yaml() -> Result<std::string::String> {
let runtime = Self::runtime().await;
AsyncHandler::spawn_blocking(move || {
let runtime_lastest = runtime.latest_arc();
@@ -216,7 +216,7 @@ impl Config {
.or_else(|| runtime_data.config.as_ref())
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
let yaml_str = serde_yaml_ng::to_string(config)?;
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str).into())
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str))
})
.await
.map_err(|join| anyhow!("runtime serialization task failed: {join}"))?
+2 -1
View File
@@ -4,8 +4,8 @@ use clash_verge_draft::DraftTransaction;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use tristr::alias::String;
use super::{Config, IVerge};
use crate::core::handle::Handle;
@@ -110,6 +110,7 @@ impl DnsOverrideState {
pub struct ProfileDnsSettings {
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub confirmation: Option<String>,
}
+2 -6
View File
@@ -7,7 +7,6 @@ use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::future::Future;
use tristr::alias::String;
const NONCE_LENGTH: usize = 12;
@@ -29,7 +28,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut combined = nonce.to_vec();
combined.extend(ciphertext);
Ok(STANDARD.encode(combined).into())
Ok(STANDARD.encode(combined))
}
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
@@ -47,10 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {e}"))?;
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8(plaintext)
.map_err(|e| e.into())
.map(Into::into)
String::from_utf8(plaintext).map_err(|e| e.into())
}
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+2 -3
View File
@@ -18,7 +18,6 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::Value;
use std::{collections::HashSet, net::SocketAddr, str::FromStr as _};
use tristr::alias::String;
#[derive(Clone, Copy)]
struct MixedPortFallback {
@@ -142,8 +141,8 @@ impl Config {
}
pub(crate) fn block_startup_core(error: &anyhow::Error) {
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string().into());
report_fallback_error(error.to_string().into());
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
report_fallback_error(error.to_string());
}
pub(crate) fn startup_core_block_reason() -> Option<String> {
+29 -9
View File
@@ -9,9 +9,9 @@ use crate::{
use anyhow::{Context as _, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::time::Duration;
use tokio::fs;
use tristr::alias::String;
// TODO, use other re-export
use reqwest_dav::re_exports::url::form_urlencoded;
use tauri::Url;
@@ -30,18 +30,24 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
#[serde(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")]
@@ -56,16 +62,20 @@ 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)]
pub struct PrfSelected {
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub now: Option<String>,
}
@@ -81,6 +91,7 @@ pub struct PrfExtra {
#[serde(default)]
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")]
@@ -103,10 +114,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>,
}
@@ -171,7 +187,7 @@ impl PrfItem {
file_data: Option<String>,
option: Option<&PrfOption>,
) -> Result<Self> {
let uid = help::get_uid("L");
let uid = help::get_uid("L").into();
let file = format!("{uid}.yaml").into();
let opt_ref = option.as_ref();
let update_interval = opt_ref.and_then(|o| o.update_interval);
@@ -344,11 +360,11 @@ impl PrfItem {
.and_then(|value| value.to_str().ok())
.and_then(normalize_profile_home_url);
let uid = help::get_uid("R");
let uid = help::get_uid("R").into();
let file = format!("{uid}.yaml").into();
let name = name
.map(|s| s.to_owned())
.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()));
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
let data = resp.text();
let data = data.trim_start_matches('\u{feff}');
@@ -414,7 +430,7 @@ impl PrfItem {
let (id, template) = if let Some(uid) = uid {
(uid, tmpl::ITEM_MERGE.into())
} else {
(help::get_uid("m"), tmpl::ITEM_MERGE_EMPTY.into())
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
};
let file = format!("{id}.yaml").into();
@@ -429,7 +445,11 @@ impl PrfItem {
}
pub(super) fn from_script(uid: Option<String>) -> Self {
let id = if let Some(uid) = uid { uid } else { help::get_uid("s") };
let id = if let Some(uid) = uid {
uid
} else {
help::get_uid("s").into()
};
let file = format!("{id}.js").into(); // js ext
Self {
uid: Some(id),
@@ -442,7 +462,7 @@ impl PrfItem {
}
fn from_rules() -> Self {
let uid = help::get_uid("r");
let uid = help::get_uid("r").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
@@ -456,7 +476,7 @@ impl PrfItem {
}
fn from_proxies() -> Self {
let uid = help::get_uid("p");
let uid = help::get_uid("p").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
@@ -470,7 +490,7 @@ impl PrfItem {
}
fn from_groups() -> Self {
let uid = help::get_uid("g");
let uid = help::get_uid("g").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
+3 -2
View File
@@ -17,6 +17,7 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::num::NonZeroUsize;
use std::{
collections::{HashMap, HashSet},
@@ -29,7 +30,6 @@ use std::{
};
use tauri_plugin_mihomo::models::{Proxies, ProxyType};
use tokio::{fs, task::JoinHandle};
use tristr::alias::String;
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
@@ -51,6 +51,7 @@ const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IProfiles {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
pub items: Option<Vec<PrfItem>>,
@@ -114,7 +115,7 @@ impl IProfiles {
let mut home_changed = false;
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
item.uid = Some(help::get_uid("d").into());
}
if item.itype.as_deref() == Some("remote") {
item.option
+1 -1
View File
@@ -1,6 +1,6 @@
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tristr::alias::String;
use crate::enhance::field::use_keys;
+1 -1
View File
@@ -8,7 +8,7 @@ use anyhow::Result;
use clash_verge_logging::{Type, logging};
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use tristr::alias::String;
use smartstring::alias::String;
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
+3 -4
View File
@@ -20,7 +20,6 @@ use tokio::{
fs,
sync::{Mutex, watch},
};
use tristr::alias::String;
const DEFAULT_INTERVAL_HOURS: u64 = 24;
const MIN_INTERVAL_HOURS: u64 = 1;
@@ -212,7 +211,7 @@ impl AutoBackupManager {
return Ok(());
}
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug())).await?;
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()).await?;
self.last_backup.store(Local::now().timestamp(), Ordering::Release);
if let Err(err) = cleanup_auto_backups().await {
@@ -240,8 +239,8 @@ impl AutoBackupManager {
fn append_auto_suffix(file_name: &str, slug: &str) -> String {
match file_name.rsplit_once('.') {
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}").into(),
None => format!("{file_name}{AUTO_MARKER}{slug}").into(),
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"),
None => format!("{file_name}{AUTO_MARKER}{slug}"),
}
}
+1 -1
View File
@@ -5,6 +5,7 @@ use arc_swap::{ArcSwap, ArcSwapOption};
use backon::{ConstantBuilder, Retryable as _};
use clash_verge_logging::{Type, logging};
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
use smartstring::alias::String;
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
@@ -14,7 +15,6 @@ use std::{
time::Duration,
};
use tokio::{fs, time::timeout};
use tristr::alias::String;
use zip::write::SimpleFileOptions;
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
+1 -1
View File
@@ -7,6 +7,7 @@ use crate::{config::Config, core::handle, core::lightweight::entry_lightweight_m
use anyhow::{Result, bail};
use arc_swap::ArcSwap;
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::{
collections::HashMap,
fmt,
@@ -17,7 +18,6 @@ use std::{
},
};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState};
use tristr::alias::String;
/// Enum representing all available hotkey functions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+10 -13
View File
@@ -10,7 +10,6 @@ use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
str::FromStr as _,
};
use tristr::alias::String;
/// The config key of the Mixed Port, named once so callers stop spelling it out.
pub(crate) const MIXED_PORT_KEY: &str = "mixed-port";
@@ -164,7 +163,7 @@ pub(crate) fn probe_listener(request: &ListenerProbe) -> ListenerProbeOutcome {
Ok(address) => address,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: error.to_string().into(),
message: error.to_string(),
};
}
};
@@ -202,7 +201,7 @@ pub(crate) fn probe_proxy_port_change(
Ok(claims) => claims,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: format!("current proxy listener configuration is invalid: {error:#}").into(),
message: format!("current proxy listener configuration is invalid: {error:#}"),
};
}
}
@@ -213,7 +212,7 @@ pub(crate) fn probe_proxy_port_change(
Ok(claims) => claims,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: format!("candidate proxy listener configuration is invalid: {error:#}").into(),
message: format!("candidate proxy listener configuration is invalid: {error:#}"),
};
}
};
@@ -402,8 +401,7 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
claim.socket_addr(),
transport_name(claim.transport),
error
)
.into(),
),
};
}
Err(error) => {
@@ -414,8 +412,7 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
claim.socket_addr(),
transport_name(claim.transport),
error
)
.into(),
),
};
}
}
@@ -742,7 +739,7 @@ mod tests {
let port = listener.local_addr()?.port();
assert_eq!(
probe_listener(&ListenerProbe {
address: format!("0.0.0.0:{port}").into(),
address: format!("0.0.0.0:{port}"),
transports: vec![ListenerTransport::Tcp],
}),
ListenerProbeOutcome::Conflict {
@@ -758,7 +755,7 @@ mod tests {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = listener.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Tcp],
});
assert_eq!(
@@ -776,7 +773,7 @@ mod tests {
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?;
let port = listener.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Tcp],
});
assert_eq!(
@@ -812,7 +809,7 @@ mod tests {
assert_eq!(
probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Tcp],
}),
ListenerProbeOutcome::Available
@@ -825,7 +822,7 @@ mod tests {
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = socket.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Udp],
});
assert_eq!(
+2 -2
View File
@@ -15,6 +15,7 @@ use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::StageRuntimeOutcome;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
collections::HashSet,
path::PathBuf,
@@ -22,7 +23,6 @@ use std::{
time::{Duration, Instant},
};
use tauri_plugin_mihomo::Error as MihomoError;
use tristr::alias::String;
/// A staging result separated from the policy that acts on it.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -92,7 +92,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
match crate::core::service::stage_runtime_by_service(path).await {
Ok(StageRequest::Answered(outcome)) => StageAttempt::Answered(outcome),
Ok(StageRequest::Refused { code, message }) => {
let message = message;
let message = message.into();
if StageRequest::is_about_the_bundle(code) {
StageAttempt::RefusedTheBundle(message)
} else {
+7 -7
View File
@@ -7,10 +7,10 @@ use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
#[cfg(target_os = "windows")]
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
use tristr::alias::String;
#[cfg(any(target_os = "windows", test))]
const fn should_wait_for_service(tun_enabled: bool, service_ready: bool, is_admin: bool) -> bool {
@@ -194,7 +194,7 @@ async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, Chec
start_service: Start,
confirm_idle: Check,
start_sidecar: Fallback,
record_unavailable: impl FnOnce(String),
record_unavailable: impl FnOnce(std::string::String),
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
@@ -217,7 +217,7 @@ where
// A lost start response can hide a running core; the shared check must resolve occupancy.
if let Err(probe_error) = confirm_idle().await {
let error = error.context(format!("Sidecar fallback was not safe: {probe_error:#}"));
record_unavailable(format!("{error:#}").into());
record_unavailable(format!("{error:#}"));
return Err(error);
}
logging!(
@@ -650,7 +650,7 @@ impl CoreManager {
|error| self.start_sidecar_after_service_failure(error),
|reason| {
crate::core::runstate::RUN_STATE
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason.into()));
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason));
},
)
.await
@@ -688,7 +688,7 @@ impl CoreManager {
}
let reason = match state.health {
ServiceHealth::Unavailable(reason) => reason,
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".into(),
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
_ => return Ok(()),
};
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available()
@@ -712,7 +712,7 @@ impl CoreManager {
if Handle::global().is_exiting() {
return Err(error.context("application exited before Sidecar fallback"));
}
let reason: String = format!("{error:#}").into();
let reason = format!("{error:#}");
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
let result = self.start_core_by_sidecar().await;
@@ -1590,7 +1590,7 @@ mod tests {
started.store(true, Ordering::SeqCst);
Ok(())
},
|reason| store.observe(ServiceHealth::Unavailable(reason.into())),
|reason| store.observe(ServiceHealth::Unavailable(reason)),
)
.await;
+8 -11
View File
@@ -16,7 +16,6 @@ use std::num::NonZeroUsize;
use std::path::Path;
use tauri_plugin_mihomo::MihomoExt as _;
use tauri_plugin_shell::ShellExt as _;
use tristr::alias::String;
pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
let detail = error.to_string();
@@ -28,7 +27,7 @@ pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
}
#[cfg(target_os = "windows")]
fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
use serde_yaml_ng::{Mapping, Value};
let mut config: Mapping = serde_yaml_ng::from_str(yaml)?;
@@ -41,7 +40,7 @@ fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
tun.as_mapping_mut()
.context("invalid Sidecar TUN configuration")?
.insert(Value::from("enable"), Value::Bool(false));
Ok(serde_yaml_ng::to_string(&config)?.into())
Ok(serde_yaml_ng::to_string(&config)?)
}
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
@@ -154,7 +153,7 @@ impl CoreManager {
pub async fn get_clash_logs(&self) -> Result<Vec<String>> {
match *self.get_running_mode() {
RunningMode::Service => service::get_clash_logs_by_service().await,
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().into_iter().map(Into::into).collect()),
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs()),
RunningMode::NotRunning => Ok(Vec::new()),
}
}
@@ -258,9 +257,7 @@ impl CoreManager {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
// from_utf8_lossy_owned exists only on the std String type; the tristr
// alias shadows the bare name. LogRing stores std Strings.
let message = std::string::String::from_utf8_lossy_owned(line);
let message = String::from_utf8_lossy_owned(line);
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message);
}
@@ -268,12 +265,12 @@ impl CoreManager {
let _ = terminated.send(());
let manager = Self::global();
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
let message: String = if let Some(code) = term.code {
format!("Process terminated with code: {}", code).into()
let message = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal).into()
format!("Process terminated by signal: {}", signal)
} else {
"Process terminated".into()
String::from("Process terminated")
};
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs();
+6 -3
View File
@@ -1,9 +1,12 @@
use super::NoticeStatus;
use crate::{APP_HANDLE, singleton};
use std::sync::atomic::{AtomicBool, Ordering};
use smartstring::alias::String;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tauri::AppHandle;
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
use tristr::alias::String;
use super::notification::{FrontendEvent, NotificationSystem};
@@ -70,7 +73,7 @@ impl Handle {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
pub fn notice(status: NoticeStatus, message: impl Into<String>) {
pub fn notice(status: NoticeStatus, message: impl Into<Arc<str>>) {
Self::send_event(FrontendEvent::NoticeMessage {
status,
message: message.into(),
+13 -9
View File
@@ -3,10 +3,11 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
#[cfg(test)]
use serde_json::json;
use smartstring::alias::String;
use std::sync::Arc;
use std::sync::LazyLock;
use std::{collections::HashMap, future::Future};
use tauri::{AppHandle, Emitter as _, Manager as _, WebviewWindow};
use tristr::alias::String;
#[derive(Debug, Clone)]
pub enum FrontendEvent<'a> {
@@ -16,7 +17,7 @@ pub enum FrontendEvent<'a> {
RefreshProxyConfig,
NoticeMessage {
status: NoticeStatus,
message: String,
message: Arc<str>,
},
ProfileChanged {
current_profile_id: &'a String,
@@ -76,8 +77,10 @@ impl FailedOperation {
#[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,6 +247,7 @@ static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::de
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
pub struct SidecarFailureSnapshot {
pub revision: u64,
#[specta(type = Option<std::string::String>)]
pub detail: Option<String>,
}
@@ -387,7 +391,7 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
(
FrontendEvent::NoticeMessage {
status: NoticeStatus::Info,
message: String::from("ok"),
message: Arc::from("ok"),
},
"verge://notice-message",
json!(["info", "ok"]),
@@ -452,16 +456,16 @@ frontend_events! {
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, String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.clone());
profile_changed: String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.clone();
timer_updated: String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.clone();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.clone() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.clone() };
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: String,
pub uid: std::string::String,
}
+7 -11
View File
@@ -2,8 +2,6 @@ use crate::utils::dirs;
use anyhow::{Context as _, Result};
use clash_verge_service_ipc::{OwnerCredentials, OwnerIdentity};
use std::path::Path;
#[cfg(windows)]
use tristr::alias::String;
pub(crate) fn current_owner_credentials() -> Result<OwnerCredentials> {
current_owner_credentials_for_root(&dirs::app_home_dir()?)
@@ -41,16 +39,16 @@ pub(crate) fn current_owner_credentials_for_root(app_root: &Path) -> Result<Owne
}
#[cfg(windows)]
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<std::string::String>)> {
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<String>)> {
let sid = windows_owner::current_sid()?;
let token = windows_owner::load_or_create_token(app_data_root, &sid)?;
Ok((OwnerIdentity::Windows { sid: sid.into() }, Some(token.into())))
Ok((OwnerIdentity::Windows { sid }, Some(token)))
}
#[cfg(windows)]
fn windows_current_identity() -> Result<OwnerIdentity> {
Ok(OwnerIdentity::Windows {
sid: windows_owner::current_sid()?.into(),
sid: windows_owner::current_sid()?,
})
}
@@ -81,7 +79,7 @@ pub(crate) fn repair_app_data_root_owner(app_data_root: &Path) -> Result<()> {
#[cfg(windows)]
pub(crate) fn current_user_pipe_sddl() -> Result<String> {
let sid = windows_owner::current_sid()?;
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)").into())
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)"))
}
#[cfg(windows)]
@@ -646,19 +644,17 @@ mod windows_owner {
}
let value = LocalWideString(value);
let length = (0..).take_while(|index| unsafe { *value.0.add(*index) } != 0).count();
// from_utf16 exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
.context("process SID is not valid UTF-16")
.map(Into::into)
}
fn encode_token(token: &[u8; TOKEN_BYTES]) -> String {
let mut encoded = std::string::String::with_capacity(TOKEN_BYTES * 2);
let mut encoded = String::with_capacity(TOKEN_BYTES * 2);
for byte in token {
use std::fmt::Write as _;
let _ = write!(encoded, "{byte:02x}");
}
encoded.into()
encoded
}
fn wide_path(path: &Path) -> Result<Vec<u16>> {
+11 -13
View File
@@ -24,7 +24,6 @@ use std::{
time::Duration,
};
use tokio::{sync::Mutex, time::timeout};
use tristr::alias::String;
const CLEAR_ATTEMPTS: u32 = 3;
const CLEAR_RETRY_DELAY: Duration = Duration::from_millis(100);
@@ -321,8 +320,7 @@ fn truncate_utf8(value: &mut String, max_bytes: usize) {
while !value.is_char_boundary(boundary) {
boundary -= 1;
}
let truncated = value.as_str()[..boundary].into();
*value = truncated;
value.truncate(boundary);
}
fn service_bypass(verge: &IVerge) -> Result<String> {
@@ -330,11 +328,11 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
let mut bypass = if custom.is_empty() {
MACOS_DEFAULT_BYPASS.into()
MACOS_DEFAULT_BYPASS.to_owned()
} else if verge.use_default_bypass.unwrap_or(true) {
format!("{MACOS_DEFAULT_BYPASS},{custom}").into()
format!("{MACOS_DEFAULT_BYPASS},{custom}")
} else {
custom.into()
custom.to_owned()
};
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
Ok(bypass)
@@ -355,7 +353,7 @@ fn service_proxy_config(verge: &IVerge, mixed_port: u16, pac_port: u16) -> Resul
Ok(MacosProxyConfig::Global {
host: LOOPBACK_HOST.to_owned(),
port: mixed_port,
bypass: service_bypass(verge)?.into(),
bypass: service_bypass(verge)?,
})
}
}
@@ -370,7 +368,7 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
// Direct fallback satisfies a disable request.
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
ProxyApplyOutcome::DirectFallback { message } => {
Err(SysproxyFailure::DirectFallback { detail: message.into() }.into())
Err(SysproxyFailure::DirectFallback { detail: message }.into())
}
}
}
@@ -646,7 +644,7 @@ const GUARD_FAILURES_BEFORE_STOPPING: u32 = 3;
async fn report_guard_stopped(captured_generation: u64, error: &anyhow::Error) {
let stopped = anyhow::Error::new(SysproxyFailure::GuardStopped {
detail: format!("{error:#}").into(),
detail: format!("{error:#}"),
});
// Do not publish a late failure from a replaced guard.
let recorded = SERVICE_PROXY_OPERATIONS
@@ -1108,7 +1106,7 @@ mod tests {
let requested = MacosProxyConfig::Global {
host: "127.0.0.1".to_owned(),
port: 7890,
bypass: std::string::String::new(),
bypass: String::new(),
};
let error = service_apply_result(
@@ -1148,8 +1146,8 @@ mod tests {
let events = Mutex::new(Vec::new());
super::settle_table(
effect,
|asked| events.lock().push(format!("retire:{asked:?}").into()),
|operation, _| events.lock().push(format!("file:{operation:?}").into()),
|asked| events.lock().push(format!("retire:{asked:?}")),
|operation, _| events.lock().push(format!("file:{operation:?}")),
);
events.into_inner()
}
@@ -1278,7 +1276,7 @@ mod tests {
let combined = rollback_failure(
mappable(),
anyhow::anyhow!("the service could not set it").context(SysproxyFailure::DirectFallback {
detail: "fell back to direct".into(),
detail: "fell back to direct".to_owned(),
}),
);
+14 -23
View File
@@ -4,7 +4,6 @@ use serde::Serialize;
use tauri_plugin_mihomo::models::{
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
};
use tristr::alias::String;
pub struct ProxyViewInput {
pub runtime_group_order: Vec<String>,
@@ -271,7 +270,7 @@ impl ProxyViewBuilder {
let global = core_groups
.remove("GLOBAL")
.map(|proxy| build_group(String::from("GLOBAL"), proxy, &resolver));
.map(|proxy| build_group("GLOBAL".to_owned(), proxy, &resolver));
let (groups, order_source) = build_ordered_groups(core_groups, runtime_group_order, &resolver);
let direct = core_node_ids.get("DIRECT").cloned();
let standalone = build_standalone(&core_node_ids);
@@ -295,7 +294,6 @@ fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String
let mut nodes = BTreeMap::new();
for (name, proxy) in proxies.proxies {
let name = String::from(name);
if proxy.all.is_some() {
groups.insert(name, proxy);
} else {
@@ -313,7 +311,7 @@ fn build_core_records(
let mut ids = BTreeMap::new();
for (index, (name, proxy)) in core_nodes.into_iter().enumerate() {
let record_id = String::from(format!("c:{index}"));
let record_id = format!("c:{index}");
ids.insert(name.clone(), record_id.clone());
records.insert(
record_id.clone(),
@@ -334,13 +332,7 @@ fn build_provider_records(
records: &mut BTreeMap<String, ProxyNodeView>,
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
let providers = providers
.map(|providers| {
providers
.providers
.into_iter()
.map(|(name, provider)| (String::from(name), provider))
.collect::<BTreeMap<_, _>>()
})
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
.unwrap_or_default();
let mut views = Vec::new();
let mut candidates = BTreeMap::new();
@@ -363,9 +355,9 @@ fn build_provider_records(
let mut proxy_record_ids = Vec::new();
for (member_index, proxy) in proxies.into_iter().enumerate() {
let record_id = String::from(format!("p:{provider_index}:{member_index}"));
let proxy_name = String::from(proxy.name.as_str());
let node_name = String::from(proxy.name.as_str());
let record_id = format!("p:{provider_index}:{member_index}");
let proxy_name = proxy.name.clone();
let node_name = proxy.name.clone();
candidates
.entry(proxy_name.clone())
.or_insert_with(Vec::new)
@@ -388,7 +380,7 @@ fn build_provider_records(
views.push(ProxyProviderView {
name: provider_name,
vehicle_type,
updated_at: updated_at.map(String::from),
updated_at,
subscription_info: subscription_info.map(|subscription_info| ProxySubscriptionInfo {
upload: subscription_info.upload,
download: subscription_info.download,
@@ -425,7 +417,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
.unwrap_or_default()
.into_iter()
.map(|member| {
let member = String::from(member);
let (occurrence, scoped) = seen
.entry(member.clone())
.or_insert_with(|| (0, resolver.scoped_candidates(&name, &member)));
@@ -438,11 +429,11 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
ProxyGroupView {
name,
proxy_type,
now: now.map(String::from),
fixed: fixed.map(String::from),
now,
fixed,
hidden,
icon: icon.map(String::from),
test_url: test_url.map(String::from),
icon,
test_url,
history,
capabilities: ProxyCapabilities {
udp,
@@ -476,10 +467,10 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
name,
proxy_type,
history,
id: id.map(String::from),
id,
hidden,
icon: icon.map(String::from),
test_url: test_url.map(String::from),
icon,
test_url,
capabilities: ProxyCapabilities {
udp,
xudp,
+7 -8
View File
@@ -64,7 +64,7 @@ impl RunStateEnv for RealEnv {
Ok(ServiceVersionReply {
core,
code: response.code,
message: response.message.into(),
message: response.message,
protocol: response.data,
})
}
@@ -123,7 +123,6 @@ mod fake {
use super::{PendingAction, RunState, RunStateEnv, ServiceVersionReply};
use crate::core::manager::RunningMode;
use tristr::alias::String;
/// A fail-closed scripted machine that records outbound effects.
#[derive(Debug)]
@@ -164,7 +163,7 @@ mod fake {
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(ProtocolInfo::current()),
}))
}
@@ -174,7 +173,7 @@ mod fake {
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: None,
}))
}
@@ -182,12 +181,12 @@ mod fake {
#[must_use]
pub fn service_unreachable(self) -> Self {
self.with_evidence(true)
.always_replying(Err("ipc transport refused".into()))
.always_replying(Err("ipc transport refused".to_owned()))
}
#[must_use]
pub fn evidence_unavailable(mut self) -> Self {
self.evidence = Err("registry probe failed".into());
self.evidence = Err("registry probe failed".to_owned());
self
}
@@ -237,7 +236,7 @@ mod fake {
#[must_use]
pub fn privileged_operations_fail(self, reason: &str) -> Self {
*self.privileged_outcome.lock() = Err(reason.into());
*self.privileged_outcome.lock() = Err(reason.to_owned());
self
}
@@ -257,7 +256,7 @@ mod fake {
replies
.first()
.cloned()
.unwrap_or_else(|| Err("no service configured".into()))
.unwrap_or_else(|| Err("no service configured".to_owned()))
};
reply.map_err(|error| anyhow!(error))
}
-1
View File
@@ -2,7 +2,6 @@
//! Core lifecycle remains in `CoreManager`.
use crate::core::manager::RunningMode;
use tristr::alias::String;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum ServiceHealth {
+5 -5
View File
@@ -173,7 +173,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
Type::Service,
"failed to inspect trusted service evidence: {error:#}"
);
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}").into());
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}"));
}
};
@@ -331,7 +331,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
}
#[cfg(target_os = "windows")]
pub fn allow_sidecar_after_service_refusal(&self, reason: tristr::alias::String) -> Result<()> {
pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> {
let mut state = self.service.lock();
if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() {
bail!("cannot fall back while a service operation is pending");
@@ -496,7 +496,7 @@ mod tests {
ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(clash_verge_service_ipc::ProtocolInfo::current()),
}
}
@@ -713,7 +713,7 @@ mod tests {
for health in [
ServiceHealth::NotInstalled,
ServiceHealth::VersionMismatch,
ServiceHealth::Unavailable("boom".into()),
ServiceHealth::Unavailable("boom".to_owned()),
] {
let store = with_env(FakeEnv::new());
store.observe(health.clone());
@@ -858,7 +858,7 @@ mod tests {
#[tokio::test]
async fn awaiting_readiness_retries_a_silent_service_then_accepts_its_reply() {
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".into()), Ok(ready_reply())]));
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".to_owned()), Ok(ready_reply())]));
let state = store
.await_ready(2, Duration::ZERO)
+12 -16
View File
@@ -9,7 +9,6 @@ use clash_verge_service_ipc::{
};
use super::health::ServiceHealth;
use tristr::alias::String;
/// What the Service replied to a protocol-version query.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -51,7 +50,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
return match &reply.core {
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}").into())
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}"))
}
_ => ServiceVersionCheck::CoreUnavailable(
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
@@ -78,12 +77,9 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
reply.code, reply.message, client.epoch
)
};
ServiceVersionCheck::NeedsReinstall(
format!(
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
)
.into(),
)
ServiceVersionCheck::NeedsReinstall(format!(
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
))
}
/// Classify a version reply into the probe outcome used by [`classify_service_health`].
@@ -108,7 +104,7 @@ pub fn classify_service_health(
match current {
CurrentServiceProbe::Ready => ServiceHealth::Ready,
CurrentServiceProbe::VersionMismatch => ServiceHealth::VersionMismatch,
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.into()),
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.to_owned()),
CurrentServiceProbe::Missing if has_install_marker => ServiceHealth::VersionMismatch,
CurrentServiceProbe::Missing => ServiceHealth::NotInstalled,
}
@@ -123,7 +119,7 @@ mod tests {
ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(ProtocolInfo::current()),
}
}
@@ -158,7 +154,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: None,
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -176,7 +172,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 7,
message: "nope".into(),
message: "nope".to_owned(),
protocol: Some(ProtocolInfo::current()),
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -192,7 +188,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(info),
};
assert!(matches!(
@@ -213,7 +209,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(info),
};
assert!(matches!(
@@ -232,7 +228,7 @@ mod tests {
let reply = ServiceVersionReply {
core: None,
code,
message: "unsupported command".into(),
message: "unsupported command".to_owned(),
protocol: protocol.clone(),
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -254,7 +250,7 @@ mod tests {
fn an_unreachable_service_keeps_the_reason() {
assert_eq!(
classify_service_health(CurrentServiceProbe::Unavailable, true, "socket refused"),
ServiceHealth::Unavailable("socket refused".into())
ServiceHealth::Unavailable("socket refused".to_owned())
);
}
+15 -17
View File
@@ -5,7 +5,6 @@ use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
use std::collections::{BTreeMap, HashSet};
use std::path::{Component, Path, PathBuf};
use tristr::alias::String;
pub(crate) const GEO_ASSETS: &[&str] = &[
"Country.mmdb",
@@ -65,7 +64,7 @@ struct ProviderPathOwners {
pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root: &Path) -> Result<()> {
let config_root = std::fs::canonicalize(config_root)?;
let mut paths = BTreeMap::<String, ProviderPathOwners>::new();
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).into()).collect();
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).to_owned()).collect();
for section in ["proxy-providers", "rule-providers"] {
let Some(providers) = config.get(section).and_then(Value::as_mapping) else {
continue;
@@ -90,7 +89,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
match (is_remote, provider.get("url").and_then(Value::as_str)) {
(true, Some(url)) => owners
.remote
.entry(url.into())
.entry(url.to_owned())
.or_default()
.push((section, name.clone())),
_ => owners.local = true,
@@ -106,7 +105,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
for (url, providers) in owners.remote {
let replacement = allocate_provider_destination(&destination, &url, &mut reserved);
for (section, name) in providers {
config[section][&name]["path"] = Value::String(replacement.clone().into());
config[section][&name]["path"] = Value::String(replacement.clone());
logging!(
warn,
Type::Config,
@@ -132,16 +131,15 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
let mut suffix = String::new();
let mut sequence = 0;
loop {
let candidate: String = path
let candidate = path
.with_file_name(format!("cvr-{digest}{suffix}{extension}"))
.to_string_lossy()
.replace('\\', "/")
.into();
.replace('\\', "/");
if reserved.insert(candidate.clone()) {
return candidate;
}
sequence += 1;
suffix = format!("-{sequence}").into();
suffix = format!("-{sequence}");
}
}
@@ -169,7 +167,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
)?;
for filename in GEO_ASSETS {
let source = config_root.join(filename);
if source.is_file() && destinations.insert((*filename).into()) {
if source.is_file() && destinations.insert((*filename).to_string()) {
assets.push(RuntimeAsset {
source: std::fs::canonicalize(&source)?.to_string_lossy().into_owned(),
destination: (*filename).to_string(),
@@ -230,9 +228,9 @@ fn collect_provider_assets(
}
remote_providers.push(RemoteProviderRef {
section,
name: name.as_str().unwrap_or_default().into(),
name: name.as_str().unwrap_or_default().to_owned(),
provider: RemoteProvider {
destination: destination.clone().into(),
destination: destination.clone(),
url,
},
});
@@ -261,13 +259,13 @@ fn collect_provider_assets(
if destinations.insert(destination.clone()) {
assets.push(RuntimeAsset {
source: source.to_string_lossy().into_owned(),
destination: destination.clone().into(),
destination: destination.clone(),
});
}
destination
}
};
provider.insert(Value::String("path".to_owned()), Value::String(destination.into()));
provider.insert(Value::String("path".to_owned()), Value::String(destination));
}
Ok(())
}
@@ -341,7 +339,7 @@ fn normalized_destination(relative: &Path) -> Result<String> {
if destination.as_os_str().is_empty() {
bail!("local runtime provider destination is empty");
}
Ok(destination.to_string_lossy().replace('\\', "/").into())
Ok(destination.to_string_lossy().replace('\\', "/"))
}
#[cfg(test)]
@@ -387,12 +385,12 @@ mod tests {
for declared in readback {
assert!(bundle.remote_providers.contains(&declared.provider));
assert_eq!(
config[declared.section][declared.name.as_str()]["path"],
config[declared.section][&declared.name]["path"],
declared.provider.destination
);
assert_eq!(
config[declared.section][declared.name.as_str()]["url"],
original[declared.section][declared.name.as_str()]["url"]
config[declared.section][&declared.name]["url"],
original[declared.section][&declared.name]["url"]
);
}
std::fs::remove_dir_all(root)?;
+27 -35
View File
@@ -36,7 +36,6 @@ use std::{
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tristr::alias::String;
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
static ACTIVE_SERVICE_SESSION: Mutex<Option<ActiveServiceSession>> = Mutex::new(None);
@@ -63,7 +62,7 @@ pub(crate) fn is_not_auto_started(reason: &str) -> bool {
#[cfg(target_os = "windows")]
pub(crate) fn notify_service_fallback(reason: &str) {
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.into());
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.to_owned());
Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, "");
}
@@ -92,7 +91,7 @@ fn app_data_owner_command(credentials: &OwnerCredentials) -> Option<String> {
return None;
};
let path = credentials.app_data_dir.replace('\'', r"'\''");
Some(format!("sudo chown -R {uid}:{gid} '{path}'").into())
Some(format!("sudo chown -R {uid}:{gid} '{path}'"))
}
/// Capabilities of the service session that owns the running Core.
@@ -244,24 +243,22 @@ fn macos_service_install_markers() -> Vec<String> {
format!(
"/Library/LaunchDaemons/{}.plist",
clash_verge_service_ipc::MACOS_SERVICE_ID
)
.into(),
),
format!(
"/Library/PrivilegedHelperTools/{}.bundle",
clash_verge_service_ipc::MACOS_SERVICE_ID
)
.into(),
),
#[cfg(not(feature = "verge-dev"))]
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".into(),
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".to_owned(),
#[cfg(not(feature = "verge-dev"))]
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".into(),
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".to_owned(),
]
}
#[cfg(target_os = "macos")]
fn macos_service_install_marker_exists() -> std::io::Result<bool> {
for marker in macos_service_install_markers() {
if path_entry_exists_without_follow(Path::new(marker.as_str()))? {
if path_entry_exists_without_follow(Path::new(&marker))? {
return Ok(true);
}
}
@@ -338,8 +335,7 @@ pub(crate) fn trusted_service_evidence() -> Result<bool> {
output.status
);
}
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8_lossy(&output.stdout).trim() != "not-found")
Ok(String::from_utf8_lossy(&output.stdout).trim() != "not-found")
}
#[cfg(target_os = "macos")]
@@ -568,12 +564,12 @@ fn service_core_path(clash_core: &str, bin_ext: &str) -> Result<PathBuf> {
#[cfg(target_os = "macos")]
fn escape_osascript_double_quoted_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"").into()
value.replace('\\', "\\\\").replace('"', "\\\"")
}
#[cfg(target_os = "macos")]
fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', r"'\''")).into()
format!("'{}'", value.replace('\'', r"'\''"))
}
fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
@@ -645,7 +641,7 @@ fn uninstall_service() -> Result<()> {
let status = if linux_running_as_root() {
StdCommand::new(&uninstall_path).status()?
} else {
let result = StdCommand::new(elevator.as_str())
let result = StdCommand::new(&elevator)
.arg("--disable-internal-agent")
.arg(&uninstall_path)
.status()?;
@@ -701,7 +697,7 @@ fn uninstall_service() -> Result<()> {
}
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
let uninstall_shell: String = uninstall_path.to_string_lossy().into();
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
@@ -847,7 +843,7 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
if response.code > 0 {
return Ok(StageRequest::Refused {
code: response.code,
message: response.message.into(),
message: response.message,
});
}
response
@@ -883,7 +879,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
if store.state().mode == crate::core::manager::RunningMode::NotRunning
&& refusal.code != ServiceErrorCode::ProxyClearFailed as u16
{
store.observe(ServiceHealth::Unavailable(refusal.to_string().into()));
store.observe(ServiceHealth::Unavailable(refusal.to_string()));
}
refusal.into()
}
@@ -891,7 +887,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
/// Sidecar stays blocked until a reinstall replaces the residual helper, so ask for repair.
pub(super) fn record_residual_service(error: &anyhow::Error) {
if let Some(residual) = error.downcast_ref::<clash_verge_service_ipc::execution::ResidualServiceError>() {
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string().into()));
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string()));
}
}
@@ -905,7 +901,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
let proposed_session_token = generate_service_session_token()?;
let request = StartClashRequest {
runtime,
proposed_session_token: proposed_session_token.clone().into(),
proposed_session_token: proposed_session_token.clone(),
macos_proxy: None,
};
@@ -943,8 +939,8 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
&RUN_STATE,
ServiceStartRefusal {
code: response.code,
core_path: request.runtime.core_path.into(),
message: err_msg.into(),
core_path: request.runtime.core_path,
message: err_msg,
},
));
}
@@ -956,7 +952,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
*ACTIVE_SERVICE_SESSION.lock() = Some(ActiveServiceSession {
proof: OwnerSessionProof {
generation: result.session.generation,
token: proposed_session_token.into(),
token: proposed_session_token,
},
supports_runtime_staging: capabilities.runtime_staging,
supports_runtime_file_read: capabilities.runtime_file_read,
@@ -1016,10 +1012,7 @@ pub(super) async fn get_clash_logs_by_service() -> Result<Vec<String>> {
bail!(err_msg);
}
Ok(response
.data
.map(|logs| logs.into_iter().map(Into::into).collect())
.unwrap_or_default())
Ok(response.data.unwrap_or_default())
}
pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
@@ -1037,8 +1030,7 @@ pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
}
let encoded = response.data.context("服务未返回核心日志快照")?;
let content = decode_hex(&encoded).context("服务返回了无效的核心日志快照")?;
// from_utf8_lossy_owned exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8_lossy_owned(content).into())
Ok(String::from_utf8_lossy_owned(content))
}
fn decode_hex(encoded: &str) -> Result<Vec<u8>> {
@@ -1352,7 +1344,7 @@ async fn read_chunk(
}
Ok(Some(match response.data.context("服务未返回运行时文件")? {
RuntimeFileOutcome::Absent => (None, 0, None),
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex.into()), len, mtime_ns),
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex), len, mtime_ns),
}))
}
@@ -1364,11 +1356,11 @@ async fn provider_load_times() -> Result<ProviderLoadTimes> {
let proxies = proxies.map_err(|error| anyhow!("failed to query proxy providers: {error}"))?;
let mut loaded = ProviderLoadTimes::new();
for (name, provider) in rules.providers {
loaded.insert(("rule-providers", name.into()), epoch_nanos(&provider.updated_at));
loaded.insert(("rule-providers", name), epoch_nanos(&provider.updated_at));
}
for (name, provider) in proxies.providers {
loaded.insert(
("proxy-providers", name.into()),
("proxy-providers", name),
provider.updated_at.as_deref().and_then(epoch_nanos),
);
}
@@ -1594,7 +1586,7 @@ const fn owner_recovery_policy(reason: OwnerRecoveryReason, is_macos: bool) -> O
fn mark_service_unavailable_after_owner_loss<E: RunStateEnv>(store: &RunStateStore<E>, reason: OwnerRecoveryReason) {
if matches!(reason, OwnerRecoveryReason::TransportFailure) {
store.observe(ServiceHealth::Unavailable(
"service control IPC unavailable after sustained transport failure".into(),
"service control IPC unavailable after sustained transport failure".to_owned(),
));
}
}
@@ -1855,7 +1847,7 @@ async fn wait_for_service_ipc() -> Result<()> {
}
Err(ReadyWaitError::Unreachable(error)) => {
tracing::Span::current().record("outcome", "unreachable");
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}").into()));
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}")));
Err(error).context(CONTEXT)
}
Err(ReadyWaitError::Rejected(error)) => {
@@ -2670,7 +2662,7 @@ mod tests {
let outcome = super::run_action_restoring_sidecar(&store, was_allowed, async {
store.perform(PendingAction::Install).await?;
// Simulate `wait_for_service_ipc` recording health before it fails.
store.observe(ServiceHealth::Unavailable("service never answered".into()));
store.observe(ServiceHealth::Unavailable("service never answered".to_owned()));
bail!("service IPC did not become available")
})
.await;
+3 -3
View File
@@ -8,6 +8,7 @@ use anyhow::Result;
use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
sync::{
Arc,
@@ -17,7 +18,6 @@ use std::{
};
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
use tokio::sync::Mutex as TokioMutex;
use tristr::alias::String;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ProxyApplyStep {
@@ -677,8 +677,8 @@ mod tests {
(calls.into_inner(), result.is_ok())
}
fn failure_text(result: anyhow::Result<()>) -> String {
result.map_or_else(|error| format!("{error:#}").into(), |()| String::from("no failure"))
fn failure_text(result: anyhow::Result<()>) -> std::string::String {
result.map_or_else(|error| format!("{error:#}"), |()| "no failure".to_owned())
}
#[test]
+2 -2
View File
@@ -8,6 +8,7 @@ use crate::{
use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error};
use parking_lot::{Mutex, RwLock};
use smartstring::alias::String;
use std::{
collections::HashMap,
sync::{
@@ -22,7 +23,6 @@ use tokio::{
};
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key};
use tristr::alias::String;
enum TimerCommand {
Apply(HashMap<String, TaskSchedule>),
@@ -502,6 +502,7 @@ mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption};
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
use std::{
sync::{
@@ -514,7 +515,6 @@ mod tests {
use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue;
use tristr::alias::String;
struct CancellationProbe {
cancelled: Arc<AtomicBool>,
+1 -1
View File
@@ -19,6 +19,7 @@ use tokio::fs;
use super::handle;
use anyhow::Result;
use smartstring::alias::String;
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Duration;
@@ -26,7 +27,6 @@ use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
};
use tristr::alias::String;
mod menu_def;
#[cfg(target_os = "macos")]
+5 -5
View File
@@ -11,7 +11,6 @@ use std::{
};
use tauri_plugin_updater::{Update, UpdaterExt as _};
use tokio_util::sync::CancellationToken;
use tristr::alias::String;
pub struct SilentUpdater {
update_ready: AtomicBool,
@@ -56,8 +55,8 @@ impl SilentUpdater {
.with_context(|| format!("failed to write update cache {}", bin_path.display()))?;
let meta = UpdateCacheMeta {
version: version.into(),
downloaded_at: Utc::now().to_rfc3339().into(),
version: version.to_string(),
downloaded_at: Utc::now().to_rfc3339(),
};
let meta_path = cache_dir.join("pending_update.json");
std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)
@@ -134,8 +133,9 @@ fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str
use base64::Engine as _;
let decode = |value: &str| -> Result<String> {
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8(base64::engine::general_purpose::STANDARD.decode(value)?)?.into())
Ok(String::from_utf8(
base64::engine::general_purpose::STANDARD.decode(value)?,
)?)
};
let config: tauri_plugin_updater::Config = serde_json::from_value(
app_handle
+9 -3
View File
@@ -1,6 +1,7 @@
use anyhow::Result;
use scopeguard::defer;
use serde::Serialize;
use smartstring::alias::String;
use std::{
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
@@ -9,7 +10,6 @@ use std::{
};
use tauri_plugin_shell::ShellExt as _;
use tokio::fs;
use tristr::alias::String;
use crate::config::Config;
use crate::core::handle;
@@ -114,8 +114,14 @@ impl fmt::Display for ValidationSkipReason {
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ValidationOutcome {
Valid,
Invalid { kind: ValidationErrorKind, message: String },
Skipped { reason: ValidationSkipReason },
Invalid {
kind: ValidationErrorKind,
#[specta(type = std::string::String)]
message: String,
},
Skipped {
reason: ValidationSkipReason,
},
Busy,
}
+1 -1
View File
@@ -4,8 +4,8 @@ use crate::{
utils::{dirs, help},
};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
#[derive(Debug, Clone)]
pub struct ChainItem {
+1 -1
View File
@@ -1,5 +1,5 @@
use serde_yaml_ng::{Mapping, Value};
use tristr::alias::String;
use smartstring::alias::String;
pub const HANDLE_FIELDS: [&str; 12] = [
"mode",
+1 -1
View File
@@ -26,9 +26,9 @@ use anyhow::{Context as _, Result};
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tokio::fs;
use tristr::alias::String;
type ResultLog = Vec<(String, String)>;
#[derive(Debug)]
+13 -13
View File
@@ -6,8 +6,8 @@ use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function:
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::sync::Arc;
use tristr::alias::String;
const MAX_OUTPUTS: usize = 1000;
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
@@ -16,7 +16,7 @@ const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
type ScriptResult = (Mapping, Vec<String>, Vec<(String, String)>);
type EvalOutcome = (Result<Mapping, String>, Vec<(String, String)>);
type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>);
/// Never fails: exceptions are logged and the input config is returned unchanged.
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
@@ -37,9 +37,9 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
let evaluated = AsyncHandler::spawn_blocking(move || eval_script(&script, &json, &name));
let (outcome, mut logs) = match tokio::time::timeout(SCRIPT_TIMEOUT, evaluated).await {
Ok(Ok(evaluated)) => evaluated,
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}").into()), vec![]),
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}")), vec![]),
Err(_elapsed) => (
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}").into()),
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}")),
vec![],
),
};
@@ -59,7 +59,7 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
(result, changed_keys, logs)
}
Err(reason) => {
logs.push(("exception".into(), reason));
logs.push(("exception".into(), reason.into()));
(config, vec![], logs)
}
}
@@ -79,7 +79,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
let outputs_clone = Arc::clone(&outputs);
let total_size_clone = Arc::clone(&total_size);
let bail = |reason: String| (Err(reason), outputs.lock().to_vec());
let bail = |reason: std::string::String| (Err(reason), outputs.lock().to_vec());
let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe {
NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| {
@@ -137,13 +137,13 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
JsValue::from(JsString::from(config_json)),
Attribute::all(),
) {
return bail(format!("failed to bind config for script: {err}").into());
return bail(format!("failed to bind config for script: {err}"));
}
// 仅处理 name 参数中的特殊字符
let safe_name = escape_js_string_for_single_quote(name);
if safe_name.len() > 1024 {
return bail("Name parameter too long".into());
return bail("Name parameter too long".to_owned());
}
let code = format!(
@@ -157,19 +157,19 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
let result = match context.eval(Source::from_bytes(code.as_str())) {
Ok(result) if result.is_string() => result,
_ => return bail("main function should return object".into()),
_ => return bail("main function should return object".to_owned()),
};
let result = match result.to_string(&mut context) {
Ok(result) => result,
Err(e) => return bail(format!("Failed to convert JS result to string: {e}").into()),
Err(e) => return bail(format!("Failed to convert JS result to string: {e}")),
};
let result = match result.to_std_string() {
Ok(result) => result,
Err(_) => return bail("Failed to convert JS string to std string".into()),
Err(_) => return bail("Failed to convert JS string to std string".to_owned()),
};
if result.len() > MAX_JSON_SIZE {
return bail("Script result exceeds maximum allowed size".into());
return bail("Script result exceeds maximum allowed size".to_owned());
}
match parse_json_safely(&result) {
@@ -180,7 +180,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
Type::Config,
"Script execution error: {err:#}. Script name: {name}"
);
bail("Script execution failed".into())
bail("Script execution failed".to_owned())
}
}
}
+9 -10
View File
@@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Sequence, Value};
use std::collections::HashSet;
use tristr::alias::String;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
@@ -13,8 +12,8 @@ pub struct SeqMap {
fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
seq.iter()
.filter_map(|item| match item {
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(Into::into),
Value::String(name) => Some(name.to_owned().into()),
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(str::to_owned),
Value::String(name) => Some(name.to_owned()),
_ => None,
})
.collect()
@@ -59,10 +58,10 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
.into_iter()
.filter(|item| {
if let Value::String(s) = item {
!delete.iter().any(|deleted| deleted == s)
!delete.contains(s)
} else if let Value::Mapping(m) = item {
if let Some(Value::String(name)) = m.get("name") {
!delete.iter().any(|deleted| deleted == name)
!delete.contains(name)
} else {
true
}
@@ -100,7 +99,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
.into_iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.iter().any(|deleted| deleted == name)
!delete.contains(name)
} else {
true
}
@@ -120,12 +119,12 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
let mut seen_proxy_names = HashSet::new();
for name in &added_proxy_names {
if seen_proxy_names.insert(name.clone()) {
merged_proxies.push(Value::String(name.clone().into()));
merged_proxies.push(Value::String(name.clone()));
}
}
for value in base_group_proxies {
if let Value::String(name) = &value
&& !seen_proxy_names.insert(name.to_owned().into())
&& !seen_proxy_names.insert(name.to_owned())
{
continue;
}
@@ -181,7 +180,7 @@ proxy-groups:
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".into()],
delete: vec!["proxy1".to_string()],
};
config = use_seq(seq, config, "proxies");
@@ -318,7 +317,7 @@ proxy-groups: "invalid"
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".into()],
delete: vec!["proxy1".to_string()],
};
config = use_seq(seq, config, "proxies");
+4 -1
View File
@@ -12,14 +12,17 @@ use chrono::Utc;
use clash_verge_logging::{Type, logging, logging_error};
use reqwest_dav::list_cmd::ListFile;
use serde::Serialize;
use smartstring::alias::String;
use std::path::PathBuf;
use tokio::fs;
use tristr::alias::String;
#[derive(Debug, Serialize, specta::Type)]
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,
}
+1 -1
View File
@@ -10,8 +10,8 @@ use crate::{
use bytes::BytesMut;
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::sync::{Arc, LazyLock};
use tristr::alias::String;
#[allow(clippy::expect_used)]
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
+10 -13
View File
@@ -21,7 +21,6 @@ use std::{
process::Command,
sync::atomic::{AtomicU64, Ordering},
};
use tristr::alias::String;
const RELEASE_VERSION_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const ALPHA_BASE_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha";
@@ -51,7 +50,7 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
// A core already broken by the in-place updater cannot report a version; upgrading is the repair.
let installed = read_core_version(&target).unwrap_or_else(|error| {
logging!(warn, Type::Core, "core upgrade: unreadable installed core: {error:#}");
String::new()
std::string::String::new()
});
let (proxy, latest) = resolve_latest_version(alpha).await?;
@@ -173,15 +172,14 @@ fn managed_core_path(core: &str) -> Result<PathBuf> {
}
/// Pins the package to the resolved version, so a release moving on mid-upgrade cannot 404.
fn package_url(alpha: bool, version: &str) -> Result<String> {
fn package_url(alpha: bool, version: &str) -> Result<std::string::String> {
let asset = asset_base_name(alpha)?;
let extension = if cfg!(windows) { "zip" } else { "gz" };
Ok(if alpha {
format!("{ALPHA_BASE_URL}/{asset}-{version}.{extension}")
} else {
format!("{RELEASE_DOWNLOAD_URL}/{version}/{asset}-{version}.{extension}")
}
.into())
})
}
/// Mirrors the asset map in `scripts/prebuild.mjs` so an upgrade keeps the build variant
@@ -220,11 +218,11 @@ fn asset_base_name(alpha: bool) -> Result<&'static str> {
/// Returns the proxy that reached GitHub so the package download reuses it.
#[tracing::instrument(skip_all, level = "debug", fields(alpha))]
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, String)> {
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::String)> {
let url = if alpha {
String::from(format!("{ALPHA_BASE_URL}/version.txt"))
format!("{ALPHA_BASE_URL}/version.txt")
} else {
String::from(RELEASE_VERSION_URL)
RELEASE_VERSION_URL.to_owned()
};
let mut last_error = None;
@@ -236,7 +234,7 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, String)> {
match attempt {
Ok(response) if response.status().is_success() => {
// An error page answered with 200 must not become a version, nor reach the URL.
let version = String::from(response.text().trim());
let version = response.text().trim().to_owned();
if !is_usable_version(&version) {
logging!(
warn,
@@ -448,7 +446,7 @@ fn new_command(program: impl AsRef<OsStr>) -> Command {
}
/// Parses the version out of `Mihomo Meta v1.19.30 darwin arm64 with go1.22.12 ...`.
fn read_core_version(path: &Path) -> Result<String> {
fn read_core_version(path: &Path) -> Result<std::string::String> {
let output = new_command(path)
.arg("-v")
.output()
@@ -458,12 +456,11 @@ fn read_core_version(path: &Path) -> Result<String> {
bail!("{} -v exited with {}", path.display(), output.status);
}
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.split_whitespace()
.nth(2)
.map(String::from)
.map(str::to_owned)
.with_context(|| format!("unexpected version output from {}: {stdout}", path.display()))
}
+5 -2
View File
@@ -2,7 +2,7 @@ use crate::core::notify::{Refresh, announce};
use anyhow::{Result, ensure};
use clash_verge_draft::DraftTransaction;
use serde::Serialize;
use tristr::alias::String;
use smartstring::alias::String;
use crate::{
config::{
@@ -19,7 +19,10 @@ use crate::{
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DnsOverrideOutcome {
Applied,
ConfirmationRequired { source: String },
ConfirmationRequired {
#[specta(type = std::string::String)]
source: String,
},
}
pub async fn set_dns_override(
+9 -10
View File
@@ -1,17 +1,20 @@
use crate::{
cmd::{CmdResult, CommandFailure, StringifyErr as _},
cmd::{CmdResult, StringifyErr as _},
utils::dirs::{self, PathBufExec as _},
};
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::path::{Component, Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
use tristr::alias::String;
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
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,
}
@@ -40,7 +43,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
Ok(icon_path)
}
fn normalized_text_prefix(content: &[u8]) -> String {
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
let start = content
.iter()
@@ -48,8 +51,7 @@ fn normalized_text_prefix(content: &[u8]) -> String {
.unwrap_or(content.len());
let end = content.len().min(start.saturating_add(2048));
let prefix = &content[start..end];
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase().into()
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase()
}
fn looks_like_html(content: &[u8]) -> bool {
@@ -93,10 +95,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
if !is_supported_icon_content(&content) {
let _ = temp_path.remove_if_exists().await;
return Err(CommandFailure::plain(format!(
"Downloaded content is not a valid image: {}",
url.as_str()
)));
return Err(format!("Downloaded content is not a valid image: {}", url.as_str()).into());
}
{
@@ -165,7 +164,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
match fs::copy(file_path, &dest_path).await {
Ok(_) => Ok(dest_path.to_string_lossy().into()),
Err(err) => Err(CommandFailure::plain(err)),
Err(err) => Err(err.to_string().into()),
}
} else {
Err("file not found".into())
+2 -2
View File
@@ -8,7 +8,7 @@ use crate::{
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
use tristr::alias::String;
use smartstring::alias::String;
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
@@ -231,7 +231,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
};
let message = mask_err(&message);
let message = if profile_persisted {
format!("订阅已保存,但未能确认已应用到内核: {message}").into()
format!("订阅已保存,但未能确认已应用到内核: {message}")
} else {
message
};
+5 -6
View File
@@ -1,5 +1,4 @@
use crate::cmd;
use tristr::alias::String;
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
tauri_specta::Builder::new()
@@ -122,7 +121,7 @@ pub struct WebDavFile {
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
Self {
href: file.href.into(),
href: file.href,
last_modified: file.last_modified,
}
}
@@ -149,20 +148,20 @@ pub struct IpAddress {
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
fn from(interface: network_interface::NetworkInterface) -> Self {
Self {
name: interface.name.into(),
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().into(),
ip: address.ip.to_string(),
}),
network_interface::Addr::V6(address) => InterfaceAddress::V6(IpAddress {
ip: address.ip.to_string().into(),
ip: address.ip.to_string(),
}),
})
.collect(),
mac_addr: interface.mac_addr.map(Into::into),
mac_addr: interface.mac_addr,
}
}
}
+14 -18
View File
@@ -11,7 +11,6 @@ use std::{
str::FromStr,
};
use tokio::io::AsyncWriteExt as _;
use tristr::alias::String;
pub async fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
@@ -35,13 +34,12 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
let bytes = tokio::fs::read(path)
.await
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
let yaml_str = std::string::String::from_utf8(bytes)
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
let yaml_str =
String::from_utf8(bytes).with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
let label = path.to_path_buf();
// Parsing a multi-megabyte profile is pure CPU; keep it off the async worker.
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str.into(), label))
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str, label))
.await
.map_err(|join| anyhow!("yaml parse task failed: {join}"))?
}
@@ -141,7 +139,7 @@ const ALPHABET: [char; 62] = [
pub fn get_uid(prefix: &str) -> String {
let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}").into()
format!("{prefix}{id}")
}
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
@@ -173,7 +171,7 @@ pub fn mask_url(url: &str) -> String {
let scheme_and_host = &path_part[..host_end];
let path = &path_part[host_end..]; // starts with '/' or empty
let mut result: String = scheme_and_host.into();
let mut result = scheme_and_host.to_owned();
if !path.is_empty() {
let masked: Vec<&str> = path
@@ -188,8 +186,8 @@ pub fn mask_url(url: &str) -> String {
let masked_query: Vec<String> = query
.split('&')
.map(|param| match param.find('=') {
Some(eq) => format!("{}=***", &param[..eq]).into(),
None => param.into(),
Some(eq) => format!("{}=***", &param[..eq]),
None => param.to_owned(),
})
.collect();
result.push_str(&masked_query.join("&"));
@@ -200,8 +198,7 @@ pub fn mask_url(url: &str) -> String {
/// Masks every HTTP(S) URL embedded in a log string while preserving surrounding text.
pub fn mask_err(err: &str) -> String {
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
let mut result = std::string::String::with_capacity(err.len());
let mut result = String::with_capacity(err.len());
let mut remaining = err;
loop {
@@ -227,7 +224,7 @@ pub fn mask_err(err: &str) -> String {
remaining = &remaining[url_end..];
}
result.into()
result
}
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
@@ -238,8 +235,7 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
Some(
percent_encoding::percent_decode_str(last_segment)
.decode_utf8_lossy()
.to_string()
.into(),
.to_string(),
)
}
@@ -281,15 +277,15 @@ pub fn linux_elevator() -> String {
Ok(output) => {
if !output.stdout.is_empty() {
if let Ok(path) = std::str::from_utf8(&output.stdout) {
path.trim().into()
path.trim().to_string()
} else {
"sudo".into()
"sudo".to_string()
}
} else {
"sudo".into()
"sudo".to_string()
}
}
Err(_) => "sudo".into(),
Err(_) => "sudo".to_string(),
}
}
+14 -15
View File
@@ -11,7 +11,6 @@ use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use tristr::alias::String;
pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<()> {
let Some(path) = mimeapps_list_path() else {
@@ -47,19 +46,19 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
if trimmed.eq_ignore_ascii_case("[Default Applications]") {
default_present = true;
current_section = Some(SectionKind::DefaultApplications);
output_lines.push("[Default Applications]".into());
output_lines.push("[Default Applications]".to_string());
continue;
} else if trimmed.eq_ignore_ascii_case("[Added Associations]") {
current_section = Some(SectionKind::AddedAssociations);
output_lines.push("[Added Associations]".into());
output_lines.push("[Added Associations]".to_string());
continue;
}
}
if current_section.is_some() {
section_buffer.push(line.into());
section_buffer.push(line.to_string());
} else {
output_lines.push(line.into());
output_lines.push(line.to_string());
}
}
@@ -79,9 +78,9 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
if output_lines.last().is_some_and(|line| !line.is_empty()) {
output_lines.push(String::new());
}
output_lines.push("[Default Applications]".into());
output_lines.push("[Default Applications]".to_string());
for &scheme in schemes {
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
}
}
@@ -172,7 +171,7 @@ fn flush_section(
.split(';')
.filter_map(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.into())
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
.collect();
@@ -188,7 +187,7 @@ fn flush_section(
.split(';')
.filter_map(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.into())
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
.collect();
@@ -204,10 +203,10 @@ fn flush_section(
merged_values.insert(0, moved);
}
} else {
merged_values.insert(0, desktop_file.into());
merged_values.insert(0, desktop_file.to_string());
}
let mut merged_line: String = format!("{existing_prefix}x-scheme-handler/{scheme}=").into();
let mut merged_line = format!("{existing_prefix}x-scheme-handler/{scheme}=");
merged_line.push_str(&merged_values.join(";"));
merged_line.push(';');
@@ -223,16 +222,16 @@ fn flush_section(
if let Some(pos) = values.iter().position(|value| value == desktop_file) {
if pos != 0 {
values.remove(pos);
values.insert(0, desktop_file.into());
values.insert(0, desktop_file.to_string());
*changed = true;
}
} else {
values.insert(0, desktop_file.into());
values.insert(0, desktop_file.to_string());
*changed = true;
}
let prefix = line.chars().take_while(|c| c.is_whitespace()).collect::<String>();
let mut new_line: String = format!("{prefix}x-scheme-handler/{scheme}=").into();
let mut new_line = format!("{prefix}x-scheme-handler/{scheme}=");
new_line.push_str(&values.join(";"));
new_line.push(';');
@@ -254,7 +253,7 @@ fn flush_section(
if ensure_all {
for &scheme in schemes {
if !seen.contains_key(scheme) {
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
*changed = true;
}
}
+6 -6
View File
@@ -5,10 +5,10 @@ use reqwest::{
Client, Proxy, StatusCode,
header::{HeaderMap, HeaderValue, USER_AGENT},
};
use smartstring::alias::String;
use std::{sync::Arc, time::Duration};
use sysproxy::Sysproxy;
use tauri::Url;
use tristr::alias::String;
#[derive(Debug)]
pub struct HttpResponse {
@@ -57,7 +57,7 @@ impl NetworkManager {
fn build_client(
&self,
proxy_url: Option<String>,
proxy_url: Option<std::string::String>,
default_headers: HeaderMap,
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
@@ -76,7 +76,7 @@ impl NetworkManager {
// 设置代理
if let Some(proxy_str) = proxy_url {
let proxy = Proxy::all(proxy_str.as_str())?;
let proxy = Proxy::all(proxy_str)?;
builder = builder.proxy(proxy);
} else {
builder = builder.no_proxy();
@@ -226,15 +226,15 @@ impl NetworkManager {
accept_invalid_certs: bool,
tls_root_mode: TlsRootMode,
) -> Result<Client> {
let proxy_url: Option<String> = match proxy_type {
let proxy_url: Option<std::string::String> = match proxy_type {
ProxyType::None => None,
ProxyType::Localhost => {
let port = MixedPort::desired().await;
Some(format!("http://127.0.0.1:{port}").into())
Some(format!("http://127.0.0.1:{port}"))
}
ProxyType::System => {
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
Some(format!("http://{}:{}", p.host, p.port).into())
Some(format!("http://{}:{}", p.host, p.port))
} else {
None
}
+7 -7
View File
@@ -1,8 +1,8 @@
use crate::core::notify::NoticeStatus;
use anyhow::Result;
use percent_encoding::percent_decode_str;
use smartstring::alias::String;
use tauri::Url;
use tristr::alias::String;
use crate::{
config::{Config, PrfItem, profiles},
@@ -39,7 +39,7 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
Ok(())
}
fn extract_subscription_info(link_parsed: &Url) -> Option<(String, Option<String>)> {
fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String, Option<String>)> {
if !matches!(link_parsed.scheme(), "clash" | "clash-verge") {
return None;
}
@@ -52,7 +52,7 @@ fn extract_subscription_info(link_parsed: &Url) -> Option<(String, Option<String
Some((url, name))
}
fn extract_subscription_url(link_parsed: &Url) -> Option<String> {
fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
let query = link_parsed.query()?;
let prefix = "url=";
let pos = query.find(prefix)?;
@@ -60,15 +60,15 @@ fn extract_subscription_url(link_parsed: &Url) -> Option<String> {
Some(decode_subscription_url(raw_url))
}
fn decode_subscription_url(raw_url: &str) -> String {
fn decode_subscription_url(raw_url: &str) -> std::string::String {
// Avoid double-decoding nested subscription URLs; decode only when needed.
if Url::parse(raw_url).is_ok() {
return String::from(raw_url);
return raw_url.to_string();
}
let mut candidate = String::from(raw_url);
let mut candidate = raw_url.to_string();
for _ in 0..2 {
let next = String::from(percent_decode_str(&candidate).decode_utf8_lossy());
let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string();
if next == candidate {
break;
}
+12 -17
View File
@@ -5,7 +5,6 @@ use std::fs;
use std::os::windows::process::CommandExt as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tristr::alias::String;
use windows::Win32::Globalization::{GetACP, GetOEMCP, MULTI_BYTE_TO_WIDE_CHAR_FLAGS, MultiByteToWideChar};
const CREATE_NO_WINDOW: u32 = 0x08000000;
@@ -73,11 +72,11 @@ fn get_task_user_id() -> Result<String> {
if let Some(domain) = domain {
let domain = domain.trim();
if !domain.is_empty() {
return Ok(format!("{domain}\\{username}").into());
return Ok(format!("{domain}\\{username}"));
}
}
Ok(username.into())
Ok(username.to_string())
}
fn get_startup_dir() -> Result<PathBuf> {
@@ -119,8 +118,7 @@ fn task_xml_path(mode: TaskMode) -> Result<PathBuf> {
}
fn xml_escape(value: &str) -> String {
// Per-char pushes must stay on the std String: TriStr growth re-materializes per push.
let mut escaped = std::string::String::with_capacity(value.len());
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => escaped.push_str("&amp;"),
@@ -131,7 +129,7 @@ fn xml_escape(value: &str) -> String {
_ => escaped.push(ch),
}
}
escaped.into()
escaped
}
fn build_task_xml(mode: TaskMode) -> Result<String> {
@@ -184,8 +182,7 @@ fn build_task_xml(mode: TaskMode) -> Result<String> {
user_id,
mode.xml_run_level(),
exe_path
)
.into())
))
}
fn encode_utf16le_with_bom(content: &str) -> Vec<u8> {
@@ -229,13 +226,12 @@ fn decode_with_code_page(bytes: &[u8], code_page: u32) -> Option<String> {
}
wide.truncate(written as usize);
// from_utf16_lossy exists only on the std String type; the tristr alias shadows the bare name.
Some(std::string::String::from_utf16_lossy(&wide).into())
Some(String::from_utf16_lossy(&wide))
}
fn decode_console_output(bytes: &[u8]) -> String {
if let Ok(text) = std::str::from_utf8(bytes) {
return text.into();
return text.to_string();
}
let oem = unsafe { GetOEMCP() };
@@ -248,8 +244,7 @@ fn decode_console_output(bytes: &[u8]) -> String {
return text;
}
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8_lossy(bytes).into()
String::from_utf8_lossy(bytes).to_string()
}
fn output_message(output: &Output) -> String {
@@ -259,10 +254,10 @@ fn output_message(output: &Output) -> String {
let stderr = stderr.trim();
match (stdout.is_empty(), stderr.is_empty()) {
(true, true) => "unknown error".into(),
(false, true) => stdout.into(),
(true, false) => stderr.into(),
(false, false) => format!("{stdout} | {stderr}").into(),
(true, true) => "unknown error".to_string(),
(false, true) => stdout.to_string(),
(true, false) => stderr.to_string(),
(false, false) => format!("{stdout} | {stderr}"),
}
}
+1 -2
View File
@@ -16,7 +16,6 @@ use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::oneshot;
use tristr::alias::String;
use warp::Filter as _;
const INSTANCE_TOKEN_HEADER: &str = "x-instance-token";
@@ -142,7 +141,7 @@ async fn notify_existing_instance(record: &InstanceRecord) -> bool {
let request = client.get(format!("http://127.0.0.1:{}/commands/visible", record.port));
request
.header(INSTANCE_TOKEN_HEADER, record.token.as_str())
.header(INSTANCE_TOKEN_HEADER, &record.token)
.send()
.await
.is_ok_and(|response| response.status().is_success())
+4 -6
View File
@@ -1,6 +1,5 @@
use serde::Serialize;
use serde_yaml_ng::Result;
use tristr::alias::String;
pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
let yaml = serde_yaml_ng::to_string(data)?;
@@ -8,8 +7,7 @@ pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
}
fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
let mut result = std::string::String::with_capacity(yaml.len());
let mut result = String::with_capacity(yaml.len());
let mut in_fake_ip_filter = false;
let mut fake_ip_filter_indent = 0;
@@ -41,7 +39,7 @@ fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
result.push('\n');
}
result.into()
result
}
fn leading_spaces(line: &str) -> usize {
@@ -52,12 +50,12 @@ fn quote_wildcard_item(item: &str) -> String {
if !item.starts_with('\"') && !item.starts_with('\'') && contains_wildcard(item) {
quote_string(item)
} else {
item.into()
item.to_string()
}
}
fn quote_string(s: &str) -> String {
("\'".to_string() + s.replace('\'', "''").as_str() + "\'").into()
"\'".to_string() + s.replace('\'', "''").as_str() + "\'"
}
fn contains_wildcard(value: &str) -> bool {
+37 -9
View File
@@ -4,10 +4,19 @@ import {
VisibilityOffOutlined,
VisibilityOutlined,
} from '@mui/icons-material'
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
import {
Box,
Button,
FormControlLabel,
IconButton,
Skeleton,
Switch,
Typography,
} from '@mui/material'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useEffect } from 'foxact/use-abortable-effect'
import { useIntersection } from 'foxact/use-intersection'
import { useLocalStorage } from 'foxact/use-local-storage'
import type { XOR } from 'foxts/ts-xor'
import {
forwardRef,
@@ -99,9 +108,13 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
export const IpInfoCard = () => {
const { t } = useTranslation()
const [showIp, setShowIp] = useState(false)
const [autoRefresh, setAutoRefresh] = useLocalStorage(
'ip-info-auto-refresh',
true,
)
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
// Once intersected, refreshes stay enabled until unmount.
// Once intersected, the card stays eligible for refresh until unmount.
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
rootMargin: '0px',
})
@@ -151,6 +164,8 @@ export const IpInfoCard = () => {
})
useEffect(() => {
if (!autoRefresh) return
let timer: number | null = null
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
@@ -190,7 +205,7 @@ export const IpInfoCard = () => {
if (timer != null) clearInterval(timer)
document.removeEventListener('visibilitychange', onVisibilityChange)
}
}, [hasIntersected])
}, [autoRefresh, hasIntersected])
const toggleShowIp = useCallback(() => {
setShowIp((prev) => !prev)
@@ -364,12 +379,25 @@ export const IpInfoCard = () => {
fontSize: '0.7rem',
}}
>
<Typography variant="caption">
{t('home.components.ipInfo.labels.autoRefresh')}
{countdown.type === 'countdown'
? `: ${countdown.remainingSeconds}s`
: '...'}
</Typography>
<FormControlLabel
sx={{ m: 0 }}
control={
<Switch
size="small"
checked={autoRefresh}
onChange={(_, checked) => setAutoRefresh(checked)}
/>
}
label={
<Typography variant="caption">
{t('home.components.ipInfo.labels.autoRefresh')}
{autoRefresh &&
(countdown.type === 'countdown'
? `: ${countdown.remainingSeconds}s`
: '...')}
</Typography>
}
/>
<Typography
variant="caption"
sx={{