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

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