Compare commits

...

12 Commits

Author SHA1 Message Date
renovate[bot]
ec570b0096
chore(deps): update npm dependencies to v9 2026-04-12 10:27:01 +00:00
renovate[bot]
9e32fba13e
chore(deps): update github actions (#6774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 10:15:18 +00:00
renovate[bot]
7a5c314d89
chore(deps): update npm dependencies to v19.2.5 (#6762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 10:15:00 +00:00
HuangTao
c358b917d6
为项目添加 github 的 Provenance 机制 (#6633)
* ci: add github provenance attestations

* ci: disable updater metadata in dev workflow

* ci: add provenance smoke test workflow

* build: fallback to alpha release assets api

* ci: remove signing env from dev workflow

* ci: disable updater artifacts in linux dev validation

* ci: support alpha manual trigger tag input

* ci: remove provenance validation scaffolding

* ci: drop redundant provenance job permissions

* ci: limit provenance to release workflow
2026-04-12 09:50:44 +00:00
Tunglies
749b6c9e30
feat(script): convert script execution to async and add timeout handling 2026-04-12 11:20:28 +08:00
Tunglies
e6a88cf9c9
refactor: improve service manager initialization by reducing lock duration 2026-04-12 11:15:34 +08:00
Tunglies
0f41f1bc8d
refactor: optimize deep_merge function using iterative stack approach 2026-04-12 11:13:17 +08:00
Tunglies
a6687a3839
feat(tls): refactor TLS configuration to use static Lazy instance 2026-04-12 03:28:24 +08:00
Tunglies
20fddc5cff
feat: add bytes dependency and optimize buffer handling in test_delay function 2026-04-12 03:26:48 +08:00
Tunglies
6fea76f7e3
feat(core): enable enhanced panic diagnostics and observability
Transitioned panic strategy from 'abort' to 'unwind' and integrated a
global panic hook into the logging framework.

This change allows the application to capture critical failure metadata—
including specific file locations, line numbers, and panic payloads—
persisting them to local logs before exit.

Note: We prioritize troubleshooting capability and long-term stability
at this stage. Reverting to 'panic = abort', `debug = false`,
`strip = true`, `remove split-debuginfo` for peak performance and
minimal binary size will only be considered once the project reaches
a mature state with near-zero community-reported crashes.
2026-04-12 03:26:48 +08:00
Tunglies
0e38ccbb9d
fix: clippy error on macOS 2026-04-10 21:40:29 +08:00
GrainFull
9e5da1a851
feat(tray): 恢复并重构托盘显示速率功能 (#6487)
* feat(tray): 恢复并重构托盘显示速率功能

* docs(changelog): add tray speed feature entry for v2.4.7

* refactor(tray): 将托盘速率显示限制为仅 macOS

* chore(style): 统一托盘速率设置相关代码风格

* refactor(tray): 统一 speed 任务调度并移除循环内配置轮询

* chore(tauri): enable createUpdaterArtifacts for updater support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(tray): refine macOS tray speed formatting and two-line alignment

* refactor(tray): move to utils

* refactor(tray): improve macOS speed display formatting, alignment, and structure

* chore: 降级 Node.js 版本至 21.7.1

* refactor(tray): 优化 macOS 托盘速率流与显示逻辑

* refactor(tray): 将速率任务重构为独立控制器并切换至 /traffic 流

* refactor(tray): 缩短速率宽度

* refactor(tray): 收敛测速流抽象并修正停止清理时序

* docs(changelog): 更新变更日志

* refactor(tray): simplify speed formatting logic and remove redundant functions

* refactor(tray): optimize speed display logic and reduce redundant attribute initialization

* refactor(tray): enhance traffic event parsing and improve stale event handling

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-04-09 14:40:32 +00:00
30 changed files with 1142 additions and 230 deletions

View File

@ -38,7 +38,7 @@ jobs:
run: bash ./scripts/extract_update_logs.sh
shell: bash
- uses: pnpm/action-setup@v5.0.0
- uses: pnpm/action-setup@v6.0.0
name: Install pnpm
with:
run_install: false
@ -102,7 +102,7 @@ jobs:
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.TAG_NAME }}
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
@ -179,7 +179,7 @@ jobs:
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- uses: pnpm/action-setup@v5.0.0
- uses: pnpm/action-setup@v6.0.0
name: Install pnpm
with:
run_install: false
@ -278,7 +278,7 @@ jobs:
cache-workspace-crates: true
- name: Install pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v6.0.0
with:
run_install: false
@ -379,7 +379,7 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.TAG_NAME }}
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
@ -423,7 +423,7 @@ jobs:
cache-workspace-crates: true
- name: Install pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v6.0.0
with:
run_install: false
@ -497,7 +497,7 @@ jobs:
}
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.TAG_NAME }}
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
@ -534,7 +534,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5.0.0
- uses: pnpm/action-setup@v6.0.0
name: Install pnpm
with:
run_install: false

View File

@ -45,7 +45,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false

View File

@ -93,7 +93,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
if: github.event.inputs[matrix.input] == 'true'
with:

View File

@ -40,7 +40,7 @@ jobs:
- name: Install pnpm
if: steps.check_frontend.outputs.frontend == 'true'
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6
with:
run_install: false

View File

@ -82,7 +82,7 @@ jobs:
GH_AW_INFO_AWMG_VERSION: ""
GH_AW_INFO_FIREWALL_TYPE: "squid"
GH_AW_COMPILED_STRICT: "true"
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@ -104,7 +104,7 @@ jobs:
sparse-checkout-cone-mode: true
fetch-depth: 1
- name: Check workflow file timestamps
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_WORKFLOW_FILE: "pr-ai-slop-review.lock.yml"
with:
@ -115,7 +115,7 @@ jobs:
await main();
- name: Compute current body text
id: sanitized
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@ -186,7 +186,7 @@ jobs:
GH_AW_PROMPT_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
with:
@ -196,7 +196,7 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs');
await main();
- name: Substitute placeholders
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_GITHUB_ACTOR: ${{ github.actor }}
@ -305,7 +305,7 @@ jobs:
id: checkout-pr
if: |
github.event.pull_request || github.event.issue.pull_request
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
with:
@ -653,7 +653,7 @@ jobs:
bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
- name: Redact secrets in logs
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@ -677,7 +677,7 @@ jobs:
- name: Ingest agent output
id: collect_output
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
@ -692,7 +692,7 @@ jobs:
await main();
- name: Parse agent logs for step summary
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
with:
@ -703,7 +703,7 @@ jobs:
await main();
- name: Parse MCP Gateway logs for step summary
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@ -776,7 +776,7 @@ jobs:
ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true
- name: Setup threat detection
if: always() && steps.detection_guard.outputs.run_detection == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
WORKFLOW_NAME: "PR AI Slop Review"
WORKFLOW_DESCRIPTION: "Reviews incoming pull requests for missing issue linkage and high-confidence\nsigns of one-shot AI-generated changes, then posts a maintainer-focused\ncomment when the risk is high enough to warrant follow-up."
@ -832,7 +832,7 @@ jobs:
- name: Parse threat detection results
id: parse_detection_results
if: always() && steps.detection_guard.outputs.run_detection == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@ -906,7 +906,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
- name: Process No-Op Messages
id: noop
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_NOOP_MAX: "1"
@ -920,7 +920,7 @@ jobs:
await main();
- name: Record Missing Tool
id: missing_tool
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
@ -934,7 +934,7 @@ jobs:
- name: Handle Agent Failure
id: handle_agent_failure
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
@ -957,7 +957,7 @@ jobs:
await main();
- name: Handle No-Op Message
id: handle_noop_message
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
@ -1025,7 +1025,7 @@ jobs:
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"

