Compare commits

...
Author SHA1 Message Date
TungliesandGLM-5.3 010c037dde refactor: replace smartstring with tristr across the codebase
Co-authored-by: GLM-5.3 <glm@z.ai>
2026-10-27 06:55:10 +08:00
TungliesandGLM-5.3 e017d4d9d4 refactor: replace smartstring with tristr across the codebase
Co-authored-by: GLM-5.3 <glm@z.ai>
2026-10-27 06:47:53 +08:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 10ca0a5d0c chore(deps): update npm dependencies (#8421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-25 16:24:26 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 324c8c9016 chore(deps): update dependency pnpm to v12.15.0 (#8418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-25 15:35:18 +00:00
oomeow 02520d7392 fix(tray): skip Win-key accelerators on Windows (#8400) 2026-10-25 09:34:09 +00:00
Tunglies 5b8edbaead feat(tun): expose mihomo tun dns-mode in TUN settings
Support the dns-mode field introduced in mihomo v1.19.33. A
button group below the stack selector offers
Auto/Disabled/Native/Hijack; Auto writes an empty string, which
the core treats as the legacy behavior. Adding dns-mode to
GUI_KEYS makes the value GUI-authoritative once saved, matching
the other TUN dialog fields.
2026-10-25 16:04:59 +08:00
Tunglies 1ed8050947 fix(perf): auto-derive WebDriver window rect scale in harness 2026-10-25 13:14:14 +08:00
❤是纱雾酱哟~ 3d9a79a3fb fix(windows): patch executable for common-controls-v6 (#8378)
- Enhance patch logic, also patch for "example" executables

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-10-22 09:59:10 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8b88ddb7f2 chore(deps): update node.js to v24.22.1 (#8389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-21 17:25:15 +00:00
Tunglies 02bd739ab6 docs: clarify issue and PR intake rules 2026-10-21 19:36:50 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
b6c7af82a7 chore(deps): update tauri (#8346)
* chore(deps): update tauri

* fix(deps): pin rust-ini to 0.21.1 for tauri-plugin-deep-link 2.6.2

tauri-plugin-deep-link 2.6.2 constructs ini::ParseOption with an
exhaustive struct literal, which fails to compile against rust-ini
0.21.2+ (new required fields enabled_indented_mutiline_value and
enabled_preserve_key_leading_whitespace, E0063 on linux). Upstream
fixed it in tauri-apps/plugins-workspace#3711 with ..Default::default()
but no release contains the fix yet, so downgrade rust-ini in Cargo.lock
until deep-link 2.6.3 ships.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-10-21 08:25:55 +00:00
107 changed files with 1226 additions and 1357 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "24.22.0",
"pnpmVersion": "12.14.0"
"version": "24.22.1",
"pnpmVersion": "12.15.0"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.1",
+33
View File
@@ -0,0 +1,33 @@
# Issue Guidelines / Issue 提交指引
Use these suggestions to make reports easier to assess. State what you observed
and what you could not verify; unknown details are fine.
以下建议有助于维护者评估问题。请说明实际观察到的现象和无法确认的部分;未知信息可以留空。
- Search existing issues by symptom, conditions, and outcome. Add new evidence
to an existing report when it describes the same problem.
按现象、条件和结果搜索已有 Issue。若描述的是同一问题,请在原 Issue 中补充新证据。
- Keep each issue to one distinct user problem or requested outcome. Split
unrelated findings instead of bundling an audit or checklist.
每个 Issue 聚焦一个独立的问题或需求结果;无关发现请拆开,不要把审计清单打包成一个 Issue。
- For bugs, include the version, operating system, steps, and actual versus
expected behavior when known. A root-cause diagnosis is not required. Share
only relevant logs and redact tokens or personal information.
反馈 Bug 时,尽可能提供版本、操作系统、复现步骤及实际与预期行为。无需先判断根因;只提供相关日志,并遮盖令牌和个人信息。
- For feature requests, describe the user task, current limitation, desired
outcome, and affected platforms. An implementation proposal is optional.
提交功能请求时,请描述用户要完成的任务、当前限制、期望结果和适用平台;实现方案不是必填项。
- Separate observations from guesses. An AI or scanner finding is a hypothesis
until its affected configuration, concrete impact, and supporting evidence
are stated; do not present a list of theoretical risks as confirmed bugs.
区分观察结果和推测。AI 或扫描器的发现应先视为假设;请说明受影响的配置、具体影响和支持证据,不要把理论风险清单写成已确认的 Bug。
- Do not post secrets, personal data, or sensitive exploit details in a public
issue. Ask maintainers how to share sensitive security information safely.
请勿在公开 Issue 中发布密钥、个人信息或敏感利用细节;如涉及安全敏感信息,请先询问维护者如何安全提供。
- AI assistance is welcome; human contributors do not need to disclose it. If
an AI agent or automation creates or co-authors an issue, add a final body line
such as `Assisted by: GPT-5.6 High`. Omit effort when the runtime does not
report it. Disclosure is transparency only, not evidence for or against issue
quality.
欢迎使用 AI 辅助,人工提交者无需披露。如果 Issue 由 AI Agent 或自动化创建或共同撰写,请在正文末尾添加披露行,例如 `Assisted by: GPT-5.6 High`。运行环境未提供 effort 时可省略。披露仅用于提高透明度,不作为 Issue 质量的正面或负面证据。
+4
View File
@@ -10,6 +10,8 @@ body:
value: |
## 在提交问题之前,请确认以下事项:
提交前可参考 [Issue 提交指引](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md),说明可观察到的问题和证据;不确定的原因请标为猜测。
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
@@ -19,6 +21,8 @@ body:
## Before submitting the issue, please make sure of the following checklist:
See the [Issue guidelines](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md) for how to describe observed behavior and evidence; mark uncertain causes as hypotheses.
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
@@ -9,12 +9,16 @@ body:
attributes:
value: |
## 在提交问题之前,请确认以下事项:
可参考 [Issue 提交指引](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md),描述用户任务、当前限制和期望结果。
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭
## Before submitting the issue, please make sure of the following checklist:
See the [Issue guidelines](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md) for describing the user task, current limitation, and desired outcome.
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
+7 -1
View File
@@ -1,6 +1,12 @@
## Problem
What breaks or what is missing. Link the issue if one exists (`Fixes #123`).
What breaks or what is missing. Product bug fixes and features require a
pre-existing issue; link it here (`Fixes #123` only when this PR fully resolves
it). Maintenance work (such as chore, perf, docs, refactor, test, CI/build,
dependency, style, routine localization, or release changes) does not require
an issue link; link one when it defines the scope. For reverts, link the PR or
commit being reverted. For security-sensitive fixes that cannot be tracked
publicly, follow maintainer-approved private tracking.
## Approach
+5 -5
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -185,7 +185,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
cache: 'pnpm'
- name: Pnpm Cache
@@ -282,7 +282,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
cache: 'pnpm'
- name: Pnpm Cache
@@ -426,7 +426,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
cache: 'pnpm'
- name: Pnpm Cache
@@ -524,7 +524,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6.1.0
name: Install pnpm
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
cache: 'pnpm'
- name: Pnpm Cache
+2 -2
View File
@@ -52,7 +52,7 @@ jobs:
- uses: actions/setup-node@v7
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: '24.22.0'
node-version: '24.22.1'
cache: 'pnpm'
- name: Restore pnpm cache
@@ -138,7 +138,7 @@ jobs:
- uses: actions/setup-node@v7
if: steps.check_inputs.outputs.contract == 'true'
with:
node-version: '24.22.0'
node-version: '24.22.1'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
+6 -6
View File
@@ -230,7 +230,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -331,7 +331,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- name: Install pnpm
uses: pnpm/action-setup@v6
@@ -466,7 +466,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -547,7 +547,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -573,7 +573,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -638,7 +638,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.22.0'
node-version: '24.22.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: '24.22.0'
node-version: '24.22.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: '24.22.0'
node-version: '24.22.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: '24.22.0'
node-version: '24.22.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -1,3 +1,3 @@
nodejs 24.22.0
pnpm 12.14.0
nodejs 24.22.1
pnpm 12.15.0
rust 1.99.1
+25 -21
View File
@@ -18,22 +18,25 @@ These rules apply to every change, whether human- or agent-authored. They match
the ownership evidence the AI-slop review evaluates (see
[`pr-ai-slop-review.md`](.github/workflows/pr-ai-slop-review.md)).
1. **Issue first.** Non-trivial changes require a pre-existing issue describing
the problem. If none exists, ask the maintainers to open or approve one
before implementing.
1. **Issue first.** Product bug fixes and features require a pre-existing issue;
self-scoped maintenance work does not. Follow the PR template for linkage and
revert references. If a required issue is missing, ask maintainers to open or
approve one before implementing. For security-sensitive fixes that cannot be
tracked publicly, follow maintainer-approved private tracking.
2. **Scope discipline.** Every changed file must be justifiable from the linked
issue. No drive-by refactors, renames, formatting churn, or dependency bumps
unrelated to the problem being fixed.
issue or the stated PR scope. No drive-by refactors, renames, formatting
churn, or dependency bumps unrelated to the work.
3. **Author accountability.** AI assistance is welcome, but the contributor owns
the result: understand the change, describe the problem and approach in your
own words, and verify the change against the reported behavior before
submitting.
4. **Tests are justified, not default.** Do not add tests, test scaffolding, or
speculative defensive code unless the linked issue demands them. When a test
is genuinely necessary — it reproduces the reported regression or guards
behavior whose breakage would otherwise go unnoticed — keep it minimal and
state in the PR body why it is needed. Bulk test files and defensive
programming for hypothetical failure modes are PR bloat, not rigor.
speculative defensive code unless the linked issue or stated task demands
them. When a test is genuinely necessary — it reproduces the reported
regression or guards behavior whose breakage would otherwise go unnoticed —
keep it minimal and state in the PR body why it is needed. Bulk test files
and defensive programming for hypothetical failure modes are PR bloat, not
rigor.
5. **Comments state constraints, not narration.** Write a comment only for a
non-obvious constraint the code cannot express; never restate what the code
does.
@@ -44,13 +47,12 @@ the ownership evidence the AI-slop review evaluates (see
real evidence instead: reproduction steps, failure output, targeted tests.
8. **Minimal diffs.** Match the surrounding code's style, naming, and comment
density. Do not introduce new dependencies or restructure working code unless
the issue demands it.
9. **Disclose AI automation.** When an agent produces or co-produces a change,
append a footer line to the PR body with the model and effort used (e.g.
`Assisted by: GPT-5.6 High`). The PR template intentionally omits this line —
the agent adds it itself, humans are not asked to declare anything. Effort may
be omitted when the runtime does not report it. Disclosure is transparency
only; it does not substitute for any rule above.
the linked issue or stated task demands it.
9. **Disclose AI automation.** When an agent creates or co-creates a PR or Issue,
append a final body line with the model and effort used (e.g.
`Assisted by: GPT-5.6 High`). Effort may be omitted when the runtime does not
report it. Human contributors are not asked to disclose AI assistance.
Disclosure is transparency only and carries no positive or negative weight.
10. **Compiled workflows.** The AI-slop review policy in
[pr-ai-slop-review.md](.github/workflows/pr-ai-slop-review.md) is compiled:
after editing it, run `gh aw compile` and commit the regenerated
@@ -59,8 +61,10 @@ the ownership evidence the AI-slop review evaluates (see
[`template/Changelog.md`](template/Changelog.md): one line per
user-visible change, no internals.
## Pull Request Shape
## Issue and PR Workflows
Describe three things, briefly: the problem (with issue link), why this approach
solves it, and what changed. See
[`PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md).
When drafting, triaging, splitting, or implementing from a community issue, use
the conditional checklist in [`.github/ISSUE_GUIDELINES.md`](.github/ISSUE_GUIDELINES.md).
Do not make GitHub issue changes without explicit authorization. For PRs, follow
the directly loaded [PR template](.github/PULL_REQUEST_TEMPLATE.md); do not
duplicate its prompts here.
Generated
+82 -173
View File
@@ -1119,7 +1119,7 @@ dependencies = [
"getrandom 0.4.3",
"libc",
"log",
"minisign-verify 0.3.0",
"minisign-verify",
"nanoid",
"network-interface",
"objc2",
@@ -1139,7 +1139,6 @@ dependencies = [
"serde_json",
"serde_yaml_ng",
"sha2 0.11.1",
"smartstring",
"socket2",
"specta",
"specta-typescript",
@@ -1169,6 +1168,7 @@ dependencies = [
"tokio-stream",
"tokio-util",
"tracing",
"tristr",
"warp",
"webpki-roots",
"windows 0.62.2",
@@ -4170,12 +4170,6 @@ dependencies = [
"unicase",
]
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "minisign-verify"
version = "0.3.0"
@@ -5999,12 +5993,13 @@ dependencies = [
[[package]]
name = "rust-ini"
version = "0.21.3"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
dependencies = [
"cfg-if",
"ordered-multimap",
"trim-in-place",
]
[[package]]
@@ -6625,7 +6620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
dependencies = [
"libc",
"sigchld",
"windows-sys 0.60.2",
]
@@ -6645,17 +6639,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "sigchld"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
dependencies = [
"libc",
"os_pipe",
"signal-hook 0.3.18",
]
[[package]]
name = "signal-hook"
version = "0.1.17"
@@ -6666,16 +6649,6 @@ dependencies = [
"signal-hook-registry",
]
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -6747,18 +6720,6 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"serde",
"static_assertions",
"version_check",
]
[[package]]
name = "socket2"
version = "0.6.5"
@@ -6881,6 +6842,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "static_vcruntime"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
[[package]]
name = "string_cache"
version = "0.9.0"
@@ -6940,9 +6907,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
dependencies = [
"base64 0.21.7",
"serde",
@@ -7189,9 +7156,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tauri"
version = "2.12.1"
version = "2.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9"
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
dependencies = [
"anyhow",
"bytes",
@@ -7237,16 +7204,16 @@ dependencies = [
"tray-icon",
"url",
"webkit2gtk",
"webview2-com 0.39.1",
"webview2-com",
"window-vibrancy",
"windows 0.62.2",
]
[[package]]
name = "tauri-build"
version = "2.7.1"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
dependencies = [
"anyhow",
"cargo_toml",
@@ -7258,6 +7225,8 @@ dependencies = [
"semver 1.0.28",
"serde",
"serde_json",
"static_vcruntime",
"strsim",
"tauri-utils",
"tauri-winres",
"walkdir",
@@ -7265,11 +7234,11 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.7.1"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
dependencies = [
"base64 0.22.1",
"base64 0.23.1",
"brotli",
"ico",
"json-patch",
@@ -7292,9 +7261,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.7.1"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -7350,9 +7319,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
checksum = "9be5ed8d94c33afe8377e885119069f6508780659271d0a25cfc54d441f2bc81"
dependencies = [
"arboard",
"log",
@@ -7365,9 +7334,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.6.1"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736"
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
dependencies = [
"dunce",
"plist",
@@ -7413,9 +7382,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.8.1"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
dependencies = [
"log",
"raw-window-handle",
@@ -7431,9 +7400,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.6.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b"
dependencies = [
"anyhow",
"dunce",
@@ -7455,9 +7424,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2"
checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d"
dependencies = [
"global-hotkey",
"log",
@@ -7470,9 +7439,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.8.0"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1"
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
dependencies = [
"bytes",
"cookie_store",
@@ -7489,7 +7458,7 @@ dependencies = [
"thiserror 2.0.20",
"tokio",
"url",
"urlpattern 0.3.0",
"urlpattern",
]
[[package]]
@@ -7519,9 +7488,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.5.1"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
dependencies = [
"log",
"notify-rust",
@@ -7534,13 +7503,14 @@ dependencies = [
"thiserror 2.0.20",
"time",
"url",
"zbus",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.7.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
dependencies = [
"dunce",
"glob",
@@ -7570,11 +7540,12 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
dependencies = [
"encoding_rs",
"libc",
"log",
"open",
"os_pipe",
@@ -7587,13 +7558,14 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.20",
"tokio",
"windows-sys 0.60.2",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.13.1"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18"
checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec"
dependencies = [
"base64 0.22.1",
"dirs 7.0.0",
@@ -7601,8 +7573,9 @@ dependencies = [
"futures-util",
"http",
"infer",
"libc",
"log",
"minisign-verify 0.2.5",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.5",
@@ -7624,9 +7597,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-wdio-webdriver"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045"
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
dependencies = [
"async-trait",
"axum",
@@ -7650,9 +7623,9 @@ dependencies = [
"tracing",
"uuid",
"webkit2gtk",
"webview2-com 0.38.2",
"windows 0.61.3",
"windows-core 0.61.2",
"webview2-com",
"windows 0.62.2",
"windows-core 0.62.2",
]
[[package]]
@@ -7672,9 +7645,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.12.1"
version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413"
checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
dependencies = [
"cookie",
"dpi",
@@ -7691,15 +7664,15 @@ dependencies = [
"thiserror 2.0.20",
"url",
"webkit2gtk",
"webview2-com 0.39.1",
"webview2-com",
"windows 0.62.2",
]
[[package]]
name = "tauri-runtime-wry"
version = "2.12.1"
version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
dependencies = [
"gtk",
"http",
@@ -7717,7 +7690,7 @@ dependencies = [
"tracing",
"url",
"webkit2gtk",
"webview2-com 0.39.1",
"webview2-com",
"windows 0.62.2",
"wry",
]
@@ -7755,9 +7728,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.10.1"
version = "2.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee"
checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
dependencies = [
"anyhow",
"brotli",
@@ -7786,7 +7759,7 @@ dependencies = [
"thiserror 2.0.20",
"toml 1.1.6+spec-1.1.0",
"url",
"urlpattern 0.6.0",
"urlpattern",
"uuid",
"walkdir",
]
@@ -7905,7 +7878,7 @@ dependencies = [
"regex",
"semver 0.11.0",
"sha2 0.9.9",
"signal-hook 0.1.17",
"signal-hook",
"terminfo",
"termios",
"thiserror 1.0.69",
@@ -8503,6 +8476,12 @@ dependencies = [
"petgraph",
]
[[package]]
name = "trim-in-place"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa60874b45b168f67f3f9ba1ee821ad77144d6fa19cef1cd1874a7450a948da"
[[package]]
name = "triomphe"
version = "0.1.16"
@@ -8514,6 +8493,14 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "tristr"
version = "0.1.0"
dependencies = [
"serde",
"specta",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -8595,47 +8582,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-ucd-ident"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
@@ -8695,18 +8641,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlpattern"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
dependencies = [
"regex",
"serde",
"unic-ucd-ident",
"url",
]
[[package]]
name = "urlpattern"
version = "0.6.0"
@@ -9098,20 +9032,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [
"webview2-com-macros",
"webview2-com-sys 0.38.2",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
]
[[package]]
name = "webview2-com"
version = "0.39.1"
@@ -9119,7 +9039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
dependencies = [
"webview2-com-macros",
"webview2-com-sys 0.39.1",
"webview2-com-sys",
"windows 0.62.2",
"windows-core 0.62.2",
]
@@ -9135,17 +9055,6 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "webview2-com-sys"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
"thiserror 2.0.20",
"windows 0.61.3",
"windows-core 0.61.2",
]
[[package]]
name = "webview2-com-sys"
version = "0.39.1"
@@ -9885,7 +9794,7 @@ dependencies = [
"url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com 0.39.1",
"webview2-com",
"windows 0.62.2",
"windows-core 0.62.2",
"windows-version",
+3 -4
View File
@@ -61,8 +61,8 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44"
tauri = { version = "2.12.1" }
tauri-plugin-clipboard-manager = "2.4.1"
tauri = { version = "2.12.4" }
tauri-plugin-clipboard-manager = "2.5.0"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
chrono = "0.4.45"
@@ -85,9 +85,8 @@ reqwest = { version = "0.13.5", features = [
] }
rust_iso3166 = "0.2.0"
smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" }
tristr = { path = "../tristr", default-features = false, features = ["serde", "specta"] }
serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
+1
View File
@@ -17,6 +17,7 @@
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
</details>
+13 -13
View File
@@ -54,13 +54,13 @@
"@mui/icons-material": "^9.4.0",
"@mui/material": "^9.4.0",
"@tanstack/react-virtual": "^3.14.13",
"@tauri-apps/api": "2.12.1",
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
"@tauri-apps/plugin-dialog": "2.8.0",
"@tauri-apps/plugin-fs": "2.6.0",
"@tauri-apps/plugin-http": "2.8.0",
"@tauri-apps/plugin-opener": "2.7.0",
"@tauri-apps/plugin-updater": "2.13.1",
"@tauri-apps/api": "2.12.4",
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
"@tauri-apps/plugin-dialog": "2.8.3",
"@tauri-apps/plugin-fs": "2.7.1",
"@tauri-apps/plugin-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2",
"@tauri-apps/plugin-updater": "2.14.0",
"ahooks": "^3.10.0",
"cidr-block": "^2.3.0",
"compare-versions": "^6.1.1",
@@ -79,7 +79,7 @@
"react-dom": "19.3.1",
"react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0",
"react-i18next": "17.0.19",
"react-i18next": "17.1.8",
"react-markdown": "10.1.0",
"react-router": "^8.4.0",
"rehype-raw": "^7.0.0",
@@ -95,7 +95,7 @@
"@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.1",
"@tauri-apps/cli": "2.12.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.13.6",
"@types/react": "19.3.0",
@@ -103,7 +103,7 @@
"@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.4.0",
"@wdio/tauri-service": "1.5.0",
"adm-zip": "^0.6.1",
"axios": "^1.20.0",
"cli-color": "^2.0.4",
@@ -123,7 +123,7 @@
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.41.0",
"knip": "6.43.0",
"lint-staged": "^17.5.1",
"sass": "^1.104.1",
"tar": "^7.5.22",
@@ -133,7 +133,7 @@
"vite": "^8.3.0",
"vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1",
"webdriverio": "10.0.2",
"webdriverio": "10.1.0",
"ws": "8.22.0"
},
"lint-staged": {
@@ -146,5 +146,5 @@
]
},
"type": "module",
"packageManager": "pnpm@12.14.0"
"packageManager": "pnpm@12.15.0"
}
+411 -615
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -434,12 +434,13 @@ for (let iteration = 0; iteration < repeat && !interrupted; iteration++) {
devicePixelRatio,
])
run.viewport = { dom: viewport, driver: await browser.getWindowRect() }
const driverScale = run.viewport.driver.width / 1000
assert(
run.viewport.driver.width / viewport[2] === 1000 &&
run.viewport.driver.height / viewport[2] === 700 &&
viewport[0] === 1000 &&
viewport[0] === 1000 &&
viewport[1] > 0 &&
viewport[1] <= 700,
viewport[1] <= 700 &&
(driverScale === 1 || driverScale === viewport[2]) &&
run.viewport.driver.height / driverScale === 700,
`unexpected window dimensions: ${JSON.stringify(run.viewport)}`,
)
assert(
+12 -12
View File
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.7.1", features = [] }
tauri-build = { version = "2.7.3", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
@@ -52,9 +52,9 @@ anyhow = { workspace = true }
tokio = { workspace = true }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tristr = { workspace = true }
serde_json = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
@@ -72,12 +72,12 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
"guard",
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.4.0"
tauri-plugin-opener = "2.7.0"
tauri-plugin-dialog = "2.8.1"
tauri-plugin-fs = "2.6.0"
tauri-plugin-shell = "2.5.0"
tauri-plugin-opener = "2.7.2"
tauri-plugin-dialog = "2.8.3"
tauri-plugin-fs = "2.7.1"
tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.1"
tauri-plugin-deep-link = "2.6.2"
tauri-plugin-window-state = "2.5.0"
zip = "9.0.4"
flate2 = "1.1.10"
@@ -90,10 +90,10 @@ getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.5.1"
tauri-plugin-notification = "2.7.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.0"
tauri-plugin-http = "2.8.3"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.2", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
@@ -120,7 +120,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
objc2 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [
"NSString",
@@ -153,8 +153,8 @@ windows-sys.workspace = true
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.7.0"
tauri-plugin-global-shortcut = "2.4.0"
tauri-plugin-updater = "2.13.1"
tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.14.0"
[dev-dependencies]
criterion = { workspace = true }
+10 -5
View File
@@ -1,10 +1,9 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
&& std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc")
&& std::env::var_os("CARGO_FEATURE_CLIPPY").is_some()
{
// Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses.
// Give mock-context builds their own manifest so tests can resolve TaskDialogIndirect.
// Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses or examples.
// Give non-app targets their own manifest so tests and examples can resolve TaskDialogIndirect.
let out_dir = std::env::var_os("OUT_DIR").ok_or_else(|| std::io::Error::other("OUT_DIR is not set"))?;
let manifest_path = std::path::PathBuf::from(out_dir).join("windows-test-manifest.xml");
std::fs::write(
@@ -25,8 +24,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
</assembly>
"#,
)?;
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{}", manifest_path.display());
let manifest_display = manifest_path.display();
if std::env::var_os("CARGO_FEATURE_CLIPPY").is_some() {
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{manifest_display}");
} else {
println!("cargo:rustc-link-arg-examples=/MANIFEST:EMBED");
println!("cargo:rustc-link-arg-examples=/MANIFESTINPUT:{manifest_display}");
}
}
#[cfg(feature = "clippy")]
+11 -14
View File
@@ -5,8 +5,8 @@ use crate::{
feat,
utils::dirs,
};
use smartstring::alias::String;
use tauri::{AppHandle, Manager as _, ipc::Channel};
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -59,10 +59,10 @@ pub async fn restart_app() -> CmdResult<()> {
#[specta::specta]
pub async fn install_update(
app_handle: AppHandle,
version: std::string::String,
version: String,
on_event: Channel<DownloadEvent>,
) -> CmdResult<bool> {
let version: String = version.into();
let version: String = version;
SilentUpdater::global()
.install_update(&app_handle, &version, |event| {
let _ = on_event.send(event);
@@ -79,25 +79,22 @@ pub fn cancel_update_download() {
#[tauri::command]
#[specta::specta]
pub fn get_app_dir() -> CmdResult<std::string::String> {
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
Ok(app_home_dir)
}
#[tauri::command]
#[specta::specta]
pub async fn download_icon_cache(
url: std::string::String,
name: std::string::String,
) -> CmdResult<std::string::String> {
let url: String = url.into();
let name: String = name.into();
feat::download_icon_cache(url, name).await.map(Into::into)
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let url: String = url;
let name: String = name;
feat::download_icon_cache(url, name).await
}
#[tauri::command]
#[specta::specta]
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
let path: String = path.into();
feat::copy_icon_file(path, icon_info).await.map(Into::into)
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
let path: String = path;
feat::copy_icon_file(path, icon_info).await
}
+11 -11
View File
@@ -1,7 +1,7 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, feat};
use feat::LocalBackupFile;
use smartstring::alias::String;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -17,15 +17,15 @@ pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
#[tauri::command]
#[specta::specta]
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
feat::delete_local_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
feat::restore_local_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
@@ -33,15 +33,15 @@ pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()
#[tauri::command]
#[specta::specta]
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
let source: String = source.into();
feat::import_local_backup(source).await.map(Into::into).stringify_err()
pub async fn import_local_backup(source: String) -> CmdResult<String> {
let source: String = source;
feat::import_local_backup(source).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
let destination: String = destination.into();
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
let filename: String = filename;
let destination: String = destination;
feat::export_local_backup(filename, destination).await.stringify_err()
}
+16 -16
View File
@@ -12,8 +12,8 @@ use crate::{
},
};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -37,8 +37,8 @@ pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
let payload: String = payload.into();
pub async fn patch_clash_mode(payload: String) -> CmdResult {
let payload: String = payload;
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
@@ -47,14 +47,14 @@ pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
#[tauri::command]
#[specta::specta]
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
Ok(Config::clash().await.data_arc().get_mode())
}
#[tauri::command]
#[specta::specta]
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core.into();
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core;
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
@@ -112,8 +112,8 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
#[tauri::command]
#[specta::specta]
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
let url: String = url.into();
pub async fn test_delay(url: String) -> CmdResult<u32> {
let url: String = url;
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
@@ -148,12 +148,12 @@ pub fn take_dns_override_notice() -> bool {
#[tauri::command]
#[specta::specta]
pub async fn set_dns_override(
profile_uid: std::string::String,
profile_uid: String,
enabled: bool,
confirmation: Option<std::string::String>,
confirmation: Option<String>,
) -> CmdResult<feat::DnsOverrideOutcome> {
let profile_uid: String = profile_uid.into();
let confirmation = confirmation.map(String::from);
let profile_uid: String = profile_uid;
let confirmation = confirmation;
feat::set_dns_override(profile_uid, enabled, confirmation)
.await
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
@@ -212,14 +212,14 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
match fs::read_to_string(&dns_path).await {
Ok(content) => Ok(Some(content)),
Ok(content) => Ok(Some(content.into())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(CommandFailure::plain(error)),
}
@@ -244,7 +244,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[specta::specta]
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
pub async fn get_clash_logs() -> CmdResult<Vec<String>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs)
Ok(logs.into_iter().collect())
}
@@ -2,6 +2,7 @@ pub use clash_verge_media_unlock::UnlockItem;
use reqwest::{Client, Proxy};
use std::time::Duration;
use tauri::{command, ipc::Channel};
use tristr::alias::String;
#[command]
#[specta::specta]
@@ -23,7 +24,7 @@ async fn create_client() -> Result<Client, String> {
.cookie_store(true)
.proxy(proxy)
.build()
.map_err(|error| format!("创建HTTP客户端失败: {error}"))
.map_err(|error| format!("创建HTTP客户端失败: {error}").into())
}
#[command]
@@ -40,5 +41,7 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
#[specta::specta]
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
let client = create_client().await?;
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
clash_verge_media_unlock::check_media_unlock_item(&client, &name)
.await
.map_err(Into::into)
}
+6 -16
View File
@@ -1,5 +1,5 @@
use anyhow::Result;
use smartstring::alias::String;
use tristr::alias::String;
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
@@ -8,9 +8,9 @@ pub type CmdResult<T = ()> = Result<T, CommandFailure>;
pub struct CommandFailure {
/// Stable frontend identifier, when classified.
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<std::string::String>,
pub code: Option<String>,
/// Human-readable diagnostic detail.
pub detail: std::string::String,
pub detail: String,
/// Requested operation when known by the command.
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<crate::core::notification::FailedOperation>,
@@ -20,8 +20,8 @@ impl CommandFailure {
/// A failure with a stable code.
pub fn coded(code: &str, detail: impl std::fmt::Display) -> Self {
Self {
code: Some(code.to_owned()),
detail: detail.to_string(),
code: Some(code.into()),
detail: detail.to_string().into(),
operation: None,
}
}
@@ -30,7 +30,7 @@ impl CommandFailure {
pub fn plain(detail: impl std::fmt::Display) -> Self {
Self {
code: None,
detail: detail.to_string(),
detail: detail.to_string().into(),
operation: None,
}
}
@@ -50,16 +50,6 @@ impl From<&str> for CommandFailure {
}
}
impl From<std::string::String> for CommandFailure {
fn from(detail: std::string::String) -> Self {
Self {
code: None,
detail,
operation: None,
}
}
}
impl From<String> for CommandFailure {
fn from(detail: String) -> Self {
Self::plain(detail)
+13 -6
View File
@@ -5,6 +5,7 @@ use crate::core::{proxy_control, sysopt::Sysopt};
use gethostname::gethostname;
use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_clash_verge_sysinfo;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -24,8 +25,8 @@ pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
Ok(SystemProxy {
enable,
server: format!("{host}:{port}"),
bypass,
server: format!("{host}:{port}").into(),
bypass: bypass.into(),
})
}
@@ -39,7 +40,10 @@ pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
};
let Autoproxy { enable, url } = auto_proxy;
Ok(AutoProxy { enable, url })
Ok(AutoProxy {
enable,
url: url.into(),
})
}
#[tauri::command]
@@ -52,10 +56,10 @@ pub fn get_embedded_server_port() -> CmdResult<u16> {
#[specta::specta]
pub fn get_system_hostname() -> String {
match gethostname().into_string() {
Ok(name) => name,
Ok(name) => name.into(),
Err(os_string) => {
let fallback = format!("{os_string:?}");
fallback.trim_matches('"').to_string()
fallback.trim_matches('"').into()
}
}
}
@@ -64,6 +68,9 @@ pub fn get_system_hostname() -> String {
#[specta::specta]
pub fn get_network_interfaces() -> Vec<String> {
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
.into_iter()
.map(Into::into)
.collect()
}
#[tauri::command]
@@ -77,7 +84,7 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfa
let mut result = Vec::new();
for interface in interfaces {
if names.contains(&interface.name) {
if names.iter().any(|name| name == &interface.name) {
result.push(interface.into());
}
}
+21 -31
View File
@@ -19,17 +19,17 @@ use crate::{
use clash_verge_draft::{Draft, SharedDraft};
use clash_verge_logging::{Type, logging, logging_error};
use scopeguard::defer;
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use tristr::alias::String;
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
fn profile_import_error(err: &anyhow::Error) -> std::string::String {
fn profile_import_error(err: &anyhow::Error) -> String {
if let Some(cause) = err.chain().find(|cause| cause.to_string().contains("TLS 1.0/1.1")) {
return cause.to_string();
return cause.to_string().into();
}
format!("导入订阅失败: {err:#}")
format!("导入订阅失败: {err:#}").into()
}
#[tauri::command]
@@ -68,7 +68,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
#[specta::specta]
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
Ok(it) => it,
Err(e) => {
@@ -98,9 +98,9 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
#[tauri::command]
#[specta::specta]
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
let active_id: String = active_id.into();
let over_id: String = over_id.into();
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
let active_id: String = active_id;
let over_id: String = over_id;
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
@@ -121,9 +121,9 @@ pub async fn reorder_profile(active_id: std::string::String, over_id: std::strin
#[tauri::command]
#[specta::specta]
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> CmdResult {
let item: PrfItem = item.into();
let file_data = file_data.map(String::from);
let file_data = file_data;
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
profiles_save_file_safe()
@@ -138,8 +138,8 @@ pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::
#[tauri::command]
#[specta::specta]
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
let index: String = index.into();
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
let index: String = index;
match feat::update_profile(&index, option.as_ref(), true).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -151,8 +151,8 @@ pub async fn update_profile(index: std::string::String, option: Option<PrfOption
#[tauri::command]
#[specta::specta]
pub async fn delete_profile(index: std::string::String) -> CmdResult {
let index: String = index.into();
pub async fn delete_profile(index: String) -> CmdResult {
let index: String = index;
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = Config::profiles().await;
@@ -325,7 +325,6 @@ async fn perform_config_update(
#[derive(Default, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct ProfileSelection {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
}
@@ -333,13 +332,9 @@ pub struct ProfileSelection {
#[serde(default)]
pub struct ProfileCreate {
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
pub option: Option<PrfOption>,
}
@@ -361,15 +356,10 @@ impl From<ProfileCreate> for PrfItem {
#[serde(default)]
pub struct ProfilePatch {
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
pub selected: Option<Vec<crate::config::PrfSelected>>,
pub extra: Option<crate::config::PrfExtra>,
@@ -436,9 +426,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
#[tauri::command]
#[specta::specta]
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
let profile: PrfItem = profile.into();
let index: String = index.into();
let index: String = index;
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
@@ -479,7 +469,7 @@ pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) ->
#[tauri::command]
#[specta::specta]
pub async fn view_profile(index: std::string::String) -> CmdResult {
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let file = profiles_ref
@@ -504,7 +494,7 @@ pub async fn view_profile(index: std::string::String) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
@@ -524,7 +514,7 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
.join(file.as_str());
match tokio::fs::try_exists(&path).await {
Ok(true) => {}
Ok(false) => return Ok(std::string::String::new()),
Ok(false) => return Ok(String::new()),
Err(err) => {
return Err(coded_error(
"PROFILE_READ_FAILED",
@@ -535,12 +525,12 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
}
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
Ok(data.into())
Ok(data)
}
#[tauri::command]
#[specta::specta]
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time)
+6 -5
View File
@@ -15,6 +15,7 @@ use std::{
collections::{BTreeMap, HashSet},
sync::atomic::{AtomicBool, Ordering},
};
use tristr::alias::String;
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
@@ -48,7 +49,7 @@ fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
.filter_map(|name| name.as_str())
.filter(|name| !name.is_empty() && *name != "GLOBAL")
.filter(|name| seen.insert(*name))
.map(str::to_owned)
.map(String::from)
.collect()
}
@@ -63,7 +64,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.into_iter()
.flat_map(|providers| providers.keys())
.filter_map(|name| name.as_str())
.map(str::to_owned)
.map(String::from)
.collect::<Vec<_>>();
all_providers.sort();
let flag = |group: &Mapping, key: &str| group.get(key).and_then(|value| value.as_bool()) == Some(true);
@@ -75,7 +76,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.flatten()
.filter_map(|group| group.as_mapping())
.filter_map(|group| {
let name = group.get("name")?.as_str()?.to_owned();
let name: String = group.get("name")?.as_str()?.into();
let providers = if flag(group, "include-all") || flag(group, "include-all-providers") {
all_providers.clone()
} else {
@@ -85,14 +86,14 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.into_iter()
.flatten()
.filter_map(|provider| provider.as_str())
.map(str::to_owned)
.map(String::from)
.collect()
};
let exclude_types = group
.get("exclude-type")
.and_then(|types| types.as_str())
.filter(|types| !types.is_empty())
.map(|types| types.split('|').map(str::to_owned).collect())
.map(|types| types.split('|').map(String::from).collect())
.unwrap_or_default();
Some((
name,
+6 -11
View File
@@ -1,8 +1,8 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
use anyhow::{Context as _, anyhow};
use smartstring::alias::String;
use std::collections::HashMap;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -17,7 +17,7 @@ pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>>
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
@@ -36,22 +36,17 @@ pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
#[derive(serde::Serialize, specta::Type)]
#[serde(transparent)]
pub struct RuntimeLogs(
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
HashMap<String, Vec<(String, String)>>,
);
pub struct RuntimeLogs(HashMap<String, Vec<(String, String)>>);
#[tauri::command]
#[specta::specta]
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
crate::enhance::take_discarded_keys_notice().map(Into::into)
pub fn take_discarded_keys_notice() -> Option<String> {
crate::enhance::take_discarded_keys_notice()
}
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_proxy_chain_config(
proxy_chain_exit_node: std::string::String,
) -> CmdResult<std::string::String> {
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
+7 -10
View File
@@ -1,4 +1,4 @@
use super::CmdResult;
use super::{CmdResult, CommandFailure};
use crate::{
cmd::StringifyErr as _,
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
@@ -11,18 +11,15 @@ use crate::{
utils::dirs,
};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
/// 保存profiles的配置
#[tauri::command]
#[specta::specta]
pub async fn save_profile_file(
index: std::string::String,
file_data: Option<std::string::String>,
) -> CmdResult<ValidationOutcome> {
let index: String = index.into();
let file_data = file_data.map(String::from);
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
let index: String = index;
let file_data = file_data;
let file_data = match file_data {
Some(d) => d,
None => return Ok(ValidationOutcome::Valid),
@@ -153,7 +150,7 @@ async fn handle_saved_profile_file(
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {e:#}");
restore_original(file_path, original_content, original_existed).await?;
return Err(e.to_string().into());
return Err(CommandFailure::plain(e));
}
}
@@ -181,7 +178,7 @@ async fn handle_saved_profile_file(
Err(err) => {
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {err:#}");
restore_original(file_path, original_content, original_existed).await?;
Err(err.to_string().into())
Err(CommandFailure::plain(err))
}
}
}
+4 -3
View File
@@ -7,6 +7,7 @@ use crate::{
service::{SERVICE_MANAGER, ServiceStatus, request_runtime_provider_sync},
},
};
use tristr::alias::String;
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
@@ -74,7 +75,7 @@ where
if reasons.is_empty() {
return Err(proxy_aware_coded_error(&error, error_code));
}
let reason = reasons.join("\n");
let reason: String = reasons.join("\n").into();
fallback(reason.clone())
.await
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))?;
@@ -179,7 +180,7 @@ mod tests {
let error = match availability {
Some(availability) => anyhow::Error::new(InstallationVerificationError {
status: InstallationStatus {
service_sha256: String::new(),
service_sha256: std::string::String::new(),
protocol: ProtocolInfo::current(),
cores: vec![CoreInspection {
name: "verge-mihomo.exe".into(),
@@ -203,7 +204,7 @@ mod tests {
assert_eq!(
result,
Ok(ServiceInstallOutcome::Sidecar {
reason: format!("verge-mihomo.exe: {reason}")
reason: format!("verge-mihomo.exe: {reason}").into()
})
);
} else {
+1
View File
@@ -2,6 +2,7 @@ use crate::core::{
notification::{self, PendingFailure},
runstate::{RUN_STATE, RunStateView},
};
use tristr::alias::String;
/// Returns one coherent core/service snapshot instead of independently refreshed state.
#[tauri::command]
+9 -13
View File
@@ -4,18 +4,14 @@ use crate::{
config::{Config, IVerge},
core, feat,
};
use smartstring::alias::String;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
pub async fn save_webdav_config(
url: std::string::String,
username: std::string::String,
password: std::string::String,
) -> CmdResult<()> {
let url: String = url.into();
let username: String = username.into();
let password: String = password.into();
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let url: String = url;
let username: String = username;
let password: String = password;
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
@@ -48,15 +44,15 @@ pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
#[tauri::command]
#[specta::specta]
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
feat::delete_webdav_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
feat::restore_webdav_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
+18 -11
View File
@@ -10,6 +10,7 @@ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr as _,
};
use tristr::alias::String;
#[derive(Default, Debug, Clone)]
pub struct IClashTemp(pub Mapping);
@@ -84,12 +85,12 @@ impl IClashTemp {
#[cfg(unix)]
map.insert(
"external-controller-unix".into(),
Self::guard_external_controller_ipc().into(),
Self::guard_external_controller_ipc().as_str().into(),
);
#[cfg(windows)]
map.insert(
"external-controller-pipe".into(),
Self::guard_external_controller_ipc().into(),
Self::guard_external_controller_ipc().as_str().into(),
);
map.insert("tun".into(), tun_config.into());
cors_map.insert("allow-private-network".into(), true.into());
@@ -134,12 +135,18 @@ impl IClashTemp {
config.insert("mixed-port".into(), mixed_port.into());
config.insert("socks-port".into(), socks_port.into());
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.into());
config.insert("external-controller".into(), ctrl.as_str().into());
#[cfg(unix)]
config.insert("external-controller-unix".into(), external_controller_unix.into());
config.insert(
"external-controller-unix".into(),
external_controller_unix.as_str().into(),
);
#[cfg(windows)]
config.insert("external-controller-pipe".into(), external_controller_pipe.into());
config.insert(
"external-controller-pipe".into(),
external_controller_pipe.as_str().into(),
);
config
}
@@ -166,9 +173,9 @@ impl IClashTemp {
port: Self::guard_port(config),
server: Self::guard_client_ctrl(config),
secret: config.get("secret").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
Value::String(val_str) => Some(val_str.clone().into()),
Value::Bool(val_bool) => Some(val_bool.to_string().into()),
Value::Number(val_num) => Some(val_num.to_string().into()),
_ => None,
}),
}
@@ -180,7 +187,7 @@ impl IClashTemp {
/// 的严格 `BaseConfig` 反序列化,因此即使核心返回的字段与插件结构体不匹配也能取到。
pub fn get_mode(&self) -> Option<String> {
self.0.get("mode").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone()),
Value::String(val_str) => Some(val_str.clone().into()),
_ => None,
})
}
@@ -276,7 +283,7 @@ impl IClashTemp {
false => Cow::Borrowed(val_str),
};
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
SocketAddr::from_str(&val).ok().map(|s| s.to_string().into())
}
None => None,
})
@@ -290,7 +297,7 @@ impl IClashTemp {
if socket.ip().is_unspecified() {
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
}
socket.to_string()
socket.to_string().into()
}
Err(_) => "127.0.0.1:9097".into(),
}
+3 -3
View File
@@ -20,10 +20,10 @@ use backon::{ExponentialBuilder, Retryable as _};
use clash_verge_draft::Draft;
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::{collections::HashSet, path::PathBuf};
use tokio::sync::{Mutex, MutexGuard, OnceCell};
use tokio::time::sleep;
use tristr::alias::String;
pub(crate) struct Config {
clash_config: Draft<IClashTemp>,
@@ -205,7 +205,7 @@ impl Config {
Self::write_runtime_file(&yaml).await
}
pub(crate) async fn runtime_config_yaml() -> Result<std::string::String> {
pub(crate) async fn runtime_config_yaml() -> Result<String> {
let runtime = Self::runtime().await;
AsyncHandler::spawn_blocking(move || {
let runtime_lastest = runtime.latest_arc();
@@ -216,7 +216,7 @@ impl Config {
.or_else(|| runtime_data.config.as_ref())
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
let yaml_str = serde_yaml_ng::to_string(config)?;
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str))
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str).into())
})
.await
.map_err(|join| anyhow!("runtime serialization task failed: {join}"))?
+1 -2
View File
@@ -4,8 +4,8 @@ use clash_verge_draft::DraftTransaction;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use tristr::alias::String;
use super::{Config, IVerge};
use crate::core::handle::Handle;
@@ -110,7 +110,6 @@ impl DnsOverrideState {
pub struct ProfileDnsSettings {
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub confirmation: Option<String>,
}
+6 -2
View File
@@ -7,6 +7,7 @@ use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::future::Future;
use tristr::alias::String;
const NONCE_LENGTH: usize = 12;
@@ -28,7 +29,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut combined = nonce.to_vec();
combined.extend(ciphertext);
Ok(STANDARD.encode(combined))
Ok(STANDARD.encode(combined).into())
}
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
@@ -46,7 +47,10 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {e}"))?;
String::from_utf8(plaintext).map_err(|e| e.into())
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8(plaintext)
.map_err(|e| e.into())
.map(Into::into)
}
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+3 -2
View File
@@ -18,6 +18,7 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::Value;
use std::{collections::HashSet, net::SocketAddr, str::FromStr as _};
use tristr::alias::String;
#[derive(Clone, Copy)]
struct MixedPortFallback {
@@ -141,8 +142,8 @@ impl Config {
}
pub(crate) fn block_startup_core(error: &anyhow::Error) {
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
report_fallback_error(error.to_string());
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string().into());
report_fallback_error(error.to_string().into());
}
pub(crate) fn startup_core_block_reason() -> Option<String> {
+9 -29
View File
@@ -9,9 +9,9 @@ use crate::{
use anyhow::{Context as _, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::time::Duration;
use tokio::fs;
use tristr::alias::String;
// TODO, use other re-export
use reqwest_dav::re_exports::url::form_urlencoded;
use tauri::Url;
@@ -30,24 +30,18 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
#[serde(default)]
pub struct PrfItem {
#[specta(type = Option<std::string::String>)]
pub uid: Option<String>,
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -62,20 +56,16 @@ pub struct PrfItem {
pub option: Option<PrfOption>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub home: Option<String>,
#[serde(skip)]
#[specta(type = Option<std::string::String>)]
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(default)]
pub struct PrfSelected {
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub now: Option<String>,
}
@@ -91,7 +81,6 @@ pub struct PrfExtra {
#[serde(default)]
pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -114,15 +103,10 @@ pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_auto_update: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub merge: Option<String>,
#[specta(type = Option<std::string::String>)]
pub script: Option<String>,
#[specta(type = Option<std::string::String>)]
pub rules: Option<String>,
#[specta(type = Option<std::string::String>)]
pub proxies: Option<String>,
#[specta(type = Option<std::string::String>)]
pub groups: Option<String>,
}
@@ -187,7 +171,7 @@ impl PrfItem {
file_data: Option<String>,
option: Option<&PrfOption>,
) -> Result<Self> {
let uid = help::get_uid("L").into();
let uid = help::get_uid("L");
let file = format!("{uid}.yaml").into();
let opt_ref = option.as_ref();
let update_interval = opt_ref.and_then(|o| o.update_interval);
@@ -360,11 +344,11 @@ impl PrfItem {
.and_then(|value| value.to_str().ok())
.and_then(normalize_profile_home_url);
let uid = help::get_uid("R").into();
let uid = help::get_uid("R");
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()));
.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()));
let data = resp.text();
let data = data.trim_start_matches('\u{feff}');
@@ -430,7 +414,7 @@ impl PrfItem {
let (id, template) = if let Some(uid) = uid {
(uid, tmpl::ITEM_MERGE.into())
} else {
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
(help::get_uid("m"), tmpl::ITEM_MERGE_EMPTY.into())
};
let file = format!("{id}.yaml").into();
@@ -445,11 +429,7 @@ impl PrfItem {
}
pub(super) fn from_script(uid: Option<String>) -> Self {
let id = if let Some(uid) = uid {
uid
} else {
help::get_uid("s").into()
};
let id = if let Some(uid) = uid { uid } else { help::get_uid("s") };
let file = format!("{id}.js").into(); // js ext
Self {
uid: Some(id),
@@ -462,7 +442,7 @@ impl PrfItem {
}
fn from_rules() -> Self {
let uid = help::get_uid("r").into();
let uid = help::get_uid("r");
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
@@ -476,7 +456,7 @@ impl PrfItem {
}
fn from_proxies() -> Self {
let uid = help::get_uid("p").into();
let uid = help::get_uid("p");
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
@@ -490,7 +470,7 @@ impl PrfItem {
}
fn from_groups() -> Self {
let uid = help::get_uid("g").into();
let uid = help::get_uid("g");
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
+2 -3
View File
@@ -17,7 +17,6 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::num::NonZeroUsize;
use std::{
collections::{HashMap, HashSet},
@@ -30,6 +29,7 @@ use std::{
};
use tauri_plugin_mihomo::models::{Proxies, ProxyType};
use tokio::{fs, task::JoinHandle};
use tristr::alias::String;
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
@@ -51,7 +51,6 @@ const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IProfiles {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
pub items: Option<Vec<PrfItem>>,
@@ -115,7 +114,7 @@ impl IProfiles {
let mut home_changed = false;
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
item.uid = Some(help::get_uid("d"));
}
if item.itype.as_deref() == Some("remote") {
item.option
+1 -1
View File
@@ -1,6 +1,6 @@
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tristr::alias::String;
use crate::enhance::field::use_keys;
+1 -1
View File
@@ -8,7 +8,7 @@ use anyhow::Result;
use clash_verge_logging::{Type, logging};
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use smartstring::alias::String;
use tristr::alias::String;
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
+1
View File
@@ -75,6 +75,7 @@ pub mod tun {
"auto-redirect",
"auto-detect-interface",
"dns-hijack",
"dns-mode",
"strict-route",
"mtu",
];
+4 -3
View File
@@ -20,6 +20,7 @@ use tokio::{
fs,
sync::{Mutex, watch},
};
use tristr::alias::String;
const DEFAULT_INTERVAL_HOURS: u64 = 24;
const MIN_INTERVAL_HOURS: u64 = 1;
@@ -211,7 +212,7 @@ impl AutoBackupManager {
return Ok(());
}
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()).await?;
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug())).await?;
self.last_backup.store(Local::now().timestamp(), Ordering::Release);
if let Err(err) = cleanup_auto_backups().await {
@@ -239,8 +240,8 @@ impl AutoBackupManager {
fn append_auto_suffix(file_name: &str, slug: &str) -> String {
match file_name.rsplit_once('.') {
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"),
None => format!("{file_name}{AUTO_MARKER}{slug}"),
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}").into(),
None => format!("{file_name}{AUTO_MARKER}{slug}").into(),
}
}
+1 -1
View File
@@ -5,7 +5,6 @@ use arc_swap::{ArcSwap, ArcSwapOption};
use backon::{ConstantBuilder, Retryable as _};
use clash_verge_logging::{Type, logging};
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
use smartstring::alias::String;
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
@@ -15,6 +14,7 @@ use std::{
time::Duration,
};
use tokio::{fs, time::timeout};
use tristr::alias::String;
use zip::write::SimpleFileOptions;
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
+1 -1
View File
@@ -7,7 +7,6 @@ use crate::{config::Config, core::handle, core::lightweight::entry_lightweight_m
use anyhow::{Result, bail};
use arc_swap::ArcSwap;
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::{
collections::HashMap,
fmt,
@@ -18,6 +17,7 @@ use std::{
},
};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState};
use tristr::alias::String;
/// Enum representing all available hotkey functions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+13 -10
View File
@@ -10,6 +10,7 @@ use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
str::FromStr as _,
};
use tristr::alias::String;
/// The config key of the Mixed Port, named once so callers stop spelling it out.
pub(crate) const MIXED_PORT_KEY: &str = "mixed-port";
@@ -163,7 +164,7 @@ pub(crate) fn probe_listener(request: &ListenerProbe) -> ListenerProbeOutcome {
Ok(address) => address,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: error.to_string(),
message: error.to_string().into(),
};
}
};
@@ -201,7 +202,7 @@ pub(crate) fn probe_proxy_port_change(
Ok(claims) => claims,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: format!("current proxy listener configuration is invalid: {error:#}"),
message: format!("current proxy listener configuration is invalid: {error:#}").into(),
};
}
}
@@ -212,7 +213,7 @@ pub(crate) fn probe_proxy_port_change(
Ok(claims) => claims,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: format!("candidate proxy listener configuration is invalid: {error:#}"),
message: format!("candidate proxy listener configuration is invalid: {error:#}").into(),
};
}
};
@@ -401,7 +402,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
claim.socket_addr(),
transport_name(claim.transport),
error
),
)
.into(),
};
}
Err(error) => {
@@ -412,7 +414,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
claim.socket_addr(),
transport_name(claim.transport),
error
),
)
.into(),
};
}
}
@@ -739,7 +742,7 @@ mod tests {
let port = listener.local_addr()?.port();
assert_eq!(
probe_listener(&ListenerProbe {
address: format!("0.0.0.0:{port}"),
address: format!("0.0.0.0:{port}").into(),
transports: vec![ListenerTransport::Tcp],
}),
ListenerProbeOutcome::Conflict {
@@ -755,7 +758,7 @@ mod tests {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = listener.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}"),
address: format!("127.0.0.1:{port}").into(),
transports: vec![ListenerTransport::Tcp],
});
assert_eq!(
@@ -773,7 +776,7 @@ mod tests {
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?;
let port = listener.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}"),
address: format!("127.0.0.1:{port}").into(),
transports: vec![ListenerTransport::Tcp],
});
assert_eq!(
@@ -809,7 +812,7 @@ mod tests {
assert_eq!(
probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}"),
address: format!("127.0.0.1:{port}").into(),
transports: vec![ListenerTransport::Tcp],
}),
ListenerProbeOutcome::Available
@@ -822,7 +825,7 @@ mod tests {
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = socket.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}"),
address: format!("127.0.0.1:{port}").into(),
transports: vec![ListenerTransport::Udp],
});
assert_eq!(
+2 -2
View File
@@ -15,7 +15,6 @@ use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::StageRuntimeOutcome;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
collections::HashSet,
path::PathBuf,
@@ -23,6 +22,7 @@ use std::{
time::{Duration, Instant},
};
use tauri_plugin_mihomo::Error as MihomoError;
use tristr::alias::String;
/// A staging result separated from the policy that acts on it.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -92,7 +92,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
match crate::core::service::stage_runtime_by_service(path).await {
Ok(StageRequest::Answered(outcome)) => StageAttempt::Answered(outcome),
Ok(StageRequest::Refused { code, message }) => {
let message = message.into();
let message = message;
if StageRequest::is_about_the_bundle(code) {
StageAttempt::RefusedTheBundle(message)
} else {
+7 -7
View File
@@ -7,10 +7,10 @@ use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
#[cfg(target_os = "windows")]
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
use tristr::alias::String;
#[cfg(any(target_os = "windows", test))]
const fn should_wait_for_service(tun_enabled: bool, service_ready: bool, is_admin: bool) -> bool {
@@ -194,7 +194,7 @@ async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, Chec
start_service: Start,
confirm_idle: Check,
start_sidecar: Fallback,
record_unavailable: impl FnOnce(std::string::String),
record_unavailable: impl FnOnce(String),
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
@@ -217,7 +217,7 @@ where
// A lost start response can hide a running core; the shared check must resolve occupancy.
if let Err(probe_error) = confirm_idle().await {
let error = error.context(format!("Sidecar fallback was not safe: {probe_error:#}"));
record_unavailable(format!("{error:#}"));
record_unavailable(format!("{error:#}").into());
return Err(error);
}
logging!(
@@ -650,7 +650,7 @@ impl CoreManager {
|error| self.start_sidecar_after_service_failure(error),
|reason| {
crate::core::runstate::RUN_STATE
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason));
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason.into()));
},
)
.await
@@ -688,7 +688,7 @@ impl CoreManager {
}
let reason = match state.health {
ServiceHealth::Unavailable(reason) => reason,
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".into(),
_ => return Ok(()),
};
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available()
@@ -712,7 +712,7 @@ impl CoreManager {
if Handle::global().is_exiting() {
return Err(error.context("application exited before Sidecar fallback"));
}
let reason = format!("{error:#}");
let reason: String = format!("{error:#}").into();
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
let result = self.start_core_by_sidecar().await;
@@ -1590,7 +1590,7 @@ mod tests {
started.store(true, Ordering::SeqCst);
Ok(())
},
|reason| store.observe(ServiceHealth::Unavailable(reason)),
|reason| store.observe(ServiceHealth::Unavailable(reason.into())),
)
.await;
+11 -8
View File
@@ -16,6 +16,7 @@ use std::num::NonZeroUsize;
use std::path::Path;
use tauri_plugin_mihomo::MihomoExt as _;
use tauri_plugin_shell::ShellExt as _;
use tristr::alias::String;
pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
let detail = error.to_string();
@@ -27,7 +28,7 @@ pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
}
#[cfg(target_os = "windows")]
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
use serde_yaml_ng::{Mapping, Value};
let mut config: Mapping = serde_yaml_ng::from_str(yaml)?;
@@ -40,7 +41,7 @@ fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
tun.as_mapping_mut()
.context("invalid Sidecar TUN configuration")?
.insert(Value::from("enable"), Value::Bool(false));
Ok(serde_yaml_ng::to_string(&config)?)
Ok(serde_yaml_ng::to_string(&config)?.into())
}
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
@@ -153,7 +154,7 @@ impl CoreManager {
pub async fn get_clash_logs(&self) -> Result<Vec<String>> {
match *self.get_running_mode() {
RunningMode::Service => service::get_clash_logs_by_service().await,
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs()),
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().into_iter().map(Into::into).collect()),
RunningMode::NotRunning => Ok(Vec::new()),
}
}
@@ -257,7 +258,9 @@ impl CoreManager {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let message = String::from_utf8_lossy_owned(line);
// from_utf8_lossy_owned exists only on the std String type; the tristr
// alias shadows the bare name. LogRing stores std Strings.
let message = std::string::String::from_utf8_lossy_owned(line);
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message);
}
@@ -265,12 +268,12 @@ impl CoreManager {
let _ = terminated.send(());
let manager = Self::global();
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
let message = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
let message: String = if let Some(code) = term.code {
format!("Process terminated with code: {}", code).into()
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal)
format!("Process terminated by signal: {}", signal).into()
} else {
String::from("Process terminated")
"Process terminated".into()
};
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs();
+3 -6
View File
@@ -1,12 +1,9 @@
use super::NoticeStatus;
use crate::{APP_HANDLE, singleton};
use smartstring::alias::String;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::AppHandle;
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
use tristr::alias::String;
use super::notification::{FrontendEvent, NotificationSystem};
@@ -73,7 +70,7 @@ impl Handle {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
pub fn notice(status: NoticeStatus, message: impl Into<Arc<str>>) {
pub fn notice(status: NoticeStatus, message: impl Into<String>) {
Self::send_event(FrontendEvent::NoticeMessage {
status,
message: message.into(),
+9 -13
View File
@@ -3,11 +3,10 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
#[cfg(test)]
use serde_json::json;
use smartstring::alias::String;
use std::sync::Arc;
use std::sync::LazyLock;
use std::{collections::HashMap, future::Future};
use tauri::{AppHandle, Emitter as _, Manager as _, WebviewWindow};
use tristr::alias::String;
#[derive(Debug, Clone)]
pub enum FrontendEvent<'a> {
@@ -17,7 +16,7 @@ pub enum FrontendEvent<'a> {
RefreshProxyConfig,
NoticeMessage {
status: NoticeStatus,
message: Arc<str>,
message: String,
},
ProfileChanged {
current_profile_id: &'a String,
@@ -77,10 +76,8 @@ impl FailedOperation {
#[serde(rename_all = "camelCase")]
pub struct PendingFailure {
/// Stable code used as the table key.
#[specta(type = std::string::String)]
pub code: String,
/// Full diagnostic context chain.
#[specta(type = std::string::String)]
pub detail: String,
pub operation: FailedOperation,
/// Monotonic identity for repeated failures under the same code.
@@ -247,7 +244,6 @@ static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::de
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
pub struct SidecarFailureSnapshot {
pub revision: u64,
#[specta(type = Option<std::string::String>)]
pub detail: Option<String>,
}
@@ -391,7 +387,7 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
(
FrontendEvent::NoticeMessage {
status: NoticeStatus::Info,
message: Arc::from("ok"),
message: String::from("ok"),
},
"verge://notice-message",
json!(["info", "ok"]),
@@ -456,16 +452,16 @@ frontend_events! {
refresh_verge: () = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => ();
refresh_profiles: () = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => ();
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
notice: (NoticeStatus, String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.clone());
profile_changed: String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.clone();
timer_updated: String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.clone();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.clone() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.clone() };
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
}
#[derive(serde::Serialize, specta::Type)]
pub struct ProfileUpdatePayload {
pub uid: std::string::String,
pub uid: String,
}
+11 -7
View File
@@ -2,6 +2,8 @@ use crate::utils::dirs;
use anyhow::{Context as _, Result};
use clash_verge_service_ipc::{OwnerCredentials, OwnerIdentity};
use std::path::Path;
#[cfg(windows)]
use tristr::alias::String;
pub(crate) fn current_owner_credentials() -> Result<OwnerCredentials> {
current_owner_credentials_for_root(&dirs::app_home_dir()?)
@@ -39,16 +41,16 @@ pub(crate) fn current_owner_credentials_for_root(app_root: &Path) -> Result<Owne
}
#[cfg(windows)]
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<String>)> {
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<std::string::String>)> {
let sid = windows_owner::current_sid()?;
let token = windows_owner::load_or_create_token(app_data_root, &sid)?;
Ok((OwnerIdentity::Windows { sid }, Some(token)))
Ok((OwnerIdentity::Windows { sid: sid.into() }, Some(token.into())))
}
#[cfg(windows)]
fn windows_current_identity() -> Result<OwnerIdentity> {
Ok(OwnerIdentity::Windows {
sid: windows_owner::current_sid()?,
sid: windows_owner::current_sid()?.into(),
})
}
@@ -79,7 +81,7 @@ pub(crate) fn repair_app_data_root_owner(app_data_root: &Path) -> Result<()> {
#[cfg(windows)]
pub(crate) fn current_user_pipe_sddl() -> Result<String> {
let sid = windows_owner::current_sid()?;
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)"))
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)").into())
}
#[cfg(windows)]
@@ -644,17 +646,19 @@ mod windows_owner {
}
let value = LocalWideString(value);
let length = (0..).take_while(|index| unsafe { *value.0.add(*index) } != 0).count();
String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
// from_utf16 exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
.context("process SID is not valid UTF-16")
.map(Into::into)
}
fn encode_token(token: &[u8; TOKEN_BYTES]) -> String {
let mut encoded = String::with_capacity(TOKEN_BYTES * 2);
let mut encoded = std::string::String::with_capacity(TOKEN_BYTES * 2);
for byte in token {
use std::fmt::Write as _;
let _ = write!(encoded, "{byte:02x}");
}
encoded
encoded.into()
}
fn wide_path(path: &Path) -> Result<Vec<u16>> {
+13 -11
View File
@@ -24,6 +24,7 @@ use std::{
time::Duration,
};
use tokio::{sync::Mutex, time::timeout};
use tristr::alias::String;
const CLEAR_ATTEMPTS: u32 = 3;
const CLEAR_RETRY_DELAY: Duration = Duration::from_millis(100);
@@ -320,7 +321,8 @@ fn truncate_utf8(value: &mut String, max_bytes: usize) {
while !value.is_char_boundary(boundary) {
boundary -= 1;
}
value.truncate(boundary);
let truncated = value.as_str()[..boundary].into();
*value = truncated;
}
fn service_bypass(verge: &IVerge) -> Result<String> {
@@ -328,11 +330,11 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
let mut bypass = if custom.is_empty() {
MACOS_DEFAULT_BYPASS.to_owned()
MACOS_DEFAULT_BYPASS.into()
} else if verge.use_default_bypass.unwrap_or(true) {
format!("{MACOS_DEFAULT_BYPASS},{custom}")
format!("{MACOS_DEFAULT_BYPASS},{custom}").into()
} else {
custom.to_owned()
custom.into()
};
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
Ok(bypass)
@@ -353,7 +355,7 @@ fn service_proxy_config(verge: &IVerge, mixed_port: u16, pac_port: u16) -> Resul
Ok(MacosProxyConfig::Global {
host: LOOPBACK_HOST.to_owned(),
port: mixed_port,
bypass: service_bypass(verge)?,
bypass: service_bypass(verge)?.into(),
})
}
}
@@ -368,7 +370,7 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
// Direct fallback satisfies a disable request.
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
ProxyApplyOutcome::DirectFallback { message } => {
Err(SysproxyFailure::DirectFallback { detail: message }.into())
Err(SysproxyFailure::DirectFallback { detail: message.into() }.into())
}
}
}
@@ -644,7 +646,7 @@ const GUARD_FAILURES_BEFORE_STOPPING: u32 = 3;
async fn report_guard_stopped(captured_generation: u64, error: &anyhow::Error) {
let stopped = anyhow::Error::new(SysproxyFailure::GuardStopped {
detail: format!("{error:#}"),
detail: format!("{error:#}").into(),
});
// Do not publish a late failure from a replaced guard.
let recorded = SERVICE_PROXY_OPERATIONS
@@ -1106,7 +1108,7 @@ mod tests {
let requested = MacosProxyConfig::Global {
host: "127.0.0.1".to_owned(),
port: 7890,
bypass: String::new(),
bypass: std::string::String::new(),
};
let error = service_apply_result(
@@ -1146,8 +1148,8 @@ mod tests {
let events = Mutex::new(Vec::new());
super::settle_table(
effect,
|asked| events.lock().push(format!("retire:{asked:?}")),
|operation, _| events.lock().push(format!("file:{operation:?}")),
|asked| events.lock().push(format!("retire:{asked:?}").into()),
|operation, _| events.lock().push(format!("file:{operation:?}").into()),
);
events.into_inner()
}
@@ -1276,7 +1278,7 @@ mod tests {
let combined = rollback_failure(
mappable(),
anyhow::anyhow!("the service could not set it").context(SysproxyFailure::DirectFallback {
detail: "fell back to direct".to_owned(),
detail: "fell back to direct".into(),
}),
);
+23 -14
View File
@@ -4,6 +4,7 @@ use serde::Serialize;
use tauri_plugin_mihomo::models::{
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
};
use tristr::alias::String;
pub struct ProxyViewInput {
pub runtime_group_order: Vec<String>,
@@ -270,7 +271,7 @@ impl ProxyViewBuilder {
let global = core_groups
.remove("GLOBAL")
.map(|proxy| build_group("GLOBAL".to_owned(), proxy, &resolver));
.map(|proxy| build_group(String::from("GLOBAL"), proxy, &resolver));
let (groups, order_source) = build_ordered_groups(core_groups, runtime_group_order, &resolver);
let direct = core_node_ids.get("DIRECT").cloned();
let standalone = build_standalone(&core_node_ids);
@@ -294,6 +295,7 @@ fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String
let mut nodes = BTreeMap::new();
for (name, proxy) in proxies.proxies {
let name = String::from(name);
if proxy.all.is_some() {
groups.insert(name, proxy);
} else {
@@ -311,7 +313,7 @@ fn build_core_records(
let mut ids = BTreeMap::new();
for (index, (name, proxy)) in core_nodes.into_iter().enumerate() {
let record_id = format!("c:{index}");
let record_id = String::from(format!("c:{index}"));
ids.insert(name.clone(), record_id.clone());
records.insert(
record_id.clone(),
@@ -332,7 +334,13 @@ fn build_provider_records(
records: &mut BTreeMap<String, ProxyNodeView>,
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
let providers = providers
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
.map(|providers| {
providers
.providers
.into_iter()
.map(|(name, provider)| (String::from(name), provider))
.collect::<BTreeMap<_, _>>()
})
.unwrap_or_default();
let mut views = Vec::new();
let mut candidates = BTreeMap::new();
@@ -355,9 +363,9 @@ fn build_provider_records(
let mut proxy_record_ids = Vec::new();
for (member_index, proxy) in proxies.into_iter().enumerate() {
let record_id = format!("p:{provider_index}:{member_index}");
let proxy_name = proxy.name.clone();
let node_name = proxy.name.clone();
let record_id = String::from(format!("p:{provider_index}:{member_index}"));
let proxy_name = String::from(proxy.name.as_str());
let node_name = String::from(proxy.name.as_str());
candidates
.entry(proxy_name.clone())
.or_insert_with(Vec::new)
@@ -380,7 +388,7 @@ fn build_provider_records(
views.push(ProxyProviderView {
name: provider_name,
vehicle_type,
updated_at,
updated_at: updated_at.map(String::from),
subscription_info: subscription_info.map(|subscription_info| ProxySubscriptionInfo {
upload: subscription_info.upload,
download: subscription_info.download,
@@ -417,6 +425,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
.unwrap_or_default()
.into_iter()
.map(|member| {
let member = String::from(member);
let (occurrence, scoped) = seen
.entry(member.clone())
.or_insert_with(|| (0, resolver.scoped_candidates(&name, &member)));
@@ -429,11 +438,11 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
ProxyGroupView {
name,
proxy_type,
now,
fixed,
now: now.map(String::from),
fixed: fixed.map(String::from),
hidden,
icon,
test_url,
icon: icon.map(String::from),
test_url: test_url.map(String::from),
history,
capabilities: ProxyCapabilities {
udp,
@@ -467,10 +476,10 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
name,
proxy_type,
history,
id,
id: id.map(String::from),
hidden,
icon,
test_url,
icon: icon.map(String::from),
test_url: test_url.map(String::from),
capabilities: ProxyCapabilities {
udp,
xudp,
+8 -7
View File
@@ -64,7 +64,7 @@ impl RunStateEnv for RealEnv {
Ok(ServiceVersionReply {
core,
code: response.code,
message: response.message,
message: response.message.into(),
protocol: response.data,
})
}
@@ -123,6 +123,7 @@ mod fake {
use super::{PendingAction, RunState, RunStateEnv, ServiceVersionReply};
use crate::core::manager::RunningMode;
use tristr::alias::String;
/// A fail-closed scripted machine that records outbound effects.
#[derive(Debug)]
@@ -163,7 +164,7 @@ mod fake {
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: Some(ProtocolInfo::current()),
}))
}
@@ -173,7 +174,7 @@ mod fake {
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: None,
}))
}
@@ -181,12 +182,12 @@ mod fake {
#[must_use]
pub fn service_unreachable(self) -> Self {
self.with_evidence(true)
.always_replying(Err("ipc transport refused".to_owned()))
.always_replying(Err("ipc transport refused".into()))
}
#[must_use]
pub fn evidence_unavailable(mut self) -> Self {
self.evidence = Err("registry probe failed".to_owned());
self.evidence = Err("registry probe failed".into());
self
}
@@ -236,7 +237,7 @@ mod fake {
#[must_use]
pub fn privileged_operations_fail(self, reason: &str) -> Self {
*self.privileged_outcome.lock() = Err(reason.to_owned());
*self.privileged_outcome.lock() = Err(reason.into());
self
}
@@ -256,7 +257,7 @@ mod fake {
replies
.first()
.cloned()
.unwrap_or_else(|| Err("no service configured".to_owned()))
.unwrap_or_else(|| Err("no service configured".into()))
};
reply.map_err(|error| anyhow!(error))
}
+1
View File
@@ -2,6 +2,7 @@
//! Core lifecycle remains in `CoreManager`.
use crate::core::manager::RunningMode;
use tristr::alias::String;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum ServiceHealth {
+5 -5
View File
@@ -173,7 +173,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
Type::Service,
"failed to inspect trusted service evidence: {error:#}"
);
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}"));
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}").into());
}
};
@@ -331,7 +331,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
}
#[cfg(target_os = "windows")]
pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> {
pub fn allow_sidecar_after_service_refusal(&self, reason: tristr::alias::String) -> Result<()> {
let mut state = self.service.lock();
if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() {
bail!("cannot fall back while a service operation is pending");
@@ -496,7 +496,7 @@ mod tests {
ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: Some(clash_verge_service_ipc::ProtocolInfo::current()),
}
}
@@ -713,7 +713,7 @@ mod tests {
for health in [
ServiceHealth::NotInstalled,
ServiceHealth::VersionMismatch,
ServiceHealth::Unavailable("boom".to_owned()),
ServiceHealth::Unavailable("boom".into()),
] {
let store = with_env(FakeEnv::new());
store.observe(health.clone());
@@ -858,7 +858,7 @@ mod tests {
#[tokio::test]
async fn awaiting_readiness_retries_a_silent_service_then_accepts_its_reply() {
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".to_owned()), Ok(ready_reply())]));
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".into()), Ok(ready_reply())]));
let state = store
.await_ready(2, Duration::ZERO)
+16 -12
View File
@@ -9,6 +9,7 @@ use clash_verge_service_ipc::{
};
use super::health::ServiceHealth;
use tristr::alias::String;
/// What the Service replied to a protocol-version query.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -50,7 +51,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
return match &reply.core {
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}"))
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}").into())
}
_ => ServiceVersionCheck::CoreUnavailable(
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
@@ -77,9 +78,12 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
reply.code, reply.message, client.epoch
)
};
ServiceVersionCheck::NeedsReinstall(format!(
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
))
ServiceVersionCheck::NeedsReinstall(
format!(
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
)
.into(),
)
}
/// Classify a version reply into the probe outcome used by [`classify_service_health`].
@@ -104,7 +108,7 @@ pub fn classify_service_health(
match current {
CurrentServiceProbe::Ready => ServiceHealth::Ready,
CurrentServiceProbe::VersionMismatch => ServiceHealth::VersionMismatch,
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.to_owned()),
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.into()),
CurrentServiceProbe::Missing if has_install_marker => ServiceHealth::VersionMismatch,
CurrentServiceProbe::Missing => ServiceHealth::NotInstalled,
}
@@ -119,7 +123,7 @@ mod tests {
ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: Some(ProtocolInfo::current()),
}
}
@@ -154,7 +158,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: None,
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -172,7 +176,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 7,
message: "nope".to_owned(),
message: "nope".into(),
protocol: Some(ProtocolInfo::current()),
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -188,7 +192,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: Some(info),
};
assert!(matches!(
@@ -209,7 +213,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".to_owned(),
message: "ok".into(),
protocol: Some(info),
};
assert!(matches!(
@@ -228,7 +232,7 @@ mod tests {
let reply = ServiceVersionReply {
core: None,
code,
message: "unsupported command".to_owned(),
message: "unsupported command".into(),
protocol: protocol.clone(),
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -250,7 +254,7 @@ mod tests {
fn an_unreachable_service_keeps_the_reason() {
assert_eq!(
classify_service_health(CurrentServiceProbe::Unavailable, true, "socket refused"),
ServiceHealth::Unavailable("socket refused".to_owned())
ServiceHealth::Unavailable("socket refused".into())
);
}
+17 -15
View File
@@ -5,6 +5,7 @@ use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
use std::collections::{BTreeMap, HashSet};
use std::path::{Component, Path, PathBuf};
use tristr::alias::String;
pub(crate) const GEO_ASSETS: &[&str] = &[
"Country.mmdb",
@@ -64,7 +65,7 @@ struct ProviderPathOwners {
pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root: &Path) -> Result<()> {
let config_root = std::fs::canonicalize(config_root)?;
let mut paths = BTreeMap::<String, ProviderPathOwners>::new();
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).to_owned()).collect();
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).into()).collect();
for section in ["proxy-providers", "rule-providers"] {
let Some(providers) = config.get(section).and_then(Value::as_mapping) else {
continue;
@@ -89,7 +90,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
match (is_remote, provider.get("url").and_then(Value::as_str)) {
(true, Some(url)) => owners
.remote
.entry(url.to_owned())
.entry(url.into())
.or_default()
.push((section, name.clone())),
_ => owners.local = true,
@@ -105,7 +106,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
for (url, providers) in owners.remote {
let replacement = allocate_provider_destination(&destination, &url, &mut reserved);
for (section, name) in providers {
config[section][&name]["path"] = Value::String(replacement.clone());
config[section][&name]["path"] = Value::String(replacement.clone().into());
logging!(
warn,
Type::Config,
@@ -131,15 +132,16 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
let mut suffix = String::new();
let mut sequence = 0;
loop {
let candidate = path
let candidate: String = path
.with_file_name(format!("cvr-{digest}{suffix}{extension}"))
.to_string_lossy()
.replace('\\', "/");
.replace('\\', "/")
.into();
if reserved.insert(candidate.clone()) {
return candidate;
}
sequence += 1;
suffix = format!("-{sequence}");
suffix = format!("-{sequence}").into();
}
}
@@ -167,7 +169,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
)?;
for filename in GEO_ASSETS {
let source = config_root.join(filename);
if source.is_file() && destinations.insert((*filename).to_string()) {
if source.is_file() && destinations.insert((*filename).into()) {
assets.push(RuntimeAsset {
source: std::fs::canonicalize(&source)?.to_string_lossy().into_owned(),
destination: (*filename).to_string(),
@@ -228,9 +230,9 @@ fn collect_provider_assets(
}
remote_providers.push(RemoteProviderRef {
section,
name: name.as_str().unwrap_or_default().to_owned(),
name: name.as_str().unwrap_or_default().into(),
provider: RemoteProvider {
destination: destination.clone(),
destination: destination.clone().into(),
url,
},
});
@@ -259,13 +261,13 @@ fn collect_provider_assets(
if destinations.insert(destination.clone()) {
assets.push(RuntimeAsset {
source: source.to_string_lossy().into_owned(),
destination: destination.clone(),
destination: destination.clone().into(),
});
}
destination
}
};
provider.insert(Value::String("path".to_owned()), Value::String(destination));
provider.insert(Value::String("path".to_owned()), Value::String(destination.into()));
}
Ok(())
}
@@ -339,7 +341,7 @@ fn normalized_destination(relative: &Path) -> Result<String> {
if destination.as_os_str().is_empty() {
bail!("local runtime provider destination is empty");
}
Ok(destination.to_string_lossy().replace('\\', "/"))
Ok(destination.to_string_lossy().replace('\\', "/").into())
}
#[cfg(test)]
@@ -385,12 +387,12 @@ mod tests {
for declared in readback {
assert!(bundle.remote_providers.contains(&declared.provider));
assert_eq!(
config[declared.section][&declared.name]["path"],
config[declared.section][declared.name.as_str()]["path"],
declared.provider.destination
);
assert_eq!(
config[declared.section][&declared.name]["url"],
original[declared.section][&declared.name]["url"]
config[declared.section][declared.name.as_str()]["url"],
original[declared.section][declared.name.as_str()]["url"]
);
}
std::fs::remove_dir_all(root)?;
+35 -27
View File
@@ -36,6 +36,7 @@ use std::{
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tristr::alias::String;
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
static ACTIVE_SERVICE_SESSION: Mutex<Option<ActiveServiceSession>> = Mutex::new(None);
@@ -62,7 +63,7 @@ pub(crate) fn is_not_auto_started(reason: &str) -> bool {
#[cfg(target_os = "windows")]
pub(crate) fn notify_service_fallback(reason: &str) {
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.to_owned());
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.into());
Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, "");
}
@@ -91,7 +92,7 @@ fn app_data_owner_command(credentials: &OwnerCredentials) -> Option<String> {
return None;
};
let path = credentials.app_data_dir.replace('\'', r"'\''");
Some(format!("sudo chown -R {uid}:{gid} '{path}'"))
Some(format!("sudo chown -R {uid}:{gid} '{path}'").into())
}
/// Capabilities of the service session that owns the running Core.
@@ -243,22 +244,24 @@ fn macos_service_install_markers() -> Vec<String> {
format!(
"/Library/LaunchDaemons/{}.plist",
clash_verge_service_ipc::MACOS_SERVICE_ID
),
)
.into(),
format!(
"/Library/PrivilegedHelperTools/{}.bundle",
clash_verge_service_ipc::MACOS_SERVICE_ID
),
)
.into(),
#[cfg(not(feature = "verge-dev"))]
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".to_owned(),
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".into(),
#[cfg(not(feature = "verge-dev"))]
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".to_owned(),
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".into(),
]
}
#[cfg(target_os = "macos")]
fn macos_service_install_marker_exists() -> std::io::Result<bool> {
for marker in macos_service_install_markers() {
if path_entry_exists_without_follow(Path::new(&marker))? {
if path_entry_exists_without_follow(Path::new(marker.as_str()))? {
return Ok(true);
}
}
@@ -335,7 +338,8 @@ pub(crate) fn trusted_service_evidence() -> Result<bool> {
output.status
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim() != "not-found")
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8_lossy(&output.stdout).trim() != "not-found")
}
#[cfg(target_os = "macos")]
@@ -564,12 +568,12 @@ fn service_core_path(clash_core: &str, bin_ext: &str) -> Result<PathBuf> {
#[cfg(target_os = "macos")]
fn escape_osascript_double_quoted_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
value.replace('\\', "\\\\").replace('"', "\\\"").into()
}
#[cfg(target_os = "macos")]
fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', r"'\''"))
format!("'{}'", value.replace('\'', r"'\''")).into()
}
fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
@@ -641,7 +645,7 @@ fn uninstall_service() -> Result<()> {
let status = if linux_running_as_root() {
StdCommand::new(&uninstall_path).status()?
} else {
let result = StdCommand::new(&elevator)
let result = StdCommand::new(elevator.as_str())
.arg("--disable-internal-agent")
.arg(&uninstall_path)
.status()?;
@@ -697,7 +701,7 @@ fn uninstall_service() -> Result<()> {
}
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
let uninstall_shell: String = uninstall_path.to_string_lossy().into();
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
@@ -843,7 +847,7 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
if response.code > 0 {
return Ok(StageRequest::Refused {
code: response.code,
message: response.message,
message: response.message.into(),
});
}
response
@@ -879,7 +883,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
if store.state().mode == crate::core::manager::RunningMode::NotRunning
&& refusal.code != ServiceErrorCode::ProxyClearFailed as u16
{
store.observe(ServiceHealth::Unavailable(refusal.to_string()));
store.observe(ServiceHealth::Unavailable(refusal.to_string().into()));
}
refusal.into()
}
@@ -887,7 +891,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
/// Sidecar stays blocked until a reinstall replaces the residual helper, so ask for repair.
pub(super) fn record_residual_service(error: &anyhow::Error) {
if let Some(residual) = error.downcast_ref::<clash_verge_service_ipc::execution::ResidualServiceError>() {
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string()));
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string().into()));
}
}
@@ -901,7 +905,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
let proposed_session_token = generate_service_session_token()?;
let request = StartClashRequest {
runtime,
proposed_session_token: proposed_session_token.clone(),
proposed_session_token: proposed_session_token.clone().into(),
macos_proxy: None,
};
@@ -939,8 +943,8 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
&RUN_STATE,
ServiceStartRefusal {
code: response.code,
core_path: request.runtime.core_path,
message: err_msg,
core_path: request.runtime.core_path.into(),
message: err_msg.into(),
},
));
}
@@ -952,7 +956,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
*ACTIVE_SERVICE_SESSION.lock() = Some(ActiveServiceSession {
proof: OwnerSessionProof {
generation: result.session.generation,
token: proposed_session_token,
token: proposed_session_token.into(),
},
supports_runtime_staging: capabilities.runtime_staging,
supports_runtime_file_read: capabilities.runtime_file_read,
@@ -1012,7 +1016,10 @@ pub(super) async fn get_clash_logs_by_service() -> Result<Vec<String>> {
bail!(err_msg);
}
Ok(response.data.unwrap_or_default())
Ok(response
.data
.map(|logs| logs.into_iter().map(Into::into).collect())
.unwrap_or_default())
}
pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
@@ -1030,7 +1037,8 @@ pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
}
let encoded = response.data.context("服务未返回核心日志快照")?;
let content = decode_hex(&encoded).context("服务返回了无效的核心日志快照")?;
Ok(String::from_utf8_lossy_owned(content))
// from_utf8_lossy_owned exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8_lossy_owned(content).into())
}
fn decode_hex(encoded: &str) -> Result<Vec<u8>> {
@@ -1344,7 +1352,7 @@ async fn read_chunk(
}
Ok(Some(match response.data.context("服务未返回运行时文件")? {
RuntimeFileOutcome::Absent => (None, 0, None),
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex), len, mtime_ns),
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex.into()), len, mtime_ns),
}))
}
@@ -1356,11 +1364,11 @@ async fn provider_load_times() -> Result<ProviderLoadTimes> {
let proxies = proxies.map_err(|error| anyhow!("failed to query proxy providers: {error}"))?;
let mut loaded = ProviderLoadTimes::new();
for (name, provider) in rules.providers {
loaded.insert(("rule-providers", name), epoch_nanos(&provider.updated_at));
loaded.insert(("rule-providers", name.into()), epoch_nanos(&provider.updated_at));
}
for (name, provider) in proxies.providers {
loaded.insert(
("proxy-providers", name),
("proxy-providers", name.into()),
provider.updated_at.as_deref().and_then(epoch_nanos),
);
}
@@ -1586,7 +1594,7 @@ const fn owner_recovery_policy(reason: OwnerRecoveryReason, is_macos: bool) -> O
fn mark_service_unavailable_after_owner_loss<E: RunStateEnv>(store: &RunStateStore<E>, reason: OwnerRecoveryReason) {
if matches!(reason, OwnerRecoveryReason::TransportFailure) {
store.observe(ServiceHealth::Unavailable(
"service control IPC unavailable after sustained transport failure".to_owned(),
"service control IPC unavailable after sustained transport failure".into(),
));
}
}
@@ -1847,7 +1855,7 @@ async fn wait_for_service_ipc() -> Result<()> {
}
Err(ReadyWaitError::Unreachable(error)) => {
tracing::Span::current().record("outcome", "unreachable");
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}")));
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}").into()));
Err(error).context(CONTEXT)
}
Err(ReadyWaitError::Rejected(error)) => {
@@ -2662,7 +2670,7 @@ mod tests {
let outcome = super::run_action_restoring_sidecar(&store, was_allowed, async {
store.perform(PendingAction::Install).await?;
// Simulate `wait_for_service_ipc` recording health before it fails.
store.observe(ServiceHealth::Unavailable("service never answered".to_owned()));
store.observe(ServiceHealth::Unavailable("service never answered".into()));
bail!("service IPC did not become available")
})
.await;
+3 -3
View File
@@ -8,7 +8,6 @@ use anyhow::Result;
use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
sync::{
Arc,
@@ -18,6 +17,7 @@ use std::{
};
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
use tokio::sync::Mutex as TokioMutex;
use tristr::alias::String;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ProxyApplyStep {
@@ -677,8 +677,8 @@ mod tests {
(calls.into_inner(), result.is_ok())
}
fn failure_text(result: anyhow::Result<()>) -> std::string::String {
result.map_or_else(|error| format!("{error:#}"), |()| "no failure".to_owned())
fn failure_text(result: anyhow::Result<()>) -> String {
result.map_or_else(|error| format!("{error:#}").into(), |()| String::from("no failure"))
}
#[test]
+2 -2
View File
@@ -8,7 +8,6 @@ use crate::{
use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error};
use parking_lot::{Mutex, RwLock};
use smartstring::alias::String;
use std::{
collections::HashMap,
sync::{
@@ -23,6 +22,7 @@ use tokio::{
};
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key};
use tristr::alias::String;
enum TimerCommand {
Apply(HashMap<String, TaskSchedule>),
@@ -502,7 +502,6 @@ mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption};
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
use std::{
sync::{
@@ -515,6 +514,7 @@ mod tests {
use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue;
use tristr::alias::String;
struct CancellationProbe {
cancelled: Arc<AtomicBool>,
+6 -7
View File
@@ -19,7 +19,6 @@ use tokio::fs;
use super::handle;
use anyhow::Result;
use smartstring::alias::String;
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Duration;
@@ -27,6 +26,7 @@ use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
};
use tristr::alias::String;
mod menu_def;
#[cfg(target_os = "macos")]
@@ -422,12 +422,11 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => {
// 托盘菜单中的 `accelerator` 属性,在 Linux/Windows 中都不支持小键盘按键的解析
if key.to_uppercase().contains("NUMPAD") {
None
} else {
Some((func, 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,
}
+5 -5
View File
@@ -11,6 +11,7 @@ use std::{
};
use tauri_plugin_updater::{Update, UpdaterExt as _};
use tokio_util::sync::CancellationToken;
use tristr::alias::String;
pub struct SilentUpdater {
update_ready: AtomicBool,
@@ -55,8 +56,8 @@ impl SilentUpdater {
.with_context(|| format!("failed to write update cache {}", bin_path.display()))?;
let meta = UpdateCacheMeta {
version: version.to_string(),
downloaded_at: Utc::now().to_rfc3339(),
version: version.into(),
downloaded_at: Utc::now().to_rfc3339().into(),
};
let meta_path = cache_dir.join("pending_update.json");
std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)
@@ -133,9 +134,8 @@ fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str
use base64::Engine as _;
let decode = |value: &str| -> Result<String> {
Ok(String::from_utf8(
base64::engine::general_purpose::STANDARD.decode(value)?,
)?)
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8(base64::engine::general_purpose::STANDARD.decode(value)?)?.into())
};
let config: tauri_plugin_updater::Config = serde_json::from_value(
app_handle
+3 -9
View File
@@ -1,7 +1,6 @@
use anyhow::Result;
use scopeguard::defer;
use serde::Serialize;
use smartstring::alias::String;
use std::{
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
@@ -10,6 +9,7 @@ use std::{
};
use tauri_plugin_shell::ShellExt as _;
use tokio::fs;
use tristr::alias::String;
use crate::config::Config;
use crate::core::handle;
@@ -114,14 +114,8 @@ impl fmt::Display for ValidationSkipReason {
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ValidationOutcome {
Valid,
Invalid {
kind: ValidationErrorKind,
#[specta(type = std::string::String)]
message: String,
},
Skipped {
reason: ValidationSkipReason,
},
Invalid { kind: ValidationErrorKind, message: String },
Skipped { reason: ValidationSkipReason },
Busy,
}
+1 -1
View File
@@ -4,8 +4,8 @@ use crate::{
utils::{dirs, help},
};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
#[derive(Debug, Clone)]
pub struct ChainItem {
+1 -1
View File
@@ -1,5 +1,5 @@
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use tristr::alias::String;
pub const HANDLE_FIELDS: [&str; 12] = [
"mode",
+1 -1
View File
@@ -26,9 +26,9 @@ use anyhow::{Context as _, Result};
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tokio::fs;
use tristr::alias::String;
type ResultLog = Vec<(String, String)>;
#[derive(Debug)]
+13 -13
View File
@@ -6,8 +6,8 @@ use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function:
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::sync::Arc;
use tristr::alias::String;
const MAX_OUTPUTS: usize = 1000;
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
@@ -16,7 +16,7 @@ const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
type ScriptResult = (Mapping, Vec<String>, Vec<(String, String)>);
type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>);
type EvalOutcome = (Result<Mapping, String>, Vec<(String, String)>);
/// Never fails: exceptions are logged and the input config is returned unchanged.
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
@@ -37,9 +37,9 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
let evaluated = AsyncHandler::spawn_blocking(move || eval_script(&script, &json, &name));
let (outcome, mut logs) = match tokio::time::timeout(SCRIPT_TIMEOUT, evaluated).await {
Ok(Ok(evaluated)) => evaluated,
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}")), vec![]),
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}").into()), vec![]),
Err(_elapsed) => (
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}")),
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}").into()),
vec![],
),
};
@@ -59,7 +59,7 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
(result, changed_keys, logs)
}
Err(reason) => {
logs.push(("exception".into(), reason.into()));
logs.push(("exception".into(), reason));
(config, vec![], logs)
}
}
@@ -79,7 +79,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
let outputs_clone = Arc::clone(&outputs);
let total_size_clone = Arc::clone(&total_size);
let bail = |reason: std::string::String| (Err(reason), outputs.lock().to_vec());
let bail = |reason: String| (Err(reason), outputs.lock().to_vec());
let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe {
NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| {
@@ -137,13 +137,13 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
JsValue::from(JsString::from(config_json)),
Attribute::all(),
) {
return bail(format!("failed to bind config for script: {err}"));
return bail(format!("failed to bind config for script: {err}").into());
}
// 仅处理 name 参数中的特殊字符
let safe_name = escape_js_string_for_single_quote(name);
if safe_name.len() > 1024 {
return bail("Name parameter too long".to_owned());
return bail("Name parameter too long".into());
}
let code = format!(
@@ -157,19 +157,19 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
let result = match context.eval(Source::from_bytes(code.as_str())) {
Ok(result) if result.is_string() => result,
_ => return bail("main function should return object".to_owned()),
_ => return bail("main function should return object".into()),
};
let result = match result.to_string(&mut context) {
Ok(result) => result,
Err(e) => return bail(format!("Failed to convert JS result to string: {e}")),
Err(e) => return bail(format!("Failed to convert JS result to string: {e}").into()),
};
let result = match result.to_std_string() {
Ok(result) => result,
Err(_) => return bail("Failed to convert JS string to std string".to_owned()),
Err(_) => return bail("Failed to convert JS string to std string".into()),
};
if result.len() > MAX_JSON_SIZE {
return bail("Script result exceeds maximum allowed size".to_owned());
return bail("Script result exceeds maximum allowed size".into());
}
match parse_json_safely(&result) {
@@ -180,7 +180,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
Type::Config,
"Script execution error: {err:#}. Script name: {name}"
);
bail("Script execution failed".to_owned())
bail("Script execution failed".into())
}
}
}
+10 -9
View File
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Sequence, Value};
use std::collections::HashSet;
use tristr::alias::String;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
@@ -12,8 +13,8 @@ pub struct SeqMap {
fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
seq.iter()
.filter_map(|item| match item {
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(str::to_owned),
Value::String(name) => Some(name.to_owned()),
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(Into::into),
Value::String(name) => Some(name.to_owned().into()),
_ => None,
})
.collect()
@@ -58,10 +59,10 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
.into_iter()
.filter(|item| {
if let Value::String(s) = item {
!delete.contains(s)
!delete.iter().any(|deleted| deleted == s)
} else if let Value::Mapping(m) = item {
if let Some(Value::String(name)) = m.get("name") {
!delete.contains(name)
!delete.iter().any(|deleted| deleted == name)
} else {
true
}
@@ -99,7 +100,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
.into_iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.contains(name)
!delete.iter().any(|deleted| deleted == name)
} else {
true
}
@@ -119,12 +120,12 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
let mut seen_proxy_names = HashSet::new();
for name in &added_proxy_names {
if seen_proxy_names.insert(name.clone()) {
merged_proxies.push(Value::String(name.clone()));
merged_proxies.push(Value::String(name.clone().into()));
}
}
for value in base_group_proxies {
if let Value::String(name) = &value
&& !seen_proxy_names.insert(name.to_owned())
&& !seen_proxy_names.insert(name.to_owned().into())
{
continue;
}
@@ -180,7 +181,7 @@ proxy-groups:
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".to_string()],
delete: vec!["proxy1".into()],
};
config = use_seq(seq, config, "proxies");
@@ -317,7 +318,7 @@ proxy-groups: "invalid"
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".to_string()],
delete: vec!["proxy1".into()],
};
config = use_seq(seq, config, "proxies");
+1 -4
View File
@@ -12,17 +12,14 @@ use chrono::Utc;
use clash_verge_logging::{Type, logging, logging_error};
use reqwest_dav::list_cmd::ListFile;
use serde::Serialize;
use smartstring::alias::String;
use std::path::PathBuf;
use tokio::fs;
use tristr::alias::String;
#[derive(Debug, Serialize, specta::Type)]
pub struct LocalBackupFile {
#[specta(type = std::string::String)]
pub filename: String,
#[specta(type = std::string::String)]
pub path: String,
#[specta(type = std::string::String)]
pub last_modified: String,
pub content_length: u64,
}
+1 -1
View File
@@ -10,8 +10,8 @@ use crate::{
use bytes::BytesMut;
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::sync::{Arc, LazyLock};
use tristr::alias::String;
#[allow(clippy::expect_used)]
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
+13 -10
View File
@@ -21,6 +21,7 @@ use std::{
process::Command,
sync::atomic::{AtomicU64, Ordering},
};
use tristr::alias::String;
const RELEASE_VERSION_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const ALPHA_BASE_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha";
@@ -50,7 +51,7 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
// A core already broken by the in-place updater cannot report a version; upgrading is the repair.
let installed = read_core_version(&target).unwrap_or_else(|error| {
logging!(warn, Type::Core, "core upgrade: unreadable installed core: {error:#}");
std::string::String::new()
String::new()
});
let (proxy, latest) = resolve_latest_version(alpha).await?;
@@ -172,14 +173,15 @@ fn managed_core_path(core: &str) -> Result<PathBuf> {
}
/// Pins the package to the resolved version, so a release moving on mid-upgrade cannot 404.
fn package_url(alpha: bool, version: &str) -> Result<std::string::String> {
fn package_url(alpha: bool, version: &str) -> Result<String> {
let asset = asset_base_name(alpha)?;
let extension = if cfg!(windows) { "zip" } else { "gz" };
Ok(if alpha {
format!("{ALPHA_BASE_URL}/{asset}-{version}.{extension}")
} else {
format!("{RELEASE_DOWNLOAD_URL}/{version}/{asset}-{version}.{extension}")
})
}
.into())
}
/// Mirrors the asset map in `scripts/prebuild.mjs` so an upgrade keeps the build variant
@@ -218,11 +220,11 @@ fn asset_base_name(alpha: bool) -> Result<&'static str> {
/// Returns the proxy that reached GitHub so the package download reuses it.
#[tracing::instrument(skip_all, level = "debug", fields(alpha))]
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::String)> {
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, String)> {
let url = if alpha {
format!("{ALPHA_BASE_URL}/version.txt")
String::from(format!("{ALPHA_BASE_URL}/version.txt"))
} else {
RELEASE_VERSION_URL.to_owned()
String::from(RELEASE_VERSION_URL)
};
let mut last_error = None;
@@ -234,7 +236,7 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::
match attempt {
Ok(response) if response.status().is_success() => {
// An error page answered with 200 must not become a version, nor reach the URL.
let version = response.text().trim().to_owned();
let version = String::from(response.text().trim());
if !is_usable_version(&version) {
logging!(
warn,
@@ -446,7 +448,7 @@ fn new_command(program: impl AsRef<OsStr>) -> Command {
}
/// Parses the version out of `Mihomo Meta v1.19.30 darwin arm64 with go1.22.12 ...`.
fn read_core_version(path: &Path) -> Result<std::string::String> {
fn read_core_version(path: &Path) -> Result<String> {
let output = new_command(path)
.arg("-v")
.output()
@@ -456,11 +458,12 @@ fn read_core_version(path: &Path) -> Result<std::string::String> {
bail!("{} -v exited with {}", path.display(), output.status);
}
let stdout = String::from_utf8_lossy(&output.stdout);
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
stdout
.split_whitespace()
.nth(2)
.map(str::to_owned)
.map(String::from)
.with_context(|| format!("unexpected version output from {}: {stdout}", path.display()))
}
+2 -5
View File
@@ -2,7 +2,7 @@ use crate::core::notify::{Refresh, announce};
use anyhow::{Result, ensure};
use clash_verge_draft::DraftTransaction;
use serde::Serialize;
use smartstring::alias::String;
use tristr::alias::String;
use crate::{
config::{
@@ -19,10 +19,7 @@ use crate::{
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DnsOverrideOutcome {
Applied,
ConfirmationRequired {
#[specta(type = std::string::String)]
source: String,
},
ConfirmationRequired { source: String },
}
pub async fn set_dns_override(
+10 -9
View File
@@ -1,20 +1,17 @@
use crate::{
cmd::{CmdResult, StringifyErr as _},
cmd::{CmdResult, CommandFailure, StringifyErr as _},
utils::dirs::{self, PathBufExec as _},
};
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::path::{Component, Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
use tristr::alias::String;
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct IconInfo {
#[specta(type = std::string::String)]
name: String,
#[specta(type = std::string::String)]
previous_t: String,
#[specta(type = std::string::String)]
current_t: String,
}
@@ -43,7 +40,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
Ok(icon_path)
}
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
fn normalized_text_prefix(content: &[u8]) -> String {
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
let start = content
.iter()
@@ -51,7 +48,8 @@ fn normalized_text_prefix(content: &[u8]) -> std::string::String {
.unwrap_or(content.len());
let end = content.len().min(start.saturating_add(2048));
let prefix = &content[start..end];
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase()
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase().into()
}
fn looks_like_html(content: &[u8]) -> bool {
@@ -95,7 +93,10 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
if !is_supported_icon_content(&content) {
let _ = temp_path.remove_if_exists().await;
return Err(format!("Downloaded content is not a valid image: {}", url.as_str()).into());
return Err(CommandFailure::plain(format!(
"Downloaded content is not a valid image: {}",
url.as_str()
)));
}
{
@@ -164,7 +165,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
match fs::copy(file_path, &dest_path).await {
Ok(_) => Ok(dest_path.to_string_lossy().into()),
Err(err) => Err(err.to_string().into()),
Err(err) => Err(CommandFailure::plain(err)),
}
} else {
Err("file not found".into())
+2 -2
View File
@@ -8,7 +8,7 @@ use crate::{
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tristr::alias::String;
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
@@ -231,7 +231,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
};
let message = mask_err(&message);
let message = if profile_persisted {
format!("订阅已保存,但未能确认已应用到内核: {message}")
format!("订阅已保存,但未能确认已应用到内核: {message}").into()
} else {
message
};
+6 -5
View File
@@ -1,4 +1,5 @@
use crate::cmd;
use tristr::alias::String;
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
tauri_specta::Builder::new()
@@ -121,7 +122,7 @@ pub struct WebDavFile {
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
Self {
href: file.href,
href: file.href.into(),
last_modified: file.last_modified,
}
}
@@ -148,20 +149,20 @@ pub struct IpAddress {
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
fn from(interface: network_interface::NetworkInterface) -> Self {
Self {
name: interface.name,
name: interface.name.into(),
addr: interface
.addr
.into_iter()
.map(|address| match address {
network_interface::Addr::V4(address) => InterfaceAddress::V4(IpAddress {
ip: address.ip.to_string(),
ip: address.ip.to_string().into(),
}),
network_interface::Addr::V6(address) => InterfaceAddress::V6(IpAddress {
ip: address.ip.to_string(),
ip: address.ip.to_string().into(),
}),
})
.collect(),
mac_addr: interface.mac_addr,
mac_addr: interface.mac_addr.map(Into::into),
}
}
}
+18 -14
View File
@@ -11,6 +11,7 @@ use std::{
str::FromStr,
};
use tokio::io::AsyncWriteExt as _;
use tristr::alias::String;
pub async fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
@@ -34,12 +35,13 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
let bytes = tokio::fs::read(path)
.await
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
let yaml_str =
String::from_utf8(bytes).with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
let yaml_str = std::string::String::from_utf8(bytes)
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
let label = path.to_path_buf();
// Parsing a multi-megabyte profile is pure CPU; keep it off the async worker.
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str, label))
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str.into(), label))
.await
.map_err(|join| anyhow!("yaml parse task failed: {join}"))?
}
@@ -139,7 +141,7 @@ const ALPHABET: [char; 62] = [
pub fn get_uid(prefix: &str) -> String {
let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}")
format!("{prefix}{id}").into()
}
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
@@ -171,7 +173,7 @@ pub fn mask_url(url: &str) -> String {
let scheme_and_host = &path_part[..host_end];
let path = &path_part[host_end..]; // starts with '/' or empty
let mut result = scheme_and_host.to_owned();
let mut result: String = scheme_and_host.into();
if !path.is_empty() {
let masked: Vec<&str> = path
@@ -186,8 +188,8 @@ pub fn mask_url(url: &str) -> String {
let masked_query: Vec<String> = query
.split('&')
.map(|param| match param.find('=') {
Some(eq) => format!("{}=***", &param[..eq]),
None => param.to_owned(),
Some(eq) => format!("{}=***", &param[..eq]).into(),
None => param.into(),
})
.collect();
result.push_str(&masked_query.join("&"));
@@ -198,7 +200,8 @@ pub fn mask_url(url: &str) -> String {
/// Masks every HTTP(S) URL embedded in a log string while preserving surrounding text.
pub fn mask_err(err: &str) -> String {
let mut result = String::with_capacity(err.len());
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
let mut result = std::string::String::with_capacity(err.len());
let mut remaining = err;
loop {
@@ -224,7 +227,7 @@ pub fn mask_err(err: &str) -> String {
remaining = &remaining[url_end..];
}
result
result.into()
}
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
@@ -235,7 +238,8 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
Some(
percent_encoding::percent_decode_str(last_segment)
.decode_utf8_lossy()
.to_string(),
.to_string()
.into(),
)
}
@@ -277,15 +281,15 @@ pub fn linux_elevator() -> String {
Ok(output) => {
if !output.stdout.is_empty() {
if let Ok(path) = std::str::from_utf8(&output.stdout) {
path.trim().to_string()
path.trim().into()
} else {
"sudo".to_string()
"sudo".into()
}
} else {
"sudo".to_string()
"sudo".into()
}
}
Err(_) => "sudo".to_string(),
Err(_) => "sudo".into(),
}
}
+15 -14
View File
@@ -11,6 +11,7 @@ use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use tristr::alias::String;
pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<()> {
let Some(path) = mimeapps_list_path() else {
@@ -46,19 +47,19 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
if trimmed.eq_ignore_ascii_case("[Default Applications]") {
default_present = true;
current_section = Some(SectionKind::DefaultApplications);
output_lines.push("[Default Applications]".to_string());
output_lines.push("[Default Applications]".into());
continue;
} else if trimmed.eq_ignore_ascii_case("[Added Associations]") {
current_section = Some(SectionKind::AddedAssociations);
output_lines.push("[Added Associations]".to_string());
output_lines.push("[Added Associations]".into());
continue;
}
}
if current_section.is_some() {
section_buffer.push(line.to_string());
section_buffer.push(line.into());
} else {
output_lines.push(line.to_string());
output_lines.push(line.into());
}
}
@@ -78,9 +79,9 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
if output_lines.last().is_some_and(|line| !line.is_empty()) {
output_lines.push(String::new());
}
output_lines.push("[Default Applications]".to_string());
output_lines.push("[Default Applications]".into());
for &scheme in schemes {
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
}
}
@@ -171,7 +172,7 @@ fn flush_section(
.split(';')
.filter_map(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
(!trimmed.is_empty()).then(|| trimmed.into())
})
.collect();
@@ -187,7 +188,7 @@ fn flush_section(
.split(';')
.filter_map(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
(!trimmed.is_empty()).then(|| trimmed.into())
})
.collect();
@@ -203,10 +204,10 @@ fn flush_section(
merged_values.insert(0, moved);
}
} else {
merged_values.insert(0, desktop_file.to_string());
merged_values.insert(0, desktop_file.into());
}
let mut merged_line = format!("{existing_prefix}x-scheme-handler/{scheme}=");
let mut merged_line: String = format!("{existing_prefix}x-scheme-handler/{scheme}=").into();
merged_line.push_str(&merged_values.join(";"));
merged_line.push(';');
@@ -222,16 +223,16 @@ fn flush_section(
if let Some(pos) = values.iter().position(|value| value == desktop_file) {
if pos != 0 {
values.remove(pos);
values.insert(0, desktop_file.to_string());
values.insert(0, desktop_file.into());
*changed = true;
}
} else {
values.insert(0, desktop_file.to_string());
values.insert(0, desktop_file.into());
*changed = true;
}
let prefix = line.chars().take_while(|c| c.is_whitespace()).collect::<String>();
let mut new_line = format!("{prefix}x-scheme-handler/{scheme}=");
let mut new_line: String = format!("{prefix}x-scheme-handler/{scheme}=").into();
new_line.push_str(&values.join(";"));
new_line.push(';');
@@ -253,7 +254,7 @@ fn flush_section(
if ensure_all {
for &scheme in schemes {
if !seen.contains_key(scheme) {
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
*changed = true;
}
}
+6 -6
View File
@@ -5,10 +5,10 @@ use reqwest::{
Client, Proxy, StatusCode,
header::{HeaderMap, HeaderValue, USER_AGENT},
};
use smartstring::alias::String;
use std::{sync::Arc, time::Duration};
use sysproxy::Sysproxy;
use tauri::Url;
use tristr::alias::String;
#[derive(Debug)]
pub struct HttpResponse {
@@ -57,7 +57,7 @@ impl NetworkManager {
fn build_client(
&self,
proxy_url: Option<std::string::String>,
proxy_url: Option<String>,
default_headers: HeaderMap,
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
@@ -76,7 +76,7 @@ impl NetworkManager {
// 设置代理
if let Some(proxy_str) = proxy_url {
let proxy = Proxy::all(proxy_str)?;
let proxy = Proxy::all(proxy_str.as_str())?;
builder = builder.proxy(proxy);
} else {
builder = builder.no_proxy();
@@ -226,15 +226,15 @@ impl NetworkManager {
accept_invalid_certs: bool,
tls_root_mode: TlsRootMode,
) -> Result<Client> {
let proxy_url: Option<std::string::String> = match proxy_type {
let proxy_url: Option<String> = match proxy_type {
ProxyType::None => None,
ProxyType::Localhost => {
let port = MixedPort::desired().await;
Some(format!("http://127.0.0.1:{port}"))
Some(format!("http://127.0.0.1:{port}").into())
}
ProxyType::System => {
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
Some(format!("http://{}:{}", p.host, p.port))
Some(format!("http://{}:{}", p.host, p.port).into())
} else {
None
}
+7 -7
View File
@@ -1,8 +1,8 @@
use crate::core::notify::NoticeStatus;
use anyhow::Result;
use percent_encoding::percent_decode_str;
use smartstring::alias::String;
use tauri::Url;
use tristr::alias::String;
use crate::{
config::{Config, PrfItem, profiles},
@@ -39,7 +39,7 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
Ok(())
}
fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String, Option<String>)> {
fn extract_subscription_info(link_parsed: &Url) -> Option<(String, Option<String>)> {
if !matches!(link_parsed.scheme(), "clash" | "clash-verge") {
return None;
}
@@ -52,7 +52,7 @@ fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String,
Some((url, name))
}
fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
fn extract_subscription_url(link_parsed: &Url) -> Option<String> {
let query = link_parsed.query()?;
let prefix = "url=";
let pos = query.find(prefix)?;
@@ -60,15 +60,15 @@ fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
Some(decode_subscription_url(raw_url))
}
fn decode_subscription_url(raw_url: &str) -> std::string::String {
fn decode_subscription_url(raw_url: &str) -> String {
// Avoid double-decoding nested subscription URLs; decode only when needed.
if Url::parse(raw_url).is_ok() {
return raw_url.to_string();
return String::from(raw_url);
}
let mut candidate = raw_url.to_string();
let mut candidate = String::from(raw_url);
for _ in 0..2 {
let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string();
let next = String::from(percent_decode_str(&candidate).decode_utf8_lossy());
if next == candidate {
break;
}
+17 -12
View File
@@ -5,6 +5,7 @@ use std::fs;
use std::os::windows::process::CommandExt as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tristr::alias::String;
use windows::Win32::Globalization::{GetACP, GetOEMCP, MULTI_BYTE_TO_WIDE_CHAR_FLAGS, MultiByteToWideChar};
const CREATE_NO_WINDOW: u32 = 0x08000000;
@@ -72,11 +73,11 @@ fn get_task_user_id() -> Result<String> {
if let Some(domain) = domain {
let domain = domain.trim();
if !domain.is_empty() {
return Ok(format!("{domain}\\{username}"));
return Ok(format!("{domain}\\{username}").into());
}
}
Ok(username.to_string())
Ok(username.into())
}
fn get_startup_dir() -> Result<PathBuf> {
@@ -118,7 +119,8 @@ fn task_xml_path(mode: TaskMode) -> Result<PathBuf> {
}
fn xml_escape(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
// Per-char pushes must stay on the std String: TriStr growth re-materializes per push.
let mut escaped = std::string::String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => escaped.push_str("&amp;"),
@@ -129,7 +131,7 @@ fn xml_escape(value: &str) -> String {
_ => escaped.push(ch),
}
}
escaped
escaped.into()
}
fn build_task_xml(mode: TaskMode) -> Result<String> {
@@ -182,7 +184,8 @@ fn build_task_xml(mode: TaskMode) -> Result<String> {
user_id,
mode.xml_run_level(),
exe_path
))
)
.into())
}
fn encode_utf16le_with_bom(content: &str) -> Vec<u8> {
@@ -226,12 +229,13 @@ fn decode_with_code_page(bytes: &[u8], code_page: u32) -> Option<String> {
}
wide.truncate(written as usize);
Some(String::from_utf16_lossy(&wide))
// from_utf16_lossy exists only on the std String type; the tristr alias shadows the bare name.
Some(std::string::String::from_utf16_lossy(&wide).into())
}
fn decode_console_output(bytes: &[u8]) -> String {
if let Ok(text) = std::str::from_utf8(bytes) {
return text.to_string();
return text.into();
}
let oem = unsafe { GetOEMCP() };
@@ -244,7 +248,8 @@ fn decode_console_output(bytes: &[u8]) -> String {
return text;
}
String::from_utf8_lossy(bytes).to_string()
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8_lossy(bytes).into()
}
fn output_message(output: &Output) -> String {
@@ -254,10 +259,10 @@ fn output_message(output: &Output) -> String {
let stderr = stderr.trim();
match (stdout.is_empty(), stderr.is_empty()) {
(true, true) => "unknown error".to_string(),
(false, true) => stdout.to_string(),
(true, false) => stderr.to_string(),
(false, false) => format!("{stdout} | {stderr}"),
(true, true) => "unknown error".into(),
(false, true) => stdout.into(),
(true, false) => stderr.into(),
(false, false) => format!("{stdout} | {stderr}").into(),
}
}
+2 -1
View File
@@ -16,6 +16,7 @@ use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::oneshot;
use tristr::alias::String;
use warp::Filter as _;
const INSTANCE_TOKEN_HEADER: &str = "x-instance-token";
@@ -141,7 +142,7 @@ async fn notify_existing_instance(record: &InstanceRecord) -> bool {
let request = client.get(format!("http://127.0.0.1:{}/commands/visible", record.port));
request
.header(INSTANCE_TOKEN_HEADER, &record.token)
.header(INSTANCE_TOKEN_HEADER, record.token.as_str())
.send()
.await
.is_ok_and(|response| response.status().is_success())
+6 -4
View File
@@ -1,5 +1,6 @@
use serde::Serialize;
use serde_yaml_ng::Result;
use tristr::alias::String;
pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
let yaml = serde_yaml_ng::to_string(data)?;
@@ -7,7 +8,8 @@ pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
}
fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
let mut result = String::with_capacity(yaml.len());
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
let mut result = std::string::String::with_capacity(yaml.len());
let mut in_fake_ip_filter = false;
let mut fake_ip_filter_indent = 0;
@@ -39,7 +41,7 @@ fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
result.push('\n');
}
result
result.into()
}
fn leading_spaces(line: &str) -> usize {
@@ -50,12 +52,12 @@ fn quote_wildcard_item(item: &str) -> String {
if !item.starts_with('\"') && !item.starts_with('\'') && contains_wildcard(item) {
quote_string(item)
} else {
item.to_string()
item.into()
}
}
fn quote_string(s: &str) -> String {
"\'".to_string() + s.replace('\'', "''").as_str() + "\'"
("\'".to_string() + s.replace('\'', "''").as_str() + "\'").into()
}
fn contains_wildcard(value: &str) -> bool {
@@ -0,0 +1,43 @@
import { Button, ButtonGroup } from '@mui/material'
interface Props {
value?: string
onChange?: (value: string) => void
}
export const DnsModeSwitch = (props: Props) => {
const { value, onChange } = props
return (
<ButtonGroup size="small" sx={{ my: '4px' }}>
<Button
variant={!value ? 'contained' : 'outlined'}
onClick={() => onChange?.('')}
sx={{ textTransform: 'capitalize' }}
>
Auto
</Button>
<Button
variant={value === 'disabled' ? 'contained' : 'outlined'}
onClick={() => onChange?.('disabled')}
sx={{ textTransform: 'capitalize' }}
>
Disabled
</Button>
<Button
variant={value === 'native' ? 'contained' : 'outlined'}
onClick={() => onChange?.('native')}
sx={{ textTransform: 'capitalize' }}
>
Native
</Button>
<Button
variant={value === 'hijack' ? 'contained' : 'outlined'}
onClick={() => onChange?.('hijack')}
sx={{ textTransform: 'capitalize' }}
>
Hijack
</Button>
</ButtonGroup>
)
}
@@ -26,6 +26,7 @@ import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system'
import { areValidIpCidrs } from '@/utils/network'
import { DnsModeSwitch } from './dns-mode-switch'
import { StackModeSwitch } from './stack-mode-switch'
const OS = getSystem()
@@ -52,6 +53,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
dnsHijack: ['any:53'],
strictRoute: false,
mtu: 1500,
dnsMode: '',
})
const routeExcludeAddressItems = splitRouteExcludeAddress(
@@ -84,6 +86,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
dnsHijack: clash?.tun['dns-hijack'] ?? ['any:53'],
strictRoute: clash?.tun['strict-route'] ?? false,
mtu: clash?.tun.mtu ?? 1500,
dnsMode: clash?.tun['dns-mode'] ?? '',
})
},
close: () => setOpen(false),
@@ -117,6 +120,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
: {}),
'auto-detect-interface': values.autoDetectInterface,
'dns-hijack': values.dnsHijack[0] === '' ? [] : values.dnsHijack,
'dns-mode': values.dnsMode,
'strict-route': values.strictRoute,
mtu: values.mtu ?? 1500,
}
@@ -163,6 +167,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
'auto-detect-interface': true,
'dns-hijack': ['any:53'],
'route-exclude-address': [],
'dns-mode': '',
'strict-route': false,
mtu: 1500,
}
@@ -176,6 +181,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
dnsHijack: ['any:53'],
strictRoute: false,
mtu: 1500,
dnsMode: '',
})
await patchClash({ tun })
await mutateClash(
@@ -212,6 +218,25 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItemText
primary={t('settings.modals.tun.fields.dnsMode')}
sx={{ maxWidth: 'fit-content' }}
/>
<TooltipIcon
title={t('settings.modals.tun.tooltips.dnsMode')}
sx={{ opacity: 0.7 }}
/>
<Box sx={{ marginLeft: 'auto' }}>
<DnsModeSwitch
value={values.dnsMode}
onChange={(value) => {
setValues((v) => ({ ...v, dnsMode: value }))
}}
/>
</Box>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItemText primary={t('settings.modals.tun.fields.device')} />
<TextField
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "توجيه صارم",
"autoDetectInterface": "الكشف التلقائي عن الواجهة",
"dnsHijack": "اختطاف DNS",
"dnsMode": "وضع DNS",
"mtu": "وحدة الإرسال القصوى",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "Please use , to separate multiple DNS servers",
"dnsMode": "يحافظ Auto على السلوك القديم؛ يتطلب hijack تفعيل توجيه صارم على Windows أو Auto Redirect على Linux",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "Strenges Routing",
"autoDetectInterface": "Netzwerkschnittstelle automatisch auswählen",
"dnsHijack": "DNS-Hijacking",
"dnsMode": "DNS-Modus",
"mtu": "Maximale Übertragungseinheit",
"autoRedirect": "Automatische Umleitung"
},
"tooltips": {
"dnsHijack": "Mehrere DNS-Server durch Kommas trennen",
"dnsMode": "Auto behält das alte Verhalten bei; hijack erfordert Strenges Routing unter Windows oder Automatische Umleitung unter Linux",
"autoRedirect": "Richtet TCP-Umleitungen per nftables/iptables automatisch ein"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "Strict Route",
"autoDetectInterface": "Auto Detect Interface",
"dnsHijack": "DNS Hijack",
"dnsMode": "DNS Mode",
"mtu": "Max Transmission Unit",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "Please use , to separate multiple DNS servers",
"dnsMode": "Auto keeps the legacy behavior; hijack requires Strict Route on Windows or Auto Redirect on Linux",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "Enrutamiento estricto",
"autoDetectInterface": "Detectar automáticamente la interfaz de salida del tráfico",
"dnsHijack": "Secuestro de DNS",
"dnsMode": "Modo DNS",
"mtu": "Unidad máxima de transmisión",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "Please use , to separate multiple DNS servers",
"dnsMode": "Auto mantiene el comportamiento anterior; hijack requiere Enrutamiento estricto en Windows o Auto Redirect en Linux",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "مسیر دقیق",
"autoDetectInterface": "تشخیص خودکار رابط",
"dnsHijack": "ربایش DNS",
"dnsMode": "حالت DNS",
"mtu": "واحد حداکثر انتقال",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "لطفا برای جدا کردن چندین سرور DNS از , استفاده کنید.",
"dnsMode": "Auto رفتار قدیمی را حفظ می‌کند؛ hijack در ویندوز نیاز به مسیر دقیق یا در لینوکس نیاز به Auto Redirect دارد",
"autoRedirect": "پیکربندی خودکار ریدایرکت‌های TCP در nftables/iptables"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "Rute Ketat",
"autoDetectInterface": "Deteksi Antarmuka Otomatis",
"dnsHijack": "Pembajakan DNS",
"dnsMode": "Mode DNS",
"mtu": "Unit Transmisi Maksimum",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "Please use , to separate multiple DNS servers",
"dnsMode": "Auto mempertahankan perilaku lama; hijack memerlukan Rute Ketat di Windows atau Auto Redirect di Linux",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "厳格なルート",
"autoDetectInterface": "トラフィックの出口インターフェースを自動選択",
"dnsHijack": "DNSハイジャック",
"dnsMode": "DNSモード",
"mtu": "最大転送単位",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "Please use , to separate multiple DNS servers",
"dnsMode": "Auto は従来の動作を維持します。hijack には Windows で厳格なルートまたは Linux で Auto Redirect が必要です",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "엄격 라우팅",
"autoDetectInterface": "인터페이스 자동 감지",
"dnsHijack": "DNS 하이재킹",
"dnsMode": "DNS 모드",
"mtu": "MTU",
"autoRedirect": "Auto Redirect"
},
"tooltips": {
"dnsHijack": "여러 DNS 서버는 쉼표(,)로 구분하세요",
"dnsMode": "Auto는 기존 동작을 유지합니다. hijack은 Windows에서 엄격 라우팅 또는 Linux에서 Auto Redirect가 필요합니다",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "Rota Estrita",
"autoDetectInterface": "Detectar Interface Automaticamente",
"dnsHijack": "Sequestro de DNS",
"dnsMode": "Modo DNS",
"mtu": "Unidade Máxima de Transmissão",
"autoRedirect": "Redirecionamento Automático"
},
"tooltips": {
"dnsHijack": "Use , para separar vários servidores DNS",
"dnsMode": "Auto mantém o comportamento antigo; hijack requer Rota Estrita no Windows ou Redirecionamento Automático no Linux",
"autoRedirect": "Configura automaticamente os redirecionamentos TCP do nftables/iptables"
},
"messages": {
+2
View File
@@ -495,11 +495,13 @@
"strictRoute": "Строгая маршрутизация",
"autoDetectInterface": "Автоопределение интерфейса",
"dnsHijack": "Перехват DNS",
"dnsMode": "Режим DNS",
"mtu": "MTU",
"autoRedirect": "Автоматическое перенаправление"
},
"tooltips": {
"dnsHijack": "Используйте запятую для разделения нескольких DNS-серверов",
"dnsMode": "Auto сохраняет прежнее поведение; для hijack нужна Строгая маршрутизация в Windows или Автоматическое перенаправление в Linux",
"autoRedirect": "Автоматически настраивает перенаправление TCP через nftables/iptables"
},
"messages": {

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