Compare commits

..
Author SHA1 Message Date
renovate[bot] 0246340254 chore(deps): update dependency rust to v1.100.0 2026-11-13 07:43:13 +00:00
Tunglies 8ca401a742 chore(rust): upgrade to 1.100 and consolidate Clippy rules (#8636) 2026-11-13 05:29:47 +00:00
Tunglies 3cfef4a990 perf(logs): skip unnecessary filtering work 2026-11-13 09:55:00 +08:00
Tunglies bbb0a0267f perf(traffic): avoid dynamic tooltip style rules 2026-11-13 09:55:00 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 119ee86082 chore(deps): update dependency knip to v6.45.1 (#8634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-12 16:55:56 +00:00
c2e36ab733 fix(home): keep the delay chip inside the current node card for long node names (#8633)
The node row is a space-between flex row, but the name column had the
default min-width: auto and the name was not truncated. A long name with
no break opportunity (for example xxx@host:443) widened the column to the
full text, pushed the delay chip past the card edge and made the main
content area scroll horizontally on narrow windows.

Let the name column shrink, truncate the name with an ellipsis (the full
name is in the title tooltip), and keep the chip from shrinking.

Fixes #8589

Co-authored-by: toBerlinWay <toBerlinWay@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:45:05 +00:00
03d5c3715f fix(macos): sync titlebar backgrounds with theme changes
The window and webview backgrounds retain their startup colors when
the resolved theme changes. With Tauri's full-size content view,
these colors affect the native titlebar even after setTheme succeeds.

Synchronize both backgrounds through WebviewWindow.setBackgroundColor
on macOS, reuse the startup palette, and allow both background commands.

Wry commit 1d1b870 implemented macOS background colors through
WKWebView.underPageBackgroundColor. CVR adopted that behavior in
7d29c0c6 (#6318), making the fixed startup colors from 683667f8 (#5637)
effective on macOS.

Closes #8205
Closes #7968
Closes #8272
Closes #8625
Upstream: https://github.com/tauri-apps/wry/commit/1d1b8707665b4638e040dde7a88cd33d77ba2ebc

Co-authored-by: qzda <qinzhidaoa@gmail.com>
Co-authored-by: Vickers Zhu <vickerszhu@163.com>
2026-11-12 10:47:22 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> b89d09e24e chore(deps): update cargo dependencies (#8630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-12 10:46:45 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> abd89a1457 chore(deps): update node.js to v26.13.1 (#8629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-12 10:46:45 +08:00
Tunglies 2c60f55f45 feat(connections): add network protocol filter (#8611)
* feat(connections): add network protocol filter

Filter connections by metadata.network (All/TCP/UDP) in both table and
list layouts, combined with search, and kept across page switches like
the existing search state.

Refs #6140

* fix(connections): keep network filter labels in English

Remove the All Networks translation and use All alongside TCP and UDP.\n\nRefs #6140

* fix(connections): narrow network filter select
2026-11-11 09:50:25 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> b8c13c0370 chore(deps): update dependency github/gh-aw to v1.2.2 (#8620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-11 04:02:03 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f86db4b344 chore(deps): update cargo dependencies (#8619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 17:29:51 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>❤是纱雾酱哟~
389dddf512 chore(deps): update github/gh-aw-actions action to v1.2.2 (#8617)
* chore(deps): update github/gh-aw-actions action to v1.2.2

* ci(workflows): recompile workflow lockfiles with gh-aw v1.2.2

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

* ci(workflows): set permissions to suppress tips

Agentic workflows of this repository are using available credits by some
collaborators' personal API Key. Configuring permissions of these
workflows to suppress warnings by GitHub Agentic Workflow extension
during lockfile compilation.

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-11-10 14:01:34 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 304a65fc36 chore(deps): update npm dependencies (#8615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 05:17:19 +00:00
70 changed files with 1164 additions and 665 deletions
+1
View File
@@ -1,2 +1,3 @@
avoid-breaking-exported-api = true
allow-unwrap-in-tests = true
cognitive-complexity-threshold = 25
+2 -2
View File
@@ -4,11 +4,11 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "26.13.0",
"version": "26.13.1",
"pnpmVersion": "12.18.0"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.1",
"version": "1.100.0",
"profile": "default"
},
"ghcr.io/devcontainers/features/git:1": {},
+3 -3
View File
@@ -5,10 +5,10 @@
"version": "v9.0.0",
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
},
"github/gh-aw-actions/setup@v1.0.1": {
"github/gh-aw-actions/setup@v1.2.2": {
"repo": "github/gh-aw-actions/setup",
"version": "v1.0.1",
"sha": "221ba4f4319de6b8e809d1a15528b3ae6a818aa5"
"version": "v1.2.2",
"sha": "53d0749e19424a918acd82b4d140264ce48f2f14"
}
},
"containers": {
+8 -8
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -154,7 +154,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -182,7 +182,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -240,7 +240,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -262,7 +262,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -373,7 +373,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -395,7 +395,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -479,7 +479,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6.1.1
name: Install pnpm
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
+3 -3
View File
@@ -31,9 +31,9 @@ jobs:
with:
persist-credentials: false
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@406c7719e31d7fe354224d4bb8fe3d4aeb47da1c # v1.2.1
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
with:
version: v1.0.1
version: v1.2.2
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -49,7 +49,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Install frontend dependencies
env:
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+3 -3
View File
@@ -74,7 +74,7 @@ jobs:
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@1.99.1
uses: dtolnay/rust-toolchain@1.100.0
- name: Rust Cache
if: github.event.inputs[matrix.input] == 'true'
@@ -104,7 +104,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -121,7 +121,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.100.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
File diff suppressed because one or more lines are too long
+1
View File
@@ -33,6 +33,7 @@ max-daily-ai-credits: 5000
permissions:
issues: read
pull-requests: read
copilot-requests: none
pre-agent-steps:
- name: Make Copilot quota exhaustion non-blocking
+288 -107
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -30,6 +30,7 @@ permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: none
steps:
- name: Load review rules from the base revision
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
if: github.event.action != 'closed'
+11 -11
View File
@@ -46,7 +46,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- name: Validate source and publish the approved snapshot
id: source
env:
@@ -203,7 +203,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -231,7 +231,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -308,7 +308,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -330,7 +330,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -439,7 +439,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -461,7 +461,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -542,7 +542,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
with:
run_install: false
@@ -565,7 +565,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -593,7 +593,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -661,7 +661,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
File diff suppressed because one or more lines are too long
@@ -28,6 +28,7 @@ permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: none
max-ai-credits: 60
max-turns: 12
max-daily-ai-credits: 500
+2 -2
View File
@@ -22,13 +22,13 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
toolchain: '1.100.0'
targets: x86_64-pc-windows-msvc
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- name: Install pnpm
uses: pnpm/action-setup@v6
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -1,3 +1,3 @@
nodejs 26.13.0
nodejs 26.13.1
pnpm 12.18.0
rust 1.99.1
rust 1.100.0
Generated
+8 -9
View File
@@ -159,9 +159,9 @@ dependencies = [
[[package]]
name = "arc-swap"
version = "1.9.4"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4c94f57a570fce5d381be3289b3f79aef6332d72948fe82ab5772e423ccf92"
checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5"
dependencies = [
"rustversion",
]
@@ -1123,7 +1123,6 @@ dependencies = [
"rfd 0.18.0",
"runas",
"rustls",
"scopeguard",
"serde",
"serde_json",
"serde_yaml_ng",
@@ -7073,9 +7072,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.40.1"
version = "0.40.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
checksum = "a86bac2f7d06e6384984118ad727f5694110f0ec44d924d05c8fdec3d030bee6"
dependencies = [
"libc",
"memchr",
@@ -9088,9 +9087,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
checksum = "7265c9301c16a8711c992c2d8da90ee77ef54b1ed51482d6dfeb00dc756b1297"
dependencies = [
"rustls-pki-types",
]
@@ -10126,9 +10125,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.4.2"
version = "9.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3845f734baabdd02de353a20580e30ceccd03897def44a4eb8e9654992e294"
checksum = "e3476fd497ac8af5b1ec7b51acab7bef380f7e8dd8e0487595bafd839417a302"
dependencies = [
"aes",
"bzip2",
+1
View File
@@ -132,6 +132,7 @@ large_const_arrays = "warn"
large_futures = "deny"
large_stack_arrays = "warn"
large_types_passed_by_value = "deny"
map_unwrap_or = "deny"
missing_const_for_fn = "deny"
mutex_atomic = "deny"
mutex_integer = "deny"
+3 -1
View File
@@ -29,17 +29,20 @@
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
- 修复窗口较窄时,首页「当前节点」卡片中过长的节点名把延迟标签挤出卡片
**🍎 macOS**
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
- 修复 macOS 切换主题后标题栏保留原有深浅色
</details>
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增连接页面按网络协议(TCP/UDP)筛选连接
</details>
@@ -48,7 +51,6 @@
- 优化连接、规则和日志筛选:切换页面后保留过滤条件
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
- 优化连接和规则页面的资源占用,减少连接列表更新及排序时的卡顿
**🍎 macOS**
+4 -4
View File
@@ -206,10 +206,10 @@ impl Logger {
.map(|s| (*s).to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Unknown panic payload".to_string());
let location = info
.location()
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
.unwrap_or_else(|| "Unknown location".to_string());
let location = info.location().map_or_else(
|| "Unknown location".to_string(),
|loc| format!("{}:{}", loc.file(), loc.line()),
);
logging!(error, Type::System, "Panic occurred at {}: {}", location, payload);
Self::global().flush_logs();
std::thread::sleep(Duration::from_millis(100));
@@ -13,14 +13,13 @@ pub(super) async fn check_chatgpt(client: &Client) -> UnlockItem {
let web_status = get_text(client, "https://api.openai.com/compliance/cookie_requirements")
.await
.map(|body| {
.map_or("Failed", |body| {
if body.to_ascii_lowercase().contains("unsupported_country") {
"Unsupported Country/Region"
} else {
"Yes"
}
})
.unwrap_or("Failed");
});
UnlockItem::checked(CHATGPT_WEB_NAME, web_status, region)
}
@@ -157,11 +157,10 @@ async fn is_unavailable(client: &Client) -> bool {
.get("https://disneyplus.com")
.send()
.await
.map(|response| {
.map_or(true, |response| {
let url = response.url().as_str();
url.contains("preview") || url.contains("unavailable")
})
.unwrap_or(true)
}
async fn fallback_region(client: &Client) -> Option<UnlockItem> {
@@ -114,11 +114,10 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
}
match response.json::<FastResponse>().await {
Ok(data) => data
.targets
.first()
.map(|target| UnlockItem::checked_region(NETFLIX_NAME, "Yes", &target.location.country))
.unwrap_or_else(|| item("Unknown")),
Ok(data) => data.targets.first().map_or_else(
|| item("Unknown"),
|target| UnlockItem::checked_region(NETFLIX_NAME, "Yes", &target.location.country),
),
Err(e) => {
logging!(error, Type::Network, "解析 Fast.com API 响应失败: {e}");
item("Failed (解析错误)")
@@ -12,7 +12,7 @@ parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] }
sysinfo = { version = "0.40.3", default-features = false, features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.191"
@@ -118,8 +118,7 @@ fn is_binary_admin() -> bool {
#[cfg(windows)]
Token::with_current_process()
.and_then(|token| token.privilege_level())
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false)
.is_ok_and(|level| level != PrivilegeLevel::NotPrivileged)
}
#[inline]
+3 -3
View File
@@ -67,7 +67,7 @@
"dayjs": "1.11.23",
"foxact": "^0.3.12",
"foxts": "^5.9.1",
"i18next": "^26.4.2",
"i18next": "^27.0.0",
"js-yaml": "^5.4.2",
"lodash-es": "^4.18.1",
"meta-json-schema": "^1.19.31",
@@ -79,7 +79,7 @@
"react-dom": "19.3.1",
"react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0",
"react-i18next": "17.1.13",
"react-i18next": "18.0.1",
"react-markdown": "10.1.0",
"react-router": "^8.4.0",
"rehype-raw": "^7.0.0",
@@ -121,7 +121,7 @@
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.44.2",
"knip": "6.45.1",
"lint-staged": "^17.5.1",
"sass": "^1.104.1",
"tar": "^7.5.22",
+27 -31
View File
@@ -239,8 +239,8 @@ importers:
specifier: ^5.9.1
version: 5.9.1
i18next:
specifier: ^26.4.2
version: 26.4.2(typescript@6.0.3)
specifier: ^27.0.0
version: 27.0.1(typescript@6.0.3)
js-yaml:
specifier: ^5.4.2
version: 5.4.2
@@ -275,8 +275,8 @@ importers:
specifier: ^7.88.0
version: 7.88.0(react@19.3.1)
react-i18next:
specifier: 17.1.13
version: 17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
specifier: 18.0.1
version: 18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
@@ -396,8 +396,8 @@ importers:
specifier: ^2.7.0
version: 2.7.0
knip:
specifier: 6.44.2
version: 6.44.2
specifier: 6.45.1
version: 6.45.1
lint-staged:
specifier: ^17.5.1
version: 17.5.1
@@ -3689,8 +3689,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next@26.4.2:
resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
i18next@27.0.1:
resolution: {integrity: sha512-KaE5ozP8UUeVpOJIwMVbbnpRKpyYQuxd9ZbseJoDzezE3nvLnZZQn20pkzyIJwPI2Uet/Qs3WzPTG812f6F/Eg==}
engines: {node: '>=22'}
peerDependencies:
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
@@ -3855,8 +3856,8 @@ packages:
keyv@5.6.0:
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
knip@6.44.2:
resolution: {integrity: sha512-6URO7pnqtOnyvSsXfW6+8C9j1lfcicxUCysXO0RgMtHTZ96HqCXh29op8iabeQW5JeIc8dJWwzSdJVWDokeScQ==}
knip@6.45.1:
resolution: {integrity: sha512-zp3zXKy2cZWiIttdlWU5fisKxVQaLSU6471ej0KGz1GdHdaHxpzTAO/LIyFNc3gU3v1W7LJaqvYumoO3l/u+6A==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -4396,18 +4397,16 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@17.1.13:
resolution: {integrity: sha512-ZxhOSlyKSKPtbUNPLwYMmE3w3bhoc0jaZJZ6bc7wGOT6cLbrbs//6m01HxCLtFdSjHvFpwFwoPhxxzIlHSs9cw==}
react-i18next@18.0.1:
resolution: {integrity: sha512-kPWXI0E/lrPj3+/umcKNzYZVnWmEedLiamMzDIHfbs+5ANf6yzvlZsjKou8f7kaGkfMMDLIZBejV+4aSN1CyBw==}
engines: {node: '>=22'}
peerDependencies:
i18next: '>= 26.2.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
'@types/react': '*'
i18next: '>= 27.0.1'
react: '>= 18.0.0'
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
react-dom:
optional: true
react-native:
'@types/react':
optional: true
typescript:
optional: true
@@ -4805,8 +4804,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
unbash@6.0.1:
resolution: {integrity: sha512-0db9eS5iqybux41zLkX69nk90KTci7F7eZ+gXzEG/Q7E0tAqEBhRh5fkZvk1N7jT7QkGWqq34i8SFGPFEe1V8A==}
unbash@6.0.4:
resolution: {integrity: sha512-t3OptUArewg9OpYGDskTSlxhyLDahfmZ4d36hE44Efb1y3HRLLpzsVqwH+AwBVymP4t7pP71xN+nnduudn2awA==}
engines: {node: '>=14'}
undici-types@6.21.0:
@@ -8533,7 +8532,7 @@ snapshots:
husky@9.1.7: {}
i18next@26.4.2(typescript@6.0.3):
i18next@27.0.1(typescript@6.0.3):
optionalDependencies:
typescript: 6.0.3
@@ -8685,7 +8684,7 @@ snapshots:
dependencies:
'@keyv/serialize': 1.1.1
knip@6.44.2:
knip@6.45.1:
dependencies:
formatly: 0.11.4
get-tsconfig: 4.14.3
@@ -8696,9 +8695,8 @@ snapshots:
smol-toml: 1.9.1
strip-json-comments: 5.0.3
tinyglobby: 0.2.17
unbash: 6.0.1
unbash: 6.0.4
yaml: 2.9.1
zod: 4.6.5
lazystream@1.0.1:
dependencies:
@@ -9357,15 +9355,13 @@ snapshots:
dependencies:
react: 19.3.1
react-i18next@17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
react-i18next@18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3):
dependencies:
'@babel/runtime': 7.29.7
html-parse-stringify: 4.0.1
i18next: 26.4.2(typescript@6.0.3)
i18next: 27.0.1(typescript@6.0.3)
react: 19.3.1
use-sync-external-store: 1.7.0(react@19.3.1)
optionalDependencies:
react-dom: 19.3.1(react@19.3.1)
'@types/react': 19.3.0
typescript: 6.0.3
react-is@16.13.1: {}
@@ -9790,7 +9786,7 @@ snapshots:
typescript@6.0.3: {}
unbash@6.0.1: {}
unbash@6.0.4: {}
undici-types@6.21.0: {}
+1 -1
View File
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.99.1"
channel = "1.100.0"
components = ["rustfmt", "clippy"]
+4 -5
View File
@@ -8,7 +8,7 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
build = "build.rs"
edition = "2024"
rust-version = "1.99"
rust-version = "1.100"
[lib]
name = "app_lib"
@@ -77,7 +77,7 @@ tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3"
tauri-plugin-window-state = "2.5.0"
zip = "9.4.2"
zip = "9.5.1"
flate2 = "1.1.11"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
@@ -86,7 +86,6 @@ sha2 = "0.11.1"
getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.8.0"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3"
@@ -97,10 +96,10 @@ async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
"client",
] }
arc-swap = "1.9.4"
arc-swap = "1.9.5"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.11"
webpki-roots = "1.0.12"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] }
+2
View File
@@ -17,6 +17,8 @@
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:window:allow-set-theme",
"core:window:allow-set-background-color",
"core:webview:allow-set-webview-background-color",
"notification:default",
"http:default",
"http:allow-fetch",
+4 -4
View File
@@ -2,10 +2,10 @@
// bundles (PR #8131), so this generator must stay an example, never a bin.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args_os().skip(1);
let output = args
.next()
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
let output = args.next().map_or_else(
|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"),
std::path::PathBuf::from,
);
if args.next().is_some() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
}
+6 -4
View File
@@ -18,9 +18,11 @@ 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 std::{
mem::DropGuard,
sync::atomic::{AtomicBool, Ordering},
};
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
@@ -400,9 +402,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(ValidationOutcome::Busy);
}
defer! {
let _switching_profile = DropGuard::new((), |()| {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
});
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = IProfiles {
+1 -2
View File
@@ -83,8 +83,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.get("exclude-type")
.and_then(|types| types.as_str())
.filter(|types| !types.is_empty())
.map(|types| types.split('|').map(str::to_owned).collect())
.unwrap_or_default();
.map_or_default(|types| types.split('|').map(str::to_owned).collect());
Some((
name,
GroupScope {
+1 -1
View File
@@ -191,7 +191,7 @@ impl Config {
}
}
} else {
let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default();
let error_msg = config_result.err().map_or_default(|err| err.to_string());
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}");
CoreManager::global()
.use_default_config(NoticeStatus::ConfigValidateError, "")
+4 -3
View File
@@ -371,9 +371,10 @@ impl PrfItem {
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.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
let name = name.map_or_else(
|| filename.map_or_else(|| "Remote File".into(), |s| s.into()),
|s| s.to_owned(),
);
let data = resp.text();
let data = data.trim_start_matches('\u{feff}');
+1 -2
View File
@@ -279,8 +279,7 @@ async fn cleanup_auto_backups() -> Result<()> {
.and_then(|meta| meta.modified())
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|dur| dur.as_secs())
.unwrap_or(0);
.map_or(0, |dur| dur.as_secs());
files.push((path, modified));
}
+4 -6
View File
@@ -539,18 +539,16 @@ fn bind_socket(claim: &BindClaim) -> io::Result<Socket> {
}
fn is_bind_conflict(error: &io::Error) -> bool {
if error.kind() == io::ErrorKind::AddrInUse {
return true;
}
#[cfg(windows)]
{
use windows_sys::Win32::Networking::WinSock::WSAEACCES;
error.raw_os_error() == Some(WSAEACCES)
error.kind() == io::ErrorKind::AddrInUse || error.raw_os_error() == Some(WSAEACCES)
}
#[cfg(not(windows))]
false
{
error.kind() == io::ErrorKind::AddrInUse
}
}
#[cfg(windows)]
+5 -5
View File
@@ -14,10 +14,10 @@ use anyhow::{Result, anyhow};
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,
mem::DropGuard,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
@@ -191,9 +191,9 @@ impl CoreManager {
logging!(debug, Type::Core, "Configuration update is already running");
return Ok(ValidationOutcome::Busy);
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
if !force && !self.should_update_config() {
logging!(debug, Type::Core, "Skipping config update due to debounce");
@@ -314,9 +314,9 @@ impl CoreManager {
logging!(debug, Type::Core, "Configuration update is already running");
return Ok(ValidationOutcome::Busy);
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
let runtime = Config::runtime().await;
let transaction = DraftTransaction::begin(vec![&runtime])?;
+11 -12
View File
@@ -6,9 +6,8 @@ use crate::core::proxy_control::{self, SysproxyFailure};
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;
use std::{mem::DropGuard, path::Path};
#[cfg(target_os = "windows")]
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
@@ -356,9 +355,9 @@ impl CoreManager {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
self.start_core_during_config_update().await
}
@@ -383,9 +382,9 @@ impl CoreManager {
service_rejection,
));
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
// Lock order is config then lifecycle. disable_tun_and_persist commits the shared Verge
// draft without claiming it, so without this it can commit another transaction's staged patch.
let config_write = Config::lock_config_write().await;
@@ -463,9 +462,9 @@ impl CoreManager {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
// Lock order is config then lifecycle, for the unclaimed draft write below.
let _config_write = Config::lock_config_write().await;
let _life = self.lifecycle_lock.lock().await;
@@ -795,9 +794,9 @@ impl CoreManager {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
self.restart_core_during_config_update().await
}
@@ -966,9 +965,9 @@ impl CoreManager {
if !self.try_start_config_update() {
return HandoffOutcome::NotReady;
}
defer! {
let _config_update = DropGuard::new((), |()| {
self.finish_config_update();
}
});
// Lock order is config then lifecycle.
let _life = self.lifecycle_lock.lock().await;
+1 -3
View File
@@ -331,9 +331,7 @@ fn build_provider_records(
providers: Option<ProxyProviders>,
records: &mut BTreeMap<String, ProxyNodeView>,
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
let providers = providers
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
.unwrap_or_default();
let providers = providers.map_or_default(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>());
let mut views = Vec::new();
let mut candidates = BTreeMap::new();
+1 -2
View File
@@ -126,8 +126,7 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
let path = Path::new(destination);
let extension = path
.extension()
.map(|ext| format!(".{}", ext.to_string_lossy()))
.unwrap_or_default();
.map_or_default(|ext| format!(".{}", ext.to_string_lossy()));
let mut suffix = String::new();
let mut sequence = 0;
loop {
+1 -4
View File
@@ -1470,10 +1470,7 @@ async fn create_sync_temp(target: &Path) -> Result<(PathBuf, tokio::fs::File)> {
.await
.with_context(|| format!("failed to create provider cache directory {}", parent.display()))?;
}
let name = target
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_default();
let name = target.file_name().map_or_default(|name| name.to_string_lossy());
use crate::utils::retry::{RetryError, RetryPolicy, retry};
retry(
RetryPolicy::fixed(std::num::NonZeroUsize::MIN.saturating_add(7), std::time::Duration::ZERO),
+3 -3
View File
@@ -7,9 +7,9 @@ use crate::{
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
mem::DropGuard,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@@ -541,9 +541,9 @@ impl Sysopt {
{
return Ok(());
}
defer! {
let _reset_sysproxy = DropGuard::new((), |()| {
self.reset_sysproxy.store(false, Ordering::SeqCst);
}
});
let mut proxy_state = self.proxy_state_lock.lock().await;
let _guard_operation = self.guard_operation_lock.lock().await;
let drained = self.stop_proxy_guard_locked().await;
+26 -29
View File
@@ -210,8 +210,7 @@ impl Tray {
.latest_arc()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.map_or("rule", |val| val.as_str().unwrap_or("rule"))
.to_owned()
};
let profiles_config = Config::profiles().await;
@@ -414,26 +413,23 @@ impl Tray {
}
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
hotkeys
.as_ref()
.map(|h| {
h.iter()
.filter_map(|item| {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => {
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
let uppercase_key = key.to_uppercase();
let unsupported =
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
if unsupported { None } else { Some((func, key)) }
}
_ => None,
hotkeys.as_ref().map_or_default(|h| {
h.iter()
.filter_map(|item| {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => {
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
let uppercase_key = key.to_uppercase();
let unsupported =
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
if unsupported { None } else { Some((func, key)) }
}
})
.collect::<HashMap<&str, &str>>()
})
.unwrap_or_default()
_ => None,
}
})
.collect::<HashMap<&str, &str>>()
})
}
fn create_profile_menu_item(
@@ -493,12 +489,14 @@ fn create_subcreate_proxy_menu_item(
.proxies
.get(proxy_str)
.and_then(|h| h.history.last())
.map(|h| match h.delay {
0 => "-ms".into(),
delay if delay >= 10000 => "-ms".into(),
_ => format!("{}ms", h.delay),
})
.unwrap_or_else(|| "-ms".into());
.map_or_else(
|| "-ms".into(),
|h| match h.delay {
0 => "-ms".into(),
delay if delay >= 10000 => "-ms".into(),
_ => format!("{}ms", h.delay),
},
);
let display_text = format!("{} | {}", proxy_str, delay_text);
@@ -611,7 +609,7 @@ async fn create_tray_menu(
config
.get("proxy-groups")
.and_then(|groups| groups.as_sequence())
.map(|groups| {
.map_or_default(|groups| {
groups
.iter()
.filter_map(|group| group.get("name"))
@@ -620,7 +618,6 @@ async fn create_tray_menu(
.map(|(index, name)| (name.into(), index))
.collect::<HashMap<String, usize>>()
})
.unwrap_or_default()
});
(proxy_nodes_data, runtime_proxy_groups_order)
+4 -5
View File
@@ -1,10 +1,10 @@
use anyhow::Result;
use scopeguard::defer;
use serde::Serialize;
use smartstring::alias::String;
use std::{
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
mem::DropGuard,
sync::LazyLock,
sync::atomic::{AtomicBool, Ordering},
};
@@ -421,9 +421,9 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
return Ok(ValidationOutcome::Busy);
}
defer! {
let _validation = DropGuard::new((), |()| {
self.finish();
}
});
logging!(info, Type::Validate, "生成临时配置文件用于验证");
// The test run reads the app dir, which never sees updates made by a service core.
@@ -449,8 +449,7 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
path.as_ref()
.extension()
.and_then(|s| s.to_str())
.map(|s| s.eq_ignore_ascii_case(ext))
.unwrap_or(false)
.is_some_and(|s| s.eq_ignore_ascii_case(ext))
}
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
+9 -15
View File
@@ -634,7 +634,7 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
let proxy_names = config
.get("proxies")
.and_then(|v| v.as_sequence())
.map(|seq| {
.map_or_default(|seq| {
seq.iter()
.filter_map(|item| match item {
Value::Mapping(map) => map
@@ -645,13 +645,12 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
_ => None,
})
.collect::<HashSet<String>>()
})
.unwrap_or_default();
});
let group_names = config
.get("proxy-groups")
.and_then(|v| v.as_sequence())
.map(|seq| {
.map_or_default(|seq| {
seq.iter()
.filter_map(|item| {
item.as_mapping()
@@ -660,19 +659,17 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
.map(std::convert::Into::into)
})
.collect::<HashSet<String>>()
})
.unwrap_or_default();
});
let provider_names = config
.get("proxy-providers")
.and_then(Value::as_mapping)
.map(|map| {
.map_or_default(|map| {
map.keys()
.filter_map(Value::as_str)
.map(std::convert::Into::into)
.collect::<HashSet<String>>()
})
.unwrap_or_default();
});
let mut allowed_names = proxy_names;
allowed_names.extend(group_names);
@@ -716,15 +713,13 @@ fn ensure_fake_ip_range6(dns: &mut Mapping) {
let is_fake_ip = dns
.get("enhanced-mode")
.and_then(|v| v.as_str())
.map(|m| m == "fake-ip")
.unwrap_or(true);
.is_none_or(|m| m == "fake-ip");
// Hand-edited YAML may leave the key present but empty.
let range6_missing = dns
.get("fake-ip-range6")
.and_then(|v| v.as_str())
.map(|s| s.trim().is_empty())
.unwrap_or(true);
.is_none_or(|s| s.trim().is_empty());
if ipv6_enabled && is_fake_ip && range6_missing {
dns.insert(Value::from("fake-ip-range6"), Value::from("2001:2::0/64"));
@@ -747,8 +742,7 @@ fn take_mapping(config: &mut Mapping, key: &str) -> Mapping {
config
.get_mut(key)
.and_then(Value::as_mapping_mut)
.map(std::mem::take)
.unwrap_or_default()
.map_or_default(std::mem::take)
}
fn merge_dns_config(mut config: Mapping, mut dns_config: Mapping) -> (Mapping, Mapping) {
+4 -11
View File
@@ -20,14 +20,10 @@ fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
}
fn is_selector_group(group_map: &Mapping) -> bool {
group_map
.get("type")
.and_then(Value::as_str)
.map(|value| {
let value = value.to_ascii_lowercase();
value == "select" || value == "selector"
})
.unwrap_or(false)
group_map.get("type").and_then(Value::as_str).is_some_and(|value| {
let value = value.to_ascii_lowercase();
value == "select" || value == "selector"
})
}
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
@@ -155,7 +151,6 @@ mod tests {
use serde_yaml_ng::Value;
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_delete_proxy_and_references() {
let config_str = r#"
@@ -233,7 +228,6 @@ proxy-groups:
}
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_add_new_proxies_to_first_selector_group() {
let config_str = r#"
@@ -303,7 +297,6 @@ proxy-groups:
}
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_preserve_non_sequence_proxy_groups() {
let config_str = r#"
+2 -4
View File
@@ -235,8 +235,7 @@ pub async fn import_local_backup(source: String) -> Result<String> {
let ext = source_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
.map_or_default(|ext| ext.to_ascii_lowercase());
if ext != "zip" {
return Err(anyhow!("Only .zip backup files are supported"));
}
@@ -314,8 +313,7 @@ pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
};
let last_modified = metadata
.modified()
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
.unwrap_or_default();
.map_or_default(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339());
backups.push(LocalBackupFile {
filename: file_name.into(),
path: path.to_string_lossy().into(),
+2 -2
View File
@@ -487,8 +487,8 @@ mod tests {
#[test]
fn package_urls_pin_the_resolved_version() {
let release = package_url(false, "v1.19.30").unwrap_or_default();
let alpha = package_url(true, "alpha-c0e43eb").unwrap_or_default();
let release = package_url(false, "v1.19.30").unwrap();
let alpha = package_url(true, "alpha-c0e43eb").unwrap();
assert!(release.contains("/releases/download/v1.19.30/"), "{release}");
assert!(
release.ends_with("-v1.19.30.gz") || release.ends_with("-v1.19.30.zip"),
+1 -1
View File
@@ -44,7 +44,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
}
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
let content = content.trim_prefix(&[0xEF, 0xBB, 0xBF]);
let start = content
.iter()
.position(|byte| !byte.is_ascii_whitespace())
+7 -7
View File
@@ -19,7 +19,7 @@ use crate::{
use anyhow::{Context as _, Result, anyhow, bail};
use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use scopeguard::{ScopeGuard, defer, guard};
use std::mem::DropGuard;
pub async fn probe_listener(request: ListenerProbe) -> Result<ListenerProbeOutcome> {
AsyncHandler::spawn_blocking(move || probe_listener_sync(&request))
@@ -35,9 +35,9 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
if !manager.try_start_config_update() {
bail!("configuration update is already running");
}
defer! {
let _config_update = DropGuard::new((), |()| {
manager.finish_config_update();
}
});
// Read first because transaction rollback discards all claimed drafts.
let current = current_runtime_mapping().await?;
@@ -52,13 +52,13 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
// Hide PAC while the staged ports are not yet serving traffic.
manager.core_starting();
// Recompute PAC state after every exit path.
defer! {
let _core_start = DropGuard::new((), |()| {
manager.core_start_settled();
}
});
// A user-picked port ends the startup fallback, but only once the save lands: until then the
// Core still serves the borrowed port, so every failing exit must put it back or the system
// proxy and PAC point at a dead port. Declared after the PAC guard so it restores first.
let borrowed_port = guard(MixedPort::session_fallback(), restore_borrowed_port);
let borrowed_port = DropGuard::new(MixedPort::session_fallback(), restore_borrowed_port);
MixedPort::clear_session_fallback();
Config::generate()
.await
@@ -125,7 +125,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
transaction.commit();
// The save landed, so the port the app had borrowed is now irrelevant.
let _ = ScopeGuard::into_inner(borrowed_port);
let _ = DropGuard::dismiss(borrowed_port);
announce(Refresh::Clash);
announce(Refresh::Verge);
logging!(info, Type::Config, "Proxy port configuration applied and persisted");
+6 -4
View File
@@ -2,10 +2,12 @@
use crate::core::notify::NoticeStatus;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
mem::DropGuard,
sync::atomic::{AtomicBool, Ordering},
};
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use crate::{
config::{Config, IVerge},
@@ -61,9 +63,9 @@ pub async fn reconcile_tun_availability() {
// The flag coalesces newly spawned tasks while the config lock serializes writers.
DISABLING_TUN.store(true, Ordering::Release);
defer! {
let _disabling_tun = DropGuard::new((), |()| {
DISABLING_TUN.store(false, Ordering::Release);
}
});
logging!(
info,
+1 -1
View File
@@ -15,7 +15,7 @@ fn main() -> ExitCode {
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
fn main() -> ExitCode {
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
let default_parallelism = std::thread::available_parallelism().map_or(1, |n| n.get());
let worker_limit = std::cmp::min(default_parallelism, 4);
let blocking_limit = 2 * worker_limit;
+3 -3
View File
@@ -3,10 +3,10 @@ use crate::{config::with_encryption, process::AsyncHandler};
use anyhow::{Context as _, Result, anyhow, bail};
use clash_verge_logging::{Type, logging};
use nanoid::nanoid;
use scopeguard::{ScopeGuard, guard};
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value};
use std::{
mem::DropGuard,
path::{Path, PathBuf},
str::FromStr,
};
@@ -168,7 +168,7 @@ pub async fn save_yaml_str(path: &Path, yaml_str: &str) -> Result<()> {
}
};
let temporary = guard(temporary, |path| {
let temporary = DropGuard::new(temporary, |path| {
let _ = std::fs::remove_file(path);
});
let mut file = tokio::fs::File::from_std(file);
@@ -195,7 +195,7 @@ pub async fn save_yaml_str(path: &Path, yaml_str: &str) -> Result<()> {
std::fs::rename(temporary.as_path(), path)
.with_context(|| format!("failed to replace file \"{}\"", path.display()))?;
let _ = ScopeGuard::into_inner(temporary);
let _ = DropGuard::dismiss(temporary);
Ok(())
}
+3 -3
View File
@@ -234,15 +234,15 @@ impl WindowManager {
}
pub fn is_main_window_visible(window: Option<&WebviewWindow<Wry>>) -> bool {
window.map(|w| w.is_visible().unwrap_or(false)).unwrap_or(false)
window.is_some_and(|w| w.is_visible().unwrap_or(false))
}
pub fn is_main_window_focused(window: Option<&WebviewWindow<Wry>>) -> bool {
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
window.is_some_and(|w| w.is_focused().unwrap_or(false))
}
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
window.is_some_and(|w| w.is_minimized().unwrap_or(false))
}
/// Keep new windows hidden until the frontend overlay renders, avoiding a theme flash.
@@ -8,7 +8,7 @@ let timerId: number | null = null
const listeners = new Set<RelativeTimeListener>()
const startTimer = () => {
if (timerId !== null || document.hidden || listeners.size === 0) return
if (timerId !== null) return
currentTime = Date.now()
timerId = window.setInterval(() => {
@@ -18,34 +18,19 @@ const startTimer = () => {
}
const stopTimer = () => {
if (timerId === null) return
if (listeners.size > 0 || timerId === null) return
window.clearInterval(timerId)
timerId = null
}
const handleVisibilityChange = () => {
if (document.hidden) {
stopTimer()
} else {
startTimer()
listeners.forEach((listener) => listener())
}
}
const subscribeRelativeTime = (listener: RelativeTimeListener) => {
listeners.add(listener)
if (listeners.size === 1) {
document.addEventListener('visibilitychange', handleVisibilityChange)
}
startTimer()
return () => {
listeners.delete(listener)
if (listeners.size === 0) {
stopTimer()
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
stopTimer()
}
}
@@ -223,16 +223,6 @@ const compareConnectionCellValue = (
right: IConnectionsItem,
getSnapshot: (row: IConnectionsItem) => TableRowSnapshot,
) => {
switch (field) {
case 'download':
return (left.download ?? 0) - (right.download ?? 0)
case 'upload':
return (left.upload ?? 0) - (right.upload ?? 0)
case 'dlSpeed':
return (left.curDownload ?? 0) - (right.curDownload ?? 0)
case 'ulSpeed':
return (left.curUpload ?? 0) - (right.curUpload ?? 0)
}
const leftValue = getConnectionCellValue(field, getSnapshot(left))
const rightValue = getConnectionCellValue(field, getSnapshot(right))
@@ -501,8 +491,6 @@ export const ConnectionTable = (props: Props) => {
const [sorting, setSorting] = useState<SortingState | null>(null)
const [viewport, setViewport] = useState({ scrollTop: 0, height: 0 })
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
const resizeCleanupRef = useRef<(() => void) | null>(null)
useEffect(() => () => resizeCleanupRef.current?.(), [])
const rowSnapshotCacheRef = useRef(new Map<string, TableRowSnapshot>())
const getRowSnapshot = useCallback((row: IConnectionsItem) => {
const cache = rowSnapshotCacheRef.current
@@ -676,7 +664,6 @@ export const ConnectionTable = (props: Props) => {
minWidth: number,
maxWidth: number | undefined,
) => {
resizeCleanupRef.current?.()
const handleMove = (clientX: number) => {
const nextWidth = Math.max(
minWidth,
@@ -699,10 +686,8 @@ export const ConnectionTable = (props: Props) => {
window.removeEventListener('touchmove', handleTouchMove)
window.removeEventListener('touchend', cleanup)
window.removeEventListener('touchcancel', cleanup)
resizeCleanupRef.current = null
}
resizeCleanupRef.current = cleanup
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', cleanup)
window.addEventListener('touchmove', handleTouchMove, { passive: true })
+8 -2
View File
@@ -1008,8 +1008,13 @@ export const CurrentProxyCard = () => {
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
}}
>
<Box>
<Typography variant="body1" sx={{ fontWeight: 'medium' }}>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography
variant="body1"
noWrap
title={currentProxy?.name}
sx={{ fontWeight: 'medium' }}
>
{currentProxy?.name ??
t('home.components.currentProxy.labels.noActiveNode')}
</Typography>
@@ -1063,6 +1068,7 @@ export const CurrentProxyCard = () => {
size="small"
label={delayManager.formatDelay(currentDelay)}
color={convertDelayColor(currentDelay)}
sx={{ flexShrink: 0, ml: 1 }}
/>
)}
</Box>
@@ -1106,10 +1106,9 @@ export const EnhancedCanvasTrafficGraph = memo(
{tooltipData.visible && (
<Box
style={{ left: tooltipData.x + 8, top: tooltipData.y - 8 }}
sx={{
position: 'absolute',
left: tooltipData.x + 8,
top: tooltipData.y - 8,
bgcolor: theme.palette.background.paper,
border: 1,
borderColor: 'divider',
+3 -4
View File
@@ -16,8 +16,7 @@ const COLOR = [
]
interface Props {
value: Rule
lineNo: number
value: Rule & { lineNo: number }
}
const parseColor = (text: string) => {
@@ -32,7 +31,7 @@ const parseColor = (text: string) => {
}
const RuleItem = (props: Props) => {
const { value, lineNo } = props
const { value } = props
return (
<Item sx={{ borderBottom: '1px solid var(--divider-color)' }}>
@@ -41,7 +40,7 @@ const RuleItem = (props: Props) => {
variant="body2"
sx={{ lineHeight: 2, minWidth: 30, mr: 2.25, textAlign: 'center' }}
>
{lineNo}
{value.lineNo}
</Typography>
<Box sx={{ userSelect: 'text' }}>
+14 -40
View File
@@ -150,49 +150,25 @@ const mergeConnectionSnapshot = (
const nextConnections = payload.connections ?? []
const previousActive = previous.activeConnections ?? []
const previousClosed = previous.closedConnections ?? []
const sameOrder =
nextConnections.length === previousActive.length &&
nextConnections.every(
(connection, i) => connection.id === previousActive[i].id,
)
let previousActiveById: Map<string, IConnectionsItem> | undefined
const previousActiveById = new Map<string, IConnectionsItem>()
if (!sameOrder) {
previousActiveById = new Map()
for (let i = 0; i < previousActive.length; i++) {
const previousConnection = previousActive[i]
previousActiveById.set(previousConnection.id, previousConnection)
}
for (let i = 0; i < previousActive.length; i++) {
const previousConnection = previousActive[i]
previousActiveById.set(previousConnection.id, previousConnection)
}
let activeConnections = sameOrder ? previousActive : []
const activeConnections: IConnectionsItem[] = []
for (let i = 0; i < nextConnections.length; i++) {
const connection = nextConnections[i]
const previousConnection = sameOrder
? previousActive[i]
: previousActiveById?.get(connection.id)
previousActiveById?.delete(connection.id)
const normalized = normalizeConnection(connection, previousConnection)
if (activeConnections[i] === normalized) continue
if (activeConnections === previousActive) {
activeConnections = previousActive.slice()
}
activeConnections[i] = normalized
const previousConnection = previousActiveById.get(connection.id)
if (previousConnection) previousActiveById.delete(connection.id)
activeConnections.push(normalizeConnection(connection, previousConnection))
}
const uploadTotal = payload.uploadTotal ?? 0
const downloadTotal = payload.downloadTotal ?? 0
if (!previousActiveById?.size) {
if (
activeConnections === previousActive &&
uploadTotal === previous.uploadTotal &&
downloadTotal === previous.downloadTotal
) {
return previous
}
if (previousActiveById.size === 0) {
return {
uploadTotal,
downloadTotal,
uploadTotal: payload.uploadTotal ?? 0,
downloadTotal: payload.downloadTotal ?? 0,
activeConnections,
closedConnections: previousClosed,
}
@@ -222,8 +198,8 @@ const mergeConnectionSnapshot = (
}
return {
uploadTotal,
downloadTotal,
uploadTotal: payload.uploadTotal ?? 0,
downloadTotal: payload.downloadTotal ?? 0,
activeConnections,
closedConnections,
}
@@ -251,9 +227,7 @@ const flushPendingMessage = () => {
lastFlushAt = Date.now()
const nextData = mergeConnectionSnapshot(payload, connectionData)
if (nextData === connectionData) return
connectionData = nextData
connectionData = mergeConnectionSnapshot(payload, connectionData)
notifyConnectionListeners()
}
@@ -9,6 +9,7 @@ import { useEffect, useMemo } from 'react'
import { useVerge } from '@/hooks/use-verge'
import { defaultDarkTheme, defaultTheme } from '@/pages/_theme'
import { useSetThemeMode, useThemeMode } from '@/services/states'
import getSystem from '@/utils/get-system'
const CSS_INJECTION_SCOPE_ROOT = '[data-css-injection-root]'
const CSS_INJECTION_SCOPE_LIMIT =
@@ -139,6 +140,12 @@ export const useCustomTheme = () => {
console.error(`Failed to set window theme to ${mode}:`, err)
})
}
if (getSystem() === 'macos' && mode) {
const background = mode === 'light' ? defaultTheme : defaultDarkTheme
appWindow.setBackgroundColor(background.background_color).catch((err) => {
console.error('Failed to set window background:', err)
})
}
}, [mode, appWindow, theme_mode])
const theme = useMemo(() => {
+42 -5
View File
@@ -71,6 +71,10 @@ const ORDER_OPTIONS = [
type OrderKey = (typeof ORDER_OPTIONS)[number]['id']
type NetworkFilter = 'all' | 'tcp' | 'udp'
let savedNetworkFilter: NetworkFilter = 'all'
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
(acc, option) => {
acc[option.id] = option.fn
@@ -86,6 +90,9 @@ const ConnectionsPage = () => {
const { searchState, match, onSearch } = usePageSearch('connections')
const hasSearch = searchState.text.length > 0
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
const [networkFilter, setNetworkFilter] =
useState<NetworkFilter>(savedNetworkFilter)
const hasFilter = networkFilter !== 'all'
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
'active',
)
@@ -112,18 +119,30 @@ const ConnectionsPage = () => {
const filterConn = useMemo(() => {
const orderFunc = orderFunctionMap[curOrderOpt]
if (isTableLayout && !hasSearch) return selectedConnections
if (!hasSearch) return orderFunc([...selectedConnections])
if (isTableLayout && !hasSearch && !hasFilter) return selectedConnections
if (!hasSearch && !hasFilter) return orderFunc([...selectedConnections])
const matchConns = selectedConnections.filter((conn) => {
const { host, destinationIP, process } = conn.metadata
const { host, destinationIP, process, network } = conn.metadata
if (networkFilter !== 'all' && network !== networkFilter) return false
return (
match(host || '') || match(destinationIP || '') || match(process || '')
!hasSearch ||
match(host || '') ||
match(destinationIP || '') ||
match(process || '')
)
})
return orderFunc ? orderFunc(matchConns) : matchConns
}, [selectedConnections, isTableLayout, hasSearch, match, curOrderOpt])
}, [
selectedConnections,
isTableLayout,
hasSearch,
hasFilter,
networkFilter,
match,
curOrderOpt,
])
const displayRows = useConnectionRowViews(
isTableLayout ? EMPTY_CONNECTIONS : filterConn,
@@ -255,6 +274,24 @@ const ConnectionsPage = () => {
))}
</BaseStyledSelect>
)}
<BaseStyledSelect
style={{ width: 80 }}
value={networkFilter}
onChange={(e) => {
savedNetworkFilter = e.target.value as NetworkFilter
setNetworkFilter(savedNetworkFilter)
}}
>
<MenuItem value="all">
<span style={{ fontSize: 14 }}>All</span>
</MenuItem>
<MenuItem value="tcp">
<span style={{ fontSize: 14 }}>TCP</span>
</MenuItem>
<MenuItem value="udp">
<span style={{ fontSize: 14 }}>UDP</span>
</MenuItem>
</BaseStyledSelect>
<Box
sx={{
flex: 1,
+8 -11
View File
@@ -39,21 +39,18 @@ const LogPage = () => {
return []
}
// Server-side filtering handles level filtering via query parameters
// We only need to apply search filtering here
const hasSearch = searchState.text.length > 0
if (!hasSearch && logState === 'all') return logData
return logData.filter((data) => {
// 构建完整的搜索文本,包含时间、类型和内容
if (logState !== 'all' && !data.type.includes(logState)) return false
if (!hasSearch) return true
const searchText =
`${data.time || ''} ${data.type} ${data.payload}`.toLowerCase()
const matchesSearch = match(searchText)
return (
(logState == 'all' ? true : data.type.includes(logState)) &&
matchesSearch
)
return match(searchText)
})
}, [logData, logState, match])
}, [logData, logState, match, searchState.text])
const filteredLogs = useMemo(
() => (isDescending ? [...filterLogs].reverse() : filterLogs),
+8 -11
View File
@@ -34,11 +34,13 @@ const RulesPage = () => {
}, [refreshRules, refreshRuleProviders, pageVisible])
const filteredRules = useMemo(() => {
const indices: number[] = []
for (let i = 0; i < rules.length; i++) {
if (match(rules[i].payload ?? '')) indices.push(i)
}
return indices
const rulesWithLineNo = rules.map((item, index) => ({
...item,
// UI-only derived data; keep app context/SWR data immutable
lineNo: index + 1,
}))
return rulesWithLineNo.filter((item) => match(item.payload ?? ''))
}, [rules, match])
const handleScroll = useCallback((e: Event) => {
@@ -88,12 +90,7 @@ const RulesPage = () => {
ref={virtuosoRef}
count={filteredRules.length}
estimateSize={40}
renderItem={(i) => (
<RuleItem
value={rules[filteredRules[i]]}
lineNo={filteredRules[i] + 1}
/>
)}
renderItem={(i) => <RuleItem value={filteredRules[i]} />}
style={{ flex: 1 }}
onScroll={handleScroll}
/>