View File

@ -126,7 +126,7 @@ jobs:
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.TAG_NAME }}
name: 'Clash Verge Rev ${{ env.TAG_NAME }}'
@ -199,7 +199,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
@ -240,6 +240,26 @@ jobs:
args: --target ${{ matrix.target }}
includeUpdaterJson: true
- name: Attest Windows bundles
if: matrix.os == 'windows-latest'
uses: actions/attest-build-provenance@v4
with:
subject-path: target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Attest macOS bundles
if: matrix.os == 'macos-latest'
uses: actions/attest-build-provenance@v4
with:
subject-path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg
- name: Attest Linux bundles
if: matrix.os == 'ubuntu-22.04'
uses: actions/attest-build-provenance@v4
with:
subject-path: |
target/${{ matrix.target }}/release/bundle/deb/*.deb
target/${{ matrix.target }}/release/bundle/rpm/*.rpm
release-for-linux-arm:
name: Release Build for Linux ARM
needs: [check_tag_version]
@ -284,7 +304,7 @@ jobs:
node-version: '24.14.1'
- name: Install pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6
with:
run_install: false
@ -367,8 +387,15 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Attest Linux bundles
uses: actions/attest-build-provenance@v4
with:
subject-path: |
target/${{ matrix.target }}/release/bundle/deb/*.deb
target/${{ matrix.target }}/release/bundle/rpm/*.rpm
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: v${{env.VERSION}}
name: 'Clash Verge Rev v${{env.VERSION}}'
@ -422,7 +449,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
@ -478,8 +505,13 @@ jobs:
Rename-Item $file.FullName $newName
}
- name: Attest Windows bundles
uses: actions/attest-build-provenance@v4
with:
subject-path: target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Upload Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: 'Clash Verge Rev v${{steps.build.outputs.appVersion}}'
@ -507,7 +539,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
@ -533,7 +565,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
@ -595,7 +627,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false

View File

@ -36,7 +36,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false

View File

@ -17,7 +17,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
@ -41,7 +41,7 @@ jobs:
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false

68
Cargo.lock generated
View File

@ -1118,6 +1118,7 @@ dependencies = [
"base64 0.22.1",
"bitflags 2.11.0",
"boa_engine",
"bytes",
"chrono",
"clash-verge-draft",
"clash-verge-i18n",
@ -1140,6 +1141,9 @@ dependencies = [
"log",
"nanoid",
"network-interface",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"once_cell",
"open",
"parking_lot",
@ -4673,9 +4677,38 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.11.0",
"block2",
"libc",
"objc2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-text",
"objc2-core-video",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-data"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-foundation",
]
@ -4703,6 +4736,41 @@ dependencies = [
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"

View File

@ -11,13 +11,14 @@ members = [
resolver = "2"
[profile.release]
panic = "abort"
panic = "unwind"
codegen-units = 1
lto = "thin"
opt-level = 3
debug = false
strip = true
debug = 1
strip = "none"
overflow-checks = false
split-debuginfo = "unpacked"
rpath = false
[profile.dev]

View File

@ -13,6 +13,7 @@
### ✨ 新增功能
- 新增 macOS 托盘速率显示
- 快捷键操作通知操作结果
### 🚀 优化改进

View File

@ -41,9 +41,9 @@
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.9",
"@mui/icons-material": "^9.0.0",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.9",
"@mui/material": "^9.0.0",
"@tanstack/react-query": "^5.96.1",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.23",
@ -67,8 +67,8 @@
"monaco-editor": "^0.55.1",
"monaco-yaml": "^5.4.1",
"nanoid": "^5.1.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"react": "19.2.5",
"react-dom": "19.2.5",
"react-error-boundary": "6.1.1",
"react-hook-form": "^7.72.0",
"react-i18next": "17.0.2",

385
pnpm-lock.yaml generated
View File

@ -10,43 +10,43 @@ importers:
dependencies:
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@dnd-kit/sortable':
specifier: ^10.0.0
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@19.2.4)
version: 3.2.2(react@19.2.5)
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@19.2.14)(react@19.2.4)
version: 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled':
specifier: ^11.14.1
version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@juggle/resize-observer':
specifier: ^3.4.0
version: 3.4.0
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@mui/icons-material':
specifier: ^7.3.9
version: 7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
specifier: ^9.0.0
version: 9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/lab':
specifier: 7.0.0-beta.17
version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@mui/material':
specifier: ^7.3.9
version: 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: ^9.0.0
version: 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tanstack/react-query':
specifier: ^5.96.1
version: 5.96.2(react@19.2.4)
version: 5.96.2(react@19.2.5)
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tanstack/react-virtual':
specifier: ^3.13.23
version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tauri-apps/api':
specifier: 2.10.1
version: 2.10.1
@ -73,7 +73,7 @@ importers:
version: 2.10.1
ahooks:
specifier: ^3.9.6
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 3.9.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
cidr-block:
specifier: ^2.3.0
version: 2.3.0
@ -82,7 +82,7 @@ importers:
version: 1.11.20
foxact:
specifier: ^0.3.0
version: 0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 0.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
foxts:
specifier: ^5.3.0
version: 5.4.0
@ -108,26 +108,26 @@ importers:
specifier: ^5.1.7
version: 5.1.7
react:
specifier: 19.2.4
version: 19.2.4
specifier: 19.2.5
version: 19.2.5
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
specifier: 19.2.5
version: 19.2.5(react@19.2.5)
react-error-boundary:
specifier: 6.1.1
version: 6.1.1(react@19.2.4)
version: 6.1.1(react@19.2.5)
react-hook-form:
specifier: ^7.72.0
version: 7.72.1(react@19.2.4)
version: 7.72.1(react@19.2.5)
react-i18next:
specifier: 17.0.2
version: 17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2)
version: 17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
version: 10.1.0(@types/react@19.2.14)(react@19.2.5)
react-router:
specifier: ^7.13.1
version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
@ -1047,14 +1047,14 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@mui/core-downloads-tracker@7.3.9':
resolution: {integrity: sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==}
'@mui/core-downloads-tracker@9.0.0':
resolution: {integrity: sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA==}
'@mui/icons-material@7.3.9':
resolution: {integrity: sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==}
'@mui/icons-material@9.0.0':
resolution: {integrity: sha512-oDwyvI6LgjWRC9MBcSGvLkPud9S9ELgSBQFYxa1rYcZn6Br55dn22SyvsPDMsn0G8OndFk53iMT45W5mNqrogw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@mui/material': ^7.3.9
'@mui/material': ^9.0.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
@ -1082,13 +1082,13 @@ packages:
'@types/react':
optional: true
'@mui/material@7.3.9':
resolution: {integrity: sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==}
'@mui/material@9.0.0':
resolution: {integrity: sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@mui/material-pigment-css': ^7.3.9
'@mui/material-pigment-css': ^9.0.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
@ -1112,6 +1112,16 @@ packages:
'@types/react':
optional: true
'@mui/private-theming@9.0.0':
resolution: {integrity: sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/styled-engine@7.3.9':
resolution: {integrity: sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==}
engines: {node: '>=14.0.0'}
@ -1125,6 +1135,19 @@ packages:
'@emotion/styled':
optional: true
'@mui/styled-engine@9.0.0':
resolution: {integrity: sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/system@7.3.9':
resolution: {integrity: sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==}
engines: {node: '>=14.0.0'}
@ -1141,6 +1164,22 @@ packages:
'@types/react':
optional: true
'@mui/system@9.0.0':
resolution: {integrity: sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/types@7.4.12':
resolution: {integrity: sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==}
peerDependencies:
@ -1149,6 +1188,14 @@ packages:
'@types/react':
optional: true
'@mui/types@9.0.0':
resolution: {integrity: sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/utils@7.3.9':
resolution: {integrity: sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==}
engines: {node: '>=14.0.0'}
@ -1159,6 +1206,16 @@ packages:
'@types/react':
optional: true
'@mui/utils@9.0.0':
resolution: {integrity: sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@ -3101,10 +3158,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
react-dom@19.2.5:
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
react: ^19.2.4
react: ^19.2.5
react-error-boundary@6.1.1:
resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==}
@ -3164,8 +3221,8 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
react@19.2.5:
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'}
readdirp@4.1.2:
@ -4331,29 +4388,29 @@ snapshots:
'@biomejs/cli-win32-x64@2.4.10':
optional: true
'@dnd-kit/accessibility@3.1.1(react@19.2.4)':
'@dnd-kit/accessibility@3.1.1(react@19.2.5)':
dependencies:
react: 19.2.4
react: 19.2.5
tslib: 2.8.1
'@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@dnd-kit/accessibility': 3.1.1(react@19.2.4)
'@dnd-kit/utilities': 3.2.2(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@dnd-kit/accessibility': 3.1.1(react@19.2.5)
'@dnd-kit/utilities': 3.2.2(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
tslib: 2.8.1
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)':
dependencies:
'@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@dnd-kit/utilities': 3.2.2(react@19.2.4)
react: 19.2.4
'@dnd-kit/core': 6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@dnd-kit/utilities': 3.2.2(react@19.2.5)
react: 19.2.5
tslib: 2.8.1
'@dnd-kit/utilities@3.2.2(react@19.2.4)':
'@dnd-kit/utilities@3.2.2(react@19.2.5)':
dependencies:
react: 19.2.4
react: 19.2.5
tslib: 2.8.1
'@emnapi/core@1.9.2':
@ -4404,17 +4461,17 @@ snapshots:
'@emotion/memoize@0.9.0': {}
'@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)':
'@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@emotion/babel-plugin': 11.13.5
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4)
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.5)
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
hoist-non-react-statics: 3.3.2
react: 19.2.4
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
transitivePeerDependencies:
@ -4430,16 +4487,16 @@ snapshots:
'@emotion/sheet@1.4.0': {}
'@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)':
'@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@emotion/babel-plugin': 11.13.5
'@emotion/is-prop-valid': 1.4.0
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4)
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.5)
'@emotion/utils': 1.4.2
react: 19.2.4
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
transitivePeerDependencies:
@ -4447,9 +4504,9 @@ snapshots:
'@emotion/unitless@0.10.0': {}
'@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.4)':
'@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.5)':
dependencies:
react: 19.2.4
react: 19.2.5
'@emotion/utils@1.4.2': {}
@ -4617,70 +4674,79 @@ snapshots:
dependencies:
state-local: 1.0.7
'@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@monaco-editor/loader': 1.7.0
monaco-editor: 0.55.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@mui/core-downloads-tracker@7.3.9': {}
'@mui/core-downloads-tracker@9.0.0': {}
'@mui/icons-material@7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)':
'@mui/icons-material@9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/material': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
'@mui/material': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/material': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mui/system': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
'@mui/material': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@mui/system': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/types': 7.4.12(@types/react@19.2.14)
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.5)
clsx: 2.1.1
prop-types: 15.8.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@types/react': 19.2.14
'@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/core-downloads-tracker': 7.3.9
'@mui/system': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
'@mui/types': 7.4.12(@types/react@19.2.14)
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
'@mui/core-downloads-tracker': 9.0.0
'@mui/system': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/types': 9.0.0(@types/react@19.2.14)
'@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@19.2.14)
clsx: 2.1.1
csstype: 3.2.3
prop-types: 15.8.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
react-is: 19.2.4
react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@types/react': 19.2.14
'@mui/private-theming@7.3.9(@types/react@19.2.14)(react@19.2.4)':
'@mui/private-theming@7.3.9(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.5)
prop-types: 15.8.1
react: 19.2.4
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@mui/styled-engine@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)':
'@mui/private-theming@9.0.0(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
prop-types: 15.8.1
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@mui/styled-engine@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@emotion/cache': 11.14.0
@ -4688,25 +4754,54 @@ snapshots:
'@emotion/sheet': 1.4.0
csstype: 3.2.3
prop-types: 15.8.1
react: 19.2.4
react: 19.2.5
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)':
'@mui/styled-engine@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/private-theming': 7.3.9(@types/react@19.2.14)(react@19.2.4)
'@mui/styled-engine': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
csstype: 3.2.3
prop-types: 15.8.1
react: 19.2.5
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/private-theming': 7.3.9(@types/react@19.2.14)(react@19.2.5)
'@mui/styled-engine': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)
'@mui/types': 7.4.12(@types/react@19.2.14)
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
'@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.5)
clsx: 2.1.1
csstype: 3.2.3
prop-types: 15.8.1
react: 19.2.4
react: 19.2.5
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@types/react': 19.2.14
'@mui/system@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/private-theming': 9.0.0(@types/react@19.2.14)(react@19.2.5)
'@mui/styled-engine': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)
'@mui/types': 9.0.0(@types/react@19.2.14)
'@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
clsx: 2.1.1
csstype: 3.2.3
prop-types: 15.8.1
react: 19.2.5
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@types/react': 19.2.14
'@mui/types@7.4.12(@types/react@19.2.14)':
@ -4715,14 +4810,32 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
'@mui/utils@7.3.9(@types/react@19.2.14)(react@19.2.4)':
'@mui/types@9.0.0(@types/react@19.2.14)':
dependencies:
'@babel/runtime': 7.29.2
optionalDependencies:
'@types/react': 19.2.14
'@mui/utils@7.3.9(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/types': 7.4.12(@types/react@19.2.14)
'@types/prop-types': 15.7.15
clsx: 2.1.1
prop-types: 15.8.1
react: 19.2.4
react: 19.2.5
react-is: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@mui/utils@9.0.0(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@mui/types': 9.0.0(@types/react@19.2.14)
'@types/prop-types': 15.7.15
clsx: 2.1.1
prop-types: 15.8.1
react: 19.2.5
react-is: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
@ -4992,22 +5105,22 @@ snapshots:
'@tanstack/query-core@5.96.2': {}
'@tanstack/react-query@5.96.2(react@19.2.4)':
'@tanstack/react-query@5.96.2(react@19.2.5)':
dependencies:
'@tanstack/query-core': 5.96.2
react: 19.2.4
react: 19.2.5
'@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@tanstack/react-table@8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@tanstack/table-core': 8.21.3
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@tanstack/react-virtual@3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@tanstack/react-virtual@3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@tanstack/virtual-core': 3.13.23
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@tanstack/table-core@8.21.3': {}
@ -5344,7 +5457,7 @@ snapshots:
agent-base@9.0.0: {}
ahooks@3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
ahooks@3.9.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@babel/runtime': 7.29.2
'@types/js-cookie': 3.0.6
@ -5352,8 +5465,8 @@ snapshots:
intersection-observer: 0.12.2
js-cookie: 3.0.5
lodash: 4.18.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
react-fast-compare: 3.2.2
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
@ -5977,14 +6090,14 @@ snapshots:
dependencies:
fetch-blob: 3.2.0
foxact@0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
foxact@0.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
client-only: 0.0.1
event-target-bus: 1.0.0
server-only: 0.0.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
foxts@5.4.0: {}
@ -6763,37 +6876,37 @@ snapshots:
punycode@2.3.1: {}
react-dom@19.2.4(react@19.2.4):
react-dom@19.2.5(react@19.2.5):
dependencies:
react: 19.2.4
react: 19.2.5
scheduler: 0.27.0
react-error-boundary@6.1.1(react@19.2.4):
react-error-boundary@6.1.1(react@19.2.5):
dependencies:
react: 19.2.4
react: 19.2.5
react-fast-compare@3.2.2: {}
react-hook-form@7.72.1(react@19.2.4):
react-hook-form@7.72.1(react@19.2.5):
dependencies:
react: 19.2.4
react: 19.2.5
react-i18next@17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2):
react-i18next@17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2):
dependencies:
'@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1
i18next: 26.0.3(typescript@6.0.2)
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
react: 19.2.5
use-sync-external-store: 1.6.0(react@19.2.5)
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
react-dom: 19.2.5(react@19.2.5)
typescript: 6.0.2
react-is@16.13.1: {}
react-is@19.2.4: {}
react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4):
react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.5):
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
@ -6802,7 +6915,7 @@ snapshots:
hast-util-to-jsx-runtime: 2.3.6
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.1
react: 19.2.4
react: 19.2.5
remark-parse: 11.0.0
remark-rehype: 11.1.2
unified: 11.0.5
@ -6811,24 +6924,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
cookie: 1.1.1
react: 19.2.4
react: 19.2.5
set-cookie-parser: 2.7.2
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
react-dom: 19.2.5(react@19.2.5)
react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@babel/runtime': 7.29.2
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
react@19.2.4: {}
react@19.2.5: {}
readdirp@4.1.2: {}
@ -7171,9 +7284,9 @@ snapshots:
dependencies:
punycode: 2.3.1
use-sync-external-store@1.6.0(react@19.2.4):
use-sync-external-store@1.6.0(react@19.2.5):
dependencies:
react: 19.2.4
react: 19.2.5
validator@13.15.35: {}

View File

@ -108,6 +108,29 @@ webpki-roots = "1.0"
rust_iso3166 = "0.1.14"
# Use the git repo until the next release after v2.0.0.
dark-light = { git = "https://github.com/rust-dark-light/dark-light" }
bytes = "1.11.1"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-foundation = { version = "0.3", features = [
"NSString",
"NSDictionary",
"NSAttributedString",
] }
objc2-app-kit = { version = "0.3", features = [
"NSAttributedString",
"NSStatusItem",
"NSStatusBarButton",
"NSButton",
"NSControl",
"NSResponder",
"NSView",
"NSFont",
"NSFontDescriptor",
"NSColor",
"NSParagraphStyle",
"NSText",
] }
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }

View File

@ -233,7 +233,7 @@ pub struct IVerge {
)]
pub webdav_password: Option<String>,
#[serde(skip)]
#[cfg(target_os = "macos")]
pub enable_tray_speed: Option<bool>,
// pub enable_tray_icon: Option<bool>,
@ -438,6 +438,7 @@ impl IVerge {
webdav_url: None,
webdav_username: None,
webdav_password: None,
#[cfg(target_os = "macos")]
enable_tray_speed: Some(false),
// enable_tray_icon: Some(true),
tray_proxy_groups_display_mode: Some("default".into()),
@ -543,6 +544,7 @@ impl IVerge {
patch!(webdav_url);
patch!(webdav_username);
patch!(webdav_password);
#[cfg(target_os = "macos")]
patch!(enable_tray_speed);
// patch!(enable_tray_icon);
patch!(tray_proxy_groups_display_mode);

View File

@ -100,6 +100,22 @@ impl Logger {
let sidecar_file_writer = self.generate_sidecar_writer()?;
*self.sidecar_file_writer.write() = Some(sidecar_file_writer);
std::panic::set_hook(Box::new(move |info| {
let payload = info
.payload()
.downcast_ref::<&str>()
.unwrap_or(&"Unknown panic payload");
let location = info
.location()
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
.unwrap_or_else(|| "Unknown location".to_string());
logging!(error, Type::System, "Panic occurred at {}: {}", location, payload);
if let Some(h) = Self::global().handle.lock().as_ref() {
h.flush();
std::thread::sleep(std::time::Duration::from_millis(100));
}
}));
Ok(())
}

View File

@ -25,7 +25,10 @@ use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
};
mod menu_def;
#[cfg(target_os = "macos")]
mod speed_task;
use menu_def::{MenuIds, MenuTexts};
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
@ -45,6 +48,8 @@ enum IconKind {
pub struct Tray {
limiter: SystemLimiter,
#[cfg(target_os = "macos")]
speed_controller: speed_task::TraySpeedController,
}
impl TrayState {
@ -113,6 +118,8 @@ impl Default for Tray {
fn default() -> Self {
Self {
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
#[cfg(target_os = "macos")]
speed_controller: speed_task::TraySpeedController::new(),
}
}
}
@ -325,6 +332,8 @@ impl Tray {
let verge = Config::verge().await.data_arc();
self.update_menu().await?;
self.update_icon(&verge).await?;
#[cfg(target_os = "macos")]
self.update_speed_task(verge.enable_tray_speed.unwrap_or(false));
self.update_tooltip().await?;
Ok(())
}
@ -382,6 +391,12 @@ impl Tray {
}
allow
}
/// 根据配置统一更新托盘速率采集任务状态macOS
#[cfg(target_os = "macos")]
pub fn update_speed_task(&self, enable_tray_speed: bool) {
self.speed_controller.update_task(enable_tray_speed);
}
}
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {

View File

@ -0,0 +1,194 @@
use crate::core::handle;
use crate::process::AsyncHandler;
use crate::utils::{connections_stream, tray_speed};
use crate::{Type, logging};
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
use tauri::async_runtime::JoinHandle;
use tauri_plugin_mihomo::models::ConnectionId;
/// 托盘速率流异常后的重连间隔。
const TRAY_SPEED_RETRY_DELAY: Duration = Duration::from_secs(1);
/// 托盘速率流运行时的空闲轮询间隔。
const TRAY_SPEED_IDLE_POLL_INTERVAL: Duration = Duration::from_millis(200);
/// 托盘速率流在此时间内收不到有效数据时,触发重连并降级到 0/0。
const TRAY_SPEED_STALE_TIMEOUT: Duration = Duration::from_secs(5);
/// macOS 托盘速率任务控制器。
#[derive(Clone)]
pub struct TraySpeedController {
speed_task: Arc<Mutex<Option<JoinHandle<()>>>>,
speed_connection_id: Arc<Mutex<Option<ConnectionId>>>,
}
impl Default for TraySpeedController {
fn default() -> Self {
Self {
speed_task: Arc::new(Mutex::new(None)),
speed_connection_id: Arc::new(Mutex::new(None)),
}
}
}
impl TraySpeedController {
pub fn new() -> Self {
Self::default()
}
pub fn update_task(&self, enable_tray_speed: bool) {
if enable_tray_speed {
self.start_task();
} else {
self.stop_task();
}
}
/// 启动托盘速率采集后台任务(基于 `/traffic` WebSocket 流)。
fn start_task(&self) {
if handle::Handle::global().is_exiting() {
return;
}
// 关键步骤:托盘不可用时不启动速率任务,避免无效连接重试。
if !Self::has_main_tray() {
logging!(warn, Type::Tray, "托盘不可用,跳过启动托盘速率任务");
return;
}
let mut guard = self.speed_task.lock();
if guard.as_ref().is_some_and(|task| !task.inner().is_finished()) {
return;
}
let speed_connection_id = Arc::clone(&self.speed_connection_id);
let task = AsyncHandler::spawn(move || async move {
loop {
if handle::Handle::global().is_exiting() {
break;
}
if !Self::has_main_tray() {
logging!(warn, Type::Tray, "托盘已不可用,停止托盘速率任务");
break;
}
let stream_connect_result = connections_stream::connect_traffic_stream().await;
let mut speed_stream = match stream_connect_result {
Ok(stream) => stream,
Err(err) => {
logging!(debug, Type::Tray, "托盘速率流连接失败,稍后重试: {err}");
Self::apply_tray_speed(0, 0);
tokio::time::sleep(TRAY_SPEED_RETRY_DELAY).await;
continue;
}
};
Self::set_speed_connection_id(&speed_connection_id, Some(speed_stream.connection_id));
loop {
let next_state = speed_stream
.next_event(TRAY_SPEED_IDLE_POLL_INTERVAL, TRAY_SPEED_STALE_TIMEOUT, || {
handle::Handle::global().is_exiting()
})
.await;
match next_state {
connections_stream::StreamConsumeState::Event(speed_event) => {
Self::apply_tray_speed(speed_event.up, speed_event.down);
}
connections_stream::StreamConsumeState::Stale => {
logging!(debug, Type::Tray, "托盘速率流长时间未收到有效数据,触发重连");
Self::apply_tray_speed(0, 0);
break;
}
connections_stream::StreamConsumeState::Closed
| connections_stream::StreamConsumeState::ExitRequested => {
break;
}
}
}
Self::disconnect_speed_connection(&speed_connection_id).await;
if handle::Handle::global().is_exiting() || !Self::has_main_tray() {
break;
}
// Stale 分支在内层 loop 中已重置为 0/0此处兜底 Closed 分支(流被远端关闭)。
Self::apply_tray_speed(0, 0);
tokio::time::sleep(TRAY_SPEED_RETRY_DELAY).await;
}
Self::set_speed_connection_id(&speed_connection_id, None);
});
*guard = Some(task);
}
/// 停止托盘速率采集后台任务并清除速率显示。
fn stop_task(&self) {
// 取出任务句柄,与 speed_connection_id 一同传入清理任务。
let task = self.speed_task.lock().take();
let speed_connection_id = Arc::clone(&self.speed_connection_id);
AsyncHandler::spawn(move || async move {
// 关键步骤:先等待 abort 完成,再断开 WebSocket 连接。
// 若直接 abort 后立即 disconnect任务可能已通过 take 取走 connection_id
// 但尚未完成断开,导致 connection_id 丢失、连接泄漏。
// await task handle 可保证原任务已退出connection_id 不再被占用。
if let Some(task) = task {
task.abort();
let _ = task.await;
}
Self::disconnect_speed_connection(&speed_connection_id).await;
});
let app_handle = handle::Handle::app_handle();
if let Some(tray) = app_handle.tray_by_id("main") {
let result = tray.with_inner_tray_icon(|inner| {
if let Some(status_item) = inner.ns_status_item() {
tray_speed::clear_speed_attributed_title(&status_item);
}
});
if let Err(err) = result {
logging!(warn, Type::Tray, "清除富文本速率失败: {err}");
}
}
}
fn has_main_tray() -> bool {
handle::Handle::app_handle().tray_by_id("main").is_some()
}
fn set_speed_connection_id(
speed_connection_id: &Arc<Mutex<Option<ConnectionId>>>,
connection_id: Option<ConnectionId>,
) {
*speed_connection_id.lock() = connection_id;
}
fn take_speed_connection_id(speed_connection_id: &Arc<Mutex<Option<ConnectionId>>>) -> Option<ConnectionId> {
speed_connection_id.lock().take()
}
async fn disconnect_speed_connection(speed_connection_id: &Arc<Mutex<Option<ConnectionId>>>) {
if let Some(connection_id) = Self::take_speed_connection_id(speed_connection_id) {
connections_stream::disconnect_connection(connection_id).await;
}
}
fn apply_tray_speed(up: u64, down: u64) {
let app_handle = handle::Handle::app_handle();
if let Some(tray) = app_handle.tray_by_id("main") {
let result = tray.with_inner_tray_icon(move |inner| {
if let Some(status_item) = inner.ns_status_item() {
tray_speed::set_speed_attributed_title(&status_item, up, down);
}
});
if let Err(err) = result {
logging!(warn, Type::Tray, "设置富文本速率失败: {err}");
}
}
}
}

View File

@ -4,14 +4,21 @@ use super::use_lowercase;
use serde_yaml_ng::{self, Mapping, Value};
fn deep_merge(a: &mut Value, b: Value) {
let mut stack: Vec<(*mut Value, Value)> = vec![(a as *mut Value, b)];
while let Some((a_ptr, b)) = stack.pop() {
let a = unsafe { &mut *a_ptr };
match (a, b) {
(&mut Value::Mapping(ref mut a), Value::Mapping(b)) => {
for (k, v) in b {
deep_merge(a.entry(k).or_insert(Value::Null), v);
(Value::Mapping(a_map), Value::Mapping(b_map)) => {
for (k, v) in b_map {
let child = a_map.entry(k).or_insert(Value::Null);
stack.push((child as *mut Value, v));
}
}
(a, b) => *a = b,
}
}
}
pub fn use_merge(merge: &Mapping, config: Mapping) -> Mapping {

View File

@ -303,7 +303,7 @@ async fn collect_profile_items() -> ProfileItems {
}
}
fn process_global_items(
async fn process_global_items(
mut config: Mapping,
global_merge: ChainItem,
global_script: ChainItem,
@ -319,7 +319,7 @@ fn process_global_items(
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, &config, profile_name) {
match use_script(script, config.clone(), profile_name.clone()).await {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@ -334,7 +334,7 @@ fn process_global_items(
}
#[allow(clippy::too_many_arguments)]
fn process_profile_items(
async fn process_profile_items(
mut config: Mapping,
mut exists_keys: Vec<String>,
mut result_map: HashMap<String, ResultLog>,
@ -364,7 +364,7 @@ fn process_profile_items(
if let ChainType::Script(script) = script_item.data {
let mut logs = vec![];
match use_script(script, &config, profile_name) {
match use_script(script, config.clone(), profile_name.clone()).await {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@ -455,16 +455,17 @@ async fn merge_default_config(
config
}
fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping {
async fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping {
if enable_builtin {
ChainItem::builtin()
let items: Vec<_> = ChainItem::builtin()
.into_iter()
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c)
.for_each(|item| {
.collect();
for item in items {
logging!(debug, Type::Core, "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, &config, &String::from("")) {
match use_script(script, config.clone(), String::from("")).await {
Ok((res_config, _)) => {
config = res_config;
}
@ -473,7 +474,7 @@ fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable
}
}
}
});
}
}
config
@ -621,7 +622,8 @@ pub async fn enhance() -> (Mapping, HashSet<String>, HashMap<String, ResultLog>)
let profile_name = profile.profile_name;
// process globals
let (config, exists_keys, result_map) = process_global_items(config, global_merge, global_script, &profile_name);
let (config, exists_keys, result_map) =
process_global_items(config, global_merge, global_script, &profile_name).await;
// process profile-specific items
let (config, exists_keys, result_map) = process_profile_items(
@ -634,7 +636,8 @@ pub async fn enhance() -> (Mapping, HashSet<String>, HashMap<String, ResultLog>)
merge_item,
script_item,
&profile_name,
);
)
.await;
// merge default clash config
let config = merge_default_config(
@ -650,7 +653,7 @@ pub async fn enhance() -> (Mapping, HashSet<String>, HashMap<String, ResultLog>)
.await;
// builtin scripts
let mut config = apply_builtin_scripts(config, clash_core, enable_builtin);
let mut config = apply_builtin_scripts(config, clash_core, enable_builtin).await;
config = cleanup_proxy_groups(config);

View File

@ -1,3 +1,5 @@
use crate::process::AsyncHandler;
use super::use_lowercase;
use anyhow::{Error, Result};
use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction};
@ -10,11 +12,25 @@ use std::sync::Arc;
const MAX_OUTPUTS: usize = 1000;
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
const MAX_JSON_SIZE: usize = 10 * 1024 * 1024; // 10MB
const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
// TODO 使用引用改进上下相关处理,避免不必要 Clone
pub fn use_script(script: String, config: &Mapping, name: &String) -> Result<(Mapping, Vec<(String, String)>)> {
pub async fn use_script(script: String, config: Mapping, name: String) -> Result<(Mapping, Vec<(String, String)>)> {
let handle = AsyncHandler::spawn_blocking(move || use_script_sync(script, &config, &name));
match tokio::time::timeout(SCRIPT_TIMEOUT, handle).await {
Ok(Ok(result)) => result,
Ok(Err(join_err)) => Err(anyhow::anyhow!("script task panicked: {join_err}")),
Err(_elapsed) => Err(anyhow::anyhow!("script execution timed out after {:?}", SCRIPT_TIMEOUT)),
}
}
fn use_script_sync(script: String, config: &Mapping, name: &String) -> Result<(Mapping, Vec<(String, String)>)> {
let mut context = Context::default();
context
.runtime_limits_mut()
.set_loop_iteration_limit(MAX_LOOP_ITERATIONS);
let outputs = Arc::new(Mutex::new(vec![]));
let total_size = Arc::new(Mutex::new(0usize));
@ -189,7 +205,7 @@ fn test_script() {
let config = &serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML");
let (config, results) =
use_script(script.into(), config, &String::from("")).expect("Script execution should succeed in test");
use_script_sync(script.into(), config, &String::from("")).expect("Script execution should succeed in test");
let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML");
let yaml_config_size = std::mem::size_of_val(&config);
@ -243,7 +259,7 @@ fn test_memory_limits() {
#[allow(clippy::expect_used)]
let config = &serde_yaml_ng::from_str("test: value").expect("Failed to parse test YAML");
let result = use_script(script.into(), config, &String::from(""));
let result = use_script_sync(script.into(), config, &String::from(""));
// 应该失败或被限制
assert!(result.is_ok()); // 会被限制但不会 panic
}

View File

@ -5,9 +5,23 @@ use crate::{
process::AsyncHandler,
utils,
};
use bytes::BytesMut;
use clash_verge_logging::{Type, logging};
use once_cell::sync::Lazy;
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::sync::Arc;
#[allow(clippy::expect_used)]
static TLS_CONFIG: Lazy<Arc<rustls::ClientConfig>> = Lazy::new(|| {
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("Failed to set TLS versions")
.with_root_certificates(root_store)
.with_no_client_auth();
Arc::new(config)
});
/// Restart the Clash core
pub async fn restart_clash_core() {
@ -127,6 +141,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
tokio::time::timeout(Duration::from_secs(10), async {
let start = Instant::now();
let mut buf = BytesMut::with_capacity(1024);
if is_https {
let stream = match proxy_port {
@ -134,22 +149,15 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let mut s = TcpStream::connect(format!("127.0.0.1:{pp}")).await?;
s.write_all(format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n").as_bytes())
.await?;
let mut buf = [0u8; 1024];
let n = s.read(&mut buf).await?;
if !std::str::from_utf8(&buf[..n]).unwrap_or("").contains("200") {
s.read_buf(&mut buf).await?;
if !buf.windows(3).any(|w| w == b"200") {
return Err(anyhow::anyhow!("Proxy CONNECT failed"));
}
s
}
None => TcpStream::connect(format!("{host}:{port}")).await?,
};
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config =
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
let connector = tokio_rustls::TlsConnector::from(Arc::clone(&TLS_CONFIG));
let server_name = rustls::pki_types::ServerName::try_from(host.as_str())
.map_err(|_| anyhow::anyhow!("Invalid DNS name: {host}"))?
.to_owned();
@ -166,7 +174,6 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
),
};
stream.write_all(req.as_bytes()).await?;
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await?;
}

View File

@ -96,7 +96,10 @@ fn determine_update_flags(patch: &IVerge) -> UpdateFlags {
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
#[cfg(target_os = "macos")]
let enable_tray_speed = patch.enable_tray_speed;
#[cfg(not(target_os = "macos"))]
let enable_tray_speed: Option<bool> = None;
// let enable_tray_icon = patch.enable_tray_icon;
let enable_global_hotkey = patch.enable_global_hotkey;
let tray_event = &patch.tray_event;
@ -235,6 +238,10 @@ async fn process_terminated_flags(update_flags: UpdateFlags, patch: &IVerge) ->
tray::Tray::global()
.update_icon(&Config::verge().await.latest_arc())
.await?;
#[cfg(target_os = "macos")]
if patch.enable_tray_speed.is_some() {
tray::Tray::global().update_speed_task(patch.enable_tray_speed.unwrap_or(false));
}
}
if update_flags.contains(UpdateFlags::SYSTRAY_TOOLTIP) {
tray::Tray::global().update_tooltip().await?;

View File

@ -0,0 +1,173 @@
use crate::{Type, core::handle, logging};
use anyhow::Result;
use serde::Deserialize;
use serde_json::Value;
use std::time::Duration;
use tauri_plugin_mihomo::models::{ConnectionId, WebSocketMessage};
use tokio::sync::mpsc;
use tokio::time::Instant;
/// Mihomo WebSocket 流的有界队列容量,避免异常场景下内存无限增长。
const MIHOMO_WS_STREAM_BUFFER_SIZE: usize = 8;
/// 断开 Mihomo WebSocket 连接时使用的关闭码RFC 6455 标准正常关闭)。
const MIHOMO_WS_STREAM_CLOSE_CODE: u64 = 1000;
/// `/traffic` 即时速率事件(字节/秒)。
#[derive(Debug, Clone, Copy)]
pub struct TrafficSpeedEvent {
pub up: u64,
pub down: u64,
}
/// Mihomo WebSocket 流消费状态。
pub enum StreamConsumeState<T> {
/// 收到一条业务事件。
Event(T),
/// 连接关闭或消息流结束。
Closed,
/// 在超时时间内未收到有效事件,需要重连。
Stale,
/// 上层请求退出消费循环。
ExitRequested,
}
enum InternalWsEvent<T> {
Data(T),
Closed,
}
/// Mihomo WebSocket 订阅句柄(通用事件流)。
pub struct MihomoWsEventStream<T> {
/// 当前订阅连接 ID用于主动断开。
pub connection_id: ConnectionId,
/// 当前订阅消息接收器。
receiver: mpsc::Receiver<InternalWsEvent<T>>,
/// 最近一次收到有效事件的时间戳。
last_valid_event_at: Instant,
}
#[derive(Deserialize)]
struct TrafficPayload {
up: u64,
down: u64,
}
fn parse_traffic_event(data: Value) -> Option<InternalWsEvent<TrafficSpeedEvent>> {
if let Ok(payload) = serde_json::from_value::<TrafficPayload>(data.clone()) {
return Some(InternalWsEvent::Data(TrafficSpeedEvent {
up: payload.up,
down: payload.down,
}));
}
if let Ok(ws_message) = WebSocketMessage::deserialize(&data) {
match ws_message {
WebSocketMessage::Text(text) => {
let payload = serde_json::from_str::<TrafficPayload>(&text).ok()?;
Some(InternalWsEvent::Data(TrafficSpeedEvent {
up: payload.up,
down: payload.down,
}))
}
WebSocketMessage::Close(_) => Some(InternalWsEvent::Closed),
_ => None,
}
} else {
None
}
}
fn try_send_internal_event<T>(message_tx: &mpsc::Sender<InternalWsEvent<T>>, event: InternalWsEvent<T>) {
if let Err(err) = message_tx.try_send(event) {
match err {
// 队列满时丢弃本次事件,下一次事件会继续覆盖更新。
tokio::sync::mpsc::error::TrySendError::Full(_) => {}
// 任务已结束时通道可能关闭,忽略即可。
tokio::sync::mpsc::error::TrySendError::Closed(_) => {}
}
}
}
/// 建立 `/traffic` WebSocket 订阅(通用流)。
pub async fn connect_traffic_stream() -> Result<MihomoWsEventStream<TrafficSpeedEvent>> {
// 使用有界 mpsc 通道承接回调事件,限制消息积压上限。
let (message_tx, message_rx) = mpsc::channel::<InternalWsEvent<TrafficSpeedEvent>>(MIHOMO_WS_STREAM_BUFFER_SIZE);
// 建立 Mihomo `/traffic` WebSocket 订阅。
let connection_id = handle::Handle::mihomo()
.await
.ws_traffic({
let message_tx = message_tx.clone();
move |message| {
if let Some(event) = parse_traffic_event(message) {
try_send_internal_event(&message_tx, event);
}
}
})
.await?;
drop(message_tx);
Ok(MihomoWsEventStream {
connection_id,
receiver: message_rx,
last_valid_event_at: Instant::now(),
})
}
impl<T> MihomoWsEventStream<T> {
/// 等待下一次可用事件或结束状态。
///
/// # Arguments
/// * `idle_poll_interval` - 空闲检查间隔
/// * `stale_timeout` - 无有效事件超时时间
/// * `should_exit` - 上层退出判定函数
pub async fn next_event<F>(
&mut self,
_idle_poll_interval: Duration, // 签名保留,但内部逻辑已进化为更高效的驱动方式
stale_timeout: Duration,
should_exit: F,
) -> StreamConsumeState<T>
where
F: Fn() -> bool,
{
let sleep = tokio::time::sleep(stale_timeout);
tokio::pin!(sleep);
loop {
if should_exit() {
return StreamConsumeState::ExitRequested;
}
tokio::select! {
maybe_event = self.receiver.recv() => {
match maybe_event {
Some(InternalWsEvent::Data(event)) => {
self.last_valid_event_at = Instant::now();
sleep.as_mut().reset(self.last_valid_event_at + stale_timeout);
return StreamConsumeState::Event(event);
}
Some(InternalWsEvent::Closed) | None => return StreamConsumeState::Closed,
}
}
_ = &mut sleep => {
if self.last_valid_event_at.elapsed() >= stale_timeout {
return StreamConsumeState::Stale;
}
sleep.as_mut().reset(self.last_valid_event_at + stale_timeout);
}
}
}
}
}
/// 断开指定 Mihomo WebSocket 连接。
///
/// # Arguments
/// * `connection_id` - 目标连接 ID
pub async fn disconnect_connection(connection_id: ConnectionId) {
if let Err(err) = handle::Handle::mihomo()
.await
.disconnect(connection_id, Some(MIHOMO_WS_STREAM_CLOSE_CODE))
.await
{
logging!(debug, Type::Tray, "断开 Mihomo WebSocket 连接失败: {err}");
}
}

View File

@ -1,3 +1,5 @@
#[cfg(target_os = "macos")]
pub mod connections_stream;
pub mod dirs;
pub mod help;
pub mod init;
@ -10,5 +12,8 @@ pub mod resolve;
pub mod schtasks;
pub mod server;
pub mod singleton;
pub mod speed;
pub mod tmpl;
#[cfg(target_os = "macos")]
pub mod tray_speed;
pub mod window_manager;

View File

@ -178,9 +178,11 @@ pub(super) async fn init_service_manager() {
if !is_service_ipc_path_exists() {
return;
}
if SERVICE_MANAGER.lock().await.init().await.is_ok() {
logging_error!(Type::Setup, SERVICE_MANAGER.lock().await.refresh().await);
let mut manager = SERVICE_MANAGER.lock().await;
if manager.init().await.is_ok() {
logging_error!(Type::Setup, manager.refresh().await);
}
drop(manager);
}
pub(super) async fn init_core_manager() {

View File

@ -0,0 +1,71 @@
//! 网络速率格式化工具
/// 速率显示升档阈值:保证显示值不超过三位数(显示层约定,与换算基数无关)
const SPEED_DISPLAY_THRESHOLD: f64 = 1000.0;
/// 速率展示单位顺序
const SPEED_UNITS: [&str; 5] = ["B/s", "K/s", "M/s", "G/s", "T/s"];
/// 预计算 1024 的幂次方,避免运行时重复计算 pow
const SCALES: [f64; 5] = [
1.0,
1024.0,
1024.0 * 1024.0,
1024.0 * 1024.0 * 1024.0,
1024.0 * 1024.0 * 1024.0 * 1024.0,
];
/// 将字节/秒格式化为可读速率字符串
///
/// # Arguments
/// * `bytes_per_sec` - 每秒字节数
pub fn format_bytes_per_second(bytes_per_sec: u64) -> String {
if bytes_per_sec < SPEED_DISPLAY_THRESHOLD as u64 {
return format!("{bytes_per_sec}B/s");
}
let mut unit_index = (bytes_per_sec.ilog2() / 10) as usize;
unit_index = unit_index.min(SPEED_UNITS.len() - 1);
let mut value = bytes_per_sec as f64 / SCALES[unit_index];
if value.round() >= SPEED_DISPLAY_THRESHOLD && unit_index < SPEED_UNITS.len() - 1 {
unit_index += 1;
value = bytes_per_sec as f64 / SCALES[unit_index];
}
if value < 9.95 {
format!("{value:.1}{}", SPEED_UNITS[unit_index])
} else {
format!("{:.0}{}", value.round(), SPEED_UNITS[unit_index])
}
}
#[cfg(test)]
mod tests {
use super::format_bytes_per_second;
#[test]
fn format_handles_byte_boundaries() {
assert_eq!(format_bytes_per_second(0), "0B/s");
assert_eq!(format_bytes_per_second(999), "999B/s");
// 1000 >= SPEED_DISPLAY_THRESHOLD升档为 K/s保证不超过三位数
assert_eq!(format_bytes_per_second(1000), "1.0K/s");
assert_eq!(format_bytes_per_second(1024), "1.0K/s");
}
#[test]
fn format_handles_decimal_and_integer_rules() {
assert_eq!(format_bytes_per_second(9 * 1024), "9.0K/s");
// 9.999 K/srounded_1dp = 10.0,不满足 < 10应显示整数 "10K/s"
assert_eq!(format_bytes_per_second(10 * 1024 - 1), "10K/s");
assert_eq!(format_bytes_per_second(10 * 1024), "10K/s");
assert_eq!(format_bytes_per_second(123 * 1024), "123K/s");
}
#[test]
fn format_handles_unit_promotion_after_rounding() {
// 999.5 K/s 四舍五入为 1000≥ SPEED_DISPLAY_THRESHOLD升档为 1.0M/s
assert_eq!(format_bytes_per_second(999 * 1024 + 512), "1.0M/s");
assert_eq!(format_bytes_per_second(1024 * 1024), "1.0M/s");
assert_eq!(format_bytes_per_second(1536 * 1024), "1.5M/s");
}
}

View File

@ -0,0 +1,152 @@
//! macOS 托盘速率富文本渲染模块
//!
//! 通过 objc2 调用 NSAttributedString 实现托盘速率的富文本显示,
//! 支持等宽字体、自适应深色/浅色模式配色、两行定宽布局。
use std::cell::RefCell;
use crate::utils::speed::format_bytes_per_second;
use crate::{Type, logging};
use objc2::MainThreadMarker;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2_app_kit::{
NSBaselineOffsetAttributeName, NSColor, NSFont, NSFontAttributeName, NSFontWeightRegular,
NSForegroundColorAttributeName, NSMutableParagraphStyle, NSParagraphStyleAttributeName, NSStatusItem,
NSTextAlignment,
};
use objc2_foundation::{NSAttributedString, NSDictionary, NSNumber, NSString};
/// 富文本渲染使用的字号(适配两行在托盘栏的高度)
const TRAY_FONT_SIZE: f64 = 9.5;
/// 两行文本的行间距(负值可压缩两行高度,便于与图标纵向居中)
const TRAY_LINE_SPACING: f64 = -1.0;
/// 两行文本整体行高倍数(用于进一步压缩文本块高度)
const TRAY_LINE_HEIGHT_MULTIPLE: f64 = 1.00;
/// 文本块段前偏移(用于将两行文本整体下移)
const TRAY_PARAGRAPH_SPACING_BEFORE: f64 = -5.0;
/// 文字基线偏移(负值向下移动,更容易与托盘图标垂直居中)
const TRAY_BASELINE_OFFSET: f64 = -4.0;
thread_local! {
/// 托盘速率富文本属性字典(主线程缓存,避免每帧重建 ObjC 对象)。
/// 仅在首次调用时初始化,后续复用同一实例。
static TRAY_SPEED_ATTRS: Retained<NSDictionary<NSString, AnyObject>> = build_attributes();
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
}
/// 将上行/下行速率格式化为两行定宽文本
///
/// # Arguments
/// * `up` - 上行速率(字节/秒)
/// * `down` - 下行速率(字节/秒)
fn format_tray_speed(up: u64, down: u64) -> String {
// 上行放在第一行,下行放在第二行;通过上下布局表达方向,不再显示箭头字符。
let up_str = format_bytes_per_second(up);
let down_str = format_bytes_per_second(down);
format!("{:>6}\n{:>6}", up_str, down_str)
}
/// 构造带富文本样式属性的 NSDictionary
///
/// 包含:等宽字体、自适应标签颜色、右对齐段落样式
fn build_attributes() -> Retained<NSDictionary<NSString, AnyObject>> {
unsafe {
// 等宽系统字体,确保数字不跳动
let font = NSFont::monospacedSystemFontOfSize_weight(TRAY_FONT_SIZE, NSFontWeightRegular);
// 自适应标签颜色(自动跟随深色/浅色模式)
let color = NSColor::labelColor();
// 段落样式:右对齐,保证定宽视觉一致
let para_style = NSMutableParagraphStyle::new();
para_style.setAlignment(NSTextAlignment::Right);
para_style.setLineSpacing(TRAY_LINE_SPACING);
para_style.setLineHeightMultiple(TRAY_LINE_HEIGHT_MULTIPLE);
para_style.setParagraphSpacingBefore(TRAY_PARAGRAPH_SPACING_BEFORE);
// 基线偏移:用于精确控制两行速率整体的纵向位置
let baseline_offset = NSNumber::new_f64(TRAY_BASELINE_OFFSET);
let keys: &[&NSString] = &[
NSFontAttributeName,
NSForegroundColorAttributeName,
NSParagraphStyleAttributeName,
NSBaselineOffsetAttributeName,
];
let values: &[&AnyObject] = &[&font, &color, &para_style, &baseline_offset];
NSDictionary::from_slices(keys, values)
}
}
/// 创建带属性的富文本
///
/// # Arguments
/// * `text` - 富文本字符串内容
/// * `attrs` - 富文本属性字典
fn create_attributed_string(
text: &NSString,
attrs: Option<&NSDictionary<NSString, AnyObject>>,
) -> Retained<NSAttributedString> {
unsafe {
NSAttributedString::initWithString_attributes(<NSAttributedString as objc2::AnyThread>::alloc(), text, attrs)
}
}
/// 在主线程下设置 NSStatusItem 按钮的富文本标题
///
/// 依赖 Tauri `with_inner_tray_icon` 保证回调在主线程执行;
/// 若意外在非主线程调用,`MainThreadMarker::new()` 返回 `None` 并记录警告。
///
/// # Arguments
/// * `status_item` - macOS 托盘 NSStatusItem 引用
/// * `text` - 富文本字符串内容
/// * `attrs` - 富文本属性字典
fn apply_status_item_attributed_title(
status_item: &NSStatusItem,
text: &NSString,
attrs: Option<&NSDictionary<NSString, AnyObject>>,
) {
let Some(mtm) = MainThreadMarker::new() else {
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
return;
};
let Some(button) = status_item.button(mtm) else {
return;
};
let attr_str = create_attributed_string(text, attrs);
button.setAttributedTitle(&attr_str);
}
/// 将速率以富文本形式设置到 NSStatusItem 的按钮上
///
/// # Arguments
/// * `status_item` - macOS 托盘 NSStatusItem 引用
/// * `up` - 上行速率(字节/秒)
/// * `down` - 下行速率(字节/秒)
pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64) {
let speed_text = format_tray_speed(up, down);
let changed = LAST_DISPLAY_STR.with(|last| {
let mut last_borrow = last.borrow_mut();
if *last_borrow == speed_text {
false
} else {
*last_borrow = speed_text.clone();
true
}
});
if !changed {
return;
}
let ns_string = NSString::from_str(&speed_text);
TRAY_SPEED_ATTRS.with(|attrs| {
apply_status_item_attributed_title(status_item, &ns_string, Some(&**attrs));
});
}
/// 清除 NSStatusItem 按钮上的富文本速率显示
///
/// # Arguments
/// * `status_item` - macOS 托盘 NSStatusItem 引用
pub fn clear_speed_attributed_title(status_item: &NSStatusItem) {
let empty = NSString::from_str("");
apply_status_item_attributed_title(status_item, &empty, None);
}

View File

@ -405,9 +405,13 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
)}
{/* {OS === "macos" && (
{OS === 'macos' && (
<Item>
<ListItemText primary={t("settings.components.verge.layout.fields.enableTraySpeed")} />
<ListItemText
primary={t(
'settings.components.verge.layout.fields.enableTraySpeed',
)}
/>
<GuardState
value={verge?.enable_tray_speed ?? false}
valueProps="checked"
@ -419,7 +423,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
<Switch edge="end" />
</GuardState>
</Item>
)} */}
)}
{/* {OS === "macos" && (
<Item>
<ListItemText primary={t("settings.components.verge.layout.fields.enableTrayIcon")} />

View File

@ -912,7 +912,7 @@ interface IVergeConfig {
common_tray_icon?: boolean
sysproxy_tray_icon?: boolean
tun_tray_icon?: boolean
// enable_tray_speed?: boolean;
enable_tray_speed?: boolean
// enable_tray_icon?: boolean;
tray_proxy_groups_display_mode?: 'default' | 'inline' | 'disable'
tray_inline_outbound_modes?: boolean