Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
40b0d29997 | ||
|
5228e33d08 | ||
|
d30243e1a6 | ||
|
47f47b86f3 | ||
|
68645a42b2 | ||
|
dff17d77e8 | ||
|
d80618e1fe | ||
|
fbe751778b | ||
|
e34605d16f |
@ -5,9 +5,3 @@ charset = utf-8
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.rs]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
indent_size = 4
|
|
||||||
insert_final_newline = true
|
|
||||||
|
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Information**
|
|
||||||
- OS: [e.g. macOS]
|
|
||||||
- Clash Verge Version: [e.g. 1.3.4]
|
|
||||||
- Clash Core: [e.g. Clash or Clash Meta]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: "[Feature]"
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
93
.github/workflows/alpha.yml
vendored
@ -1,93 +0,0 @@
|
|||||||
name: Alpha CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
debug:
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest, ubuntu-20.04, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Delete current release assets
|
|
||||||
if: startsWith(matrix.os, 'ubuntu-')
|
|
||||||
uses: mknejp/delete-release-assets@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
tag: alpha
|
|
||||||
fail-if-no-assets: false
|
|
||||||
fail-if-no-release: false
|
|
||||||
assets: |
|
|
||||||
*.zip
|
|
||||||
*.gz
|
|
||||||
*.AppImage
|
|
||||||
*.deb
|
|
||||||
*.dmg
|
|
||||||
*.msi
|
|
||||||
*.sig
|
|
||||||
*.exe
|
|
||||||
|
|
||||||
- name: Install Dependencies (ubuntu only)
|
|
||||||
if: startsWith(matrix.os, 'ubuntu-')
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
|
||||||
|
|
||||||
- name: Yarn install and check
|
|
||||||
run: |
|
|
||||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
|
||||||
yarn run check --force
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
with:
|
|
||||||
tagName: alpha
|
|
||||||
releaseName: "Clash Verge Alpha"
|
|
||||||
releaseBody: "Alpha Version (include debug)"
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: true
|
|
||||||
includeDebug: ${{ github.event.inputs.debug }}
|
|
||||||
|
|
||||||
- name: Portable Bundle
|
|
||||||
if: startsWith(matrix.os, 'windows-')
|
|
||||||
run: |
|
|
||||||
yarn build
|
|
||||||
yarn run portable
|
|
||||||
env:
|
|
||||||
TAG_NAME: alpha
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
VITE_WIN_PORTABLE: 1
|
|
98
.github/workflows/ci.yml
vendored
@ -1,98 +0,0 @@
|
|||||||
name: Release CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v**
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Install Dependencies (ubuntu only)
|
|
||||||
if: startsWith(matrix.os, 'ubuntu-')
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
|
||||||
|
|
||||||
- name: Yarn install and check
|
|
||||||
run: |
|
|
||||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
|
||||||
yarn run check
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
# enable cache even though failed
|
|
||||||
# continue-on-error: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
with:
|
|
||||||
tagName: v__VERSION__
|
|
||||||
releaseName: "Clash Verge v__VERSION__"
|
|
||||||
releaseBody: "More new features are now supported."
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
- name: Portable Bundle
|
|
||||||
if: startsWith(matrix.os, 'windows-')
|
|
||||||
# rebuild with env settings
|
|
||||||
run: |
|
|
||||||
yarn build
|
|
||||||
yarn run portable
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
VITE_WIN_PORTABLE: 1
|
|
||||||
|
|
||||||
release-update:
|
|
||||||
needs: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
startsWith(github.repository, 'zzzgydi') &&
|
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Yarn install
|
|
||||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Release updater file
|
|
||||||
run: yarn run updater
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
102
.github/workflows/compatible.yml
vendored
@ -1,102 +0,0 @@
|
|||||||
name: Compatible CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# push:
|
|
||||||
# tags:
|
|
||||||
# - v**
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
targets:
|
|
||||||
- tag: macOS-10.15
|
|
||||||
os: macos-10.15
|
|
||||||
- tag: Ubuntu18
|
|
||||||
os: ubuntu-18.04
|
|
||||||
- tag: Ubuntu22
|
|
||||||
os: ubuntu-22.04
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.targets.os }}
|
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
# - name: Install Dependencies (ubuntu18 only)
|
|
||||||
# if: matrix.targets.os == 'ubuntu-18.04'
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev libayatana-appindicator3-dev
|
|
||||||
|
|
||||||
- name: Install Dependencies (ubuntu22 only)
|
|
||||||
if: startsWith(matrix.targets.os, 'ubuntu-')
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Get yarn cache dir path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Yarn Cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: yarn-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Yarn install and check
|
|
||||||
run: |
|
|
||||||
yarn install --network-timeout 1000000
|
|
||||||
yarn run check
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
with:
|
|
||||||
tagName: ${{ matrix.targets.tag }}
|
|
||||||
releaseName: "Compatible For ${{ matrix.targets.tag }}"
|
|
||||||
releaseBody: "More new features are now supported."
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
# - name: Portable Bundle
|
|
||||||
# if: matrix.os == 'windows-latest'
|
|
||||||
# # rebuild with env settings
|
|
||||||
# run: |
|
|
||||||
# yarn build
|
|
||||||
# yarn run portable
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
# TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
# VITE_WIN_PORTABLE: 1
|
|
107
.github/workflows/meta.yml
vendored
@ -1,107 +0,0 @@
|
|||||||
name: Meta CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v**
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: src-tauri
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
- name: Delete current release assets
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
uses: mknejp/delete-release-assets@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
tag: meta
|
|
||||||
fail-if-no-assets: false
|
|
||||||
fail-if-no-release: false
|
|
||||||
assets: |
|
|
||||||
*.zip
|
|
||||||
*.gz
|
|
||||||
*.AppImage
|
|
||||||
*.deb
|
|
||||||
*.dmg
|
|
||||||
*.msi
|
|
||||||
*.sig
|
|
||||||
|
|
||||||
- name: Install Dependencies (ubuntu only)
|
|
||||||
if: startsWith(matrix.os, 'ubuntu-')
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
|
||||||
|
|
||||||
- name: Get yarn cache dir path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Yarn Cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: yarn-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Yarn install and check
|
|
||||||
run: |
|
|
||||||
yarn install --network-timeout 1000000
|
|
||||||
yarn run check
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
with:
|
|
||||||
tagName: meta
|
|
||||||
releaseName: "Clash Verge Meta"
|
|
||||||
releaseBody: ""
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: true
|
|
||||||
args: -f default-meta
|
|
||||||
|
|
||||||
- name: Portable Bundle
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: |
|
|
||||||
yarn build -f default-meta
|
|
||||||
yarn run portable
|
|
||||||
env:
|
|
||||||
TAG_NAME: meta
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
VITE_WIN_PORTABLE: 1
|
|
106
.github/workflows/test.yml
vendored
@ -1,76 +1,54 @@
|
|||||||
name: Test CI
|
name: Test CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
branches: [test]
|
||||||
os:
|
|
||||||
description: "Runs on OS"
|
|
||||||
required: true
|
|
||||||
default: windows-latest
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- windows-latest
|
|
||||||
- ubuntu-latest
|
|
||||||
- macos-latest
|
|
||||||
- ubuntu-18.04
|
|
||||||
- ubuntu-20.04
|
|
||||||
- ubuntu-22.04
|
|
||||||
- macos-10.15
|
|
||||||
- macos-11
|
|
||||||
- macos-12
|
|
||||||
- windows-2019
|
|
||||||
- windows-2022
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
build-tauri:
|
||||||
runs-on: ${{ github.event.inputs.os }}
|
strategy:
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [windows-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: System Version
|
- uses: actions/checkout@v2
|
||||||
run: |
|
- name: setup node
|
||||||
echo ${{ github.event.inputs.os }}
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
- name: Checkout repository
|
node-version: 14
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: install Rust stable
|
- name: install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rs/toolchain@v1
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
toolchain: stable
|
||||||
|
- name: Get yarn cache directory path
|
||||||
- name: Install Node
|
id: yarn-cache-dir-path
|
||||||
uses: actions/setup-node@v4
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
cache: "yarn"
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
- name: Install Dependencies (ubuntu only)
|
${{ runner.os }}-yarn-
|
||||||
if: startsWith(github.event.inputs.os, 'ubuntu-')
|
- uses: actions/cache@v2
|
||||||
run: |
|
with:
|
||||||
sudo apt-get update
|
path: |
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
- name: Yarn install and check
|
~/.cargo/registry/cache/
|
||||||
run: |
|
~/.cargo/git/db/
|
||||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
src-tauri/target/
|
||||||
yarn run check
|
src-tauri/WixTools/
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Tauri build
|
- name: install app dependencies and build it
|
||||||
uses: tauri-apps/tauri-action@v0
|
run: yarn && yarn run predev
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
with:
|
with:
|
||||||
tagName: alpha
|
tagName: clash-verge-v__VERSION__
|
||||||
releaseName: "Clash Verge Alpha"
|
releaseName: "Clash Verge v__VERSION__"
|
||||||
releaseBody: "Alpha Version (include debug)"
|
releaseBody: "This is a test release."
|
||||||
releaseDraft: false
|
releaseDraft: true
|
||||||
includeUpdaterJson: false
|
prerelease: false
|
||||||
|
25
.github/workflows/updater.yml
vendored
@ -1,25 +0,0 @@
|
|||||||
name: Updater CI
|
|
||||||
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Yarn install
|
|
||||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Release updater file
|
|
||||||
run: yarn run updater
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
5
.gitignore
vendored
@ -3,6 +3,5 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
update.json
|
package-lock.json
|
||||||
scripts/_env.sh
|
yarn.lock
|
||||||
.vscode
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
yarn pretty-quick --staged
|
|
159
README.md
@ -1,158 +1,5 @@
|
|||||||
<h1 align="center">
|
# Clash Verge
|
||||||
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
|
||||||
<br>
|
|
||||||
Clash Verge
|
|
||||||
<br>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h3 align="center">
|
work in progress...
|
||||||
A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="https://github.com/tauri-apps/tauri">tauri</a>.
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
## Features
|
## Todo
|
||||||
|
|
||||||
- Full `clash` config supported, Partial `clash premium` config supported.
|
|
||||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
|
|
||||||
- Simple UI and supports custom theme color.
|
|
||||||
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
|
|
||||||
- System proxy setting and guard.
|
|
||||||
|
|
||||||
## Promotion
|
|
||||||
|
|
||||||
[狗狗加速 —— 技术流机场 Doggygo VPN](https://dg1.top)
|
|
||||||
|
|
||||||
- High-performance overseas VPN, free trial, discounted packages, unlock streaming media, the world's first to support Hysteria protocol.
|
|
||||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
|
||||||
- 使用 Clash Verge 专属邀请链接注册送 15 天,每天 1G 流量免费试用:https://panel.dg1.top/#/register?code=sFCDayZf
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Promotion Detail</summary>
|
|
||||||
|
|
||||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
|
||||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
|
||||||
- 海外团队,无跑路风险,高达 50% 返佣
|
|
||||||
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
|
||||||
- 全球首家 Hysteria 协议机场,将在今年 10 月上线更快的 `tuic` 协议(Clash Verge 客户端最佳搭配)
|
|
||||||
- 解锁流媒体及 ChatGPT
|
|
||||||
- 官网:https://dg1.top
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
[EEVPN —— 海外运营机场 ※ 支持 ChatGPT](https://www.eejsq.net/#/register?code=yRr6qBO3)
|
|
||||||
|
|
||||||
- 年付低至 9.99 元,价格低,速度不减
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Promotion Detail</summary>
|
|
||||||
|
|
||||||
- 中国大陆 BGP 网络接入
|
|
||||||
- IEPL 专线网络
|
|
||||||
- 最高 2500Mbps 速率可用
|
|
||||||
- 不限制在线客户端
|
|
||||||
- 解锁流媒体及 ChatGPT
|
|
||||||
- 海外运营 数据安全
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
|
||||||
|
|
||||||
- [Windows x64](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64_en-US.msi)
|
|
||||||
- [macOS intel](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64.dmg)
|
|
||||||
- [macOS arm](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_aarch64.dmg)
|
|
||||||
- [Linux AppImage](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.AppImage)
|
|
||||||
- [Linux deb](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.deb)
|
|
||||||
- [Fedora Linux](https://github.com/zzzgydi/clash-verge/issues/352)
|
|
||||||
|
|
||||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
|
||||||
|
|
||||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
|
||||||
|
|
||||||
### FAQ
|
|
||||||
|
|
||||||
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
|
||||||
|
|
||||||
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# force update to latest version
|
|
||||||
# yarn run check --force
|
|
||||||
|
|
||||||
yarn run check
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# run it in another way if app instance exists
|
|
||||||
yarn dev:diff
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can build it
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Todos
|
|
||||||
|
|
||||||
> This keng is a little big...
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="./docs/demo1.png" alt="demo1" width="32%" />
|
|
||||||
<img src="./docs/demo2.png" alt="demo2" width="32%" />
|
|
||||||
<img src="./docs/demo3.png" alt="demo3" width="32%" />
|
|
||||||
<img src="./docs/demo4.png" alt="demo4" width="32%" />
|
|
||||||
<img src="./docs/demo5.png" alt="demo5" width="32%" />
|
|
||||||
<img src="./docs/demo6.png" alt="demo6" width="32%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Custom Theme
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="./docs/color1.png" alt="demo1" width="16%" />
|
|
||||||
<img src="./docs/color2.png" alt="demo2" width="16%" />
|
|
||||||
<img src="./docs/color3.png" alt="demo3" width="16%" />
|
|
||||||
<img src="./docs/color4.png" alt="demo4" width="16%" />
|
|
||||||
<img src="./docs/color5.png" alt="demo5" width="16%" />
|
|
||||||
<img src="./docs/color6.png" alt="demo6" width="16%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
This is a learning project for Rust practice.
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
Issue and PR welcome!
|
|
||||||
|
|
||||||
## Acknowledgement
|
|
||||||
|
|
||||||
Clash Verge was based on or inspired by these projects and so on:
|
|
||||||
|
|
||||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
|
|
||||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
|
|
||||||
- [MetaCubeX/Clash.Meta](https://github.com/MetaCubeX/Clash.Meta): A rule-based tunnel in Go.
|
|
||||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
|
|
||||||
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
GPL-3.0 License. See [License here](./LICENSE) for details.
|
|
||||||
|
472
UPDATELOG.md
@ -1,472 +0,0 @@
|
|||||||
## v1.3.8
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash meta core
|
|
||||||
- add default valid keys
|
|
||||||
- adjust the delay display interval and color
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix connections page undefined exception
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.7
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash meta core
|
|
||||||
- profiles page add paste button
|
|
||||||
- subscriptions url textfield use multi lines
|
|
||||||
- set min window size
|
|
||||||
- add check for updates buttons
|
|
||||||
- add open dashboard to the hotkey list
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix profiles page undefined exception
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.6
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- add russian translation
|
|
||||||
- support to show connection detail
|
|
||||||
- support clash meta memory usage display
|
|
||||||
- support proxy provider update ui
|
|
||||||
- update geo data file from meta repo
|
|
||||||
- adjust setting page
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- center the window when it is out of screen
|
|
||||||
- use `sudo` when `pkexec` not found (Linux)
|
|
||||||
- reconnect websocket when window focus
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.5
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash core
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix blurry system tray icon (Windows)
|
|
||||||
- fix v1.3.4 wintun.dll not found (Windows)
|
|
||||||
- fix v1.3.4 clash core not found (macOS, Linux)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.4
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash meta core
|
|
||||||
- optimize traffic graph high CPU usage when window hidden
|
|
||||||
- use polkit to elevate permission (Linux)
|
|
||||||
- support app log level setting
|
|
||||||
- support copy environment variable
|
|
||||||
- overwrite resource file according to file modified
|
|
||||||
- save window size and position
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- remove fallback group select status
|
|
||||||
- enable context menu on editable element (Windows)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.3
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash meta core
|
|
||||||
- show tray icon variants in different system proxy status (Windows)
|
|
||||||
- close all connections when mode changed
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- encode controller secret into uri
|
|
||||||
- error boundary for each page
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.2
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash meta core
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix import url issue
|
|
||||||
- fix profile undefined issue
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.1
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash meta core
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix open url issue
|
|
||||||
- fix appimage path panic
|
|
||||||
- fix grant root permission in macOS
|
|
||||||
- fix linux system proxy default bypass
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3.0
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash meta
|
|
||||||
- support opening dir on tray
|
|
||||||
- support updating all profiles with one click
|
|
||||||
- support granting root permission to clash core(Linux, macOS)
|
|
||||||
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- deb add openssl depend(Linux)
|
|
||||||
- fix the AppImage auto launch path(Linux)
|
|
||||||
- fix get the default network service(macOS)
|
|
||||||
- remove the esc key listener in macOS, cmd+w instead(macOS)
|
|
||||||
- fix infinite retry when websocket error
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.2.3
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash
|
|
||||||
- adjust macOS window style
|
|
||||||
- profile supports UTF8 with BOM
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix selected proxy
|
|
||||||
- fix error log
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.2.2
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash meta
|
|
||||||
- recover clash core after panic
|
|
||||||
- use system window decorations(Linux)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- flush system proxy settings(Windows)
|
|
||||||
- fix parse log panic
|
|
||||||
- fix ui bug
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.2.1
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash version
|
|
||||||
- proxy groups support multi columns
|
|
||||||
- optimize ui
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix ui websocket connection
|
|
||||||
- adjust delay check concurrency
|
|
||||||
- avoid setting login item repeatedly(macOS)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.2.0
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash meta version
|
|
||||||
- support to change external-controller
|
|
||||||
- support to change default latency test URL
|
|
||||||
- close all connections when proxy changed or profile changed
|
|
||||||
- check the config by using the core
|
|
||||||
- increase the robustness of the program
|
|
||||||
- optimize windows service mode (need to reinstall)
|
|
||||||
- optimize ui
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- invalid hotkey cause panic
|
|
||||||
- invalid theme setting cause panic
|
|
||||||
- fix some other glitches
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.1.2
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- the system tray follows i18n
|
|
||||||
- change the proxy group ui of global mode
|
|
||||||
- support to update profile with the system proxy/clash proxy
|
|
||||||
- check the remote profile more strictly
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- use app version as default user agent
|
|
||||||
- the clash not exit in service mode
|
|
||||||
- reset the system proxy when quit the app
|
|
||||||
- fix some other glitches
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.1.1
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- optimize clash config feedback
|
|
||||||
- hide macOS dock icon
|
|
||||||
- use clash meta compatible version (Linux)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix some other glitches
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.1.0
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- add rule page
|
|
||||||
- supports proxy providers delay check
|
|
||||||
- add proxy delay check loading status
|
|
||||||
- supports hotkey/shortcut management
|
|
||||||
- supports displaying connections data in table layout(refer to yacd)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- supports yaml merge key in clash config
|
|
||||||
- detect the network interface and set the system proxy(macOS)
|
|
||||||
- fix some other glitches
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.6
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash and clash.meta
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- only script profile display console
|
|
||||||
- automatic configuration update on demand at launch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.5
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- reimplement profile enhanced mode with quick-js
|
|
||||||
- optimize the runtime config generation process
|
|
||||||
- support web ui management
|
|
||||||
- support clash field management
|
|
||||||
- support viewing the runtime config
|
|
||||||
- adjust some pages style
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix silent start
|
|
||||||
- fix incorrectly reset system proxy on exit
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.4
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash core and clash meta version
|
|
||||||
- support switch clash mode on system tray
|
|
||||||
- theme mode support follows system
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- config load error on first use
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.3
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- save some states such as URL test, filter, etc
|
|
||||||
- update clash core and clash-meta core
|
|
||||||
- new icon for macOS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.2
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- supports for switching clash core
|
|
||||||
- supports release UI processes
|
|
||||||
- supports script mode setting
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix service mode bug (Windows)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.1
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- adjust default theme settings
|
|
||||||
- reduce gpu usage of traffic graph when hidden
|
|
||||||
- supports more remote profile response header setting
|
|
||||||
- check remote profile data format when imported
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- service mode install and start issue (Windows)
|
|
||||||
- fix launch panic (Some Windows)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.0
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash core
|
|
||||||
- optimize traffic graph animation
|
|
||||||
- supports interval update profiles
|
|
||||||
- supports service mode (Windows)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- reset system proxy when exit from dock (macOS)
|
|
||||||
- adjust clash dns config process strategy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.29
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- sort proxy node
|
|
||||||
- custom proxy test url
|
|
||||||
- logs page filter
|
|
||||||
- connections page filter
|
|
||||||
- default user agent for subscription
|
|
||||||
- system tray add tun mode toggle
|
|
||||||
- enable to change the config dir (Windows only)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.28
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- enable to use clash config fields (UI)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- remove the character
|
|
||||||
- fix some icon color
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.27
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- supports custom theme color
|
|
||||||
- tun mode setting control the final config
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix transition flickers (macOS)
|
|
||||||
- reduce proxy page render
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.26
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- silent start
|
|
||||||
- profile editor
|
|
||||||
- profile enhance mode supports more fields
|
|
||||||
- optimize profile enhance mode strategy
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- fix csp restriction on macOS
|
|
||||||
- window controllers on Linux
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.25
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- update clash core version
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- app updater error
|
|
||||||
- display window controllers on Linux
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
If you can't update the app properly, please consider downloading the latest version from github release.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.24
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Connections page
|
|
||||||
- add wintun.dll (Windows)
|
|
||||||
- supports create local profile with selected file (Windows)
|
|
||||||
- system tray enable set system proxy
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- open dir error
|
|
||||||
- auto launch path (Windows)
|
|
||||||
- fix some clash config error
|
|
||||||
- reduce the impact of the enhanced mode
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v0.0.23
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- i18n supports
|
|
||||||
- Remote profile User Agent supports
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- clash config file case ignore
|
|
||||||
- clash `external-controller` only port
|
|
BIN
docs/color1.png
Before Width: | Height: | Size: 29 KiB |
BIN
docs/color2.png
Before Width: | Height: | Size: 30 KiB |
BIN
docs/color3.png
Before Width: | Height: | Size: 29 KiB |
BIN
docs/color4.png
Before Width: | Height: | Size: 29 KiB |
BIN
docs/color5.png
Before Width: | Height: | Size: 32 KiB |
BIN
docs/color6.png
Before Width: | Height: | Size: 31 KiB |
BIN
docs/demo1.png
Before Width: | Height: | Size: 21 KiB |
BIN
docs/demo2.png
Before Width: | Height: | Size: 30 KiB |
BIN
docs/demo3.png
Before Width: | Height: | Size: 28 KiB |
BIN
docs/demo4.png
Before Width: | Height: | Size: 24 KiB |
BIN
docs/demo5.png
Before Width: | Height: | Size: 27 KiB |
BIN
docs/demo6.png
Before Width: | Height: | Size: 28 KiB |
83
package.json
@ -1,74 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "1.3.8",
|
"version": "0.0.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
|
||||||
"dev:diff": "tauri dev -f verge-dev",
|
|
||||||
"build": "tauri build",
|
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
|
"dev": "cargo tauri dev",
|
||||||
|
"build": "cargo tauri build",
|
||||||
"web:dev": "vite",
|
"web:dev": "vite",
|
||||||
"web:build": "tsc && vite build",
|
"web:build": "tsc && vite build",
|
||||||
"web:serve": "vite preview",
|
"web:serve": "vite preview",
|
||||||
"aarch": "node scripts/aarch.mjs",
|
"predev": "node scripts/pre-dev.mjs"
|
||||||
"check": "node scripts/check.mjs",
|
|
||||||
"updater": "node scripts/updater.mjs",
|
|
||||||
"publish": "node scripts/publish.mjs",
|
|
||||||
"portable": "node scripts/portable.mjs",
|
|
||||||
"prepare": "husky install"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.7.0",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.6.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@mui/icons-material": "^5.2.1",
|
||||||
"@mui/icons-material": "^5.10.9",
|
"@mui/material": "^5.2.3",
|
||||||
"@mui/material": "^5.10.13",
|
"@tauri-apps/api": "^1.0.0-beta.8",
|
||||||
"@mui/x-data-grid": "^5.17.11",
|
"axios": "^0.24.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"dayjs": "^1.10.7",
|
||||||
"ahooks": "^3.7.2",
|
"react": "^17.0.0",
|
||||||
"axios": "^1.1.3",
|
"react-dom": "^17.0.0",
|
||||||
"dayjs": "1.11.5",
|
"react-router-dom": "^6.0.2",
|
||||||
"i18next": "^22.0.4",
|
"react-virtuoso": "^2.3.1",
|
||||||
"lodash-es": "^4.17.21",
|
"recoil": "^0.5.2",
|
||||||
"monaco-editor": "^0.34.1",
|
"swr": "^1.1.2-beta.0"
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-error-boundary": "^3.1.4",
|
|
||||||
"react-hook-form": "^7.39.5",
|
|
||||||
"react-i18next": "^12.0.0",
|
|
||||||
"react-router-dom": "^6.4.3",
|
|
||||||
"react-virtuoso": "^3.1.3",
|
|
||||||
"recoil": "^0.7.6",
|
|
||||||
"snarkdown": "^2.0.0",
|
|
||||||
"swr": "^1.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^5.0.3",
|
"@tauri-apps/cli": "^1.0.0-beta.10",
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@types/react": "^17.0.0",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/js-cookie": "^3.0.2",
|
"@vitejs/plugin-react": "^1.1.1",
|
||||||
"@types/lodash": "^4.14.180",
|
|
||||||
"@types/lodash-es": "^4.17.7",
|
|
||||||
"@types/react-dom": "^18.0.11",
|
|
||||||
"@vitejs/plugin-react": "^2.0.1",
|
|
||||||
"adm-zip": "^0.5.9",
|
"adm-zip": "^0.5.9",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"https-proxy-agent": "^5.0.1",
|
"node-fetch": "^3.1.0",
|
||||||
"husky": "^7.0.0",
|
"sass": "^1.44.0",
|
||||||
"node-fetch": "^3.2.6",
|
"typescript": "^4.5.2",
|
||||||
"prettier": "^2.7.1",
|
"vite": "^2.7.1"
|
||||||
"pretty-quick": "^3.1.3",
|
|
||||||
"sass": "^1.54.0",
|
|
||||||
"typescript": "^4.7.4",
|
|
||||||
"vite": "^3.2.5",
|
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
|
||||||
"vite-plugin-svgr": "^2.2.1"
|
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"endOfLine": "lf"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
/**
|
|
||||||
* Build and upload assets
|
|
||||||
* for macOS(aarch)
|
|
||||||
*/
|
|
||||||
import fs from "fs-extra";
|
|
||||||
import path from "path";
|
|
||||||
import { exit } from "process";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
|
||||||
|
|
||||||
// to `meta` tag
|
|
||||||
const META = process.argv.includes("--meta");
|
|
||||||
// to `alpha` tag
|
|
||||||
const ALPHA = process.argv.includes("--alpha");
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
async function resolve() {
|
|
||||||
if (!process.env.GITHUB_TOKEN) {
|
|
||||||
throw new Error("GITHUB_TOKEN is required");
|
|
||||||
}
|
|
||||||
if (!process.env.GITHUB_REPOSITORY) {
|
|
||||||
throw new Error("GITHUB_REPOSITORY is required");
|
|
||||||
}
|
|
||||||
if (!process.env.TAURI_PRIVATE_KEY) {
|
|
||||||
throw new Error("TAURI_PRIVATE_KEY is required");
|
|
||||||
}
|
|
||||||
if (!process.env.TAURI_KEY_PASSWORD) {
|
|
||||||
throw new Error("TAURI_KEY_PASSWORD is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { version } = require("../package.json");
|
|
||||||
|
|
||||||
const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`;
|
|
||||||
const buildCmd = META ? `yarn build -f default-meta` : `yarn build`;
|
|
||||||
|
|
||||||
console.log(`[INFO]: Upload to tag "${tag}"`);
|
|
||||||
console.log(`[INFO]: Building app. "${buildCmd}"`);
|
|
||||||
|
|
||||||
execSync(buildCmd);
|
|
||||||
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
|
|
||||||
const join = (p) => path.join(bundlePath, p);
|
|
||||||
|
|
||||||
const appPathList = [
|
|
||||||
join("macos/Clash Verge.aarch64.app.tar.gz"),
|
|
||||||
join("macos/Clash Verge.aarch64.app.tar.gz.sig"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const appPath of appPathList) {
|
|
||||||
if (fs.pathExistsSync(appPath)) {
|
|
||||||
fs.removeSync(appPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz"), appPathList[0]);
|
|
||||||
fs.copyFileSync(join("macos/Clash Verge.app.tar.gz.sig"), appPathList[1]);
|
|
||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
|
||||||
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
|
||||||
...options,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!release.id) throw new Error("failed to find the release");
|
|
||||||
|
|
||||||
await uploadAssets(release.id, [
|
|
||||||
join(`dmg/Clash Verge_${version}_aarch64.dmg`),
|
|
||||||
...appPathList,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// From tauri-apps/tauri-action
|
|
||||||
// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts
|
|
||||||
async function uploadAssets(releaseId, assets) {
|
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
|
||||||
|
|
||||||
// Determine content-length for header to upload asset
|
|
||||||
const contentLength = (filePath) => fs.statSync(filePath).size;
|
|
||||||
|
|
||||||
for (const assetPath of assets) {
|
|
||||||
const headers = {
|
|
||||||
"content-type": "application/zip",
|
|
||||||
"content-length": contentLength(assetPath),
|
|
||||||
};
|
|
||||||
|
|
||||||
const ext = path.extname(assetPath);
|
|
||||||
const filename = path.basename(assetPath).replace(ext, "");
|
|
||||||
const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`)
|
|
||||||
? `${filename}-debug${ext}`
|
|
||||||
: `${filename}${ext}`;
|
|
||||||
|
|
||||||
console.log(`[INFO]: Uploading ${assetName}...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
|
||||||
headers,
|
|
||||||
name: assetName,
|
|
||||||
data: fs.readFileSync(assetPath),
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
release_id: releaseId,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "darwin" && process.arch === "arm64") {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
console.error("invalid");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
@ -1,332 +0,0 @@
|
|||||||
import fs from "fs-extra";
|
|
||||||
import zlib from "zlib";
|
|
||||||
import path from "path";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
import proxyAgent from "https-proxy-agent";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
|
||||||
const FORCE = process.argv.includes("--force");
|
|
||||||
|
|
||||||
const SIDECAR_HOST = execSync("rustc -vV")
|
|
||||||
.toString()
|
|
||||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
|
||||||
|
|
||||||
/* ======= clash ======= */
|
|
||||||
const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/";
|
|
||||||
const CLASH_URL_PREFIX =
|
|
||||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
|
||||||
const CLASH_LATEST_DATE = "latest";
|
|
||||||
|
|
||||||
const CLASH_MAP = {
|
|
||||||
"win32-x64": "clash-windows-amd64",
|
|
||||||
"darwin-x64": "clash-darwin-amd64",
|
|
||||||
"darwin-arm64": "clash-darwin-arm64",
|
|
||||||
"linux-x64": "clash-linux-amd64",
|
|
||||||
"linux-arm64": "clash-linux-arm64",
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ======= clash meta ======= */
|
|
||||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
|
|
||||||
const META_VERSION = "v1.16.0";
|
|
||||||
|
|
||||||
const META_MAP = {
|
|
||||||
"win32-x64": "clash.meta-windows-amd64-compatible",
|
|
||||||
"darwin-x64": "clash.meta-darwin-amd64",
|
|
||||||
"darwin-arm64": "clash.meta-darwin-arm64",
|
|
||||||
"linux-x64": "clash.meta-linux-amd64-compatible",
|
|
||||||
"linux-arm64": "clash.meta-linux-arm64",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check available
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { platform, arch } = process;
|
|
||||||
if (!CLASH_MAP[`${platform}-${arch}`]) {
|
|
||||||
throw new Error(`clash unsupported platform "${platform}-${arch}"`);
|
|
||||||
}
|
|
||||||
if (!META_MAP[`${platform}-${arch}`]) {
|
|
||||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clash() {
|
|
||||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
|
||||||
|
|
||||||
const isWin = platform === "win32";
|
|
||||||
const urlExt = isWin ? "zip" : "gz";
|
|
||||||
const downloadURL = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
|
||||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
|
||||||
const zipFile = `${name}.${urlExt}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "clash",
|
|
||||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
|
||||||
exeFile,
|
|
||||||
zipFile,
|
|
||||||
downloadURL,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function clashS3() {
|
|
||||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
|
||||||
|
|
||||||
const isWin = platform === "win32";
|
|
||||||
const urlExt = isWin ? "zip" : "gz";
|
|
||||||
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
|
||||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
|
||||||
const zipFile = `${name}.${urlExt}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "clash",
|
|
||||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
|
||||||
exeFile,
|
|
||||||
zipFile,
|
|
||||||
downloadURL,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function clashMeta() {
|
|
||||||
const name = META_MAP[`${platform}-${arch}`];
|
|
||||||
const isWin = platform === "win32";
|
|
||||||
const urlExt = isWin ? "zip" : "gz";
|
|
||||||
const downloadURL = `${META_URL_PREFIX}${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
|
||||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
|
||||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "clash-meta",
|
|
||||||
targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
|
||||||
exeFile,
|
|
||||||
zipFile,
|
|
||||||
downloadURL,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* download sidecar and rename
|
|
||||||
*/
|
|
||||||
async function resolveSidecar(binInfo) {
|
|
||||||
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
|
||||||
|
|
||||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
|
||||||
const sidecarPath = path.join(sidecarDir, targetFile);
|
|
||||||
|
|
||||||
await fs.mkdirp(sidecarDir);
|
|
||||||
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
|
|
||||||
|
|
||||||
const tempDir = path.join(TEMP_DIR, name);
|
|
||||||
const tempZip = path.join(tempDir, zipFile);
|
|
||||||
const tempExe = path.join(tempDir, exeFile);
|
|
||||||
|
|
||||||
await fs.mkdirp(tempDir);
|
|
||||||
try {
|
|
||||||
if (!(await fs.pathExists(tempZip))) {
|
|
||||||
await downloadFile(downloadURL, tempZip);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zipFile.endsWith(".zip")) {
|
|
||||||
const zip = new AdmZip(tempZip);
|
|
||||||
zip.getEntries().forEach((entry) => {
|
|
||||||
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
|
|
||||||
});
|
|
||||||
zip.extractAllTo(tempDir, true);
|
|
||||||
await fs.rename(tempExe, sidecarPath);
|
|
||||||
console.log(`[INFO]: "${name}" unzip finished`);
|
|
||||||
} else {
|
|
||||||
// gz
|
|
||||||
const readStream = fs.createReadStream(tempZip);
|
|
||||||
const writeStream = fs.createWriteStream(sidecarPath);
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const onError = (error) => {
|
|
||||||
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
readStream
|
|
||||||
.pipe(zlib.createGunzip().on("error", onError))
|
|
||||||
.pipe(writeStream)
|
|
||||||
.on("finish", () => {
|
|
||||||
console.log(`[INFO]: "${name}" gunzip finished`);
|
|
||||||
execSync(`chmod 755 ${sidecarPath}`);
|
|
||||||
console.log(`[INFO]: "${name}" chmod binary finished`);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on("error", onError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// 需要删除文件
|
|
||||||
await fs.remove(sidecarPath);
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
// delete temp dir
|
|
||||||
await fs.remove(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* prepare clash core
|
|
||||||
* if the core version is not updated in time, use S3 storage as a backup.
|
|
||||||
*/
|
|
||||||
async function resolveClash() {
|
|
||||||
try {
|
|
||||||
return await resolveSidecar(clash());
|
|
||||||
} catch {
|
|
||||||
console.log(`[WARN]: clash core needs to be updated`);
|
|
||||||
return await resolveSidecar(clashS3());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* only Windows
|
|
||||||
* get the wintun.dll (not required)
|
|
||||||
*/
|
|
||||||
async function resolveWintun() {
|
|
||||||
const { platform } = process;
|
|
||||||
|
|
||||||
if (platform !== "win32") return;
|
|
||||||
|
|
||||||
const url = "https://www.wintun.net/builds/wintun-0.14.1.zip";
|
|
||||||
|
|
||||||
const tempDir = path.join(TEMP_DIR, "wintun");
|
|
||||||
const tempZip = path.join(tempDir, "wintun.zip");
|
|
||||||
|
|
||||||
const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll");
|
|
||||||
const targetPath = path.join(cwd, "src-tauri/resources", "wintun.dll");
|
|
||||||
|
|
||||||
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
|
||||||
|
|
||||||
await fs.mkdirp(tempDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(tempZip))) {
|
|
||||||
await downloadFile(url, tempZip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// unzip
|
|
||||||
const zip = new AdmZip(tempZip);
|
|
||||||
zip.extractAllTo(tempDir, true);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(wintunPath))) {
|
|
||||||
throw new Error(`path not found "${wintunPath}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.rename(wintunPath, targetPath);
|
|
||||||
await fs.remove(tempDir);
|
|
||||||
|
|
||||||
console.log(`[INFO]: resolve wintun.dll finished`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* download the file to the resources dir
|
|
||||||
*/
|
|
||||||
async function resolveResource(binInfo) {
|
|
||||||
const { file, downloadURL } = binInfo;
|
|
||||||
|
|
||||||
const resDir = path.join(cwd, "src-tauri/resources");
|
|
||||||
const targetPath = path.join(resDir, file);
|
|
||||||
|
|
||||||
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
|
||||||
|
|
||||||
await fs.mkdirp(resDir);
|
|
||||||
await downloadFile(downloadURL, targetPath);
|
|
||||||
|
|
||||||
console.log(`[INFO]: ${file} finished`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* download file and save to `path`
|
|
||||||
*/
|
|
||||||
async function downloadFile(url, path) {
|
|
||||||
const options = {};
|
|
||||||
|
|
||||||
const httpProxy =
|
|
||||||
process.env.HTTP_PROXY ||
|
|
||||||
process.env.http_proxy ||
|
|
||||||
process.env.HTTPS_PROXY ||
|
|
||||||
process.env.https_proxy;
|
|
||||||
|
|
||||||
if (httpProxy) {
|
|
||||||
options.agent = proxyAgent(httpProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
method: "GET",
|
|
||||||
headers: { "Content-Type": "application/octet-stream" },
|
|
||||||
});
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
await fs.writeFile(path, new Uint8Array(buffer));
|
|
||||||
|
|
||||||
console.log(`[INFO]: download finished "${url}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main
|
|
||||||
*/
|
|
||||||
const SERVICE_URL =
|
|
||||||
"https://github.com/zzzgydi/clash-verge-service/releases/download/latest";
|
|
||||||
|
|
||||||
const resolveService = () =>
|
|
||||||
resolveResource({
|
|
||||||
file: "clash-verge-service.exe",
|
|
||||||
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
|
|
||||||
});
|
|
||||||
const resolveInstall = () =>
|
|
||||||
resolveResource({
|
|
||||||
file: "install-service.exe",
|
|
||||||
downloadURL: `${SERVICE_URL}/install-service.exe`,
|
|
||||||
});
|
|
||||||
const resolveUninstall = () =>
|
|
||||||
resolveResource({
|
|
||||||
file: "uninstall-service.exe",
|
|
||||||
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
|
|
||||||
});
|
|
||||||
const resolveMmdb = () =>
|
|
||||||
resolveResource({
|
|
||||||
file: "Country.mmdb",
|
|
||||||
downloadURL: `https://github.com/Dreamacro/maxmind-geoip/releases/download/20230812/Country.mmdb`,
|
|
||||||
});
|
|
||||||
const resolveGeosite = () =>
|
|
||||||
resolveResource({
|
|
||||||
file: "geosite.dat",
|
|
||||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
|
|
||||||
});
|
|
||||||
const resolveGeoIP = () =>
|
|
||||||
resolveResource({
|
|
||||||
file: "geoip.dat",
|
|
||||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tasks = [
|
|
||||||
{ name: "clash", func: () => resolveSidecar(clashS3()), retry: 5 },
|
|
||||||
{ name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 },
|
|
||||||
{ name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
|
|
||||||
{ name: "service", func: resolveService, retry: 5, winOnly: true },
|
|
||||||
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
|
||||||
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
|
||||||
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
|
||||||
{ name: "geosite", func: resolveGeosite, retry: 5 },
|
|
||||||
{ name: "geoip", func: resolveGeoIP, retry: 5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
async function runTask() {
|
|
||||||
const task = tasks.shift();
|
|
||||||
if (!task) return;
|
|
||||||
if (task.winOnly && process.platform !== "win32") return runTask();
|
|
||||||
|
|
||||||
for (let i = 0; i < task.retry; i++) {
|
|
||||||
try {
|
|
||||||
await task.func();
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
|
|
||||||
if (i === task.retry - 1) throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
runTask();
|
|
||||||
runTask();
|
|
@ -1,59 +0,0 @@
|
|||||||
import fs from "fs-extra";
|
|
||||||
import path from "path";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
|
||||||
|
|
||||||
/// Script for ci
|
|
||||||
/// 打包绿色版/便携版 (only Windows)
|
|
||||||
async function resolvePortable() {
|
|
||||||
if (process.platform !== "win32") return;
|
|
||||||
|
|
||||||
const releaseDir = "./src-tauri/target/release";
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(releaseDir))) {
|
|
||||||
throw new Error("could not found the release dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
const zip = new AdmZip();
|
|
||||||
|
|
||||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
|
||||||
zip.addLocalFile(path.join(releaseDir, "clash.exe"));
|
|
||||||
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
|
|
||||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const packageJson = require("../package.json");
|
|
||||||
const { version } = packageJson;
|
|
||||||
|
|
||||||
const zipFile = `Clash.Verge_${version}_x64_portable.zip`;
|
|
||||||
zip.writeZip(zipFile);
|
|
||||||
|
|
||||||
console.log("[INFO]: create portable zip successfully");
|
|
||||||
|
|
||||||
// push release assets
|
|
||||||
if (process.env.GITHUB_TOKEN === undefined) {
|
|
||||||
throw new Error("GITHUB_TOKEN is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
|
||||||
|
|
||||||
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
|
|
||||||
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
|
||||||
...options,
|
|
||||||
tag: process.env.TAG_NAME || `v${version}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(release.name);
|
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
|
||||||
...options,
|
|
||||||
release_id: release.id,
|
|
||||||
name: zipFile,
|
|
||||||
data: zip.toBuffer(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvePortable().catch(console.error);
|
|
104
scripts/pre-dev.mjs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import fs from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const CLASH_URL_PREFIX =
|
||||||
|
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||||
|
const CLASH_LATEST_DATE = "2021.12.07";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the correct clash release infomation
|
||||||
|
*/
|
||||||
|
function resolveClash() {
|
||||||
|
const { platform, arch } = process;
|
||||||
|
|
||||||
|
let name = "";
|
||||||
|
|
||||||
|
// todo
|
||||||
|
if (platform === "win32" && arch === "x64") {
|
||||||
|
name = `clash-windows-386`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
throw new Error("todo");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWin = platform === "win32";
|
||||||
|
const zip = isWin ? "zip" : "gz";
|
||||||
|
const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`;
|
||||||
|
const exefile = `${name}${isWin ? ".exe" : ""}`;
|
||||||
|
const zipfile = `${name}.${zip}`;
|
||||||
|
|
||||||
|
return { url, zip, exefile, zipfile };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the sidecar bin
|
||||||
|
*/
|
||||||
|
async function resolveSidecar() {
|
||||||
|
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||||
|
|
||||||
|
const host = execSync("rustc -vV | grep host").toString().slice(6).trim();
|
||||||
|
const ext = process.platform === "win32" ? ".exe" : "";
|
||||||
|
const sidecarFile = `clash-${host}${ext}`;
|
||||||
|
const sidecarPath = path.join(sidecarDir, sidecarFile);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(sidecarDir))) await fs.mkdir(sidecarDir);
|
||||||
|
if (await fs.pathExists(sidecarPath)) return;
|
||||||
|
|
||||||
|
// download sidecar
|
||||||
|
const binInfo = resolveClash();
|
||||||
|
const tempDir = path.join(cwd, "pre-dev-temp");
|
||||||
|
const tempZip = path.join(tempDir, binInfo.zipfile);
|
||||||
|
const tempExe = path.join(tempDir, binInfo.exefile);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(tempDir))) await fs.mkdir(tempDir);
|
||||||
|
if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip);
|
||||||
|
|
||||||
|
// Todo: support gz
|
||||||
|
const zip = new AdmZip(tempZip);
|
||||||
|
zip.getEntries().forEach((entry) => {
|
||||||
|
console.log("[INFO]: entry name", entry.entryName);
|
||||||
|
});
|
||||||
|
zip.extractAllTo(tempDir, true);
|
||||||
|
|
||||||
|
// save as sidecar
|
||||||
|
await fs.rename(tempExe, sidecarPath);
|
||||||
|
|
||||||
|
// delete temp dir
|
||||||
|
await fs.remove(tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the Country.mmdb (not required)
|
||||||
|
*/
|
||||||
|
async function resolveMmdb() {
|
||||||
|
const url =
|
||||||
|
"https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb";
|
||||||
|
|
||||||
|
const resPath = path.join(cwd, "src-tauri", "resources", "Country.mmdb");
|
||||||
|
if (await fs.pathExists(resPath)) return;
|
||||||
|
await downloadFile(url, resPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download file and save to `path`
|
||||||
|
*/
|
||||||
|
async function downloadFile(url, path) {
|
||||||
|
console.log(`[INFO]: downloading from "${url}"`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/octet-stream" },
|
||||||
|
});
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
await fs.writeFile(path, new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// main
|
||||||
|
resolveSidecar();
|
||||||
|
resolveMmdb();
|
@ -1,53 +0,0 @@
|
|||||||
import fs from "fs-extra";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
// publish
|
|
||||||
async function resolvePublish() {
|
|
||||||
const flag = process.argv[2] ?? "patch";
|
|
||||||
const packageJson = require("../package.json");
|
|
||||||
const tauriJson = require("../src-tauri/tauri.conf.json");
|
|
||||||
|
|
||||||
let [a, b, c] = packageJson.version.split(".").map(Number);
|
|
||||||
|
|
||||||
if (flag === "major") {
|
|
||||||
a += 1;
|
|
||||||
b = 0;
|
|
||||||
c = 0;
|
|
||||||
} else if (flag === "minor") {
|
|
||||||
b += 1;
|
|
||||||
c = 0;
|
|
||||||
} else if (flag === "patch") {
|
|
||||||
c += 1;
|
|
||||||
} else throw new Error(`invalid flag "${flag}"`);
|
|
||||||
|
|
||||||
const nextVersion = `${a}.${b}.${c}`;
|
|
||||||
packageJson.version = nextVersion;
|
|
||||||
tauriJson.package.version = nextVersion;
|
|
||||||
|
|
||||||
// 发布更新前先写更新日志
|
|
||||||
const nextTag = `v${nextVersion}`;
|
|
||||||
await resolveUpdateLog(nextTag);
|
|
||||||
|
|
||||||
await fs.writeFile(
|
|
||||||
"./package.json",
|
|
||||||
JSON.stringify(packageJson, undefined, 2)
|
|
||||||
);
|
|
||||||
await fs.writeFile(
|
|
||||||
"./src-tauri/tauri.conf.json",
|
|
||||||
JSON.stringify(tauriJson, undefined, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
execSync("git add ./package.json");
|
|
||||||
execSync("git add ./src-tauri/tauri.conf.json");
|
|
||||||
execSync(`git commit -m "v${nextVersion}"`);
|
|
||||||
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
|
|
||||||
execSync(`git push`);
|
|
||||||
execSync(`git push origin v${nextVersion}`);
|
|
||||||
console.log(`Publish Successfully...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvePublish();
|
|
@ -1,44 +0,0 @@
|
|||||||
import fs from "fs-extra";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const UPDATE_LOG = "UPDATELOG.md";
|
|
||||||
|
|
||||||
// parse the UPDATELOG.md
|
|
||||||
export async function resolveUpdateLog(tag) {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
|
|
||||||
const reTitle = /^## v[\d\.]+/;
|
|
||||||
const reEnd = /^---/;
|
|
||||||
|
|
||||||
const file = path.join(cwd, UPDATE_LOG);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(file))) {
|
|
||||||
throw new Error("could not found UPDATELOG.md");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fs.readFile(file).then((d) => d.toString("utf8"));
|
|
||||||
|
|
||||||
const map = {};
|
|
||||||
let p = "";
|
|
||||||
|
|
||||||
data.split("\n").forEach((line) => {
|
|
||||||
if (reTitle.test(line)) {
|
|
||||||
p = line.slice(3).trim();
|
|
||||||
if (!map[p]) {
|
|
||||||
map[p] = [];
|
|
||||||
} else {
|
|
||||||
throw new Error(`Tag ${p} dup`);
|
|
||||||
}
|
|
||||||
} else if (reEnd.test(line)) {
|
|
||||||
p = "";
|
|
||||||
} else if (p) {
|
|
||||||
map[p].push(line);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!map[tag]) {
|
|
||||||
throw new Error(`could not found "${tag}" in UPDATELOG.md`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[tag].join("\n").trim();
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
|
||||||
|
|
||||||
const UPDATE_TAG_NAME = "updater";
|
|
||||||
const UPDATE_JSON_FILE = "update.json";
|
|
||||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
|
||||||
|
|
||||||
/// generate update.json
|
|
||||||
/// upload to update tag's release asset
|
|
||||||
async function resolveUpdater() {
|
|
||||||
if (process.env.GITHUB_TOKEN === undefined) {
|
|
||||||
throw new Error("GITHUB_TOKEN is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
|
||||||
|
|
||||||
const { data: tags } = await github.rest.repos.listTags({
|
|
||||||
...options,
|
|
||||||
per_page: 10,
|
|
||||||
page: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// get the latest publish tag
|
|
||||||
const tag = tags.find((t) => t.name.startsWith("v"));
|
|
||||||
|
|
||||||
console.log(tag);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
|
||||||
...options,
|
|
||||||
tag: tag.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
name: tag.name,
|
|
||||||
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
|
||||||
pub_date: new Date().toISOString(),
|
|
||||||
platforms: {
|
|
||||||
win64: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
linux: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
"darwin-aarch64": { signature: "", url: "" },
|
|
||||||
"darwin-intel": { signature: "", url: "" },
|
|
||||||
"darwin-x86_64": { signature: "", url: "" },
|
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
|
||||||
"windows-i686": { signature: "", url: "" }, // no supported
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const promises = latestRelease.assets.map(async (asset) => {
|
|
||||||
const { name, browser_download_url } = asset;
|
|
||||||
|
|
||||||
// win64 url
|
|
||||||
if (name.endsWith(".msi.zip") && name.includes("en-US")) {
|
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win64 signature
|
|
||||||
if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms.win64.signature = sig;
|
|
||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// darwin url (intel)
|
|
||||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
|
||||||
updateData.platforms.darwin.url = browser_download_url;
|
|
||||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
|
||||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// darwin signature (intel)
|
|
||||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms.darwin.signature = sig;
|
|
||||||
updateData.platforms["darwin-intel"].signature = sig;
|
|
||||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// darwin url (aarch)
|
|
||||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
|
||||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// darwin signature (aarch)
|
|
||||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// linux url
|
|
||||||
if (name.endsWith(".AppImage.tar.gz")) {
|
|
||||||
updateData.platforms.linux.url = browser_download_url;
|
|
||||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// linux signature
|
|
||||||
if (name.endsWith(".AppImage.tar.gz.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms.linux.signature = sig;
|
|
||||||
updateData.platforms["linux-x86_64"].signature = sig;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
|
||||||
console.log(updateData);
|
|
||||||
|
|
||||||
// maybe should test the signature as well
|
|
||||||
// delete the null field
|
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
|
||||||
if (!value.url) {
|
|
||||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
|
||||||
delete updateData.platforms[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成一个代理github的更新文件
|
|
||||||
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
|
||||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
|
||||||
|
|
||||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
|
||||||
if (value.url) {
|
|
||||||
updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url;
|
|
||||||
} else {
|
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// update the update.json
|
|
||||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
|
||||||
...options,
|
|
||||||
tag: UPDATE_TAG_NAME,
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete the old assets
|
|
||||||
for (let asset of updateRelease.assets) {
|
|
||||||
if (asset.name === UPDATE_JSON_FILE) {
|
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
|
||||||
...options,
|
|
||||||
asset_id: asset.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asset.name === UPDATE_JSON_PROXY) {
|
|
||||||
await github.rest.repos
|
|
||||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
|
||||||
.catch(console.error); // do not break the pipeline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// upload new assets
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
|
||||||
...options,
|
|
||||||
release_id: updateRelease.id,
|
|
||||||
name: UPDATE_JSON_FILE,
|
|
||||||
data: JSON.stringify(updateData, null, 2),
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
|
||||||
...options,
|
|
||||||
release_id: updateRelease.id,
|
|
||||||
name: UPDATE_JSON_PROXY,
|
|
||||||
data: JSON.stringify(updateDataNew, null, 2),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the signature file content
|
|
||||||
async function getSignature(url) {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: { "Content-Type": "application/octet-stream" },
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveUpdater().catch(console.error);
|
|
2
src-tauri/.gitignore
vendored
@ -2,5 +2,5 @@
|
|||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
WixTools
|
WixTools
|
||||||
resources
|
resources/Country.mmdb
|
||||||
sidecar
|
sidecar
|
||||||
|
4243
src-tauri/Cargo.lock
generated
@ -1,69 +1,34 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi"]
|
authors = ["zzzgydi"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
repository = "https://github.com/zzzgydi/clash-verge.git"
|
repository = ""
|
||||||
default-run = "clash-verge"
|
default-run = "app"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1", features = [] }
|
tauri-build = { version = "1.0.0-beta.4" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.3"
|
dirs = "4.0.0"
|
||||||
which = "4.2.2"
|
|
||||||
anyhow = "1.0"
|
|
||||||
dirs = "5.0.0"
|
|
||||||
open = "4.0.1"
|
|
||||||
log = "0.4.14"
|
|
||||||
ctrlc = "3.2.3"
|
|
||||||
dunce = "1.0.2"
|
|
||||||
log4rs = "1.0.0"
|
|
||||||
nanoid = "0.4.0"
|
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
sysinfo = "0.29"
|
|
||||||
sysproxy = "0.3"
|
|
||||||
rquickjs = "0.1.7"
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.8"
|
||||||
auto-launch = "0.5"
|
|
||||||
once_cell = "1.14.0"
|
|
||||||
port_scanner = "0.1.5"
|
|
||||||
delay_timer = "0.11.1"
|
|
||||||
parking_lot = "0.12.0"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
reqwest = { version = "0.11", features = ["json","rustls-tls"] }
|
tauri = { version = "1.0.0-beta.8", features = ["api-all", "system-tray"] }
|
||||||
tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
window-vibrancy = { version = "0.3.0" }
|
tokio = { version = "1", features = ["full"] }
|
||||||
window-shadows = { version = "0.2.0" }
|
log = "0.4.14"
|
||||||
wry = { version = "0.24.3" }
|
log4rs = "1.0.0"
|
||||||
|
warp = "0.3"
|
||||||
|
port_scanner = "0.1.5"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "1.1.0"
|
winreg = { version = "0.10", features = ["transactions"] }
|
||||||
deelevate = "0.2.0"
|
|
||||||
winreg = { version = "0.50", features = ["transactions"] }
|
|
||||||
windows-sys = { version = "0.48", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies.tauri]
|
|
||||||
features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"]
|
|
||||||
|
|
||||||
[target.'cfg(linux)'.dependencies.tauri]
|
|
||||||
features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "native-tls-vendored", "reqwest-native-tls-vendored"]
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "custom-protocol" ]
|
default = [ "custom-protocol" ]
|
||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
verge-dev = []
|
|
||||||
default-meta = []
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
||||||
lto = true
|
|
||||||
opt-level = "s"
|
|
||||||
|
BIN
src-tauri/icons/256x256.ico
Normal file
After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB |
7
src-tauri/resources/config_tmp.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Default Config For Clash Core
|
||||||
|
|
||||||
|
mixed-port: 7890
|
||||||
|
log-level: info
|
||||||
|
allow-lan: false
|
||||||
|
external-controller: 127.0.0.1:9090
|
||||||
|
secret: ""
|
3
src-tauri/resources/profiles_tmp.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Profiles Config for Clash Verge
|
||||||
|
|
||||||
|
current: 0
|
3
src-tauri/resources/verge_tmp.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Defaulf Config For Clash Verge
|
||||||
|
|
||||||
|
nothing: ohh!
|
@ -1,6 +1,6 @@
|
|||||||
max_width = 100
|
max_width = 100
|
||||||
hard_tabs = false
|
hard_tabs = false
|
||||||
tab_spaces = 4
|
tab_spaces = 2
|
||||||
newline_style = "Auto"
|
newline_style = "Auto"
|
||||||
use_small_heuristics = "Default"
|
use_small_heuristics = "Default"
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
|
@ -1,280 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
config::*,
|
|
||||||
core::*,
|
|
||||||
feat,
|
|
||||||
utils::{dirs, help},
|
|
||||||
};
|
|
||||||
use crate::{ret_err, wrap_err};
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
|
||||||
use sysproxy::Sysproxy;
|
|
||||||
|
|
||||||
type CmdResult<T = ()> = Result<T, String>;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
|
||||||
Ok(Config::profiles().data().clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn enhance_profiles() -> CmdResult {
|
|
||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
|
||||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
|
||||||
wrap_err!(Config::profiles().data().append_item(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
|
||||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
|
||||||
wrap_err!(Config::profiles().data().append_item(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
|
||||||
wrap_err!(feat::update_profile(index, option).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn delete_profile(index: String) -> CmdResult {
|
|
||||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
|
||||||
if should_update {
|
|
||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 修改profiles的
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
|
||||||
|
|
||||||
match CoreManager::global().update_config().await {
|
|
||||||
Ok(_) => {
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
Config::profiles().apply();
|
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
Config::profiles().discard();
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
Err(format!("{err}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 修改某个profile item的
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
|
||||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
|
||||||
wrap_err!(timer::Timer::global().refresh())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn view_profile(index: String) -> CmdResult {
|
|
||||||
let file = {
|
|
||||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
|
||||||
.file
|
|
||||||
.clone()
|
|
||||||
.ok_or("the file field is null")
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
|
||||||
if !path.exists() {
|
|
||||||
ret_err!("the file not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
wrap_err!(help::open_file(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let profiles = profiles.latest();
|
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
|
||||||
let data = wrap_err!(item.read_file())?;
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
|
||||||
if file_data.is_none() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let profiles = profiles.latest();
|
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
|
||||||
wrap_err!(item.save_file(file_data.unwrap()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
|
||||||
Ok(Config::clash().latest().get_client_info())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
|
||||||
Ok(Config::runtime().latest().config.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
|
||||||
let runtime = Config::runtime();
|
|
||||||
let runtime = runtime.latest();
|
|
||||||
let config = runtime.config.as_ref();
|
|
||||||
wrap_err!(config
|
|
||||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
|
||||||
.and_then(
|
|
||||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
|
||||||
Ok(Config::runtime().latest().exists_keys.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
|
||||||
Ok(Config::runtime().latest().chain_logs.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
|
||||||
wrap_err!(feat::patch_clash(payload).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_verge_config() -> CmdResult<IVerge> {
|
|
||||||
Ok(Config::verge().data().clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
|
||||||
wrap_err!(feat::patch_verge(payload).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
|
||||||
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// restart the sidecar
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn restart_sidecar() -> CmdResult {
|
|
||||||
wrap_err!(CoreManager::global().run_core().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn grant_permission(core: String) -> CmdResult {
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
|
||||||
return wrap_err!(manager::grant_permission(core));
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
|
||||||
return Err("Unsupported target".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the system proxy
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
|
||||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
|
||||||
map.insert("enable".into(), current.enable.into());
|
|
||||||
map.insert(
|
|
||||||
"server".into(),
|
|
||||||
format!("{}:{}", current.host, current.port).into(),
|
|
||||||
);
|
|
||||||
map.insert("bypass".into(), current.bypass.into());
|
|
||||||
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
|
||||||
Ok(logger::Logger::global().get_log())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn open_app_dir() -> CmdResult<()> {
|
|
||||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
|
||||||
wrap_err!(open::that(app_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn open_core_dir() -> CmdResult<()> {
|
|
||||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
|
||||||
let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?;
|
|
||||||
wrap_err!(open::that(core_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn open_logs_dir() -> CmdResult<()> {
|
|
||||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
|
||||||
wrap_err!(open::that(log_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
|
||||||
wrap_err!(open::that(url))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn clash_api_get_proxy_delay(
|
|
||||||
name: String,
|
|
||||||
url: Option<String>,
|
|
||||||
) -> CmdResult<clash_api::DelayRes> {
|
|
||||||
match clash_api::get_proxy_delay(name, url).await {
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => Err(format!("{}", err.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub mod service {
|
|
||||||
use super::*;
|
|
||||||
use crate::core::win_service;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
|
|
||||||
wrap_err!(win_service::check_service().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn install_service() -> CmdResult {
|
|
||||||
wrap_err!(win_service::install_service().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn uninstall_service() -> CmdResult {
|
|
||||||
wrap_err!(win_service::uninstall_service().await)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
pub mod service {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn check_service() -> CmdResult {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn install_service() -> CmdResult {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn uninstall_service() -> CmdResult {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
2
src-tauri/src/cmds/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod profile;
|
||||||
|
pub mod some;
|
190
src-tauri/src/cmds/profile.rs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
use crate::{
|
||||||
|
config::{ProfileItem, ProfilesConfig},
|
||||||
|
events::state::{ClashInfoState, ProfileLock},
|
||||||
|
utils::{
|
||||||
|
app_home_dir,
|
||||||
|
clash::put_clash_profile,
|
||||||
|
config::{read_profiles, save_profiles},
|
||||||
|
fetch::fetch_profile,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
/// Import the profile from url
|
||||||
|
/// and save to `profiles.yaml`
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_profile(url: String, lock: State<'_, ProfileLock>) -> Result<(), String> {
|
||||||
|
let result = match fetch_profile(&url).await {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
log::error!("failed to fetch profile from `{}`", url);
|
||||||
|
return Err(format!("failed to fetch profile from `{}`", url));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get lock
|
||||||
|
if lock.0.lock().is_err() {
|
||||||
|
return Err(format!("can not get file lock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the profile file
|
||||||
|
let path = app_home_dir().join("profiles").join(&result.file);
|
||||||
|
let file_data = result.data.as_bytes();
|
||||||
|
File::create(path).unwrap().write(file_data).unwrap();
|
||||||
|
|
||||||
|
// update `profiles.yaml`
|
||||||
|
let mut profiles = read_profiles();
|
||||||
|
let mut items = profiles.items.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
items.push(ProfileItem {
|
||||||
|
name: Some(result.name),
|
||||||
|
file: Some(result.file),
|
||||||
|
mode: Some(format!("rule")),
|
||||||
|
url: Some(url),
|
||||||
|
selected: Some(vec![]),
|
||||||
|
extra: Some(result.extra),
|
||||||
|
updated: Some(now as usize),
|
||||||
|
});
|
||||||
|
profiles.items = Some(items);
|
||||||
|
save_profiles(&profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the profile
|
||||||
|
/// and save to `profiles.yaml`
|
||||||
|
/// http request firstly
|
||||||
|
/// then acquire the lock of `profiles.yaml`
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_profile(index: usize, lock: State<'_, ProfileLock>) -> Result<(), String> {
|
||||||
|
// get lock
|
||||||
|
if lock.0.lock().is_err() {
|
||||||
|
return Err(format!("can not get file lock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// update `profiles.yaml`
|
||||||
|
let mut profiles = read_profiles();
|
||||||
|
let mut items = profiles.items.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
if index >= items.len() {
|
||||||
|
return Err(format!("the index out of bound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = match &items[index].url {
|
||||||
|
Some(u) => u,
|
||||||
|
None => return Err(format!("invalid url")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match fetch_profile(&url).await {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
log::error!("failed to fetch profile from `{}`", url);
|
||||||
|
return Err(format!("failed to fetch profile from `{}`", url));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs() as usize;
|
||||||
|
|
||||||
|
// update file
|
||||||
|
let file_path = &items[index].file.as_ref().unwrap();
|
||||||
|
let file_path = app_home_dir().join("profiles").join(file_path);
|
||||||
|
let file_data = result.data.as_bytes();
|
||||||
|
File::create(file_path).unwrap().write(file_data).unwrap();
|
||||||
|
|
||||||
|
items[index].name = Some(result.name);
|
||||||
|
items[index].extra = Some(result.extra);
|
||||||
|
items[index].updated = Some(now);
|
||||||
|
profiles.items = Some(items);
|
||||||
|
save_profiles(&profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get all profiles from `profiles.yaml`
|
||||||
|
/// do not acquire the lock of ProfileLock
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_profiles() -> Result<ProfilesConfig, String> {
|
||||||
|
Ok(read_profiles())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// patch the profile config
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_profiles(
|
||||||
|
index: usize,
|
||||||
|
profile: ProfileItem,
|
||||||
|
lock: State<'_, ProfileLock>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// get lock
|
||||||
|
if lock.0.lock().is_err() {
|
||||||
|
return Err(format!("can not get file lock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut profiles = read_profiles();
|
||||||
|
let mut items = profiles.items.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
if index >= items.len() {
|
||||||
|
return Err(format!("the index out of bound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile.name.is_some() {
|
||||||
|
items[index].name = profile.name;
|
||||||
|
}
|
||||||
|
if profile.file.is_some() {
|
||||||
|
items[index].file = profile.file;
|
||||||
|
}
|
||||||
|
if profile.mode.is_some() {
|
||||||
|
items[index].mode = profile.mode;
|
||||||
|
}
|
||||||
|
if profile.url.is_some() {
|
||||||
|
items[index].url = profile.url;
|
||||||
|
}
|
||||||
|
if profile.selected.is_some() {
|
||||||
|
items[index].selected = profile.selected;
|
||||||
|
}
|
||||||
|
if profile.extra.is_some() {
|
||||||
|
items[index].extra = profile.extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles.items = Some(items);
|
||||||
|
save_profiles(&profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// change the current profile
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn put_profiles(
|
||||||
|
current: usize,
|
||||||
|
lock: State<'_, ProfileLock>,
|
||||||
|
clash_info: State<'_, ClashInfoState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if lock.0.lock().is_err() {
|
||||||
|
return Err(format!("can not get file lock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let clash_info = match clash_info.0.lock() {
|
||||||
|
Ok(arc) => arc.clone(),
|
||||||
|
_ => return Err(format!("can not get clash info")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut profiles = read_profiles();
|
||||||
|
let items_len = match &profiles.items {
|
||||||
|
Some(list) => list.len(),
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if current >= items_len {
|
||||||
|
return Err(format!("the index out of bound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles.current = Some(current);
|
||||||
|
match save_profiles(&profiles) {
|
||||||
|
Ok(_) => put_clash_profile(&clash_info).await,
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
140
src-tauri/src/cmds/some.rs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
use crate::{
|
||||||
|
config::VergeConfig,
|
||||||
|
events::{
|
||||||
|
emit::ClashInfoPayload,
|
||||||
|
state::{ClashInfoState, VergeConfLock},
|
||||||
|
},
|
||||||
|
utils::{
|
||||||
|
clash::run_clash_bin,
|
||||||
|
config::{read_clash, save_clash, save_verge},
|
||||||
|
sysopt::{get_proxy_config, set_proxy_config, SysProxyConfig},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use tauri::{api::process::kill_children, AppHandle, State};
|
||||||
|
|
||||||
|
/// restart the sidecar
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn restart_sidecar(app_handle: AppHandle, clash_info: State<'_, ClashInfoState>) {
|
||||||
|
kill_children();
|
||||||
|
let payload = run_clash_bin(&app_handle);
|
||||||
|
|
||||||
|
if let Ok(mut arc) = clash_info.0.lock() {
|
||||||
|
*arc = payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the clash core info from the state
|
||||||
|
/// the caller can also get the infomation by clash's api
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_clash_info(clash_info: State<'_, ClashInfoState>) -> Result<ClashInfoPayload, String> {
|
||||||
|
match clash_info.0.lock() {
|
||||||
|
Ok(arc) => Ok(arc.clone()),
|
||||||
|
Err(_) => Err(format!("can not get clash info")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update the clash core config
|
||||||
|
/// after putting the change to the clash core
|
||||||
|
/// then we should save the latest config
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn patch_clash_config(payload: Mapping) -> Result<(), String> {
|
||||||
|
let mut config = read_clash();
|
||||||
|
for (key, value) in payload.iter() {
|
||||||
|
if config.contains_key(key) {
|
||||||
|
config[key] = value.clone();
|
||||||
|
} else {
|
||||||
|
config.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save_clash(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the system proxy
|
||||||
|
/// Tips: only support windows now
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_sys_proxy(enable: bool, clash_info: State<'_, ClashInfoState>) -> Result<(), String> {
|
||||||
|
let clash_info = match clash_info.0.lock() {
|
||||||
|
Ok(arc) => arc.clone(),
|
||||||
|
_ => return Err(format!("can not get clash info")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let port = match clash_info.controller {
|
||||||
|
Some(ctrl) => ctrl.port,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if port.is_none() {
|
||||||
|
return Err(format!("can not get clash core's port"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = if enable {
|
||||||
|
let server = format!("127.0.0.1:{}", port.unwrap());
|
||||||
|
// todo
|
||||||
|
let bypass = String::from("localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>");
|
||||||
|
|
||||||
|
SysProxyConfig {
|
||||||
|
enable,
|
||||||
|
server,
|
||||||
|
bypass,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SysProxyConfig {
|
||||||
|
enable,
|
||||||
|
server: String::from(""),
|
||||||
|
bypass: String::from(""),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match set_proxy_config(&config) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(format!("can not set proxy")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the system proxy
|
||||||
|
/// Tips: only support windows now
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
|
||||||
|
match get_proxy_config() {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the verge config
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_verge_config(verge_lock: State<'_, VergeConfLock>) -> Result<VergeConfig, String> {
|
||||||
|
match verge_lock.0.lock() {
|
||||||
|
Ok(arc) => Ok(arc.clone()),
|
||||||
|
Err(_) => Err(format!("can not get the lock")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// patch the verge config
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_verge_config(
|
||||||
|
payload: VergeConfig,
|
||||||
|
verge_lock: State<'_, VergeConfLock>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut verge = match verge_lock.0.lock() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return Err(format!("can not get the lock")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if payload.theme_mode.is_some() {
|
||||||
|
verge.theme_mode = payload.theme_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
if payload.enable_self_startup.is_some() {
|
||||||
|
verge.enable_self_startup = payload.enable_self_startup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
if payload.enable_system_proxy.is_some() {
|
||||||
|
verge.enable_system_proxy = payload.enable_system_proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
save_verge(&verge)
|
||||||
|
}
|
@ -1,262 +1,30 @@
|
|||||||
use crate::utils::{dirs, help};
|
|
||||||
use anyhow::Result;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::{
|
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
/// ### `config.yaml` schema
|
||||||
pub struct IClashTemp(pub Mapping);
|
/// here should contain all configuration options.
|
||||||
|
/// See: https://github.com/Dreamacro/clash/wiki/configuration for details
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ClashConfig {
|
||||||
|
pub port: Option<u32>,
|
||||||
|
|
||||||
impl IClashTemp {
|
/// alias to `mixed-port`
|
||||||
pub fn new() -> Self {
|
pub mixed_port: Option<u32>,
|
||||||
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
|
|
||||||
Ok(map) => Self(Self::guard(map)),
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
Self::template()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn template() -> Self {
|
/// alias to `allow-lan`
|
||||||
let mut map = Mapping::new();
|
|
||||||
|
|
||||||
map.insert(
|
|
||||||
"mixed-port".into(),
|
|
||||||
match cfg!(feature = "default-meta") {
|
|
||||||
false => 7890.into(),
|
|
||||||
true => 7898.into(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
map.insert("log-level".into(), "info".into());
|
|
||||||
map.insert("allow-lan".into(), false.into());
|
|
||||||
map.insert("mode".into(), "rule".into());
|
|
||||||
map.insert(
|
|
||||||
"external-controller".into(),
|
|
||||||
match cfg!(feature = "default-meta") {
|
|
||||||
false => "127.0.0.1:9090".into(),
|
|
||||||
true => "127.0.0.1:9098".into(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
map.insert("secret".into(), "".into());
|
|
||||||
|
|
||||||
Self(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn guard(mut config: Mapping) -> Mapping {
|
|
||||||
let port = Self::guard_mixed_port(&config);
|
|
||||||
let ctrl = Self::guard_server_ctrl(&config);
|
|
||||||
|
|
||||||
config.insert("mixed-port".into(), port.into());
|
|
||||||
config.insert("external-controller".into(), ctrl.into());
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn patch_config(&mut self, patch: Mapping) {
|
|
||||||
for (key, value) in patch.into_iter() {
|
|
||||||
self.0.insert(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_config(&self) -> Result<()> {
|
|
||||||
help::save_yaml(
|
|
||||||
&dirs::clash_path()?,
|
|
||||||
&self.0,
|
|
||||||
Some("# Generated by Clash Verge"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mixed_port(&self) -> u16 {
|
|
||||||
Self::guard_mixed_port(&self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_client_info(&self) -> ClashInfo {
|
|
||||||
let config = &self.0;
|
|
||||||
|
|
||||||
ClashInfo {
|
|
||||||
port: Self::guard_mixed_port(&config),
|
|
||||||
server: Self::guard_client_ctrl(&config),
|
|
||||||
secret: config.get("secret").and_then(|value| match value {
|
|
||||||
Value::String(val_str) => Some(val_str.clone()),
|
|
||||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
|
||||||
Value::Number(val_num) => Some(val_num.to_string()),
|
|
||||||
_ => None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn guard_mixed_port(config: &Mapping) -> u16 {
|
|
||||||
let mut port = config
|
|
||||||
.get("mixed-port")
|
|
||||||
.and_then(|value| match value {
|
|
||||||
Value::String(val_str) => val_str.parse().ok(),
|
|
||||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.unwrap_or(7890);
|
|
||||||
if port == 0 {
|
|
||||||
port = 7890;
|
|
||||||
}
|
|
||||||
port
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
|
||||||
config
|
|
||||||
.get("external-controller")
|
|
||||||
.and_then(|value| match value.as_str() {
|
|
||||||
Some(val_str) => {
|
|
||||||
let val_str = val_str.trim();
|
|
||||||
|
|
||||||
let val = match val_str.starts_with(":") {
|
|
||||||
true => format!("127.0.0.1{val_str}"),
|
|
||||||
false => val_str.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
SocketAddr::from_str(val.as_str())
|
|
||||||
.ok()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
})
|
|
||||||
.unwrap_or("127.0.0.1:9090".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
|
||||||
let value = Self::guard_server_ctrl(config);
|
|
||||||
match SocketAddr::from_str(value.as_str()) {
|
|
||||||
Ok(mut socket) => {
|
|
||||||
if socket.ip().is_unspecified() {
|
|
||||||
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
||||||
}
|
|
||||||
socket.to_string()
|
|
||||||
}
|
|
||||||
Err(_) => "127.0.0.1:9090".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
pub struct ClashInfo {
|
|
||||||
/// clash core port
|
|
||||||
pub port: u16,
|
|
||||||
/// same as `external-controller`
|
|
||||||
pub server: String,
|
|
||||||
/// clash secret
|
|
||||||
pub secret: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_clash_info() {
|
|
||||||
fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
|
|
||||||
let mut map = Mapping::new();
|
|
||||||
map.insert("mixed-port".into(), mp.into());
|
|
||||||
map.insert("external-controller".into(), ec.into());
|
|
||||||
|
|
||||||
IClashTemp(IClashTemp::guard(map)).get_client_info()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
|
||||||
ClashInfo {
|
|
||||||
port,
|
|
||||||
server: server.into(),
|
|
||||||
secret: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
|
|
||||||
get_result(7890, "127.0.0.1:9090")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(get_case("", ""), get_result(7890, "127.0.0.1:9090"));
|
|
||||||
|
|
||||||
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9090"));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, "127.0.0.1:8888"),
|
|
||||||
get_result(8888, "127.0.0.1:8888")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, " :98888 "),
|
|
||||||
get_result(8888, "127.0.0.1:9090")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, "0.0.0.0:8080 "),
|
|
||||||
get_result(8888, "127.0.0.1:8080")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, "0.0.0.0:8080"),
|
|
||||||
get_result(8888, "127.0.0.1:8080")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, "[::]:8080"),
|
|
||||||
get_result(8888, "127.0.0.1:8080")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, "192.168.1.1:8080"),
|
|
||||||
get_result(8888, "192.168.1.1:8080")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_case(8888, "192.168.1.1:80800"),
|
|
||||||
get_result(8888, "127.0.0.1:9090")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct IClash {
|
|
||||||
pub mixed_port: Option<u16>,
|
|
||||||
pub allow_lan: Option<bool>,
|
pub allow_lan: Option<bool>,
|
||||||
pub log_level: Option<String>,
|
|
||||||
pub ipv6: Option<bool>,
|
/// alias to `external-controller`
|
||||||
pub mode: Option<String>,
|
pub external_ctrl: Option<String>,
|
||||||
pub external_controller: Option<String>,
|
|
||||||
pub secret: Option<String>,
|
pub secret: Option<String>,
|
||||||
pub dns: Option<IClashDNS>,
|
|
||||||
pub tun: Option<IClashTUN>,
|
|
||||||
pub interface_name: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
pub struct ClashController {
|
||||||
pub struct IClashTUN {
|
/// clash core port
|
||||||
pub enable: Option<bool>,
|
pub port: Option<String>,
|
||||||
pub stack: Option<String>,
|
|
||||||
pub auto_route: Option<bool>,
|
|
||||||
pub auto_detect_interface: Option<bool>,
|
|
||||||
pub dns_hijack: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
/// same as `external-controller`
|
||||||
#[serde(rename_all = "kebab-case")]
|
pub server: Option<String>,
|
||||||
pub struct IClashDNS {
|
pub secret: Option<String>,
|
||||||
pub enable: Option<bool>,
|
|
||||||
pub listen: Option<String>,
|
|
||||||
pub default_nameserver: Option<Vec<String>>,
|
|
||||||
pub enhanced_mode: Option<String>,
|
|
||||||
pub fake_ip_range: Option<String>,
|
|
||||||
pub use_hosts: Option<bool>,
|
|
||||||
pub fake_ip_filter: Option<Vec<String>>,
|
|
||||||
pub nameserver: Option<Vec<String>>,
|
|
||||||
pub fallback: Option<Vec<String>>,
|
|
||||||
pub fallback_filter: Option<IClashFallbackFilter>,
|
|
||||||
pub nameserver_policy: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct IClashFallbackFilter {
|
|
||||||
pub geoip: Option<bool>,
|
|
||||||
pub geoip_code: Option<String>,
|
|
||||||
pub ipcidr: Option<Vec<String>>,
|
|
||||||
pub domain: Option<Vec<String>>,
|
|
||||||
}
|
}
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
|
||||||
use crate::{
|
|
||||||
enhance,
|
|
||||||
utils::{dirs, help},
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::{env::temp_dir, path::PathBuf};
|
|
||||||
|
|
||||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
|
||||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
clash_config: Draft<IClashTemp>,
|
|
||||||
verge_config: Draft<IVerge>,
|
|
||||||
profiles_config: Draft<IProfiles>,
|
|
||||||
runtime_config: Draft<IRuntime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn global() -> &'static Config {
|
|
||||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
|
||||||
|
|
||||||
CONFIG.get_or_init(|| Config {
|
|
||||||
clash_config: Draft::from(IClashTemp::new()),
|
|
||||||
verge_config: Draft::from(IVerge::new()),
|
|
||||||
profiles_config: Draft::from(IProfiles::new()),
|
|
||||||
runtime_config: Draft::from(IRuntime::new()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clash() -> Draft<IClashTemp> {
|
|
||||||
Self::global().clash_config.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verge() -> Draft<IVerge> {
|
|
||||||
Self::global().verge_config.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn profiles() -> Draft<IProfiles> {
|
|
||||||
Self::global().profiles_config.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runtime() -> Draft<IRuntime> {
|
|
||||||
Self::global().runtime_config.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 初始化配置
|
|
||||||
pub fn init_config() -> Result<()> {
|
|
||||||
crate::log_err!(Self::generate());
|
|
||||||
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
|
|
||||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
|
||||||
// 如果不存在就将默认的clash文件拿过来
|
|
||||||
if !runtime_path.exists() {
|
|
||||||
help::save_yaml(
|
|
||||||
&runtime_path,
|
|
||||||
&Config::clash().latest().0,
|
|
||||||
Some("# Clash Verge Runtime"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将配置丢到对应的文件中
|
|
||||||
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
|
||||||
let path = match typ {
|
|
||||||
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
|
||||||
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
|
|
||||||
};
|
|
||||||
|
|
||||||
let runtime = Config::runtime();
|
|
||||||
let runtime = runtime.latest();
|
|
||||||
let config = runtime
|
|
||||||
.config
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
|
||||||
|
|
||||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成配置存好
|
|
||||||
pub fn generate() -> Result<()> {
|
|
||||||
let (config, exists_keys, logs) = enhance::enhance();
|
|
||||||
|
|
||||||
*Config::runtime().draft() = IRuntime {
|
|
||||||
config: Some(config),
|
|
||||||
exists_keys,
|
|
||||||
chain_logs: logs,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ConfigType {
|
|
||||||
Run,
|
|
||||||
Check,
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
|
||||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Draft<T: Clone + ToOwned> {
|
|
||||||
inner: Arc<Mutex<(T, Option<T>)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! draft_define {
|
|
||||||
($id: ident) => {
|
|
||||||
impl Draft<$id> {
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn data(&self) -> MappedMutexGuard<$id> {
|
|
||||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
|
||||||
MutexGuard::map(self.inner.lock(), |inner| {
|
|
||||||
if inner.1.is_none() {
|
|
||||||
&mut inner.0
|
|
||||||
} else {
|
|
||||||
inner.1.as_mut().unwrap()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
|
||||||
MutexGuard::map(self.inner.lock(), |inner| {
|
|
||||||
if inner.1.is_none() {
|
|
||||||
inner.1 = Some(inner.0.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
inner.1.as_mut().unwrap()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply(&self) -> Option<$id> {
|
|
||||||
let mut inner = self.inner.lock();
|
|
||||||
|
|
||||||
match inner.1.take() {
|
|
||||||
Some(draft) => {
|
|
||||||
let old_value = inner.0.to_owned();
|
|
||||||
inner.0 = draft.to_owned();
|
|
||||||
Some(old_value)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn discard(&self) -> Option<$id> {
|
|
||||||
let mut inner = self.inner.lock();
|
|
||||||
inner.1.take()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<$id> for Draft<$id> {
|
|
||||||
fn from(data: $id) -> Self {
|
|
||||||
Draft {
|
|
||||||
inner: Arc::new(Mutex::new((data, None))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// draft_define!(IClash);
|
|
||||||
draft_define!(IClashTemp);
|
|
||||||
draft_define!(IProfiles);
|
|
||||||
draft_define!(IRuntime);
|
|
||||||
draft_define!(IVerge);
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_draft() {
|
|
||||||
let verge = IVerge {
|
|
||||||
enable_auto_launch: Some(true),
|
|
||||||
enable_tun_mode: Some(false),
|
|
||||||
..IVerge::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let draft = Draft::from(verge);
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
|
||||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
|
||||||
|
|
||||||
let mut d = draft.draft();
|
|
||||||
d.enable_auto_launch = Some(false);
|
|
||||||
d.enable_tun_mode = Some(true);
|
|
||||||
drop(d);
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
|
||||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
|
||||||
|
|
||||||
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
|
||||||
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
|
||||||
|
|
||||||
assert!(draft.apply().is_some());
|
|
||||||
assert!(draft.apply().is_none());
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
|
||||||
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
|
||||||
|
|
||||||
let mut d = draft.draft();
|
|
||||||
d.enable_auto_launch = Some(true);
|
|
||||||
drop(d);
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
|
||||||
|
|
||||||
assert!(draft.discard().is_some());
|
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
|
||||||
|
|
||||||
assert!(draft.discard().is_none());
|
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
|
||||||
}
|
|
@ -1,15 +1,7 @@
|
|||||||
mod clash;
|
mod clash;
|
||||||
mod config;
|
|
||||||
mod draft;
|
|
||||||
mod prfitem;
|
|
||||||
mod profiles;
|
mod profiles;
|
||||||
mod runtime;
|
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
pub use self::clash::*;
|
pub use self::clash::*;
|
||||||
pub use self::config::*;
|
|
||||||
pub use self::draft::*;
|
|
||||||
pub use self::prfitem::*;
|
|
||||||
pub use self::profiles::*;
|
pub use self::profiles::*;
|
||||||
pub use self::runtime::*;
|
|
||||||
pub use self::verge::*;
|
pub use self::verge::*;
|
||||||
|
@ -1,374 +0,0 @@
|
|||||||
use crate::utils::{dirs, help, tmpl};
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use reqwest::StatusCode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::fs;
|
|
||||||
use sysproxy::Sysproxy;
|
|
||||||
|
|
||||||
use super::Config;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct PrfItem {
|
|
||||||
pub uid: Option<String>,
|
|
||||||
|
|
||||||
/// profile item type
|
|
||||||
/// enum value: remote | local | script | merge
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub itype: Option<String>,
|
|
||||||
|
|
||||||
/// profile name
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
/// profile file
|
|
||||||
pub file: Option<String>,
|
|
||||||
|
|
||||||
/// profile description
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub desc: Option<String>,
|
|
||||||
|
|
||||||
/// source url
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub url: Option<String>,
|
|
||||||
|
|
||||||
/// selected information
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub selected: Option<Vec<PrfSelected>>,
|
|
||||||
|
|
||||||
/// subscription user info
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub extra: Option<PrfExtra>,
|
|
||||||
|
|
||||||
/// updated time
|
|
||||||
pub updated: Option<usize>,
|
|
||||||
|
|
||||||
/// some options of the item
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub option: Option<PrfOption>,
|
|
||||||
|
|
||||||
/// the file data
|
|
||||||
#[serde(skip)]
|
|
||||||
pub file_data: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct PrfSelected {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub now: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
|
||||||
pub struct PrfExtra {
|
|
||||||
pub upload: usize,
|
|
||||||
pub download: usize,
|
|
||||||
pub total: usize,
|
|
||||||
pub expire: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
pub struct PrfOption {
|
|
||||||
/// for `remote` profile's http request
|
|
||||||
/// see issue #13
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_agent: Option<String>,
|
|
||||||
|
|
||||||
/// for `remote` profile
|
|
||||||
/// use system proxy
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub with_proxy: Option<bool>,
|
|
||||||
|
|
||||||
/// for `remote` profile
|
|
||||||
/// use self proxy
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub self_proxy: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub update_interval: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrfOption {
|
|
||||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
|
||||||
match (one, other) {
|
|
||||||
(Some(mut a), Some(b)) => {
|
|
||||||
a.user_agent = b.user_agent.or(a.user_agent);
|
|
||||||
a.with_proxy = b.with_proxy.or(a.with_proxy);
|
|
||||||
a.self_proxy = b.self_proxy.or(a.self_proxy);
|
|
||||||
a.update_interval = b.update_interval.or(a.update_interval);
|
|
||||||
Some(a)
|
|
||||||
}
|
|
||||||
t @ _ => t.0.or(t.1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PrfItem {
|
|
||||||
fn default() -> Self {
|
|
||||||
PrfItem {
|
|
||||||
uid: None,
|
|
||||||
itype: None,
|
|
||||||
name: None,
|
|
||||||
desc: None,
|
|
||||||
file: None,
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
updated: None,
|
|
||||||
option: None,
|
|
||||||
file_data: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrfItem {
|
|
||||||
/// From partial item
|
|
||||||
/// must contain `itype`
|
|
||||||
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
|
||||||
if item.itype.is_none() {
|
|
||||||
bail!("type should not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
match item.itype.unwrap().as_str() {
|
|
||||||
"remote" => {
|
|
||||||
if item.url.is_none() {
|
|
||||||
bail!("url should not be null");
|
|
||||||
}
|
|
||||||
let url = item.url.as_ref().unwrap().as_str();
|
|
||||||
let name = item.name;
|
|
||||||
let desc = item.desc;
|
|
||||||
PrfItem::from_url(url, name, desc, item.option).await
|
|
||||||
}
|
|
||||||
"local" => {
|
|
||||||
let name = item.name.unwrap_or("Local File".into());
|
|
||||||
let desc = item.desc.unwrap_or("".into());
|
|
||||||
PrfItem::from_local(name, desc, file_data)
|
|
||||||
}
|
|
||||||
"merge" => {
|
|
||||||
let name = item.name.unwrap_or("Merge".into());
|
|
||||||
let desc = item.desc.unwrap_or("".into());
|
|
||||||
PrfItem::from_merge(name, desc)
|
|
||||||
}
|
|
||||||
"script" => {
|
|
||||||
let name = item.name.unwrap_or("Script".into());
|
|
||||||
let desc = item.desc.unwrap_or("".into());
|
|
||||||
PrfItem::from_script(name, desc)
|
|
||||||
}
|
|
||||||
typ @ _ => bail!("invalid profile item type \"{typ}\""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Local type
|
|
||||||
/// create a new item from name/desc
|
|
||||||
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
|
|
||||||
let uid = help::get_uid("l");
|
|
||||||
let file = format!("{uid}.yaml");
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("local".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc: Some(desc),
|
|
||||||
file: Some(file),
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
option: None,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
|
||||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Remote type
|
|
||||||
/// create a new item from url
|
|
||||||
pub async fn from_url(
|
|
||||||
url: &str,
|
|
||||||
name: Option<String>,
|
|
||||||
desc: Option<String>,
|
|
||||||
option: Option<PrfOption>,
|
|
||||||
) -> Result<PrfItem> {
|
|
||||||
let opt_ref = option.as_ref();
|
|
||||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
|
||||||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
|
||||||
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
|
|
||||||
|
|
||||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
|
||||||
|
|
||||||
// 使用软件自己的代理
|
|
||||||
if self_proxy {
|
|
||||||
let port = Config::clash().data().get_mixed_port();
|
|
||||||
|
|
||||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
|
||||||
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 使用系统代理
|
|
||||||
else if with_proxy {
|
|
||||||
match Sysproxy::get_system_proxy() {
|
|
||||||
Ok(p @ Sysproxy { enable: true, .. }) => {
|
|
||||||
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
|
||||||
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let version = unsafe { dirs::APP_VERSION };
|
|
||||||
let version = format!("clash-verge/{version}");
|
|
||||||
builder = builder.user_agent(user_agent.unwrap_or(version));
|
|
||||||
|
|
||||||
let resp = builder.build()?.get(url).send().await?;
|
|
||||||
|
|
||||||
let status_code = resp.status();
|
|
||||||
if !StatusCode::is_success(&status_code) {
|
|
||||||
bail!("failed to fetch remote profile with status {status_code}")
|
|
||||||
}
|
|
||||||
|
|
||||||
let header = resp.headers();
|
|
||||||
|
|
||||||
// parse the Subscription UserInfo
|
|
||||||
let extra = match header.get("Subscription-Userinfo") {
|
|
||||||
Some(value) => {
|
|
||||||
let sub_info = value.to_str().unwrap_or("");
|
|
||||||
|
|
||||||
Some(PrfExtra {
|
|
||||||
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
|
||||||
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
|
||||||
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
|
||||||
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// parse the Content-Disposition
|
|
||||||
let filename = match header.get("Content-Disposition") {
|
|
||||||
Some(value) => {
|
|
||||||
let filename = value.to_str().unwrap_or("");
|
|
||||||
help::parse_str::<String>(filename, "filename=")
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// parse the profile-update-interval
|
|
||||||
let option = match header.get("profile-update-interval") {
|
|
||||||
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
|
||||||
Ok(val) => Some(PrfOption {
|
|
||||||
update_interval: Some(val * 60), // hour -> min
|
|
||||||
..PrfOption::default()
|
|
||||||
}),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let uid = help::get_uid("r");
|
|
||||||
let file = format!("{uid}.yaml");
|
|
||||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
|
||||||
let data = resp.text_with_charset("utf-8").await?;
|
|
||||||
|
|
||||||
// process the charset "UTF-8 with BOM"
|
|
||||||
let data = data.trim_start_matches('\u{feff}');
|
|
||||||
|
|
||||||
// check the data whether the valid yaml format
|
|
||||||
let yaml = serde_yaml::from_str::<Mapping>(data)
|
|
||||||
.context("the remote profile data is invalid yaml")?;
|
|
||||||
|
|
||||||
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
|
||||||
bail!("profile does not contain `proxies` or `proxy-providers`");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("remote".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc,
|
|
||||||
file: Some(file),
|
|
||||||
url: Some(url.into()),
|
|
||||||
selected: None,
|
|
||||||
extra,
|
|
||||||
option,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
|
||||||
file_data: Some(data.into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Merge type (enhance)
|
|
||||||
/// create the enhanced item by using `merge` rule
|
|
||||||
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
|
|
||||||
let uid = help::get_uid("m");
|
|
||||||
let file = format!("{uid}.yaml");
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("merge".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc: Some(desc),
|
|
||||||
file: Some(file),
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
option: None,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
|
||||||
file_data: Some(tmpl::ITEM_MERGE.into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Script type (enhance)
|
|
||||||
/// create the enhanced item by using javascript quick.js
|
|
||||||
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
|
|
||||||
let uid = help::get_uid("s");
|
|
||||||
let file = format!("{uid}.js"); // js ext
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("script".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc: Some(desc),
|
|
||||||
file: Some(file),
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
option: None,
|
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
|
||||||
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the file data
|
|
||||||
pub fn read_file(&self) -> Result<String> {
|
|
||||||
if self.file.is_none() {
|
|
||||||
bail!("could not find the file");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = self.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir()?.join(file);
|
|
||||||
fs::read_to_string(path).context("failed to read the file")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the file data
|
|
||||||
pub fn save_file(&self, data: String) -> Result<()> {
|
|
||||||
if self.file.is_none() {
|
|
||||||
bail!("could not find the file");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = self.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir()?.join(file);
|
|
||||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,280 +1,52 @@
|
|||||||
use super::prfitem::PrfItem;
|
|
||||||
use crate::utils::{dirs, help};
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::{fs, io::Write};
|
|
||||||
|
|
||||||
/// Define the `profiles.yaml` schema
|
/// Define the `profiles.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct IProfiles {
|
pub struct ProfilesConfig {
|
||||||
/// same as PrfConfig.current
|
/// current profile's name
|
||||||
pub current: Option<String>,
|
pub current: Option<usize>,
|
||||||
|
|
||||||
/// same as PrfConfig.chain
|
|
||||||
pub chain: Option<Vec<String>>,
|
|
||||||
|
|
||||||
/// record valid fields for clash
|
|
||||||
pub valid: Option<Vec<String>>,
|
|
||||||
|
|
||||||
/// profile list
|
/// profile list
|
||||||
pub items: Option<Vec<PrfItem>>,
|
pub items: Option<Vec<ProfileItem>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! patch {
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
($lv: expr, $rv: expr, $key: tt) => {
|
pub struct ProfileItem {
|
||||||
if ($rv.$key).is_some() {
|
/// profile name
|
||||||
$lv.$key = $rv.$key;
|
pub name: Option<String>,
|
||||||
}
|
/// profile file
|
||||||
};
|
pub file: Option<String>,
|
||||||
|
/// current mode
|
||||||
|
pub mode: Option<String>,
|
||||||
|
/// source url
|
||||||
|
pub url: Option<String>,
|
||||||
|
/// selected infomation
|
||||||
|
pub selected: Option<Vec<ProfileSelected>>,
|
||||||
|
/// user info
|
||||||
|
pub extra: Option<ProfileExtra>,
|
||||||
|
/// updated time
|
||||||
|
pub updated: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IProfiles {
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
pub fn new() -> Self {
|
pub struct ProfileSelected {
|
||||||
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
|
pub name: Option<String>,
|
||||||
Ok(mut profiles) => {
|
pub now: Option<String>,
|
||||||
if profiles.items.is_none() {
|
|
||||||
profiles.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
// compatible with the old old old version
|
|
||||||
profiles.items.as_mut().map(|items| {
|
|
||||||
for item in items.iter_mut() {
|
|
||||||
if item.uid.is_none() {
|
|
||||||
item.uid = Some(help::get_uid("d"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
profiles
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
Self::template()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn template() -> Self {
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
Self {
|
pub struct ProfileExtra {
|
||||||
valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into()]),
|
pub upload: usize,
|
||||||
items: Some(vec![]),
|
pub download: usize,
|
||||||
..Self::default()
|
pub total: usize,
|
||||||
}
|
pub expire: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_file(&self) -> Result<()> {
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
help::save_yaml(
|
/// the result from url
|
||||||
&dirs::profiles_path()?,
|
pub struct ProfileResponse {
|
||||||
self,
|
pub name: String,
|
||||||
Some("# Profiles Config for Clash Verge"),
|
pub file: String,
|
||||||
)
|
pub data: String,
|
||||||
}
|
pub extra: ProfileExtra,
|
||||||
|
|
||||||
/// 只修改current,valid和chain
|
|
||||||
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(current) = patch.current {
|
|
||||||
let items = self.items.as_ref().unwrap();
|
|
||||||
let some_uid = Some(current);
|
|
||||||
|
|
||||||
if items.iter().any(|e| e.uid == some_uid) {
|
|
||||||
self.current = some_uid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(chain) = patch.chain {
|
|
||||||
self.chain = Some(chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(valid) = patch.valid {
|
|
||||||
self.valid = Some(valid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current(&self) -> Option<String> {
|
|
||||||
self.current.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get items ref
|
|
||||||
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
|
||||||
self.items.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// find the item by the uid
|
|
||||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
|
||||||
if let Some(items) = self.items.as_ref() {
|
|
||||||
let some_uid = Some(uid.clone());
|
|
||||||
|
|
||||||
for each in items.iter() {
|
|
||||||
if each.uid == some_uid {
|
|
||||||
return Ok(each);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("failed to get the profile item \"uid:{uid}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// append new item
|
|
||||||
/// if the file_data is some
|
|
||||||
/// then should save the data to file
|
|
||||||
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
|
||||||
if item.uid.is_none() {
|
|
||||||
bail!("the uid should not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the file data
|
|
||||||
// move the field value after save
|
|
||||||
if let Some(file_data) = item.file_data.take() {
|
|
||||||
if item.file.is_none() {
|
|
||||||
bail!("the file should not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = item.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
|
||||||
|
|
||||||
fs::File::create(path)
|
|
||||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
|
||||||
.write(file_data.as_bytes())
|
|
||||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items.as_mut().map(|items| items.push(item));
|
|
||||||
self.save_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the item value
|
|
||||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
|
||||||
let mut items = self.items.take().unwrap_or(vec![]);
|
|
||||||
|
|
||||||
for each in items.iter_mut() {
|
|
||||||
if each.uid == Some(uid.clone()) {
|
|
||||||
patch!(each, item, itype);
|
|
||||||
patch!(each, item, name);
|
|
||||||
patch!(each, item, desc);
|
|
||||||
patch!(each, item, file);
|
|
||||||
patch!(each, item, url);
|
|
||||||
patch!(each, item, selected);
|
|
||||||
patch!(each, item, extra);
|
|
||||||
patch!(each, item, updated);
|
|
||||||
patch!(each, item, option);
|
|
||||||
|
|
||||||
self.items = Some(items);
|
|
||||||
return self.save_file();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items = Some(items);
|
|
||||||
bail!("failed to find the profile item \"uid:{uid}\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// be used to update the remote item
|
|
||||||
/// only patch `updated` `extra` `file_data`
|
|
||||||
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the item
|
|
||||||
let _ = self.get_item(&uid)?;
|
|
||||||
|
|
||||||
if let Some(items) = self.items.as_mut() {
|
|
||||||
let some_uid = Some(uid.clone());
|
|
||||||
|
|
||||||
for each in items.iter_mut() {
|
|
||||||
if each.uid == some_uid {
|
|
||||||
each.extra = item.extra;
|
|
||||||
each.updated = item.updated;
|
|
||||||
|
|
||||||
// save the file data
|
|
||||||
// move the field value after save
|
|
||||||
if let Some(file_data) = item.file_data.take() {
|
|
||||||
let file = each.file.take();
|
|
||||||
let file =
|
|
||||||
file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
|
|
||||||
|
|
||||||
// the file must exists
|
|
||||||
each.file = Some(file.clone());
|
|
||||||
|
|
||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
|
||||||
|
|
||||||
fs::File::create(path)
|
|
||||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
|
||||||
.write(file_data.as_bytes())
|
|
||||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// delete item
|
|
||||||
/// if delete the current then return true
|
|
||||||
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
|
||||||
let current = self.current.as_ref().unwrap_or(&uid);
|
|
||||||
let current = current.clone();
|
|
||||||
|
|
||||||
let mut items = self.items.take().unwrap_or(vec![]);
|
|
||||||
let mut index = None;
|
|
||||||
|
|
||||||
// get the index
|
|
||||||
for i in 0..items.len() {
|
|
||||||
if items[i].uid == Some(uid.clone()) {
|
|
||||||
index = Some(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(index) = index {
|
|
||||||
items.remove(index).file.map(|file| {
|
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
|
||||||
let path = path.join(file);
|
|
||||||
if path.exists() {
|
|
||||||
let _ = fs::remove_file(path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the original uid
|
|
||||||
if current == uid {
|
|
||||||
self.current = match items.len() > 0 {
|
|
||||||
true => items[0].uid.clone(),
|
|
||||||
false => None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items = Some(items);
|
|
||||||
self.save_file()?;
|
|
||||||
Ok(current == uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取current指向的配置内容
|
|
||||||
pub fn current_mapping(&self) -> Result<Mapping> {
|
|
||||||
match (self.current.as_ref(), self.items.as_ref()) {
|
|
||||||
(Some(current), Some(items)) => {
|
|
||||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
|
||||||
let file_path = match item.file.as_ref() {
|
|
||||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
|
||||||
None => bail!("failed to get the file field"),
|
|
||||||
};
|
|
||||||
return Ok(help::read_merge_mapping(&file_path)?);
|
|
||||||
}
|
|
||||||
bail!("failed to find the current profile \"uid:{current}\"");
|
|
||||||
}
|
|
||||||
_ => Ok(Mapping::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct IRuntime {
|
|
||||||
pub config: Option<Mapping>,
|
|
||||||
// 记录在配置中(包括merge和script生成的)出现过的keys
|
|
||||||
// 这些keys不一定都生效
|
|
||||||
pub exists_keys: Vec<String>,
|
|
||||||
pub chain_logs: HashMap<String, Vec<(String, String)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IRuntime {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里只更改 allow-lan | ipv6 | log-level
|
|
||||||
pub fn patch_config(&mut self, patch: Mapping) {
|
|
||||||
if let Some(config) = self.config.as_mut() {
|
|
||||||
["allow-lan", "ipv6", "log-level"]
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|key| {
|
|
||||||
if let Some(value) = patch.get(key).to_owned() {
|
|
||||||
config.insert(key.into(), value.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,224 +1,14 @@
|
|||||||
use crate::utils::{dirs, help};
|
|
||||||
use anyhow::Result;
|
|
||||||
use log::LevelFilter;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// ### `verge.yaml` schema
|
/// ### `verge.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct IVerge {
|
pub struct VergeConfig {
|
||||||
/// app listening port for app singleton
|
/// `light` or `dark`
|
||||||
pub app_singleton_port: Option<u16>,
|
|
||||||
|
|
||||||
/// app log level
|
|
||||||
/// silent | error | warn | info | debug | trace
|
|
||||||
pub app_log_level: Option<String>,
|
|
||||||
|
|
||||||
// i18n
|
|
||||||
pub language: Option<String>,
|
|
||||||
|
|
||||||
/// `light` or `dark` or `system`
|
|
||||||
pub theme_mode: Option<String>,
|
pub theme_mode: Option<String>,
|
||||||
|
|
||||||
/// enable blur mode
|
|
||||||
/// maybe be able to set the alpha
|
|
||||||
pub theme_blur: Option<bool>,
|
|
||||||
|
|
||||||
/// enable traffic graph default is true
|
|
||||||
pub traffic_graph: Option<bool>,
|
|
||||||
|
|
||||||
/// show memory info (only for Clash Meta)
|
|
||||||
pub enable_memory_usage: Option<bool>,
|
|
||||||
|
|
||||||
/// clash tun mode
|
|
||||||
pub enable_tun_mode: Option<bool>,
|
|
||||||
|
|
||||||
/// windows service mode
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub enable_service_mode: Option<bool>,
|
|
||||||
|
|
||||||
/// can the app auto startup
|
/// can the app auto startup
|
||||||
pub enable_auto_launch: Option<bool>,
|
pub enable_self_startup: Option<bool>,
|
||||||
|
|
||||||
/// not show the window on launch
|
|
||||||
pub enable_silent_start: Option<bool>,
|
|
||||||
|
|
||||||
/// set system proxy
|
/// set system proxy
|
||||||
pub enable_system_proxy: Option<bool>,
|
pub enable_system_proxy: Option<bool>,
|
||||||
|
|
||||||
/// enable proxy guard
|
|
||||||
pub enable_proxy_guard: Option<bool>,
|
|
||||||
|
|
||||||
/// set system proxy bypass
|
|
||||||
pub system_proxy_bypass: Option<String>,
|
|
||||||
|
|
||||||
/// proxy guard duration
|
|
||||||
pub proxy_guard_duration: Option<u64>,
|
|
||||||
|
|
||||||
/// theme setting
|
|
||||||
pub theme_setting: Option<IVergeTheme>,
|
|
||||||
|
|
||||||
/// web ui list
|
|
||||||
pub web_ui_list: Option<Vec<String>>,
|
|
||||||
|
|
||||||
/// clash core path
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub clash_core: Option<String>,
|
|
||||||
|
|
||||||
/// hotkey map
|
|
||||||
/// format: {func},{key}
|
|
||||||
pub hotkeys: Option<Vec<String>>,
|
|
||||||
|
|
||||||
/// 切换代理时自动关闭连接
|
|
||||||
pub auto_close_connection: Option<bool>,
|
|
||||||
|
|
||||||
/// 默认的延迟测试连接
|
|
||||||
pub default_latency_test: Option<String>,
|
|
||||||
|
|
||||||
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为真
|
|
||||||
pub enable_clash_fields: Option<bool>,
|
|
||||||
|
|
||||||
/// 是否使用内部的脚本支持,默认为真
|
|
||||||
pub enable_builtin_enhanced: Option<bool>,
|
|
||||||
|
|
||||||
/// proxy 页面布局 列数
|
|
||||||
pub proxy_layout_column: Option<i32>,
|
|
||||||
|
|
||||||
/// 日志清理
|
|
||||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
|
||||||
pub auto_log_clean: Option<i32>,
|
|
||||||
|
|
||||||
/// window size and position
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub window_size_position: Option<Vec<f64>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct IVergeTheme {
|
|
||||||
pub primary_color: Option<String>,
|
|
||||||
pub secondary_color: Option<String>,
|
|
||||||
pub primary_text: Option<String>,
|
|
||||||
pub secondary_text: Option<String>,
|
|
||||||
|
|
||||||
pub info_color: Option<String>,
|
|
||||||
pub error_color: Option<String>,
|
|
||||||
pub warning_color: Option<String>,
|
|
||||||
pub success_color: Option<String>,
|
|
||||||
|
|
||||||
pub font_family: Option<String>,
|
|
||||||
pub css_injection: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IVerge {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
Self::template()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn template() -> Self {
|
|
||||||
Self {
|
|
||||||
clash_core: match cfg!(feature = "default-meta") {
|
|
||||||
false => Some("clash".into()),
|
|
||||||
true => Some("clash-meta".into()),
|
|
||||||
},
|
|
||||||
language: match cfg!(feature = "default-meta") {
|
|
||||||
false => Some("en".into()),
|
|
||||||
true => Some("zh".into()),
|
|
||||||
},
|
|
||||||
theme_mode: Some("system".into()),
|
|
||||||
theme_blur: Some(false),
|
|
||||||
traffic_graph: Some(true),
|
|
||||||
enable_memory_usage: Some(true),
|
|
||||||
enable_auto_launch: Some(false),
|
|
||||||
enable_silent_start: Some(false),
|
|
||||||
enable_system_proxy: Some(false),
|
|
||||||
enable_proxy_guard: Some(false),
|
|
||||||
proxy_guard_duration: Some(30),
|
|
||||||
auto_close_connection: Some(true),
|
|
||||||
enable_builtin_enhanced: Some(true),
|
|
||||||
enable_clash_fields: Some(true),
|
|
||||||
auto_log_clean: Some(3),
|
|
||||||
..Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save IVerge App Config
|
|
||||||
pub fn save_file(&self) -> Result<()> {
|
|
||||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// patch verge config
|
|
||||||
/// only save to file
|
|
||||||
pub fn patch_config(&mut self, patch: IVerge) {
|
|
||||||
macro_rules! patch {
|
|
||||||
($key: tt) => {
|
|
||||||
if patch.$key.is_some() {
|
|
||||||
self.$key = patch.$key;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
patch!(app_log_level);
|
|
||||||
patch!(language);
|
|
||||||
patch!(theme_mode);
|
|
||||||
patch!(theme_blur);
|
|
||||||
patch!(traffic_graph);
|
|
||||||
patch!(enable_memory_usage);
|
|
||||||
|
|
||||||
patch!(enable_tun_mode);
|
|
||||||
patch!(enable_service_mode);
|
|
||||||
patch!(enable_auto_launch);
|
|
||||||
patch!(enable_silent_start);
|
|
||||||
patch!(enable_system_proxy);
|
|
||||||
patch!(enable_proxy_guard);
|
|
||||||
patch!(system_proxy_bypass);
|
|
||||||
patch!(proxy_guard_duration);
|
|
||||||
|
|
||||||
patch!(theme_setting);
|
|
||||||
patch!(web_ui_list);
|
|
||||||
patch!(clash_core);
|
|
||||||
patch!(hotkeys);
|
|
||||||
|
|
||||||
patch!(auto_close_connection);
|
|
||||||
patch!(default_latency_test);
|
|
||||||
patch!(enable_builtin_enhanced);
|
|
||||||
patch!(proxy_layout_column);
|
|
||||||
patch!(enable_clash_fields);
|
|
||||||
patch!(auto_log_clean);
|
|
||||||
patch!(window_size_position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
|
||||||
pub fn get_singleton_port() -> u16 {
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
|
||||||
const SERVER_PORT: u16 = 33331;
|
|
||||||
#[cfg(feature = "verge-dev")]
|
|
||||||
const SERVER_PORT: u16 = 11233;
|
|
||||||
|
|
||||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
|
||||||
Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
|
|
||||||
Err(_) => SERVER_PORT, // 这里就不log错误了
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取日志等级
|
|
||||||
pub fn get_log_level(&self) -> LevelFilter {
|
|
||||||
if let Some(level) = self.app_log_level.as_ref() {
|
|
||||||
match level.to_lowercase().as_str() {
|
|
||||||
"silent" => LevelFilter::Off,
|
|
||||||
"error" => LevelFilter::Error,
|
|
||||||
"warn" => LevelFilter::Warn,
|
|
||||||
"info" => LevelFilter::Info,
|
|
||||||
"debug" => LevelFilter::Debug,
|
|
||||||
"trace" => LevelFilter::Trace,
|
|
||||||
_ => LevelFilter::Info,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LevelFilter::Info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// PUT /configs
|
|
||||||
/// path 是绝对路径
|
|
||||||
pub async fn put_configs(path: &str) -> Result<()> {
|
|
||||||
let (url, headers) = clash_client_info()?;
|
|
||||||
let url = format!("{url}/configs");
|
|
||||||
|
|
||||||
let mut data = HashMap::new();
|
|
||||||
data.insert("path", path);
|
|
||||||
|
|
||||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
|
||||||
let builder = client.put(&url).headers(headers).json(&data);
|
|
||||||
let response = builder.send().await?;
|
|
||||||
|
|
||||||
match response.status().as_u16() {
|
|
||||||
204 => Ok(()),
|
|
||||||
status @ _ => {
|
|
||||||
bail!("failed to put configs with status \"{status}\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PATCH /configs
|
|
||||||
pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
|
||||||
let (url, headers) = clash_client_info()?;
|
|
||||||
let url = format!("{url}/configs");
|
|
||||||
|
|
||||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
|
||||||
let builder = client.patch(&url).headers(headers.clone()).json(config);
|
|
||||||
builder.send().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct DelayRes {
|
|
||||||
delay: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET /proxies/{name}/delay
|
|
||||||
/// 获取代理延迟
|
|
||||||
pub async fn get_proxy_delay(name: String, test_url: Option<String>) -> Result<DelayRes> {
|
|
||||||
let (url, headers) = clash_client_info()?;
|
|
||||||
let url = format!("{url}/proxies/{name}/delay");
|
|
||||||
|
|
||||||
let default_url = "http://www.gstatic.com/generate_204";
|
|
||||||
let test_url = test_url
|
|
||||||
.map(|s| if s.is_empty() { default_url.into() } else { s })
|
|
||||||
.unwrap_or(default_url.into());
|
|
||||||
|
|
||||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
|
||||||
let builder = client
|
|
||||||
.get(&url)
|
|
||||||
.headers(headers)
|
|
||||||
.query(&[("timeout", "10000"), ("url", &test_url)]);
|
|
||||||
let response = builder.send().await?;
|
|
||||||
|
|
||||||
Ok(response.json::<DelayRes>().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 根据clash info获取clash服务地址和请求头
|
|
||||||
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
|
||||||
let client = { Config::clash().data().get_client_info() };
|
|
||||||
|
|
||||||
let server = format!("http://{}", client.server);
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("Content-Type", "application/json".parse()?);
|
|
||||||
|
|
||||||
if let Some(secret) = client.secret {
|
|
||||||
let secret = format!("Bearer {}", secret).parse()?;
|
|
||||||
headers.insert("Authorization", secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((server, headers))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 缩短clash的日志
|
|
||||||
pub fn parse_log(log: String) -> String {
|
|
||||||
if log.starts_with("time=") && log.len() > 33 {
|
|
||||||
return (&log[33..]).to_owned();
|
|
||||||
}
|
|
||||||
if log.len() > 9 {
|
|
||||||
return (&log[9..]).to_owned();
|
|
||||||
}
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 缩短clash -t的错误输出
|
|
||||||
/// 仅适配 clash p核 8-26、clash meta 1.13.1
|
|
||||||
pub fn parse_check_output(log: String) -> String {
|
|
||||||
let t = log.find("time=");
|
|
||||||
let m = log.find("msg=");
|
|
||||||
let mr = log.rfind('"');
|
|
||||||
|
|
||||||
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
|
|
||||||
let e = match log.find("level=error msg=") {
|
|
||||||
Some(e) => e + 17,
|
|
||||||
None => m + 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
if mr > m {
|
|
||||||
return (&log[e..mr]).to_owned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let l = log.find("error=");
|
|
||||||
let r = log.find("path=").or(Some(log.len()));
|
|
||||||
|
|
||||||
if let (Some(l), Some(r)) = (l, r) {
|
|
||||||
return (&log[(l + 6)..(r - 1)]).to_owned();
|
|
||||||
}
|
|
||||||
|
|
||||||
log
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_check_output() {
|
|
||||||
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
|
|
||||||
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
|
|
||||||
let str3 = r#"
|
|
||||||
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
|
|
||||||
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
|
|
||||||
configuration file xxx\n
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let res1 = parse_check_output(str1.into());
|
|
||||||
let res2 = parse_check_output(str2.into());
|
|
||||||
let res3 = parse_check_output(str3.into());
|
|
||||||
|
|
||||||
println!("res1: {res1}");
|
|
||||||
println!("res2: {res2}");
|
|
||||||
println!("res3: {res3}");
|
|
||||||
|
|
||||||
assert_eq!(res1, res3);
|
|
||||||
}
|
|
@ -1,325 +0,0 @@
|
|||||||
use super::{clash_api, logger::Logger};
|
|
||||||
use crate::log_err;
|
|
||||||
use crate::{config::*, utils::dirs};
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::{fs, io::Write, sync::Arc, time::Duration};
|
|
||||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
|
||||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CoreManager {
|
|
||||||
sidecar: Arc<Mutex<Option<CommandChild>>>,
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
use_service_mode: Arc<Mutex<bool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoreManager {
|
|
||||||
pub fn global() -> &'static CoreManager {
|
|
||||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
|
||||||
|
|
||||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
|
||||||
sidecar: Arc::new(Mutex::new(None)),
|
|
||||||
use_service_mode: Arc::new(Mutex::new(false)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(&self) -> Result<()> {
|
|
||||||
// kill old clash process
|
|
||||||
let _ = dirs::clash_pid_path()
|
|
||||||
.and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
|
|
||||||
.and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
|
|
||||||
.map(|pid| {
|
|
||||||
let mut system = System::new();
|
|
||||||
system.refresh_all();
|
|
||||||
system.process(Pid::from_u32(pid)).map(|proc| {
|
|
||||||
if proc.name().contains("clash") {
|
|
||||||
log::debug!(target: "app", "kill old clash process");
|
|
||||||
proc.kill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
// 启动clash
|
|
||||||
log_err!(Self::global().run_core().await);
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查配置是否正确
|
|
||||||
pub fn check_config(&self) -> Result<()> {
|
|
||||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
|
||||||
let config_path = dirs::path_to_str(&config_path)?;
|
|
||||||
|
|
||||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
|
||||||
let clash_core = clash_core.unwrap_or("clash".into());
|
|
||||||
|
|
||||||
let app_dir = dirs::app_home_dir()?;
|
|
||||||
let app_dir = dirs::path_to_str(&app_dir)?;
|
|
||||||
|
|
||||||
let output = Command::new_sidecar(clash_core)?
|
|
||||||
.args(["-t", "-d", app_dir, "-f", config_path])
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let error = clash_api::parse_check_output(output.stdout.clone());
|
|
||||||
let error = match error.len() > 0 {
|
|
||||||
true => error,
|
|
||||||
false => output.stdout.clone(),
|
|
||||||
};
|
|
||||||
Logger::global().set_log(output.stdout);
|
|
||||||
bail!("{error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 启动核心
|
|
||||||
pub async fn run_core(&self) -> Result<()> {
|
|
||||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut should_kill = match self.sidecar.lock().take() {
|
|
||||||
Some(child) => {
|
|
||||||
log::debug!(target: "app", "stop the core by sidecar");
|
|
||||||
let _ = child.kill();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if *self.use_service_mode.lock() {
|
|
||||||
log::debug!(target: "app", "stop the core by service");
|
|
||||||
log_err!(super::win_service::stop_core_by_service().await);
|
|
||||||
should_kill = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里得等一会儿
|
|
||||||
if should_kill {
|
|
||||||
sleep(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use super::win_service;
|
|
||||||
|
|
||||||
// 服务模式
|
|
||||||
let enable = { Config::verge().latest().enable_service_mode.clone() };
|
|
||||||
let enable = enable.unwrap_or(false);
|
|
||||||
|
|
||||||
*self.use_service_mode.lock() = enable;
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
// 服务模式启动失败就直接运行sidecar
|
|
||||||
log::debug!(target: "app", "try to run core in service mode");
|
|
||||||
|
|
||||||
match (|| async {
|
|
||||||
win_service::check_service().await?;
|
|
||||||
win_service::run_core_by_service(&config_path).await
|
|
||||||
})()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => return Ok(()),
|
|
||||||
Err(err) => {
|
|
||||||
// 修改这个值,免得stop出错
|
|
||||||
*self.use_service_mode.lock() = false;
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_dir = dirs::app_home_dir()?;
|
|
||||||
let app_dir = dirs::path_to_str(&app_dir)?;
|
|
||||||
|
|
||||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
|
||||||
let clash_core = clash_core.unwrap_or("clash".into());
|
|
||||||
let is_clash = clash_core == "clash";
|
|
||||||
|
|
||||||
let config_path = dirs::path_to_str(&config_path)?;
|
|
||||||
|
|
||||||
// fix #212
|
|
||||||
let args = match clash_core.as_str() {
|
|
||||||
"clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
|
|
||||||
_ => vec!["-d", app_dir, "-f", config_path],
|
|
||||||
};
|
|
||||||
|
|
||||||
let cmd = Command::new_sidecar(clash_core)?;
|
|
||||||
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
|
|
||||||
|
|
||||||
// 将pid写入文件中
|
|
||||||
crate::log_err!((|| {
|
|
||||||
let pid = cmd_child.pid();
|
|
||||||
let path = dirs::clash_pid_path()?;
|
|
||||||
fs::File::create(path)
|
|
||||||
.context("failed to create the pid file")?
|
|
||||||
.write(format!("{pid}").as_bytes())
|
|
||||||
.context("failed to write pid to the file")?;
|
|
||||||
<Result<()>>::Ok(())
|
|
||||||
})());
|
|
||||||
|
|
||||||
let mut sidecar = self.sidecar.lock();
|
|
||||||
*sidecar = Some(cmd_child);
|
|
||||||
drop(sidecar);
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
while let Some(event) = rx.recv().await {
|
|
||||||
match event {
|
|
||||||
CommandEvent::Stdout(line) => {
|
|
||||||
if is_clash {
|
|
||||||
let stdout = clash_api::parse_log(line.clone());
|
|
||||||
log::info!(target: "app", "[clash]: {stdout}");
|
|
||||||
} else {
|
|
||||||
log::info!(target: "app", "[clash]: {line}");
|
|
||||||
};
|
|
||||||
Logger::global().set_log(line);
|
|
||||||
}
|
|
||||||
CommandEvent::Stderr(err) => {
|
|
||||||
// let stdout = clash_api::parse_log(err.clone());
|
|
||||||
log::error!(target: "app", "[clash]: {err}");
|
|
||||||
Logger::global().set_log(err);
|
|
||||||
}
|
|
||||||
CommandEvent::Error(err) => {
|
|
||||||
log::error!(target: "app", "[clash]: {err}");
|
|
||||||
Logger::global().set_log(err);
|
|
||||||
}
|
|
||||||
CommandEvent::Terminated(_) => {
|
|
||||||
log::info!(target: "app", "clash core terminated");
|
|
||||||
let _ = CoreManager::global().recover_core();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重启内核
|
|
||||||
pub fn recover_core(&'static self) -> Result<()> {
|
|
||||||
// 服务模式不管
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if *self.use_service_mode.lock() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空原来的sidecar值
|
|
||||||
if let Some(sidecar) = self.sidecar.lock().take() {
|
|
||||||
let _ = sidecar.kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
// 6秒之后再查看服务是否正常 (时间随便搞的)
|
|
||||||
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
|
|
||||||
sleep(Duration::from_millis(6666)).await;
|
|
||||||
|
|
||||||
if self.sidecar.lock().is_none() {
|
|
||||||
log::info!(target: "app", "recover clash core");
|
|
||||||
|
|
||||||
// 重新启动app
|
|
||||||
if let Err(err) = self.run_core().await {
|
|
||||||
log::error!(target: "app", "failed to recover clash core");
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
|
|
||||||
let _ = self.recover_core();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 停止核心运行
|
|
||||||
pub fn stop_core(&self) -> Result<()> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if *self.use_service_mode.lock() {
|
|
||||||
log::debug!(target: "app", "stop the core by service");
|
|
||||||
tauri::async_runtime::block_on(async move {
|
|
||||||
log_err!(super::win_service::stop_core_by_service().await);
|
|
||||||
});
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sidecar = self.sidecar.lock();
|
|
||||||
if let Some(child) = sidecar.take() {
|
|
||||||
log::debug!(target: "app", "stop the core by sidecar");
|
|
||||||
let _ = child.kill();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 切换核心
|
|
||||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
|
||||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
|
||||||
|
|
||||||
if &clash_core != "clash" && &clash_core != "clash-meta" {
|
|
||||||
bail!("invalid clash core name \"{clash_core}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!(target: "app", "change core to `{clash_core}`");
|
|
||||||
|
|
||||||
Config::verge().draft().clash_core = Some(clash_core);
|
|
||||||
|
|
||||||
// 更新配置
|
|
||||||
Config::generate()?;
|
|
||||||
|
|
||||||
self.check_config()?;
|
|
||||||
|
|
||||||
// 清掉旧日志
|
|
||||||
Logger::global().clear_log();
|
|
||||||
|
|
||||||
match self.run_core().await {
|
|
||||||
Ok(_) => {
|
|
||||||
Config::verge().apply();
|
|
||||||
Config::runtime().apply();
|
|
||||||
log_err!(Config::verge().latest().save_file());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
Config::verge().discard();
|
|
||||||
Config::runtime().discard();
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 更新proxies那些
|
|
||||||
/// 如果涉及端口和外部控制则需要重启
|
|
||||||
pub async fn update_config(&self) -> Result<()> {
|
|
||||||
log::debug!(target: "app", "try to update clash config");
|
|
||||||
|
|
||||||
// 更新配置
|
|
||||||
Config::generate()?;
|
|
||||||
|
|
||||||
// 检查配置是否正常
|
|
||||||
self.check_config()?;
|
|
||||||
|
|
||||||
// 更新运行时配置
|
|
||||||
let path = Config::generate_file(ConfigType::Run)?;
|
|
||||||
let path = dirs::path_to_str(&path)?;
|
|
||||||
|
|
||||||
// 发送请求 发送5次
|
|
||||||
for i in 0..5 {
|
|
||||||
match clash_api::put_configs(path).await {
|
|
||||||
Ok(_) => break,
|
|
||||||
Err(err) => {
|
|
||||||
if i < 4 {
|
|
||||||
log::info!(target: "app", "{err}");
|
|
||||||
} else {
|
|
||||||
bail!(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(250)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
use super::tray::Tray;
|
|
||||||
use crate::log_err;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tauri::{AppHandle, Manager, Window};
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct Handle {
|
|
||||||
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle {
|
|
||||||
pub fn global() -> &'static Handle {
|
|
||||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
|
||||||
|
|
||||||
HANDLE.get_or_init(|| Handle {
|
|
||||||
app_handle: Arc::new(Mutex::new(None)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(&self, app_handle: AppHandle) {
|
|
||||||
*self.app_handle.lock() = Some(app_handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_window(&self) -> Option<Window> {
|
|
||||||
self.app_handle
|
|
||||||
.lock()
|
|
||||||
.as_ref()
|
|
||||||
.map_or(None, |a| a.get_window("main"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh_clash() {
|
|
||||||
if let Some(window) = Self::global().get_window() {
|
|
||||||
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh_verge() {
|
|
||||||
if let Some(window) = Self::global().get_window() {
|
|
||||||
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn refresh_profiles() {
|
|
||||||
if let Some(window) = Self::global().get_window() {
|
|
||||||
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
|
||||||
if let Some(window) = Self::global().get_window() {
|
|
||||||
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_systray() -> Result<()> {
|
|
||||||
let app_handle = Self::global().app_handle.lock();
|
|
||||||
if app_handle.is_none() {
|
|
||||||
bail!("update_systray unhandled error");
|
|
||||||
}
|
|
||||||
Tray::update_systray(app_handle.as_ref().unwrap())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the system tray state
|
|
||||||
pub fn update_systray_part() -> Result<()> {
|
|
||||||
let app_handle = Self::global().app_handle.lock();
|
|
||||||
if app_handle.is_none() {
|
|
||||||
bail!("update_systray unhandled error");
|
|
||||||
}
|
|
||||||
Tray::update_part(app_handle.as_ref().unwrap())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
use crate::{config::Config, feat, log_err};
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
use tauri::{AppHandle, GlobalShortcutManager};
|
|
||||||
use wry::application::accelerator::Accelerator;
|
|
||||||
|
|
||||||
pub struct Hotkey {
|
|
||||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
|
||||||
|
|
||||||
app_handle: Arc<Mutex<Option<AppHandle>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hotkey {
|
|
||||||
pub fn global() -> &'static Hotkey {
|
|
||||||
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
|
|
||||||
|
|
||||||
HOTKEY.get_or_init(|| Hotkey {
|
|
||||||
current: Arc::new(Mutex::new(Vec::new())),
|
|
||||||
app_handle: Arc::new(Mutex::new(None)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(&self, app_handle: AppHandle) -> Result<()> {
|
|
||||||
*self.app_handle.lock() = Some(app_handle);
|
|
||||||
|
|
||||||
let verge = Config::verge();
|
|
||||||
|
|
||||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
|
||||||
for hotkey in hotkeys.iter() {
|
|
||||||
let mut iter = hotkey.split(',');
|
|
||||||
let func = iter.next();
|
|
||||||
let key = iter.next();
|
|
||||||
|
|
||||||
match (key, func) {
|
|
||||||
(Some(key), Some(func)) => {
|
|
||||||
log_err!(Self::check_key(key).and_then(|_| self.register(key, func)));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let key = key.unwrap_or("None");
|
|
||||||
let func = func.unwrap_or("None");
|
|
||||||
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*self.current.lock() = hotkeys.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查一个键是否合法
|
|
||||||
fn check_key(hotkey: &str) -> Result<()> {
|
|
||||||
// fix #287
|
|
||||||
// tauri的这几个方法全部有Result expect,会panic,先检测一遍避免挂了
|
|
||||||
if hotkey.parse::<Accelerator>().is_err() {
|
|
||||||
bail!("invalid hotkey `{hotkey}`");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
|
|
||||||
let app_handle = self.app_handle.lock();
|
|
||||||
if app_handle.is_none() {
|
|
||||||
bail!("failed to get the hotkey manager");
|
|
||||||
}
|
|
||||||
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
|
||||||
let mut manager = self.get_manager()?;
|
|
||||||
|
|
||||||
if manager.is_registered(hotkey)? {
|
|
||||||
manager.unregister(hotkey)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let f = match func.trim() {
|
|
||||||
"open_dashboard" => || feat::open_dashboard(),
|
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
|
||||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
|
||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
|
||||||
"clash_mode_script" => || feat::change_clash_mode("script".into()),
|
|
||||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
|
||||||
"enable_system_proxy" => || feat::enable_system_proxy(),
|
|
||||||
"disable_system_proxy" => || feat::disable_system_proxy(),
|
|
||||||
"toggle_tun_mode" => || feat::toggle_tun_mode(),
|
|
||||||
"enable_tun_mode" => || feat::enable_tun_mode(),
|
|
||||||
"disable_tun_mode" => || feat::disable_tun_mode(),
|
|
||||||
|
|
||||||
_ => bail!("invalid function \"{func}\""),
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.register(hotkey, f)?;
|
|
||||||
log::info!(target: "app", "register hotkey {hotkey} {func}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unregister(&self, hotkey: &str) -> Result<()> {
|
|
||||||
self.get_manager()?.unregister(&hotkey)?;
|
|
||||||
log::info!(target: "app", "unregister hotkey {hotkey}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
|
||||||
let mut current = self.current.lock();
|
|
||||||
let old_map = Self::get_map_from_vec(¤t);
|
|
||||||
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
|
||||||
|
|
||||||
let (del, add) = Self::get_diff(old_map, new_map);
|
|
||||||
|
|
||||||
// 先检查一遍所有新的热键是不是可以用的
|
|
||||||
for (hotkey, _) in add.iter() {
|
|
||||||
Self::check_key(hotkey)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
del.iter().for_each(|key| {
|
|
||||||
let _ = self.unregister(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
add.iter().for_each(|(key, func)| {
|
|
||||||
log_err!(self.register(key, func));
|
|
||||||
});
|
|
||||||
|
|
||||||
*current = new_hotkeys;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
|
|
||||||
hotkeys.iter().for_each(|hotkey| {
|
|
||||||
let mut iter = hotkey.split(',');
|
|
||||||
let func = iter.next();
|
|
||||||
let key = iter.next();
|
|
||||||
|
|
||||||
if func.is_some() && key.is_some() {
|
|
||||||
let func = func.unwrap().trim();
|
|
||||||
let key = key.unwrap().trim();
|
|
||||||
map.insert(key, func);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
map
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_diff<'a>(
|
|
||||||
old_map: HashMap<&'a str, &'a str>,
|
|
||||||
new_map: HashMap<&'a str, &'a str>,
|
|
||||||
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
|
|
||||||
let mut del_list = vec![];
|
|
||||||
let mut add_list = vec![];
|
|
||||||
|
|
||||||
old_map.iter().for_each(|(&key, func)| {
|
|
||||||
match new_map.get(key) {
|
|
||||||
Some(new_func) => {
|
|
||||||
if new_func != func {
|
|
||||||
del_list.push(key);
|
|
||||||
add_list.push((key, *new_func));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => del_list.push(key),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
new_map.iter().for_each(|(&key, &func)| {
|
|
||||||
if old_map.get(key).is_none() {
|
|
||||||
add_list.push((key, func));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(del_list, add_list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Hotkey {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Ok(mut manager) = self.get_manager() {
|
|
||||||
let _ = manager.unregister_all();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::{collections::VecDeque, sync::Arc};
|
|
||||||
|
|
||||||
const LOGS_QUEUE_LEN: usize = 100;
|
|
||||||
|
|
||||||
pub struct Logger {
|
|
||||||
log_data: Arc<Mutex<VecDeque<String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Logger {
|
|
||||||
pub fn global() -> &'static Logger {
|
|
||||||
static LOGGER: OnceCell<Logger> = OnceCell::new();
|
|
||||||
|
|
||||||
LOGGER.get_or_init(|| Logger {
|
|
||||||
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_log(&self) -> VecDeque<String> {
|
|
||||||
self.log_data.lock().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_log(&self, text: String) {
|
|
||||||
let mut logs = self.log_data.lock();
|
|
||||||
if logs.len() > LOGS_QUEUE_LEN {
|
|
||||||
logs.pop_front();
|
|
||||||
}
|
|
||||||
logs.push_back(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_log(&self) {
|
|
||||||
let mut logs = self.log_data.lock();
|
|
||||||
logs.clear();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
/// 给clash内核的tun模式授权
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
|
||||||
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
|
||||||
use std::process::Command;
|
|
||||||
use tauri::utils::platform::current_exe;
|
|
||||||
|
|
||||||
let path = current_exe()?.with_file_name(core).canonicalize()?;
|
|
||||||
let path = path.display().to_string();
|
|
||||||
|
|
||||||
log::debug!("grant_permission path: {path}");
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let output = {
|
|
||||||
// the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash
|
|
||||||
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
|
|
||||||
// let path = escape(&path);
|
|
||||||
let path = path.replace(' ', "\\\\ ");
|
|
||||||
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
|
||||||
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
|
||||||
Command::new("osascript")
|
|
||||||
.args(vec!["-e", &command])
|
|
||||||
.output()?
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let output = {
|
|
||||||
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
|
||||||
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
|
|
||||||
|
|
||||||
let sudo = match Command::new("which").arg("pkexec").output() {
|
|
||||||
Ok(output) => {
|
|
||||||
if output.stdout.is_empty() {
|
|
||||||
"sudo"
|
|
||||||
} else {
|
|
||||||
"pkexec"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "sudo",
|
|
||||||
};
|
|
||||||
|
|
||||||
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
|
|
||||||
};
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
|
|
||||||
anyhow::bail!("{stderr}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn escape<'a>(text: &'a str) -> Cow<'a, str> {
|
|
||||||
let bytes = text.as_bytes();
|
|
||||||
|
|
||||||
let mut owned = None;
|
|
||||||
|
|
||||||
for pos in 0..bytes.len() {
|
|
||||||
let special = match bytes[pos] {
|
|
||||||
b' ' => Some(b' '),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(s) = special {
|
|
||||||
if owned.is_none() {
|
|
||||||
owned = Some(bytes[0..pos].to_owned());
|
|
||||||
}
|
|
||||||
owned.as_mut().unwrap().push(b'\\');
|
|
||||||
owned.as_mut().unwrap().push(b'\\');
|
|
||||||
owned.as_mut().unwrap().push(s);
|
|
||||||
} else if let Some(owned) = owned.as_mut() {
|
|
||||||
owned.push(bytes[pos]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(owned) = owned {
|
|
||||||
unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) }
|
|
||||||
} else {
|
|
||||||
unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
pub mod clash_api;
|
|
||||||
mod core;
|
|
||||||
pub mod handle;
|
|
||||||
pub mod hotkey;
|
|
||||||
pub mod logger;
|
|
||||||
pub mod manager;
|
|
||||||
pub mod sysopt;
|
|
||||||
pub mod timer;
|
|
||||||
pub mod tray;
|
|
||||||
pub mod win_service;
|
|
||||||
|
|
||||||
pub use self::core::*;
|
|
@ -1,304 +0,0 @@
|
|||||||
use crate::{config::Config, log_err};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use sysproxy::Sysproxy;
|
|
||||||
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
|
|
||||||
|
|
||||||
pub struct Sysopt {
|
|
||||||
/// current system proxy setting
|
|
||||||
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
|
|
||||||
|
|
||||||
/// record the original system proxy
|
|
||||||
/// recover it when exit
|
|
||||||
old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
|
|
||||||
|
|
||||||
/// helps to auto launch the app
|
|
||||||
auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
|
|
||||||
|
|
||||||
/// record whether the guard async is running or not
|
|
||||||
guard_state: Arc<TokioMutex<bool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>";
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,::1";
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
|
|
||||||
|
|
||||||
impl Sysopt {
|
|
||||||
pub fn global() -> &'static Sysopt {
|
|
||||||
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
|
|
||||||
|
|
||||||
SYSOPT.get_or_init(|| Sysopt {
|
|
||||||
cur_sysproxy: Arc::new(Mutex::new(None)),
|
|
||||||
old_sysproxy: Arc::new(Mutex::new(None)),
|
|
||||||
auto_launch: Arc::new(Mutex::new(None)),
|
|
||||||
guard_state: Arc::new(TokioMutex::new(false)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// init the sysproxy
|
|
||||||
pub fn init_sysproxy(&self) -> Result<()> {
|
|
||||||
let port = { Config::clash().latest().get_mixed_port() };
|
|
||||||
|
|
||||||
let (enable, bypass) = {
|
|
||||||
let verge = Config::verge();
|
|
||||||
let verge = verge.latest();
|
|
||||||
(
|
|
||||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
|
||||||
verge.system_proxy_bypass.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let current = Sysproxy {
|
|
||||||
enable,
|
|
||||||
host: String::from("127.0.0.1"),
|
|
||||||
port,
|
|
||||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
|
|
||||||
current.set_system_proxy()?;
|
|
||||||
|
|
||||||
*self.old_sysproxy.lock() = old;
|
|
||||||
*self.cur_sysproxy.lock() = Some(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// run the system proxy guard
|
|
||||||
self.guard_proxy();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the system proxy
|
|
||||||
pub fn update_sysproxy(&self) -> Result<()> {
|
|
||||||
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
|
||||||
let old_sysproxy = self.old_sysproxy.lock();
|
|
||||||
|
|
||||||
if cur_sysproxy.is_none() || old_sysproxy.is_none() {
|
|
||||||
drop(cur_sysproxy);
|
|
||||||
drop(old_sysproxy);
|
|
||||||
return self.init_sysproxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (enable, bypass) = {
|
|
||||||
let verge = Config::verge();
|
|
||||||
let verge = verge.latest();
|
|
||||||
(
|
|
||||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
|
||||||
verge.system_proxy_bypass.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let mut sysproxy = cur_sysproxy.take().unwrap();
|
|
||||||
|
|
||||||
sysproxy.enable = enable;
|
|
||||||
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
|
||||||
|
|
||||||
sysproxy.set_system_proxy()?;
|
|
||||||
*cur_sysproxy = Some(sysproxy);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// reset the sysproxy
|
|
||||||
pub fn reset_sysproxy(&self) -> Result<()> {
|
|
||||||
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
|
||||||
let mut old_sysproxy = self.old_sysproxy.lock();
|
|
||||||
|
|
||||||
let cur_sysproxy = cur_sysproxy.take();
|
|
||||||
|
|
||||||
if let Some(mut old) = old_sysproxy.take() {
|
|
||||||
// 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置
|
|
||||||
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
|
|
||||||
let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
|
|
||||||
|
|
||||||
if old.enable && port_same {
|
|
||||||
old.enable = false;
|
|
||||||
log::info!(target: "app", "reset proxy by disabling the original proxy");
|
|
||||||
} else {
|
|
||||||
log::info!(target: "app", "reset proxy to the original proxy");
|
|
||||||
}
|
|
||||||
|
|
||||||
old.set_system_proxy()?;
|
|
||||||
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
|
|
||||||
// 没有原代理,就按现在的代理设置disable即可
|
|
||||||
log::info!(target: "app", "reset proxy by disabling the current proxy");
|
|
||||||
cur.enable = false;
|
|
||||||
cur.set_system_proxy()?;
|
|
||||||
} else {
|
|
||||||
log::info!(target: "app", "reset proxy with no action");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// init the auto launch
|
|
||||||
pub fn init_launch(&self) -> Result<()> {
|
|
||||||
let enable = { Config::verge().latest().enable_auto_launch.clone() };
|
|
||||||
let enable = enable.unwrap_or(false);
|
|
||||||
|
|
||||||
let app_exe = current_exe()?;
|
|
||||||
let app_exe = dunce::canonicalize(app_exe)?;
|
|
||||||
let app_name = app_exe
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|f| f.to_str())
|
|
||||||
.ok_or(anyhow!("failed to get file stem"))?;
|
|
||||||
|
|
||||||
let app_path = app_exe
|
|
||||||
.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.ok_or(anyhow!("failed to get app_path"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// fix issue #26
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let app_path = format!("\"{app_path}\"");
|
|
||||||
|
|
||||||
// use the /Applications/Clash Verge.app path
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let app_path = (|| -> Option<String> {
|
|
||||||
let path = std::path::PathBuf::from(&app_path);
|
|
||||||
let path = path.parent()?.parent()?.parent()?;
|
|
||||||
let extension = path.extension()?.to_str()?;
|
|
||||||
match extension == "app" {
|
|
||||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
.unwrap_or(app_path);
|
|
||||||
|
|
||||||
// fix #403
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let app_path = {
|
|
||||||
use crate::core::handle::Handle;
|
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
let handle = Handle::global();
|
|
||||||
match handle.app_handle.lock().as_ref() {
|
|
||||||
Some(app_handle) => {
|
|
||||||
let appimage = app_handle.env().appimage;
|
|
||||||
appimage
|
|
||||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
|
||||||
.unwrap_or(app_path)
|
|
||||||
}
|
|
||||||
None => app_path,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let auto = AutoLaunchBuilder::new()
|
|
||||||
.set_app_name(app_name)
|
|
||||||
.set_app_path(&app_path)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
// 避免在开发时将自启动关了
|
|
||||||
#[cfg(feature = "verge-dev")]
|
|
||||||
if !enable {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
|
||||||
// 避免重复设置登录项
|
|
||||||
let _ = auto.disable();
|
|
||||||
auto.enable()?;
|
|
||||||
} else if !enable {
|
|
||||||
let _ = auto.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
if enable {
|
|
||||||
auto.enable()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
*self.auto_launch.lock() = Some(auto);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the startup
|
|
||||||
pub fn update_launch(&self) -> Result<()> {
|
|
||||||
let auto_launch = self.auto_launch.lock();
|
|
||||||
|
|
||||||
if auto_launch.is_none() {
|
|
||||||
drop(auto_launch);
|
|
||||||
return self.init_launch();
|
|
||||||
}
|
|
||||||
let enable = { Config::verge().latest().enable_auto_launch.clone() };
|
|
||||||
let enable = enable.unwrap_or(false);
|
|
||||||
let auto_launch = auto_launch.as_ref().unwrap();
|
|
||||||
|
|
||||||
match enable {
|
|
||||||
true => auto_launch.enable()?,
|
|
||||||
false => log_err!(auto_launch.disable()), // 忽略关闭的错误
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// launch a system proxy guard
|
|
||||||
/// read config from file directly
|
|
||||||
pub fn guard_proxy(&self) {
|
|
||||||
use tokio::time::{sleep, Duration};
|
|
||||||
|
|
||||||
let guard_state = self.guard_state.clone();
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
// if it is running, exit
|
|
||||||
let mut state = guard_state.lock().await;
|
|
||||||
if *state {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*state = true;
|
|
||||||
drop(state);
|
|
||||||
|
|
||||||
// default duration is 10s
|
|
||||||
let mut wait_secs = 10u64;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(wait_secs)).await;
|
|
||||||
|
|
||||||
let (enable, guard, guard_duration, bypass) = {
|
|
||||||
let verge = Config::verge();
|
|
||||||
let verge = verge.latest();
|
|
||||||
(
|
|
||||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
|
||||||
verge.enable_proxy_guard.clone().unwrap_or(false),
|
|
||||||
verge.proxy_guard_duration.clone().unwrap_or(10),
|
|
||||||
verge.system_proxy_bypass.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// stop loop
|
|
||||||
if !enable || !guard {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update duration
|
|
||||||
wait_secs = guard_duration;
|
|
||||||
|
|
||||||
log::debug!(target: "app", "try to guard the system proxy");
|
|
||||||
|
|
||||||
let port = { Config::clash().latest().get_mixed_port() };
|
|
||||||
|
|
||||||
let sysproxy = Sysproxy {
|
|
||||||
enable: true,
|
|
||||||
host: "127.0.0.1".into(),
|
|
||||||
port,
|
|
||||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
log_err!(sysproxy.set_system_proxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = guard_state.lock().await;
|
|
||||||
*state = false;
|
|
||||||
drop(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,184 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use crate::feat;
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
type TaskID = u64;
|
|
||||||
|
|
||||||
pub struct Timer {
|
|
||||||
/// cron manager
|
|
||||||
delay_timer: Arc<Mutex<DelayTimer>>,
|
|
||||||
|
|
||||||
/// save the current state
|
|
||||||
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
|
||||||
|
|
||||||
/// increment id
|
|
||||||
timer_count: Arc<Mutex<TaskID>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Timer {
|
|
||||||
pub fn global() -> &'static Timer {
|
|
||||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
|
||||||
|
|
||||||
TIMER.get_or_init(|| Timer {
|
|
||||||
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
|
||||||
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
|
||||||
timer_count: Arc::new(Mutex::new(1)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// restore timer
|
|
||||||
pub fn init(&self) -> Result<()> {
|
|
||||||
self.refresh()?;
|
|
||||||
|
|
||||||
let cur_timestamp = chrono::Local::now().timestamp();
|
|
||||||
|
|
||||||
let timer_map = self.timer_map.lock();
|
|
||||||
let delay_timer = self.delay_timer.lock();
|
|
||||||
|
|
||||||
Config::profiles().latest().get_items().map(|items| {
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.filter_map(|item| {
|
|
||||||
// mins to seconds
|
|
||||||
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
|
||||||
let updated = item.updated? as i64;
|
|
||||||
|
|
||||||
if interval > 0 && cur_timestamp - updated >= interval {
|
|
||||||
Some(item)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.for_each(|item| {
|
|
||||||
if let Some(uid) = item.uid.as_ref() {
|
|
||||||
if let Some((task_id, _)) = timer_map.get(uid) {
|
|
||||||
crate::log_err!(delay_timer.advance_task(*task_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Correctly update all cron tasks
|
|
||||||
pub fn refresh(&self) -> Result<()> {
|
|
||||||
let diff_map = self.gen_diff();
|
|
||||||
|
|
||||||
let mut timer_map = self.timer_map.lock();
|
|
||||||
let mut delay_timer = self.delay_timer.lock();
|
|
||||||
|
|
||||||
for (uid, diff) in diff_map.into_iter() {
|
|
||||||
match diff {
|
|
||||||
DiffFlag::Del(tid) => {
|
|
||||||
let _ = timer_map.remove(&uid);
|
|
||||||
crate::log_err!(delay_timer.remove_task(tid));
|
|
||||||
}
|
|
||||||
DiffFlag::Add(tid, val) => {
|
|
||||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
|
||||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
|
||||||
}
|
|
||||||
DiffFlag::Mod(tid, val) => {
|
|
||||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
|
||||||
crate::log_err!(delay_timer.remove_task(tid));
|
|
||||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generate a uid -> update_interval map
|
|
||||||
fn gen_map(&self) -> HashMap<String, u64> {
|
|
||||||
let mut new_map = HashMap::new();
|
|
||||||
|
|
||||||
if let Some(items) = Config::profiles().latest().get_items() {
|
|
||||||
for item in items.iter() {
|
|
||||||
if item.option.is_some() {
|
|
||||||
let option = item.option.as_ref().unwrap();
|
|
||||||
let interval = option.update_interval.unwrap_or(0);
|
|
||||||
|
|
||||||
if interval > 0 {
|
|
||||||
new_map.insert(item.uid.clone().unwrap(), interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new_map
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generate the diff map for refresh
|
|
||||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
|
||||||
let mut diff_map = HashMap::new();
|
|
||||||
|
|
||||||
let timer_map = self.timer_map.lock();
|
|
||||||
|
|
||||||
let new_map = self.gen_map();
|
|
||||||
let cur_map = &timer_map;
|
|
||||||
|
|
||||||
cur_map.iter().for_each(|(uid, (tid, val))| {
|
|
||||||
let new_val = new_map.get(uid).unwrap_or(&0);
|
|
||||||
|
|
||||||
if *new_val == 0 {
|
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
|
||||||
} else if new_val != val {
|
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut count = self.timer_count.lock();
|
|
||||||
|
|
||||||
new_map.iter().for_each(|(uid, val)| {
|
|
||||||
if cur_map.get(uid).is_none() {
|
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
|
||||||
|
|
||||||
*count += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
diff_map
|
|
||||||
}
|
|
||||||
|
|
||||||
/// add a cron task
|
|
||||||
fn add_task(
|
|
||||||
&self,
|
|
||||||
delay_timer: &mut DelayTimer,
|
|
||||||
uid: String,
|
|
||||||
tid: TaskID,
|
|
||||||
minutes: u64,
|
|
||||||
) -> Result<()> {
|
|
||||||
let task = TaskBuilder::default()
|
|
||||||
.set_task_id(tid)
|
|
||||||
.set_maximum_parallel_runnable_num(1)
|
|
||||||
.set_frequency_repeated_by_minutes(minutes)
|
|
||||||
// .set_frequency_repeated_by_seconds(minutes) // for test
|
|
||||||
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
|
||||||
.context("failed to create timer task")?;
|
|
||||||
|
|
||||||
delay_timer
|
|
||||||
.add_task(task)
|
|
||||||
.context("failed to add timer task")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the task runner
|
|
||||||
async fn async_task(uid: String) {
|
|
||||||
log::info!(target: "app", "running timer task `{uid}`");
|
|
||||||
crate::log_err!(feat::update_profile(uid, None).await);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum DiffFlag {
|
|
||||||
Del(TaskID),
|
|
||||||
Add(TaskID, u64),
|
|
||||||
Mod(TaskID, u64),
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
use crate::{cmds, config::Config, feat, utils::resolve};
|
|
||||||
use anyhow::Result;
|
|
||||||
use tauri::{
|
|
||||||
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
|
||||||
SystemTraySubmenu,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Tray {}
|
|
||||||
|
|
||||||
impl Tray {
|
|
||||||
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
|
|
||||||
let zh = { Config::verge().latest().language == Some("zh".into()) };
|
|
||||||
|
|
||||||
let version = app_handle.package_info().version.to_string();
|
|
||||||
|
|
||||||
macro_rules! t {
|
|
||||||
($en: expr, $zh: expr) => {
|
|
||||||
if zh {
|
|
||||||
$zh
|
|
||||||
} else {
|
|
||||||
$en
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemTrayMenu::new()
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"open_window",
|
|
||||||
t!("Dashboard", "打开面板"),
|
|
||||||
))
|
|
||||||
.add_native_item(SystemTrayMenuItem::Separator)
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"rule_mode",
|
|
||||||
t!("Rule Mode", "规则模式"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"global_mode",
|
|
||||||
t!("Global Mode", "全局模式"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"direct_mode",
|
|
||||||
t!("Direct Mode", "直连模式"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"script_mode",
|
|
||||||
t!("Script Mode", "脚本模式"),
|
|
||||||
))
|
|
||||||
.add_native_item(SystemTrayMenuItem::Separator)
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"system_proxy",
|
|
||||||
t!("System Proxy", "系统代理"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"copy_env",
|
|
||||||
t!("Copy Env", "复制环境变量"),
|
|
||||||
))
|
|
||||||
.add_submenu(SystemTraySubmenu::new(
|
|
||||||
t!("Open Dir", "打开目录"),
|
|
||||||
SystemTrayMenu::new()
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"open_app_dir",
|
|
||||||
t!("App Dir", "应用目录"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"open_core_dir",
|
|
||||||
t!("Core Dir", "内核目录"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"open_logs_dir",
|
|
||||||
t!("Logs Dir", "日志目录"),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.add_submenu(SystemTraySubmenu::new(
|
|
||||||
t!("More", "更多"),
|
|
||||||
SystemTrayMenu::new()
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"restart_clash",
|
|
||||||
t!("Restart Clash", "重启 Clash"),
|
|
||||||
))
|
|
||||||
.add_item(CustomMenuItem::new(
|
|
||||||
"restart_app",
|
|
||||||
t!("Restart App", "重启应用"),
|
|
||||||
))
|
|
||||||
.add_item(
|
|
||||||
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.add_native_item(SystemTrayMenuItem::Separator)
|
|
||||||
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
|
||||||
app_handle
|
|
||||||
.tray_handle()
|
|
||||||
.set_menu(Tray::tray_menu(app_handle))?;
|
|
||||||
Tray::update_part(app_handle)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
|
|
||||||
let mode = {
|
|
||||||
Config::clash()
|
|
||||||
.latest()
|
|
||||||
.0
|
|
||||||
.get("mode")
|
|
||||||
.map(|val| val.as_str().unwrap_or("rule"))
|
|
||||||
.unwrap_or("rule")
|
|
||||||
.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let tray = app_handle.tray_handle();
|
|
||||||
|
|
||||||
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
|
|
||||||
let _ = tray.get_item("global_mode").set_selected(mode == "global");
|
|
||||||
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
|
|
||||||
let _ = tray.get_item("script_mode").set_selected(mode == "script");
|
|
||||||
|
|
||||||
let verge = Config::verge();
|
|
||||||
let verge = verge.latest();
|
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
|
||||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let indication_icon = if *system_proxy {
|
|
||||||
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
|
|
||||||
} else {
|
|
||||||
include_bytes!("../../icons/win-tray-icon.png").to_vec()
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
|
|
||||||
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
|
||||||
match event {
|
|
||||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
|
||||||
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
|
|
||||||
let mode = &mode[0..mode.len() - 5];
|
|
||||||
feat::change_clash_mode(mode.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
"open_window" => resolve::create_window(app_handle),
|
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
|
||||||
"tun_mode" => feat::toggle_tun_mode(),
|
|
||||||
"copy_env" => feat::copy_clash_env(),
|
|
||||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
|
||||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
|
||||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
|
||||||
"restart_clash" => feat::restart_clash_core(),
|
|
||||||
"restart_app" => api::process::restart(&app_handle.env()),
|
|
||||||
"quit" => {
|
|
||||||
let _ = resolve::save_window_size_position(app_handle, true);
|
|
||||||
|
|
||||||
resolve::resolve_reset();
|
|
||||||
api::process::kill_children();
|
|
||||||
app_handle.exit(0);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
SystemTrayEvent::LeftClick { .. } => {
|
|
||||||
resolve::create_window(app_handle);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,178 +0,0 @@
|
|||||||
#![cfg(target_os = "windows")]
|
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::utils::dirs;
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use deelevate::{PrivilegeLevel, Token};
|
|
||||||
use runas::Command as RunasCommand;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::{env::current_exe, process::Command as StdCommand};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct ResponseBody {
|
|
||||||
pub core_type: Option<String>,
|
|
||||||
pub bin_path: String,
|
|
||||||
pub config_dir: String,
|
|
||||||
pub log_file: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct JsonResponse {
|
|
||||||
pub code: u64,
|
|
||||||
pub msg: String,
|
|
||||||
pub data: Option<ResponseBody>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install the Clash Verge Service
|
|
||||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
|
||||||
pub async fn install_service() -> Result<()> {
|
|
||||||
let binary_path = dirs::service_path()?;
|
|
||||||
let install_path = binary_path.with_file_name("install-service.exe");
|
|
||||||
|
|
||||||
if !install_path.exists() {
|
|
||||||
bail!("installer exe not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let token = Token::with_current_process()?;
|
|
||||||
let level = token.privilege_level()?;
|
|
||||||
|
|
||||||
let status = match level {
|
|
||||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
|
|
||||||
_ => StdCommand::new(install_path)
|
|
||||||
.creation_flags(0x08000000)
|
|
||||||
.status()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"failed to install service with status {}",
|
|
||||||
status.code().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uninstall the Clash Verge Service
|
|
||||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
|
||||||
pub async fn uninstall_service() -> Result<()> {
|
|
||||||
let binary_path = dirs::service_path()?;
|
|
||||||
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
|
||||||
|
|
||||||
if !uninstall_path.exists() {
|
|
||||||
bail!("uninstaller exe not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let token = Token::with_current_process()?;
|
|
||||||
let level = token.privilege_level()?;
|
|
||||||
|
|
||||||
let status = match level {
|
|
||||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
|
||||||
_ => StdCommand::new(uninstall_path)
|
|
||||||
.creation_flags(0x08000000)
|
|
||||||
.status()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"failed to uninstall service with status {}",
|
|
||||||
status.code().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// check the windows service status
|
|
||||||
pub async fn check_service() -> Result<JsonResponse> {
|
|
||||||
let url = format!("{SERVICE_URL}/get_clash");
|
|
||||||
let response = reqwest::ClientBuilder::new()
|
|
||||||
.no_proxy()
|
|
||||||
.build()?
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.context("failed to connect to the Clash Verge Service")?
|
|
||||||
.json::<JsonResponse>()
|
|
||||||
.await
|
|
||||||
.context("failed to parse the Clash Verge Service response")?;
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// start the clash by service
|
|
||||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|
||||||
let status = check_service().await?;
|
|
||||||
|
|
||||||
if status.code == 0 {
|
|
||||||
stop_core_by_service().await?;
|
|
||||||
sleep(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
|
||||||
let clash_core = clash_core.unwrap_or("clash".into());
|
|
||||||
|
|
||||||
let clash_bin = format!("{clash_core}.exe");
|
|
||||||
let bin_path = current_exe()?.with_file_name(clash_bin);
|
|
||||||
let bin_path = dirs::path_to_str(&bin_path)?;
|
|
||||||
|
|
||||||
let config_dir = dirs::app_home_dir()?;
|
|
||||||
let config_dir = dirs::path_to_str(&config_dir)?;
|
|
||||||
|
|
||||||
let log_path = dirs::service_log_file()?;
|
|
||||||
let log_path = dirs::path_to_str(&log_path)?;
|
|
||||||
|
|
||||||
let config_file = dirs::path_to_str(config_file)?;
|
|
||||||
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
map.insert("core_type", clash_core.as_str());
|
|
||||||
map.insert("bin_path", bin_path);
|
|
||||||
map.insert("config_dir", config_dir);
|
|
||||||
map.insert("config_file", config_file);
|
|
||||||
map.insert("log_file", log_path);
|
|
||||||
|
|
||||||
let url = format!("{SERVICE_URL}/start_clash");
|
|
||||||
let res = reqwest::ClientBuilder::new()
|
|
||||||
.no_proxy()
|
|
||||||
.build()?
|
|
||||||
.post(url)
|
|
||||||
.json(&map)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<JsonResponse>()
|
|
||||||
.await
|
|
||||||
.context("failed to connect to the Clash Verge Service")?;
|
|
||||||
|
|
||||||
if res.code != 0 {
|
|
||||||
bail!(res.msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// stop the clash by service
|
|
||||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
|
||||||
let url = format!("{SERVICE_URL}/stop_clash");
|
|
||||||
let res = reqwest::ClientBuilder::new()
|
|
||||||
.no_proxy()
|
|
||||||
.build()?
|
|
||||||
.post(url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<JsonResponse>()
|
|
||||||
.await
|
|
||||||
.context("failed to connect to the Clash Verge Service")?;
|
|
||||||
|
|
||||||
if res.code != 0 {
|
|
||||||
bail!(res.msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
function main(params) {
|
|
||||||
if (params.mode === "script") {
|
|
||||||
params.mode = "rule";
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
function main(params) {
|
|
||||||
if (Array.isArray(params.proxies)) {
|
|
||||||
params.proxies.forEach((p, i) => {
|
|
||||||
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
|
||||||
params.proxies[i].alpn = [p.alpn];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
config::PrfItem,
|
|
||||||
utils::{dirs, help},
|
|
||||||
};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ChainItem {
|
|
||||||
pub uid: String,
|
|
||||||
pub data: ChainType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ChainType {
|
|
||||||
Merge(Mapping),
|
|
||||||
Script(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ChainSupport {
|
|
||||||
Clash,
|
|
||||||
ClashMeta,
|
|
||||||
All,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&PrfItem> for Option<ChainItem> {
|
|
||||||
fn from(item: &PrfItem) -> Self {
|
|
||||||
let itype = item.itype.as_ref()?.as_str();
|
|
||||||
let file = item.file.clone()?;
|
|
||||||
let uid = item.uid.clone().unwrap_or("".into());
|
|
||||||
let path = dirs::app_profiles_dir().ok()?.join(file);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
match itype {
|
|
||||||
"script" => Some(ChainItem {
|
|
||||||
uid,
|
|
||||||
data: ChainType::Script(fs::read_to_string(path).ok()?),
|
|
||||||
}),
|
|
||||||
"merge" => Some(ChainItem {
|
|
||||||
uid,
|
|
||||||
data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChainItem {
|
|
||||||
/// 内建支持一些脚本
|
|
||||||
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
|
||||||
// meta 的一些处理
|
|
||||||
let meta_guard =
|
|
||||||
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
|
||||||
|
|
||||||
// meta 1.13.2 alpn string 转 数组
|
|
||||||
let hy_alpn =
|
|
||||||
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
|
||||||
|
|
||||||
vec![
|
|
||||||
(ChainSupport::ClashMeta, hy_alpn),
|
|
||||||
(ChainSupport::ClashMeta, meta_guard),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
|
|
||||||
Self {
|
|
||||||
uid: uid.into(),
|
|
||||||
data: ChainType::Script(data.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChainSupport {
|
|
||||||
pub fn is_support(&self, core: Option<&String>) -> bool {
|
|
||||||
match core {
|
|
||||||
Some(core) => match (self, core.as_str()) {
|
|
||||||
(ChainSupport::All, _) => true,
|
|
||||||
(ChainSupport::Clash, "clash") => true,
|
|
||||||
(ChainSupport::ClashMeta, "clash-meta") => true,
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
None => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
|
||||||
"mode",
|
|
||||||
"port",
|
|
||||||
"socks-port",
|
|
||||||
"mixed-port",
|
|
||||||
"allow-lan",
|
|
||||||
"log-level",
|
|
||||||
"ipv6",
|
|
||||||
"secret",
|
|
||||||
"external-controller",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub const DEFAULT_FIELDS: [&str; 5] = [
|
|
||||||
"proxies",
|
|
||||||
"proxy-groups",
|
|
||||||
"proxy-providers",
|
|
||||||
"rules",
|
|
||||||
"rule-providers",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub const OTHERS_FIELDS: [&str; 30] = [
|
|
||||||
"dns",
|
|
||||||
"tun",
|
|
||||||
"ebpf",
|
|
||||||
"hosts",
|
|
||||||
"script",
|
|
||||||
"profile",
|
|
||||||
"payload",
|
|
||||||
"tunnels",
|
|
||||||
"auto-redir",
|
|
||||||
"experimental",
|
|
||||||
"interface-name",
|
|
||||||
"routing-mark",
|
|
||||||
"redir-port",
|
|
||||||
"tproxy-port",
|
|
||||||
"iptables",
|
|
||||||
"external-ui",
|
|
||||||
"bind-address",
|
|
||||||
"authentication",
|
|
||||||
"tls", // meta
|
|
||||||
"sniffer", // meta
|
|
||||||
"geox-url", // meta
|
|
||||||
"listeners", // meta
|
|
||||||
"sub-rules", // meta
|
|
||||||
"geodata-mode", // meta
|
|
||||||
"unified-delay", // meta
|
|
||||||
"tcp-concurrent", // meta
|
|
||||||
"enable-process", // meta
|
|
||||||
"find-process-mode", // meta
|
|
||||||
"external-controller-tls", // meta
|
|
||||||
"global-client-fingerprint", // meta
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn use_clash_fields() -> Vec<String> {
|
|
||||||
DEFAULT_FIELDS
|
|
||||||
.into_iter()
|
|
||||||
.chain(HANDLE_FIELDS)
|
|
||||||
.chain(OTHERS_FIELDS)
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
|
|
||||||
let others = Vec::from(OTHERS_FIELDS);
|
|
||||||
|
|
||||||
valid.iter_mut().for_each(|s| s.make_ascii_lowercase());
|
|
||||||
valid
|
|
||||||
.into_iter()
|
|
||||||
.filter(|s| others.contains(&s.as_str()))
|
|
||||||
.chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_filter(config: Mapping, filter: &Vec<String>, enable: bool) -> Mapping {
|
|
||||||
if !enable {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ret = Mapping::new();
|
|
||||||
|
|
||||||
for (key, value) in config.into_iter() {
|
|
||||||
if let Some(key) = key.as_str() {
|
|
||||||
if filter.contains(&key.to_string()) {
|
|
||||||
ret.insert(Value::from(key), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_lowercase(config: Mapping) -> Mapping {
|
|
||||||
let mut ret = Mapping::new();
|
|
||||||
|
|
||||||
for (key, value) in config.into_iter() {
|
|
||||||
if let Some(key_str) = key.as_str() {
|
|
||||||
let mut key_str = String::from(key_str);
|
|
||||||
key_str.make_ascii_lowercase();
|
|
||||||
ret.insert(Value::from(key_str), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
|
||||||
let mut ret = Mapping::new();
|
|
||||||
|
|
||||||
HANDLE_FIELDS
|
|
||||||
.into_iter()
|
|
||||||
.chain(OTHERS_FIELDS)
|
|
||||||
.chain(DEFAULT_FIELDS)
|
|
||||||
.for_each(|key| {
|
|
||||||
let key = Value::from(key);
|
|
||||||
config.get(&key).map(|value| {
|
|
||||||
ret.insert(key, value.clone());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if !enable_filter {
|
|
||||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS
|
|
||||||
.into_iter()
|
|
||||||
.chain(OTHERS_FIELDS)
|
|
||||||
.chain(DEFAULT_FIELDS)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let config_keys: HashSet<&str> = config
|
|
||||||
.keys()
|
|
||||||
.filter_map(|e| e.as_str())
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
|
||||||
let key = Value::from(key);
|
|
||||||
config.get(&key).map(|value| {
|
|
||||||
ret.insert(key, value.clone());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_keys(config: &Mapping) -> Vec<String> {
|
|
||||||
config
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(key, _)| key.as_str())
|
|
||||||
.map(|s| {
|
|
||||||
let mut s = s.to_string();
|
|
||||||
s.make_ascii_lowercase();
|
|
||||||
return s;
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
use super::{use_filter, use_lowercase};
|
|
||||||
use serde_yaml::{self, Mapping, Sequence, Value};
|
|
||||||
|
|
||||||
const MERGE_FIELDS: [&str; 6] = [
|
|
||||||
"prepend-rules",
|
|
||||||
"append-rules",
|
|
||||||
"prepend-proxies",
|
|
||||||
"append-proxies",
|
|
||||||
"prepend-proxy-groups",
|
|
||||||
"append-proxy-groups",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
|
||||||
// 直接覆盖原字段
|
|
||||||
use_lowercase(merge.clone())
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|(key, value)| {
|
|
||||||
config.insert(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
|
||||||
let merge = use_filter(merge, &merge_list.collect(), true);
|
|
||||||
|
|
||||||
["rules", "proxies", "proxy-groups"]
|
|
||||||
.iter()
|
|
||||||
.for_each(|key_str| {
|
|
||||||
let key_val = Value::from(key_str.to_string());
|
|
||||||
|
|
||||||
let mut list = Sequence::default();
|
|
||||||
list = config.get(&key_val).map_or(list.clone(), |val| {
|
|
||||||
val.as_sequence().map_or(list, |v| v.clone())
|
|
||||||
});
|
|
||||||
|
|
||||||
let pre_key = Value::from(format!("prepend-{key_str}"));
|
|
||||||
let post_key = Value::from(format!("append-{key_str}"));
|
|
||||||
|
|
||||||
if let Some(pre_val) = merge.get(&pre_key) {
|
|
||||||
if pre_val.is_sequence() {
|
|
||||||
let mut pre_val = pre_val.as_sequence().unwrap().clone();
|
|
||||||
pre_val.extend(list);
|
|
||||||
list = pre_val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(post_val) = merge.get(&post_key) {
|
|
||||||
if post_val.is_sequence() {
|
|
||||||
list.extend(post_val.as_sequence().unwrap().clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.insert(key_val, Value::from(list));
|
|
||||||
});
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_merge() -> anyhow::Result<()> {
|
|
||||||
let merge = r"
|
|
||||||
prepend-rules:
|
|
||||||
- prepend
|
|
||||||
- 1123123
|
|
||||||
append-rules:
|
|
||||||
- append
|
|
||||||
prepend-proxies:
|
|
||||||
- 9999
|
|
||||||
append-proxies:
|
|
||||||
- 1111
|
|
||||||
rules:
|
|
||||||
- replace
|
|
||||||
proxy-groups:
|
|
||||||
- 123781923810
|
|
||||||
tun:
|
|
||||||
enable: true
|
|
||||||
dns:
|
|
||||||
enable: true
|
|
||||||
";
|
|
||||||
|
|
||||||
let config = r"
|
|
||||||
rules:
|
|
||||||
- aaaaa
|
|
||||||
script1: test
|
|
||||||
";
|
|
||||||
|
|
||||||
let merge = serde_yaml::from_str::<Mapping>(merge)?;
|
|
||||||
let config = serde_yaml::from_str::<Mapping>(config)?;
|
|
||||||
|
|
||||||
let result = serde_yaml::to_string(&use_merge(merge, config))?;
|
|
||||||
|
|
||||||
println!("{result}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
mod chain;
|
|
||||||
mod field;
|
|
||||||
mod merge;
|
|
||||||
mod script;
|
|
||||||
mod tun;
|
|
||||||
|
|
||||||
pub(self) use self::field::*;
|
|
||||||
|
|
||||||
use self::chain::*;
|
|
||||||
use self::merge::*;
|
|
||||||
use self::script::*;
|
|
||||||
use self::tun::*;
|
|
||||||
use crate::config::Config;
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
type ResultLog = Vec<(String, String)>;
|
|
||||||
|
|
||||||
/// Enhance mode
|
|
||||||
/// 返回最终配置、该配置包含的键、和script执行的结果
|
|
||||||
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|
||||||
// config.yaml 的配置
|
|
||||||
let clash_config = { Config::clash().latest().0.clone() };
|
|
||||||
|
|
||||||
let (clash_core, enable_tun, enable_builtin, enable_filter) = {
|
|
||||||
let verge = Config::verge();
|
|
||||||
let verge = verge.latest();
|
|
||||||
(
|
|
||||||
verge.clash_core.clone(),
|
|
||||||
verge.enable_tun_mode.clone().unwrap_or(false),
|
|
||||||
verge.enable_builtin_enhanced.clone().unwrap_or(true),
|
|
||||||
verge.enable_clash_fields.clone().unwrap_or(true),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从profiles里拿东西
|
|
||||||
let (mut config, chain, valid) = {
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let profiles = profiles.latest();
|
|
||||||
|
|
||||||
let current = profiles.current_mapping().unwrap_or(Mapping::new());
|
|
||||||
|
|
||||||
let chain = match profiles.chain.as_ref() {
|
|
||||||
Some(chain) => chain
|
|
||||||
.iter()
|
|
||||||
.filter_map(|uid| profiles.get_item(uid).ok())
|
|
||||||
.filter_map(|item| <Option<ChainItem>>::from(item))
|
|
||||||
.collect::<Vec<ChainItem>>(),
|
|
||||||
None => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let valid = profiles.valid.clone().unwrap_or(vec![]);
|
|
||||||
|
|
||||||
(current, chain, valid)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result_map = HashMap::new(); // 保存脚本日志
|
|
||||||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
|
||||||
|
|
||||||
let valid = use_valid_fields(valid);
|
|
||||||
config = use_filter(config, &valid, enable_filter);
|
|
||||||
|
|
||||||
// 处理用户的profile
|
|
||||||
chain.into_iter().for_each(|item| match item.data {
|
|
||||||
ChainType::Merge(merge) => {
|
|
||||||
exists_keys.extend(use_keys(&merge));
|
|
||||||
config = use_merge(merge, config.to_owned());
|
|
||||||
config = use_filter(config.to_owned(), &valid, enable_filter);
|
|
||||||
}
|
|
||||||
ChainType::Script(script) => {
|
|
||||||
let mut logs = vec![];
|
|
||||||
|
|
||||||
match use_script(script, config.to_owned()) {
|
|
||||||
Ok((res_config, res_logs)) => {
|
|
||||||
exists_keys.extend(use_keys(&res_config));
|
|
||||||
config = use_filter(res_config, &valid, enable_filter);
|
|
||||||
logs.extend(res_logs);
|
|
||||||
}
|
|
||||||
Err(err) => logs.push(("exception".into(), err.to_string())),
|
|
||||||
}
|
|
||||||
|
|
||||||
result_map.insert(item.uid, logs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 合并默认的config
|
|
||||||
for (key, value) in clash_config.into_iter() {
|
|
||||||
config.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let clash_fields = use_clash_fields();
|
|
||||||
|
|
||||||
// 内建脚本最后跑
|
|
||||||
if enable_builtin {
|
|
||||||
ChainItem::builtin()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
|
||||||
.map(|(_, c)| c)
|
|
||||||
.for_each(|item| {
|
|
||||||
log::debug!(target: "app", "run builtin script {}", item.uid);
|
|
||||||
|
|
||||||
match item.data {
|
|
||||||
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
|
||||||
Ok((res_config, _)) => {
|
|
||||||
config = use_filter(res_config, &clash_fields, enable_filter);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "builtin script error `{err}`");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
config = use_filter(config, &clash_fields, enable_filter);
|
|
||||||
config = use_tun(config, enable_tun);
|
|
||||||
config = use_sort(config, enable_filter);
|
|
||||||
|
|
||||||
let mut exists_set = HashSet::new();
|
|
||||||
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
|
|
||||||
exists_keys = exists_set.into_iter().collect();
|
|
||||||
|
|
||||||
(config, exists_keys, result_map)
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
use super::use_lowercase;
|
|
||||||
use anyhow::Result;
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
|
|
||||||
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
|
|
||||||
use rquickjs::{Context, Func, Runtime};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
let runtime = Runtime::new().unwrap();
|
|
||||||
let context = Context::full(&runtime).unwrap();
|
|
||||||
let outputs = Arc::new(Mutex::new(vec![]));
|
|
||||||
|
|
||||||
let copy_outputs = outputs.clone();
|
|
||||||
let result = context.with(|ctx| -> Result<Mapping> {
|
|
||||||
ctx.globals().set(
|
|
||||||
"__verge_log__",
|
|
||||||
Func::from(move |level: String, data: String| {
|
|
||||||
let mut out = copy_outputs.lock().unwrap();
|
|
||||||
out.push((level, data));
|
|
||||||
}),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
ctx.eval(
|
|
||||||
r#"var console = Object.freeze({
|
|
||||||
log(data){__verge_log__("log",JSON.stringify(data))},
|
|
||||||
info(data){__verge_log__("info",JSON.stringify(data))},
|
|
||||||
error(data){__verge_log__("error",JSON.stringify(data))},
|
|
||||||
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
|
||||||
});"#,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let config = use_lowercase(config.clone());
|
|
||||||
let config_str = serde_json::to_string(&config)?;
|
|
||||||
|
|
||||||
let code = format!(
|
|
||||||
r#"try{{
|
|
||||||
{script};
|
|
||||||
JSON.stringify(main({config_str})||'')
|
|
||||||
}} catch(err) {{
|
|
||||||
`__error_flag__ ${{err.toString()}}`
|
|
||||||
}}"#
|
|
||||||
);
|
|
||||||
let result: String = ctx.eval(code.as_str())?;
|
|
||||||
if result.starts_with("__error_flag__") {
|
|
||||||
anyhow::bail!(result[15..].to_owned());
|
|
||||||
}
|
|
||||||
if result == "\"\"" {
|
|
||||||
anyhow::bail!("main function should return object");
|
|
||||||
}
|
|
||||||
return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut out = outputs.lock().unwrap();
|
|
||||||
match result {
|
|
||||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
|
||||||
Err(err) => {
|
|
||||||
out.push(("exception".into(), err.to_string()));
|
|
||||||
Ok((config, out.to_vec()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_script() {
|
|
||||||
let script = r#"
|
|
||||||
function main(config) {
|
|
||||||
if (Array.isArray(config.rules)) {
|
|
||||||
config.rules = [...config.rules, "add"];
|
|
||||||
}
|
|
||||||
console.log(config);
|
|
||||||
config.proxies = ["111"];
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let config = r#"
|
|
||||||
rules:
|
|
||||||
- 111
|
|
||||||
- 222
|
|
||||||
tun:
|
|
||||||
enable: false
|
|
||||||
dns:
|
|
||||||
enable: false
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let config = serde_yaml::from_str(config).unwrap();
|
|
||||||
let (config, results) = use_script(script.into(), config).unwrap();
|
|
||||||
|
|
||||||
let config_str = serde_yaml::to_string(&config).unwrap();
|
|
||||||
|
|
||||||
println!("{config_str}");
|
|
||||||
|
|
||||||
dbg!(results);
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
|
|
||||||
macro_rules! revise {
|
|
||||||
($map: expr, $key: expr, $val: expr) => {
|
|
||||||
let ret_key = Value::String($key.into());
|
|
||||||
$map.insert(ret_key, Value::from($val));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// if key not exists then append value
|
|
||||||
macro_rules! append {
|
|
||||||
($map: expr, $key: expr, $val: expr) => {
|
|
||||||
let ret_key = Value::String($key.into());
|
|
||||||
if !$map.contains_key(&ret_key) {
|
|
||||||
$map.insert(ret_key, Value::from($val));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|
||||||
let tun_key = Value::from("tun");
|
|
||||||
let tun_val = config.get(&tun_key);
|
|
||||||
|
|
||||||
if !enable && tun_val.is_none() {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
|
||||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
|
||||||
});
|
|
||||||
|
|
||||||
revise!(tun_val, "enable", enable);
|
|
||||||
if enable {
|
|
||||||
append!(tun_val, "stack", "gvisor");
|
|
||||||
append!(tun_val, "dns-hijack", vec!["any:53"]);
|
|
||||||
append!(tun_val, "auto-route", true);
|
|
||||||
append!(tun_val, "auto-detect-interface", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
revise!(config, "tun", tun_val);
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
use_dns_for_tun(config)
|
|
||||||
} else {
|
|
||||||
config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
|
|
||||||
let dns_key = Value::from("dns");
|
|
||||||
let dns_val = config.get(&dns_key);
|
|
||||||
|
|
||||||
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
|
||||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
|
||||||
});
|
|
||||||
|
|
||||||
// 开启tun将同时开启dns
|
|
||||||
revise!(dns_val, "enable", true);
|
|
||||||
|
|
||||||
append!(dns_val, "enhanced-mode", "fake-ip");
|
|
||||||
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
|
||||||
append!(
|
|
||||||
dns_val,
|
|
||||||
"nameserver",
|
|
||||||
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
|
|
||||||
);
|
|
||||||
append!(dns_val, "fallback", vec![] as Vec<&str>);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
append!(
|
|
||||||
dns_val,
|
|
||||||
"fake-ip-filter",
|
|
||||||
vec![
|
|
||||||
"dns.msftncsi.com",
|
|
||||||
"www.msftncsi.com",
|
|
||||||
"www.msftconnecttest.com"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
revise!(config, "dns", dns_val);
|
|
||||||
config
|
|
||||||
}
|
|
26
src-tauri/src/events/emit.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
use crate::config::ClashController;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ClashInfoPayload {
|
||||||
|
/// value between `success` and `error`
|
||||||
|
pub status: String,
|
||||||
|
|
||||||
|
/// the clash core's external controller infomation
|
||||||
|
pub controller: Option<ClashController>,
|
||||||
|
|
||||||
|
/// some message
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// emit `clash_runtime` to the main windows
|
||||||
|
pub fn clash_start(app_handle: &AppHandle, payload: &ClashInfoPayload) {
|
||||||
|
match app_handle.get_window("main") {
|
||||||
|
Some(main_win) => {
|
||||||
|
main_win.emit("clash_start", payload).unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
2
src-tauri/src/events/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod emit;
|
||||||
|
pub mod state;
|
13
src-tauri/src/events/state.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use super::emit::ClashInfoPayload;
|
||||||
|
use crate::config::VergeConfig;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ClashInfoState(pub Arc<Mutex<ClashInfoPayload>>);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ProfileLock(pub Mutex<bool>);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct VergeConfLock(pub Arc<Mutex<VergeConfig>>);
|
@ -1,341 +0,0 @@
|
|||||||
//!
|
|
||||||
//! feat mod 里的函数主要用于
|
|
||||||
//! - hotkey 快捷键
|
|
||||||
//! - timer 定时器
|
|
||||||
//! - cmds 页面调用
|
|
||||||
//!
|
|
||||||
use crate::config::*;
|
|
||||||
use crate::core::*;
|
|
||||||
use crate::log_err;
|
|
||||||
use crate::utils::resolve;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use wry::application::clipboard::Clipboard;
|
|
||||||
|
|
||||||
// 打开面板
|
|
||||||
pub fn open_dashboard() {
|
|
||||||
let handle = handle::Handle::global();
|
|
||||||
let app_handle = handle.app_handle.lock();
|
|
||||||
if let Some(app_handle) = app_handle.as_ref() {
|
|
||||||
resolve::create_window(app_handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重启clash
|
|
||||||
pub fn restart_clash_core() {
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
match CoreManager::global().run_core().await {
|
|
||||||
Ok(_) => {
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
handle::Handle::notice_message("set_config::ok", "ok");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
|
||||||
log::error!(target:"app", "{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换模式 rule/global/direct/script mode
|
|
||||||
pub fn change_clash_mode(mode: String) {
|
|
||||||
let mut mapping = Mapping::new();
|
|
||||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
log::debug!(target: "app", "change clash mode to {mode}");
|
|
||||||
|
|
||||||
match clash_api::patch_configs(&mapping).await {
|
|
||||||
Ok(_) => {
|
|
||||||
// 更新配置
|
|
||||||
Config::clash().data().patch_config(mapping);
|
|
||||||
|
|
||||||
if Config::clash().data().save_config().is_ok() {
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
log_err!(handle::Handle::update_systray_part());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换系统代理
|
|
||||||
pub fn toggle_system_proxy() {
|
|
||||||
let enable = Config::verge().draft().enable_system_proxy.clone();
|
|
||||||
let enable = enable.unwrap_or(false);
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
match patch_verge(IVerge {
|
|
||||||
enable_system_proxy: Some(!enable),
|
|
||||||
..IVerge::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开系统代理
|
|
||||||
pub fn enable_system_proxy() {
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
match patch_verge(IVerge {
|
|
||||||
enable_system_proxy: Some(true),
|
|
||||||
..IVerge::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭系统代理
|
|
||||||
pub fn disable_system_proxy() {
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
match patch_verge(IVerge {
|
|
||||||
enable_system_proxy: Some(false),
|
|
||||||
..IVerge::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换tun模式
|
|
||||||
pub fn toggle_tun_mode() {
|
|
||||||
let enable = Config::verge().data().enable_tun_mode.clone();
|
|
||||||
let enable = enable.unwrap_or(false);
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
match patch_verge(IVerge {
|
|
||||||
enable_tun_mode: Some(!enable),
|
|
||||||
..IVerge::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开tun模式
|
|
||||||
pub fn enable_tun_mode() {
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
match patch_verge(IVerge {
|
|
||||||
enable_tun_mode: Some(true),
|
|
||||||
..IVerge::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭tun模式
|
|
||||||
pub fn disable_tun_mode() {
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
match patch_verge(IVerge {
|
|
||||||
enable_tun_mode: Some(false),
|
|
||||||
..IVerge::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 修改clash的配置
|
|
||||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|
||||||
Config::clash().draft().patch_config(patch.clone());
|
|
||||||
|
|
||||||
match {
|
|
||||||
let mixed_port = patch.get("mixed-port");
|
|
||||||
if mixed_port.is_some() {
|
|
||||||
let changed = mixed_port != Config::clash().data().0.get("mixed-port");
|
|
||||||
// 检查端口占用
|
|
||||||
if changed {
|
|
||||||
if let Some(port) = mixed_port.clone().unwrap().as_u64() {
|
|
||||||
if !port_scanner::local_port_available(port as u16) {
|
|
||||||
Config::clash().discard();
|
|
||||||
bail!("port already in use");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 激活配置
|
|
||||||
if mixed_port.is_some()
|
|
||||||
|| patch.get("secret").is_some()
|
|
||||||
|| patch.get("external-controller").is_some()
|
|
||||||
{
|
|
||||||
Config::generate()?;
|
|
||||||
CoreManager::global().run_core().await?;
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新系统代理
|
|
||||||
if mixed_port.is_some() {
|
|
||||||
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
if patch.get("mode").is_some() {
|
|
||||||
log_err!(handle::Handle::update_systray_part());
|
|
||||||
}
|
|
||||||
|
|
||||||
Config::runtime().latest().patch_config(patch);
|
|
||||||
|
|
||||||
<Result<()>>::Ok(())
|
|
||||||
} {
|
|
||||||
Ok(()) => {
|
|
||||||
Config::clash().apply();
|
|
||||||
Config::clash().data().save_config()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
Config::clash().discard();
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 修改verge的配置
|
|
||||||
/// 一般都是一个个的修改
|
|
||||||
pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
|
||||||
Config::verge().draft().patch_config(patch.clone());
|
|
||||||
|
|
||||||
let tun_mode = patch.enable_tun_mode;
|
|
||||||
let auto_launch = patch.enable_auto_launch;
|
|
||||||
let system_proxy = patch.enable_system_proxy;
|
|
||||||
let proxy_bypass = patch.system_proxy_bypass;
|
|
||||||
let language = patch.language;
|
|
||||||
|
|
||||||
match {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let service_mode = patch.enable_service_mode;
|
|
||||||
|
|
||||||
if service_mode.is_some() {
|
|
||||||
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
|
|
||||||
|
|
||||||
Config::generate()?;
|
|
||||||
CoreManager::global().run_core().await?;
|
|
||||||
} else if tun_mode.is_some() {
|
|
||||||
update_core_config().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
if tun_mode.is_some() {
|
|
||||||
update_core_config().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if auto_launch.is_some() {
|
|
||||||
sysopt::Sysopt::global().update_launch()?;
|
|
||||||
}
|
|
||||||
if system_proxy.is_some() || proxy_bypass.is_some() {
|
|
||||||
sysopt::Sysopt::global().update_sysproxy()?;
|
|
||||||
sysopt::Sysopt::global().guard_proxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(true) = patch.enable_proxy_guard {
|
|
||||||
sysopt::Sysopt::global().guard_proxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(hotkeys) = patch.hotkeys {
|
|
||||||
hotkey::Hotkey::global().update(hotkeys)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if language.is_some() {
|
|
||||||
handle::Handle::update_systray()?;
|
|
||||||
} else if system_proxy.or(tun_mode).is_some() {
|
|
||||||
handle::Handle::update_systray_part()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
<Result<()>>::Ok(())
|
|
||||||
} {
|
|
||||||
Ok(()) => {
|
|
||||||
Config::verge().apply();
|
|
||||||
Config::verge().data().save_file()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
Config::verge().discard();
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 更新某个profile
|
|
||||||
/// 如果更新当前配置就激活配置
|
|
||||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
|
||||||
let url_opt = {
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let profiles = profiles.latest();
|
|
||||||
let item = profiles.get_item(&uid)?;
|
|
||||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
|
||||||
|
|
||||||
if !is_remote {
|
|
||||||
None // 直接更新
|
|
||||||
} else if item.url.is_none() {
|
|
||||||
bail!("failed to get the profile item url");
|
|
||||||
} else {
|
|
||||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let should_update = match url_opt {
|
|
||||||
Some((url, opt)) => {
|
|
||||||
let merged_opt = PrfOption::merge(opt, option);
|
|
||||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
|
||||||
|
|
||||||
let profiles = Config::profiles();
|
|
||||||
let mut profiles = profiles.latest();
|
|
||||||
profiles.update_item(uid.clone(), item)?;
|
|
||||||
|
|
||||||
Some(uid) == profiles.get_current()
|
|
||||||
}
|
|
||||||
None => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_update {
|
|
||||||
update_core_config().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 更新配置
|
|
||||||
async fn update_core_config() -> Result<()> {
|
|
||||||
match CoreManager::global().update_config().await {
|
|
||||||
Ok(_) => {
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
handle::Handle::notice_message("set_config::ok", "ok");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// copy env variable
|
|
||||||
pub fn copy_clash_env() {
|
|
||||||
let port = { Config::clash().data().get_client_info().port };
|
|
||||||
let text = format!("export https_proxy=http://127.0.0.1:{port} http_proxy=http://127.0.0.1:{port} all_proxy=socks5://127.0.0.1:{port}");
|
|
||||||
|
|
||||||
let mut cliboard = Clipboard::new();
|
|
||||||
cliboard.write_text(text);
|
|
||||||
}
|
|
@ -3,137 +3,119 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
extern crate tauri;
|
||||||
|
|
||||||
mod cmds;
|
mod cmds;
|
||||||
mod config;
|
mod config;
|
||||||
mod core;
|
mod events;
|
||||||
mod enhance;
|
|
||||||
mod feat;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::utils::{init, resolve, server};
|
use crate::{
|
||||||
use tauri::{api, SystemTray};
|
events::state,
|
||||||
|
utils::{
|
||||||
|
clash::put_clash_profile,
|
||||||
|
config::read_verge,
|
||||||
|
server::{check_singleton, embed_server},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::{
|
||||||
|
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||||
|
SystemTraySubmenu,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
// 单例检测
|
if check_singleton().is_err() {
|
||||||
if server::check_singleton().is_err() {
|
|
||||||
println!("app exists");
|
println!("app exists");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::log_err!(init::init_config());
|
let sub_menu = SystemTraySubmenu::new(
|
||||||
|
"出站规则",
|
||||||
#[allow(unused_mut)]
|
SystemTrayMenu::new()
|
||||||
let mut builder = tauri::Builder::default()
|
.add_item(CustomMenuItem::new("rway_global", "全局连接"))
|
||||||
.system_tray(SystemTray::new())
|
.add_item(CustomMenuItem::new("rway_rule", "规则连接").selected())
|
||||||
.setup(|app| Ok(resolve::resolve_setup(app)))
|
.add_item(CustomMenuItem::new("rway_direct", "直接连接")),
|
||||||
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
// common
|
|
||||||
cmds::get_sys_proxy,
|
|
||||||
cmds::open_app_dir,
|
|
||||||
cmds::open_logs_dir,
|
|
||||||
cmds::open_web_url,
|
|
||||||
cmds::open_core_dir,
|
|
||||||
// cmds::kill_sidecar,
|
|
||||||
cmds::restart_sidecar,
|
|
||||||
cmds::grant_permission,
|
|
||||||
// clash
|
|
||||||
cmds::get_clash_info,
|
|
||||||
cmds::get_clash_logs,
|
|
||||||
cmds::patch_clash_config,
|
|
||||||
cmds::change_clash_core,
|
|
||||||
cmds::get_runtime_config,
|
|
||||||
cmds::get_runtime_yaml,
|
|
||||||
cmds::get_runtime_exists,
|
|
||||||
cmds::get_runtime_logs,
|
|
||||||
// verge
|
|
||||||
cmds::get_verge_config,
|
|
||||||
cmds::patch_verge_config,
|
|
||||||
// cmds::update_hotkeys,
|
|
||||||
// profile
|
|
||||||
cmds::get_profiles,
|
|
||||||
cmds::enhance_profiles,
|
|
||||||
cmds::patch_profiles_config,
|
|
||||||
cmds::view_profile,
|
|
||||||
cmds::patch_profile,
|
|
||||||
cmds::create_profile,
|
|
||||||
cmds::import_profile,
|
|
||||||
cmds::update_profile,
|
|
||||||
cmds::delete_profile,
|
|
||||||
cmds::read_profile_file,
|
|
||||||
cmds::save_profile_file,
|
|
||||||
// service mode
|
|
||||||
cmds::service::check_service,
|
|
||||||
cmds::service::install_service,
|
|
||||||
cmds::service::uninstall_service,
|
|
||||||
// clash api
|
|
||||||
cmds::clash_api_get_proxy_delay
|
|
||||||
]);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
use tauri::{Menu, MenuItem, Submenu};
|
|
||||||
|
|
||||||
builder = builder.menu(
|
|
||||||
Menu::new().add_submenu(Submenu::new(
|
|
||||||
"Edit",
|
|
||||||
Menu::new()
|
|
||||||
.add_native_item(MenuItem::Undo)
|
|
||||||
.add_native_item(MenuItem::Redo)
|
|
||||||
.add_native_item(MenuItem::Copy)
|
|
||||||
.add_native_item(MenuItem::Paste)
|
|
||||||
.add_native_item(MenuItem::Cut)
|
|
||||||
.add_native_item(MenuItem::SelectAll)
|
|
||||||
.add_native_item(MenuItem::CloseWindow)
|
|
||||||
.add_native_item(MenuItem::Quit),
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
}
|
let menu = SystemTrayMenu::new()
|
||||||
|
.add_submenu(sub_menu)
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new("syste_proxy", "设置为系统代理"))
|
||||||
|
.add_item(CustomMenuItem::new("self_startup", "开机启动").selected())
|
||||||
|
.add_item(CustomMenuItem::new("open_window", "显示应用"))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q"));
|
||||||
|
|
||||||
let app = builder
|
let app = tauri::Builder::default()
|
||||||
|
.system_tray(SystemTray::new().with_menu(menu))
|
||||||
|
.on_system_tray_event(move |app, event| match event {
|
||||||
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
|
"open_window" => {
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
window.show().unwrap();
|
||||||
|
window.set_focus().unwrap();
|
||||||
|
}
|
||||||
|
"quit" => {
|
||||||
|
api::process::kill_children();
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
SystemTrayEvent::LeftClick { .. } => {
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
window.show().unwrap();
|
||||||
|
window.set_focus().unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
cmds::some::restart_sidecar,
|
||||||
|
cmds::some::set_sys_proxy,
|
||||||
|
cmds::some::get_sys_proxy,
|
||||||
|
cmds::some::get_clash_info,
|
||||||
|
cmds::some::patch_clash_config,
|
||||||
|
cmds::some::get_verge_config,
|
||||||
|
cmds::some::patch_verge_config,
|
||||||
|
cmds::profile::import_profile,
|
||||||
|
cmds::profile::update_profile,
|
||||||
|
cmds::profile::get_profiles,
|
||||||
|
cmds::profile::set_profiles,
|
||||||
|
cmds::profile::put_profiles,
|
||||||
|
])
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
||||||
|
// a simple http server
|
||||||
|
embed_server(&app.handle());
|
||||||
|
|
||||||
|
// init app config
|
||||||
|
utils::init::init_app(app.package_info());
|
||||||
|
// run clash sidecar
|
||||||
|
let info = utils::clash::run_clash_bin(&app.handle());
|
||||||
|
// update the profile
|
||||||
|
let info_copy = info.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
match put_clash_profile(&info_copy).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => log::error!("failed to put config for `{}`", err),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
app.manage(state::VergeConfLock(Arc::new(Mutex::new(read_verge()))));
|
||||||
|
app.manage(state::ClashInfoState(Arc::new(Mutex::new(info))));
|
||||||
|
app.manage(state::ProfileLock::default());
|
||||||
|
|
||||||
app.run(|app_handle, e| match e {
|
app.run(|app_handle, e| match e {
|
||||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
tauri::Event::CloseRequested { label, api, .. } => {
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
api.prevent_close();
|
||||||
|
app_handle.get_window(&label).unwrap().hide().unwrap();
|
||||||
|
}
|
||||||
|
tauri::Event::ExitRequested { api, .. } => {
|
||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
}
|
}
|
||||||
tauri::RunEvent::Exit => {
|
tauri::Event::Exit => {
|
||||||
resolve::resolve_reset();
|
|
||||||
api::process::kill_children();
|
api::process::kill_children();
|
||||||
app_handle.exit(0);
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
if label == "main" {
|
|
||||||
match event {
|
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
|
||||||
api.prevent_close();
|
|
||||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
|
||||||
|
|
||||||
app_handle.get_window("main").map(|win| {
|
|
||||||
let _ = win.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
|
||||||
if label == "main" {
|
|
||||||
match event {
|
|
||||||
tauri::WindowEvent::CloseRequested { .. } => {
|
|
||||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
|
||||||
}
|
|
||||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
|
||||||
let _ = resolve::save_window_size_position(&app_handle, false);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
|
150
src-tauri/src/utils/clash.rs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
extern crate log;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
events::emit::{clash_start, ClashInfoPayload},
|
||||||
|
utils::{
|
||||||
|
app_home_dir,
|
||||||
|
config::{read_clash_controller, read_profiles, read_yaml, save_yaml},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{collections::HashMap, env::temp_dir};
|
||||||
|
use tauri::{
|
||||||
|
api::process::{Command, CommandEvent},
|
||||||
|
AppHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Run the clash bin
|
||||||
|
pub fn run_clash_bin(app_handle: &AppHandle) -> ClashInfoPayload {
|
||||||
|
let app_dir = app_home_dir();
|
||||||
|
let app_dir = app_dir.as_os_str().to_str().unwrap();
|
||||||
|
|
||||||
|
let mut payload = ClashInfoPayload {
|
||||||
|
status: "success".to_string(),
|
||||||
|
controller: None,
|
||||||
|
message: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match Command::new_sidecar("clash") {
|
||||||
|
Ok(cmd) => match cmd.args(["-d", app_dir]).spawn() {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
},
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok((mut rx, _)) => {
|
||||||
|
log::info!("Successfully execute clash sidecar");
|
||||||
|
payload.controller = Some(read_clash_controller());
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
while let Some(event) = rx.recv().await {
|
||||||
|
match event {
|
||||||
|
CommandEvent::Stdout(line) => log::info!("{}", line),
|
||||||
|
CommandEvent::Stderr(err) => log::error!("{}", err),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to execute clash sidecar for \"{}\"", err);
|
||||||
|
payload.status = "error".to_string();
|
||||||
|
payload.message = Some(err.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clash_start(app_handle, &payload);
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the clash profile firstly
|
||||||
|
pub async fn put_clash_profile(payload: &ClashInfoPayload) -> Result<(), String> {
|
||||||
|
let profile = {
|
||||||
|
let profiles = read_profiles();
|
||||||
|
let current = profiles.current.unwrap_or(0) as usize;
|
||||||
|
match profiles.items {
|
||||||
|
Some(items) => {
|
||||||
|
if items.len() == 0 {
|
||||||
|
return Err("can not read profiles".to_string());
|
||||||
|
}
|
||||||
|
let idx = if current < items.len() { current } else { 0 };
|
||||||
|
items[idx].clone()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err("can not read profiles".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// temp profile's path
|
||||||
|
let temp_path = temp_dir().join("clash-verge-runtime.yaml");
|
||||||
|
|
||||||
|
// generate temp profile
|
||||||
|
{
|
||||||
|
let file_name = match profile.file {
|
||||||
|
Some(file_name) => file_name.clone(),
|
||||||
|
None => {
|
||||||
|
return Err(format!("profile item should have `file` field"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_path = app_home_dir().join("profiles").join(file_name);
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Err(format!("profile `{:?}` not exists", file_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the following fields are allowed:
|
||||||
|
// proxies/proxy-providers/proxy-groups/rule-providers/rules
|
||||||
|
let config = read_yaml::<Mapping>(file_path.clone());
|
||||||
|
let mut new_config = Mapping::new();
|
||||||
|
vec![
|
||||||
|
"proxies",
|
||||||
|
"proxy-providers",
|
||||||
|
"proxy-groups",
|
||||||
|
"rule-providers",
|
||||||
|
"rules",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|item| Value::String(item.to_string()))
|
||||||
|
.for_each(|key| {
|
||||||
|
if config.contains_key(&key) {
|
||||||
|
let value = config[&key].clone();
|
||||||
|
new_config.insert(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match save_yaml(
|
||||||
|
temp_path.clone(),
|
||||||
|
&new_config,
|
||||||
|
Some("# Clash Verge Temp File"),
|
||||||
|
) {
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctrl = payload.controller.clone().unwrap();
|
||||||
|
let server = format!("http://{}/configs", ctrl.server.unwrap());
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||||
|
|
||||||
|
if let Some(secret) = ctrl.secret {
|
||||||
|
headers.insert(
|
||||||
|
"Authorization",
|
||||||
|
format!("Bearer {}", secret).parse().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
data.insert("path", temp_path.as_os_str().to_str().unwrap());
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
match client.put(server).headers(headers).json(&data).send().await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => Err(format!("request failed `{}`", err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
132
src-tauri/src/utils/config.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
use crate::{
|
||||||
|
config::{ClashController, ProfilesConfig, VergeConfig},
|
||||||
|
utils::app_home_dir,
|
||||||
|
};
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
/// read data from yaml as struct T
|
||||||
|
pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
|
||||||
|
let yaml_str = fs::read_to_string(path).unwrap_or("".into());
|
||||||
|
serde_yaml::from_str::<T>(&yaml_str).unwrap_or(T::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - save the data to the file
|
||||||
|
/// - can set `prefix` string to add some comments
|
||||||
|
pub fn save_yaml<T: Serialize>(
|
||||||
|
path: PathBuf,
|
||||||
|
data: &T,
|
||||||
|
prefix: Option<&str>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Ok(data_str) = serde_yaml::to_string(data) {
|
||||||
|
let yaml_str = if prefix.is_some() {
|
||||||
|
prefix.unwrap().to_string() + &data_str
|
||||||
|
} else {
|
||||||
|
data_str
|
||||||
|
};
|
||||||
|
|
||||||
|
if fs::write(path.clone(), yaml_str.as_bytes()).is_err() {
|
||||||
|
Err(format!("can not save file `{:?}`", path))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(String::from("can not convert the data to yaml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Clash Core Config `config.yaml`
|
||||||
|
pub fn read_clash() -> Mapping {
|
||||||
|
read_yaml::<Mapping>(app_home_dir().join("config.yaml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the clash core Config `config.yaml`
|
||||||
|
pub fn save_clash(config: &Mapping) -> Result<(), String> {
|
||||||
|
save_yaml(
|
||||||
|
app_home_dir().join("config.yaml"),
|
||||||
|
config,
|
||||||
|
Some("# Default Config For Clash Core\n\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get infomation of the clash's `external-controller` and `secret`
|
||||||
|
pub fn read_clash_controller() -> ClashController {
|
||||||
|
let config = read_clash();
|
||||||
|
|
||||||
|
let key_port_1 = Value::String("port".to_string());
|
||||||
|
let key_port_2 = Value::String("mixed-port".to_string());
|
||||||
|
let key_server = Value::String("external-controller".to_string());
|
||||||
|
let key_secret = Value::String("secret".to_string());
|
||||||
|
|
||||||
|
let port = match config.get(&key_port_1) {
|
||||||
|
Some(value) => match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
Value::Number(val_num) => Some(val_num.to_string()),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let port = match port {
|
||||||
|
Some(_) => port,
|
||||||
|
None => match config.get(&key_port_2) {
|
||||||
|
Some(value) => match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
Value::Number(val_num) => Some(val_num.to_string()),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = match config.get(&key_server) {
|
||||||
|
Some(value) => match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let secret = match config.get(&key_secret) {
|
||||||
|
Some(value) => match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||||
|
Value::Number(val_num) => Some(val_num.to_string()),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
ClashController {
|
||||||
|
port,
|
||||||
|
server,
|
||||||
|
secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Profiles Config
|
||||||
|
pub fn read_profiles() -> ProfilesConfig {
|
||||||
|
read_yaml::<ProfilesConfig>(app_home_dir().join("profiles.yaml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save Verge App Config
|
||||||
|
pub fn save_profiles(profiles: &ProfilesConfig) -> Result<(), String> {
|
||||||
|
save_yaml(
|
||||||
|
app_home_dir().join("profiles.yaml"),
|
||||||
|
profiles,
|
||||||
|
Some("# Profiles Config for Clash Verge\n\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `verge.yaml`
|
||||||
|
pub fn read_verge() -> VergeConfig {
|
||||||
|
read_yaml::<VergeConfig>(app_home_dir().join("verge.yaml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save Verge App Config
|
||||||
|
pub fn save_verge(verge: &VergeConfig) -> Result<(), String> {
|
||||||
|
save_yaml(
|
||||||
|
app_home_dir().join("verge.yaml"),
|
||||||
|
verge,
|
||||||
|
Some("# The Config for Clash Verge App\n\n"),
|
||||||
|
)
|
||||||
|
}
|
@ -1,159 +1,18 @@
|
|||||||
use anyhow::Result;
|
use std::path::{Path, PathBuf};
|
||||||
use std::path::PathBuf;
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
api::path::{home_dir, resource_dir},
|
api::path::{home_dir, resource_dir},
|
||||||
Env, PackageInfo,
|
PackageInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
|
||||||
static APP_DIR: &str = "clash-verge";
|
|
||||||
#[cfg(feature = "verge-dev")]
|
|
||||||
static APP_DIR: &str = "clash-verge-dev";
|
|
||||||
|
|
||||||
static CLASH_CONFIG: &str = "config.yaml";
|
|
||||||
static VERGE_CONFIG: &str = "verge.yaml";
|
|
||||||
static PROFILE_YAML: &str = "profiles.yaml";
|
|
||||||
|
|
||||||
static mut RESOURCE_DIR: Option<PathBuf> = None;
|
|
||||||
|
|
||||||
/// portable flag
|
|
||||||
#[allow(unused)]
|
|
||||||
static mut PORTABLE_FLAG: bool = false;
|
|
||||||
|
|
||||||
pub static mut APP_VERSION: &str = "v1.2.0";
|
|
||||||
|
|
||||||
/// initialize portable flag
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub unsafe fn init_portable_flag() -> Result<()> {
|
|
||||||
use tauri::utils::platform::current_exe;
|
|
||||||
|
|
||||||
let exe = current_exe()?;
|
|
||||||
|
|
||||||
if let Some(dir) = exe.parent() {
|
|
||||||
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
|
||||||
|
|
||||||
if dir.exists() {
|
|
||||||
PORTABLE_FLAG = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the verge app home dir
|
/// get the verge app home dir
|
||||||
pub fn app_home_dir() -> Result<PathBuf> {
|
pub fn app_home_dir() -> PathBuf {
|
||||||
#[cfg(target_os = "windows")]
|
home_dir()
|
||||||
unsafe {
|
.unwrap()
|
||||||
use tauri::utils::platform::current_exe;
|
.join(Path::new(".config"))
|
||||||
|
.join(Path::new("clash-verge"))
|
||||||
if !PORTABLE_FLAG {
|
|
||||||
Ok(home_dir()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
|
||||||
.join(".config")
|
|
||||||
.join(APP_DIR))
|
|
||||||
} else {
|
|
||||||
let app_exe = current_exe()?;
|
|
||||||
let app_exe = dunce::canonicalize(app_exe)?;
|
|
||||||
let app_dir = app_exe
|
|
||||||
.parent()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
|
||||||
Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
Ok(home_dir()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
|
||||||
.join(".config")
|
|
||||||
.join(APP_DIR))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the resources dir
|
/// get the resources dir
|
||||||
pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
|
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
|
||||||
let res_dir = resource_dir(package_info, &Env::default())
|
resource_dir(package_info).unwrap().join("resources")
|
||||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
|
||||||
.join("resources");
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
RESOURCE_DIR = Some(res_dir.clone());
|
|
||||||
|
|
||||||
let ver = package_info.version.to_string();
|
|
||||||
let ver_str = format!("v{ver}");
|
|
||||||
APP_VERSION = Box::leak(Box::new(ver_str));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// profiles dir
|
|
||||||
pub fn app_profiles_dir() -> Result<PathBuf> {
|
|
||||||
Ok(app_home_dir()?.join("profiles"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// logs dir
|
|
||||||
pub fn app_logs_dir() -> Result<PathBuf> {
|
|
||||||
Ok(app_home_dir()?.join("logs"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clash_path() -> Result<PathBuf> {
|
|
||||||
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verge_path() -> Result<PathBuf> {
|
|
||||||
Ok(app_home_dir()?.join(VERGE_CONFIG))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn profiles_path() -> Result<PathBuf> {
|
|
||||||
Ok(app_home_dir()?.join(PROFILE_YAML))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn app_res_dir() -> Result<PathBuf> {
|
|
||||||
unsafe {
|
|
||||||
Ok(RESOURCE_DIR
|
|
||||||
.clone()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clash_pid_path() -> Result<PathBuf> {
|
|
||||||
unsafe {
|
|
||||||
Ok(RESOURCE_DIR
|
|
||||||
.clone()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
|
||||||
.join("clash.pid"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub fn service_path() -> Result<PathBuf> {
|
|
||||||
unsafe {
|
|
||||||
let res_dir = RESOURCE_DIR
|
|
||||||
.clone()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?;
|
|
||||||
Ok(res_dir.join("clash-verge-service.exe"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub fn service_log_file() -> Result<PathBuf> {
|
|
||||||
use chrono::Local;
|
|
||||||
|
|
||||||
let log_dir = app_logs_dir()?.join("service");
|
|
||||||
|
|
||||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
|
||||||
let log_file = format!("{}.log", local_time);
|
|
||||||
let log_file = log_dir.join(log_file);
|
|
||||||
|
|
||||||
let _ = std::fs::create_dir_all(&log_dir);
|
|
||||||
|
|
||||||
Ok(log_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
|
|
||||||
let path_str = path
|
|
||||||
.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
|
|
||||||
Ok(path_str)
|
|
||||||
}
|
}
|
||||||
|
96
src-tauri/src/utils/fetch.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use crate::config::{ProfileExtra, ProfileResponse};
|
||||||
|
use std::{
|
||||||
|
str::FromStr,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// parse the string
|
||||||
|
fn parse_string<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||||
|
match target.find(key) {
|
||||||
|
Some(idx) => {
|
||||||
|
let idx = idx + key.len();
|
||||||
|
let value = &target[idx..];
|
||||||
|
match match value.split(';').nth(0) {
|
||||||
|
Some(value) => value.trim().parse(),
|
||||||
|
None => value.trim().parse(),
|
||||||
|
} {
|
||||||
|
Ok(r) => Some(r),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch and parse the profile
|
||||||
|
pub async fn fetch_profile(url: &str) -> Option<ProfileResponse> {
|
||||||
|
let resp = match reqwest::get(url).await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
let header = resp.headers();
|
||||||
|
|
||||||
|
// parse the Subscription Userinfo
|
||||||
|
let extra = {
|
||||||
|
let sub_info = match header.get("Subscription-Userinfo") {
|
||||||
|
Some(value) => value.to_str().unwrap_or(""),
|
||||||
|
None => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileExtra {
|
||||||
|
upload: parse_string(sub_info, "upload=").unwrap_or(0),
|
||||||
|
download: parse_string(sub_info, "download=").unwrap_or(0),
|
||||||
|
total: parse_string(sub_info, "total=").unwrap_or(0),
|
||||||
|
expire: parse_string(sub_info, "expire=").unwrap_or(0),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the `name` and `file`
|
||||||
|
let (name, file) = {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
let file = format!("{}.yaml", now);
|
||||||
|
let name = header.get("Content-Disposition").unwrap().to_str().unwrap();
|
||||||
|
let name = parse_string::<String>(name, "filename=");
|
||||||
|
|
||||||
|
match name {
|
||||||
|
Some(f) => (f, file),
|
||||||
|
None => (file.clone(), file),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get the data
|
||||||
|
let data = match resp.text_with_charset("utf-8").await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ProfileResponse {
|
||||||
|
file,
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
extra,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_value() {
|
||||||
|
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||||
|
let test_2 = "attachment; filename=Clash.yaml";
|
||||||
|
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "upload=").unwrap(), 111);
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "download=").unwrap(), 2222);
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "total=").unwrap(), 3333);
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "expire=").unwrap(), 444);
|
||||||
|
assert_eq!(
|
||||||
|
parse_string::<String>(test_2, "filename=").unwrap(),
|
||||||
|
format!("Clash.yaml")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "aaa="), None);
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "upload1="), None);
|
||||||
|
assert_eq!(parse_string::<usize>(test_1, "expire1="), None);
|
||||||
|
assert_eq!(parse_string::<usize>(test_2, "attachment="), None);
|
||||||
|
}
|
@ -1,172 +0,0 @@
|
|||||||
use anyhow::{anyhow, bail, Context, Result};
|
|
||||||
use nanoid::nanoid;
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::{fs, path::PathBuf, str::FromStr};
|
|
||||||
|
|
||||||
/// read data from yaml as struct T
|
|
||||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
|
||||||
if !path.exists() {
|
|
||||||
bail!("file not found \"{}\"", path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
let yaml_str = fs::read_to_string(&path)
|
|
||||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
|
||||||
|
|
||||||
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to read the file with yaml format \"{}\"",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// read mapping from yaml fix #165
|
|
||||||
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
|
|
||||||
let mut val: Value = read_yaml(path)?;
|
|
||||||
val.apply_merge()
|
|
||||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
|
||||||
|
|
||||||
Ok(val
|
|
||||||
.as_mapping()
|
|
||||||
.ok_or(anyhow!(
|
|
||||||
"failed to transform to yaml mapping \"{}\"",
|
|
||||||
path.display()
|
|
||||||
))?
|
|
||||||
.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the data to the file
|
|
||||||
/// can set `prefix` string to add some comments
|
|
||||||
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
|
||||||
let data_str = serde_yaml::to_string(data)?;
|
|
||||||
|
|
||||||
let yaml_str = match prefix {
|
|
||||||
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
|
||||||
None => data_str,
|
|
||||||
};
|
|
||||||
|
|
||||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
|
||||||
fs::write(path, yaml_str.as_bytes())
|
|
||||||
.with_context(|| format!("failed to save file \"{path_str}\""))
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALPHABET: [char; 62] = [
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
|
||||||
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
|
||||||
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
|
||||||
'V', 'W', 'X', 'Y', 'Z',
|
|
||||||
];
|
|
||||||
|
|
||||||
/// generate the uid
|
|
||||||
pub fn get_uid(prefix: &str) -> String {
|
|
||||||
let id = nanoid!(11, &ALPHABET);
|
|
||||||
format!("{prefix}{id}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse the string
|
|
||||||
/// xxx=123123; => 123123
|
|
||||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
|
||||||
target.find(key).and_then(|idx| {
|
|
||||||
let idx = idx + key.len();
|
|
||||||
let value = &target[idx..];
|
|
||||||
|
|
||||||
match value.split(';').nth(0) {
|
|
||||||
Some(value) => value.trim().parse(),
|
|
||||||
None => value.trim().parse(),
|
|
||||||
}
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// open file
|
|
||||||
/// use vscode by default
|
|
||||||
pub fn open_file(path: PathBuf) -> Result<()> {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let code = "Visual Studio Code";
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let code = "code";
|
|
||||||
|
|
||||||
// use vscode first
|
|
||||||
if let Err(err) = open::with(&path, code) {
|
|
||||||
log::error!(target: "app", "failed to open file with VScode `{err}`");
|
|
||||||
// default open
|
|
||||||
open::that(path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! error {
|
|
||||||
($result: expr) => {
|
|
||||||
log::error!(target: "app", "{}", $result);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! log_err {
|
|
||||||
($result: expr) => {
|
|
||||||
if let Err(err) = $result {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
($result: expr, $err_str: expr) => {
|
|
||||||
if let Err(_) = $result {
|
|
||||||
log::error!(target: "app", "{}", $err_str);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! trace_err {
|
|
||||||
($result: expr, $err_str: expr) => {
|
|
||||||
if let Err(err) = $result {
|
|
||||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// wrap the anyhow error
|
|
||||||
/// transform the error to String
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! wrap_err {
|
|
||||||
($stat: expr) => {
|
|
||||||
match $stat {
|
|
||||||
Ok(a) => Ok(a),
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{}", err.to_string());
|
|
||||||
Err(format!("{}", err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return the string literal error
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! ret_err {
|
|
||||||
($str: expr) => {
|
|
||||||
return Err($str.into())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_value() {
|
|
||||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
|
||||||
let test_2 = "attachment; filename=Clash.yaml";
|
|
||||||
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
|
||||||
assert_eq!(
|
|
||||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
|
||||||
format!("Clash.yaml")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
|
|
||||||
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
|
|
||||||
}
|
|
@ -1,243 +1,106 @@
|
|||||||
use crate::config::*;
|
extern crate serde_yaml;
|
||||||
use crate::utils::{dirs, help};
|
|
||||||
use anyhow::Result;
|
use chrono::Local;
|
||||||
use chrono::{DateTime, Local};
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
use log4rs::config::{Appender, Logger, Root};
|
use log4rs::config::{Appender, Config, Root};
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
use std::fs::{self, DirEntry};
|
use std::fs::{self, File};
|
||||||
use std::str::FromStr;
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tauri::PackageInfo;
|
use tauri::PackageInfo;
|
||||||
|
|
||||||
|
use crate::utils::{app_home_dir, app_resources_dir};
|
||||||
|
|
||||||
/// initialize this instance's log file
|
/// initialize this instance's log file
|
||||||
fn init_log() -> Result<()> {
|
fn init_log(log_dir: &PathBuf) {
|
||||||
let log_dir = dirs::app_logs_dir()?;
|
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
|
||||||
if !log_dir.exists() {
|
|
||||||
let _ = fs::create_dir_all(&log_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
let log_level = Config::verge().data().get_log_level();
|
|
||||||
if log_level == LevelFilter::Off {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
|
||||||
let log_file = format!("{}.log", local_time);
|
let log_file = format!("{}.log", local_time);
|
||||||
let log_file = log_dir.join(log_file);
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
let log_pattern = match log_level {
|
let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}";
|
||||||
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
|
let stdout = ConsoleAppender::builder()
|
||||||
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
.encoder(Box::new(PatternEncoder::new(time_format)))
|
||||||
};
|
.build();
|
||||||
|
let tofile = FileAppender::builder()
|
||||||
|
.encoder(Box::new(PatternEncoder::new(time_format)))
|
||||||
|
.build(log_file)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let encode = Box::new(PatternEncoder::new(log_pattern));
|
let config = Config::builder()
|
||||||
|
|
||||||
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
|
|
||||||
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
|
|
||||||
|
|
||||||
let mut logger_builder = Logger::builder();
|
|
||||||
let mut root_builder = Root::builder();
|
|
||||||
|
|
||||||
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
|
|
||||||
|
|
||||||
#[cfg(feature = "verge-dev")]
|
|
||||||
{
|
|
||||||
logger_builder = logger_builder.appenders(["file", "stdout"]);
|
|
||||||
if log_more {
|
|
||||||
root_builder = root_builder.appenders(["file", "stdout"]);
|
|
||||||
} else {
|
|
||||||
root_builder = root_builder.appenders(["stdout"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
|
||||||
{
|
|
||||||
logger_builder = logger_builder.appenders(["file"]);
|
|
||||||
if log_more {
|
|
||||||
root_builder = root_builder.appenders(["file"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (config, _) = log4rs::config::Config::builder()
|
|
||||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||||
.appender(Appender::builder().build("file", Box::new(tofile)))
|
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||||
.logger(logger_builder.additive(false).build("app", log_level))
|
.build(
|
||||||
.build_lossy(root_builder.build(log_level));
|
Root::builder()
|
||||||
|
.appenders(["stdout", "file"])
|
||||||
|
.build(LevelFilter::Debug),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
log4rs::init_config(config)?;
|
log4rs::init_config(config).unwrap();
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除log文件
|
/// Initialize all the files from resources
|
||||||
pub fn delete_log() -> Result<()> {
|
fn init_config_file(app_dir: &PathBuf, res_dir: &PathBuf) {
|
||||||
let log_dir = dirs::app_logs_dir()?;
|
// target path
|
||||||
if !log_dir.exists() {
|
let clash_path = app_dir.join("config.yaml");
|
||||||
return Ok(());
|
let verge_path = app_dir.join("verge.yaml");
|
||||||
}
|
let profile_path = app_dir.join("profiles.yaml");
|
||||||
|
let mmdb_path = app_dir.join("Country.mmdb");
|
||||||
|
|
||||||
let auto_log_clean = {
|
// template path
|
||||||
let verge = Config::verge();
|
let clash_tmpl = res_dir.join("config_tmp.yaml");
|
||||||
let verge = verge.data();
|
let verge_tmpl = res_dir.join("verge_tmp.yaml");
|
||||||
verge.auto_log_clean.clone().unwrap_or(0)
|
let profiles_tmpl = res_dir.join("profiles_tmp.yaml");
|
||||||
};
|
let mmdb_tmpl = res_dir.join("Country.mmdb");
|
||||||
|
|
||||||
let day = match auto_log_clean {
|
if !clash_path.exists() {
|
||||||
1 => 7,
|
if clash_tmpl.exists() {
|
||||||
2 => 30,
|
fs::copy(clash_tmpl, clash_path).unwrap();
|
||||||
3 => 90,
|
|
||||||
_ => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!(target: "app", "try to delete log files, day: {day}");
|
|
||||||
|
|
||||||
// %Y-%m-%d to NaiveDateTime
|
|
||||||
let parse_time_str = |s: &str| {
|
|
||||||
let sa: Vec<&str> = s.split('-').collect();
|
|
||||||
if sa.len() != 4 {
|
|
||||||
return Err(anyhow::anyhow!("invalid time str"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let year = i32::from_str(sa[0])?;
|
|
||||||
let month = u32::from_str(sa[1])?;
|
|
||||||
let day = u32::from_str(sa[2])?;
|
|
||||||
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
|
|
||||||
.ok_or(anyhow::anyhow!("invalid time str"))?
|
|
||||||
.and_hms_opt(0, 0, 0)
|
|
||||||
.ok_or(anyhow::anyhow!("invalid time str"))?;
|
|
||||||
Ok(time)
|
|
||||||
};
|
|
||||||
|
|
||||||
let process_file = |file: DirEntry| -> Result<()> {
|
|
||||||
let file_name = file.file_name();
|
|
||||||
let file_name = file_name.to_str().unwrap_or_default();
|
|
||||||
|
|
||||||
if file_name.ends_with(".log") {
|
|
||||||
let now = Local::now();
|
|
||||||
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
|
||||||
let file_time = DateTime::<Local>::from_local(created_time, now.offset().clone());
|
|
||||||
|
|
||||||
let duration = now.signed_duration_since(file_time);
|
|
||||||
if duration.num_days() > day {
|
|
||||||
let file_path = file.path();
|
|
||||||
let _ = fs::remove_file(file_path);
|
|
||||||
log::info!(target: "app", "delete log file: {file_name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
for file in fs::read_dir(&log_dir)? {
|
|
||||||
if let Ok(file) = file {
|
|
||||||
let _ = process_file(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize all the config files
|
|
||||||
/// before tauri setup
|
|
||||||
pub fn init_config() -> Result<()> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
unsafe {
|
|
||||||
let _ = dirs::init_portable_flag();
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = init_log();
|
|
||||||
let _ = delete_log();
|
|
||||||
|
|
||||||
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
|
||||||
if !app_dir.exists() {
|
|
||||||
let _ = fs::create_dir_all(&app_dir);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {
|
|
||||||
if !profiles_dir.exists() {
|
|
||||||
let _ = fs::create_dir_all(&profiles_dir);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
crate::log_err!(dirs::clash_path().map(|path| {
|
|
||||||
if !path.exists() {
|
|
||||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Verge"))?;
|
|
||||||
}
|
|
||||||
<Result<()>>::Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
crate::log_err!(dirs::verge_path().map(|path| {
|
|
||||||
if !path.exists() {
|
|
||||||
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
|
|
||||||
}
|
|
||||||
<Result<()>>::Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
crate::log_err!(dirs::profiles_path().map(|path| {
|
|
||||||
if !path.exists() {
|
|
||||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
|
||||||
}
|
|
||||||
<Result<()>>::Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// initialize app resources
|
|
||||||
/// after tauri setup
|
|
||||||
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
|
||||||
let app_dir = dirs::app_home_dir()?;
|
|
||||||
let res_dir = dirs::app_resources_dir(package_info)?;
|
|
||||||
|
|
||||||
if !app_dir.exists() {
|
|
||||||
let _ = fs::create_dir_all(&app_dir);
|
|
||||||
}
|
|
||||||
if !res_dir.exists() {
|
|
||||||
let _ = fs::create_dir_all(&res_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"];
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
|
||||||
|
|
||||||
// copy the resource file
|
|
||||||
// if the source file is newer than the destination file, copy it over
|
|
||||||
for file in file_list.iter() {
|
|
||||||
let src_path = res_dir.join(file);
|
|
||||||
let dest_path = app_dir.join(file);
|
|
||||||
|
|
||||||
let handle_copy = || {
|
|
||||||
match fs::copy(&src_path, &dest_path) {
|
|
||||||
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "failed to copy resources '{file}', {err}")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if src_path.exists() && !dest_path.exists() {
|
|
||||||
handle_copy();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
|
||||||
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
|
||||||
|
|
||||||
match (src_modified, dest_modified) {
|
|
||||||
(Ok(src_modified), Ok(dest_modified)) => {
|
|
||||||
if src_modified > dest_modified {
|
|
||||||
handle_copy();
|
|
||||||
} else {
|
} else {
|
||||||
log::debug!(target: "app", "skipping resource copy '{file}'");
|
// make sure that the config.yaml not null
|
||||||
|
let content = b"\
|
||||||
|
mixed-port: 7890\n\
|
||||||
|
log-level: info\n\
|
||||||
|
allow-lan: false\n\
|
||||||
|
external-controller: 127.0.0.1:9090\n\
|
||||||
|
secret: \"\"\n";
|
||||||
|
File::create(clash_path).unwrap().write(content).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
log::debug!(target: "app", "failed to get modified '{file}'");
|
|
||||||
handle_copy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
// only copy it
|
||||||
|
if !verge_path.exists() && verge_tmpl.exists() {
|
||||||
|
fs::copy(verge_tmpl, verge_path).unwrap();
|
||||||
|
}
|
||||||
|
if !profile_path.exists() && profiles_tmpl.exists() {
|
||||||
|
fs::copy(profiles_tmpl, profile_path).unwrap();
|
||||||
|
}
|
||||||
|
if !mmdb_path.exists() && mmdb_tmpl.exists() {
|
||||||
|
fs::copy(mmdb_tmpl, mmdb_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// initialize app
|
||||||
|
pub fn init_app(package_info: &PackageInfo) {
|
||||||
|
// create app dir
|
||||||
|
let app_dir = app_home_dir();
|
||||||
|
let log_dir = app_dir.join("logs");
|
||||||
|
let profiles_dir = app_dir.join("profiles");
|
||||||
|
|
||||||
|
let res_dir = app_resources_dir(package_info);
|
||||||
|
|
||||||
|
if !app_dir.exists() {
|
||||||
|
fs::create_dir(&app_dir).unwrap();
|
||||||
|
}
|
||||||
|
if !log_dir.exists() {
|
||||||
|
fs::create_dir(&log_dir).unwrap();
|
||||||
|
}
|
||||||
|
if !profiles_dir.exists() {
|
||||||
|
fs::create_dir(&profiles_dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
init_log(&log_dir);
|
||||||
|
init_config_file(&app_dir, &res_dir);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
pub mod dirs;
|
mod dirs;
|
||||||
pub mod help;
|
pub use self::dirs::*;
|
||||||
|
|
||||||
|
pub mod clash;
|
||||||
|
pub mod config;
|
||||||
|
pub mod fetch;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod resolve;
|
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod sysopt;
|
||||||
// mod winhelp;
|
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
use crate::{config::Config, core::*, utils::init, utils::server};
|
|
||||||
use crate::{log_err, trace_err};
|
|
||||||
use anyhow::Result;
|
|
||||||
use tauri::{App, AppHandle, Manager};
|
|
||||||
|
|
||||||
/// handle something when start app
|
|
||||||
pub fn resolve_setup(app: &mut App) {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
|
||||||
|
|
||||||
handle::Handle::global().init(app.app_handle());
|
|
||||||
|
|
||||||
log_err!(init::init_resources(app.package_info()));
|
|
||||||
|
|
||||||
// 启动核心
|
|
||||||
log::trace!("init config");
|
|
||||||
log_err!(Config::init_config());
|
|
||||||
|
|
||||||
log::trace!("launch core");
|
|
||||||
log_err!(CoreManager::global().init());
|
|
||||||
|
|
||||||
// setup a simple http server for singleton
|
|
||||||
log::trace!("launch embed server");
|
|
||||||
server::embed_server(app.app_handle());
|
|
||||||
|
|
||||||
log::trace!("init system tray");
|
|
||||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
|
||||||
|
|
||||||
let silent_start = { Config::verge().data().enable_silent_start.clone() };
|
|
||||||
if !silent_start.unwrap_or(false) {
|
|
||||||
create_window(&app.app_handle());
|
|
||||||
}
|
|
||||||
|
|
||||||
log_err!(sysopt::Sysopt::global().init_launch());
|
|
||||||
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
|
||||||
|
|
||||||
log_err!(handle::Handle::update_systray_part());
|
|
||||||
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
|
||||||
log_err!(timer::Timer::global().init());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// reset system proxy
|
|
||||||
pub fn resolve_reset() {
|
|
||||||
log_err!(sysopt::Sysopt::global().reset_sysproxy());
|
|
||||||
log_err!(CoreManager::global().stop_core());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// create main window
|
|
||||||
pub fn create_window(app_handle: &AppHandle) {
|
|
||||||
if let Some(window) = app_handle.get_window("main") {
|
|
||||||
trace_err!(window.unminimize(), "set win unminimize");
|
|
||||||
trace_err!(window.show(), "set win visible");
|
|
||||||
trace_err!(window.set_focus(), "set win focus");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut builder = tauri::window::WindowBuilder::new(
|
|
||||||
app_handle,
|
|
||||||
"main".to_string(),
|
|
||||||
tauri::WindowUrl::App("index.html".into()),
|
|
||||||
)
|
|
||||||
.title("Clash Verge")
|
|
||||||
.fullscreen(false)
|
|
||||||
.min_inner_size(600.0, 520.0);
|
|
||||||
|
|
||||||
match Config::verge().latest().window_size_position.clone() {
|
|
||||||
Some(size_pos) if size_pos.len() == 4 => {
|
|
||||||
let size = (size_pos[0], size_pos[1]);
|
|
||||||
let pos = (size_pos[2], size_pos[3]);
|
|
||||||
let w = size.0.clamp(600.0, f64::INFINITY);
|
|
||||||
let h = size.1.clamp(520.0, f64::INFINITY);
|
|
||||||
builder = builder.inner_size(w, h).position(pos.0, pos.1);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
builder = builder.inner_size(800.0, 636.0).center();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
builder = builder.inner_size(800.0, 642.0).center();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
builder = builder.inner_size(800.0, 642.0).center();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
use window_shadows::set_shadow;
|
|
||||||
|
|
||||||
match builder
|
|
||||||
.decorations(false)
|
|
||||||
.transparent(true)
|
|
||||||
.visible(false)
|
|
||||||
.build()
|
|
||||||
{
|
|
||||||
Ok(win) => {
|
|
||||||
log::trace!("try to calculate the monitor size");
|
|
||||||
let center = (|| -> Result<bool> {
|
|
||||||
let mut center = false;
|
|
||||||
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
|
||||||
let size = monitor.size();
|
|
||||||
let pos = win.outer_position()?;
|
|
||||||
|
|
||||||
if pos.x < -400
|
|
||||||
|| pos.x > (size.width - 200).try_into()?
|
|
||||||
|| pos.y < -200
|
|
||||||
|| pos.y > (size.height - 200).try_into()?
|
|
||||||
{
|
|
||||||
center = true;
|
|
||||||
}
|
|
||||||
Ok(center)
|
|
||||||
})();
|
|
||||||
|
|
||||||
if center.unwrap_or(true) {
|
|
||||||
trace_err!(win.center(), "set win center");
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("try to create window");
|
|
||||||
let app_handle = app_handle.clone();
|
|
||||||
|
|
||||||
// 加点延迟避免界面闪一下
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
sleep(Duration::from_millis(888)).await;
|
|
||||||
|
|
||||||
if let Some(window) = app_handle.get_window("main") {
|
|
||||||
trace_err!(set_shadow(&window, true), "set win shadow");
|
|
||||||
trace_err!(window.show(), "set win visible");
|
|
||||||
trace_err!(window.unminimize(), "set win unminimize");
|
|
||||||
trace_err!(window.set_focus(), "set win focus");
|
|
||||||
} else {
|
|
||||||
log::error!(target: "app", "failed to create window, get_window is None")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
crate::log_err!(builder
|
|
||||||
.decorations(true)
|
|
||||||
.hidden_title(true)
|
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
crate::log_err!(builder.decorations(true).transparent(false).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save window size and position
|
|
||||||
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
|
||||||
let win = app_handle
|
|
||||||
.get_window("main")
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
|
||||||
|
|
||||||
let scale = win.scale_factor()?;
|
|
||||||
let size = win.inner_size()?;
|
|
||||||
let size = size.to_logical::<f64>(scale);
|
|
||||||
let pos = win.outer_position()?;
|
|
||||||
let pos = pos.to_logical::<f64>(scale);
|
|
||||||
|
|
||||||
let verge = Config::verge();
|
|
||||||
let mut verge = verge.latest();
|
|
||||||
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
|
||||||
|
|
||||||
if save_to_file {
|
|
||||||
verge.save_file()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,27 +1,18 @@
|
|||||||
extern crate warp;
|
extern crate warp;
|
||||||
|
|
||||||
use super::resolve;
|
|
||||||
use crate::config::IVerge;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use port_scanner::local_port_available;
|
use port_scanner::local_port_available;
|
||||||
use tauri::AppHandle;
|
use tauri::{AppHandle, Manager};
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
|
||||||
|
const SERVER_PORT: u16 = 33333;
|
||||||
|
|
||||||
/// check whether there is already exists
|
/// check whether there is already exists
|
||||||
pub fn check_singleton() -> Result<()> {
|
pub fn check_singleton() -> Result<(), ()> {
|
||||||
let port = IVerge::get_singleton_port();
|
if !local_port_available(SERVER_PORT) {
|
||||||
|
|
||||||
if !local_port_available(port) {
|
|
||||||
tauri::async_runtime::block_on(async {
|
tauri::async_runtime::block_on(async {
|
||||||
let url = format!("http://127.0.0.1:{port}/commands/visible");
|
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT);
|
||||||
let resp = reqwest::get(url).await?.text().await?;
|
reqwest::get(url).await.unwrap();
|
||||||
|
Err(())
|
||||||
if &resp == "ok" {
|
|
||||||
bail!("app exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
log::error!("failed to setup singleton listen server");
|
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -30,15 +21,18 @@ pub fn check_singleton() -> Result<()> {
|
|||||||
|
|
||||||
/// The embed server only be used to implement singleton process
|
/// The embed server only be used to implement singleton process
|
||||||
/// maybe it can be used as pac server later
|
/// maybe it can be used as pac server later
|
||||||
pub fn embed_server(app_handle: AppHandle) {
|
pub fn embed_server(app: &AppHandle) {
|
||||||
let port = IVerge::get_singleton_port();
|
let window = app.get_window("main").unwrap();
|
||||||
|
|
||||||
|
let commands = warp::path!("commands" / "visible").map(move || {
|
||||||
|
window.show().unwrap();
|
||||||
|
window.set_focus().unwrap();
|
||||||
|
return format!("ok");
|
||||||
|
});
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let commands = warp::path!("commands" / "visible").map(move || {
|
warp::serve(commands)
|
||||||
resolve::create_window(&app_handle);
|
.bind(([127, 0, 0, 1], SERVER_PORT))
|
||||||
format!("ok")
|
.await;
|
||||||
});
|
|
||||||
|
|
||||||
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
71
src-tauri/src/utils/sysopt.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct SysProxyConfig {
|
||||||
|
pub enable: bool,
|
||||||
|
pub server: String,
|
||||||
|
pub bypass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod win {
|
||||||
|
use super::*;
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
/// Get the windows system proxy config
|
||||||
|
pub fn get_proxy_config() -> io::Result<SysProxyConfig> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let cur_var = hkcu.open_subkey_with_flags(
|
||||||
|
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||||
|
KEY_READ,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(SysProxyConfig {
|
||||||
|
enable: cur_var.get_value::<u32, _>("ProxyEnable")? == 1u32,
|
||||||
|
server: cur_var.get_value("ProxyServer")?,
|
||||||
|
bypass: cur_var.get_value("ProxyOverride")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the windows system proxy config
|
||||||
|
pub fn set_proxy_config(config: &SysProxyConfig) -> io::Result<()> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let cur_var = hkcu.open_subkey_with_flags(
|
||||||
|
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||||
|
KEY_SET_VALUE,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let enable: u32 = if config.enable { 1u32 } else { 0u32 };
|
||||||
|
|
||||||
|
cur_var.set_value("ProxyEnable", &enable)?;
|
||||||
|
cur_var.set_value("ProxyServer", &config.server)?;
|
||||||
|
cur_var.set_value("ProxyOverride", &config.bypass)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn get_proxy_config() -> io::Result<SysProxyConfig> {
|
||||||
|
Ok(SysProxyConfig {
|
||||||
|
enable: false,
|
||||||
|
server: "server".into(),
|
||||||
|
bypass: "bypass".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_proxy_config(config: &SysProxyConfig) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use win::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub use macos::*;
|
@ -1,36 +0,0 @@
|
|||||||
///! Some config file template
|
|
||||||
|
|
||||||
/// template for new a profile item
|
|
||||||
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
|
||||||
|
|
||||||
proxies:
|
|
||||||
|
|
||||||
proxy-groups:
|
|
||||||
|
|
||||||
rules:
|
|
||||||
";
|
|
||||||
|
|
||||||
/// enhanced profile
|
|
||||||
pub const ITEM_MERGE: &str = "# Merge Template for clash verge
|
|
||||||
# The `Merge` format used to enhance profile
|
|
||||||
|
|
||||||
prepend-rules:
|
|
||||||
|
|
||||||
prepend-proxies:
|
|
||||||
|
|
||||||
prepend-proxy-groups:
|
|
||||||
|
|
||||||
append-rules:
|
|
||||||
|
|
||||||
append-proxies:
|
|
||||||
|
|
||||||
append-proxy-groups:
|
|
||||||
";
|
|
||||||
|
|
||||||
/// enhanced profile
|
|
||||||
pub const ITEM_SCRIPT: &str = "// Define the `main` function
|
|
||||||
|
|
||||||
function main(params) {
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
";
|
|
@ -1,69 +0,0 @@
|
|||||||
#![cfg(target_os = "windows")]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
#![allow(non_camel_case_types)]
|
|
||||||
|
|
||||||
//!
|
|
||||||
//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs
|
|
||||||
//!
|
|
||||||
|
|
||||||
use windows_sys::Win32::{
|
|
||||||
Foundation::*,
|
|
||||||
System::{LibraryLoader::*, SystemInformation::*},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {
|
|
||||||
assert_eq!(library.chars().last(), Some('\0'));
|
|
||||||
assert_eq!(function.chars().last(), Some('\0'));
|
|
||||||
|
|
||||||
let module = unsafe { LoadLibraryA(library.as_ptr()) };
|
|
||||||
if module == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(unsafe { GetProcAddress(module, function.as_ptr()) })
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! get_function {
|
|
||||||
($lib:expr, $func:ident) => {
|
|
||||||
get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe {
|
|
||||||
std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a tuple of (major, minor, buildnumber)
|
|
||||||
fn get_windows_ver() -> Option<(u32, u32, u32)> {
|
|
||||||
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32;
|
|
||||||
let handle = get_function!("ntdll.dll", RtlGetVersion);
|
|
||||||
if let Some(rtl_get_version) = handle {
|
|
||||||
unsafe {
|
|
||||||
let mut vi = OSVERSIONINFOW {
|
|
||||||
dwOSVersionInfoSize: 0,
|
|
||||||
dwMajorVersion: 0,
|
|
||||||
dwMinorVersion: 0,
|
|
||||||
dwBuildNumber: 0,
|
|
||||||
dwPlatformId: 0,
|
|
||||||
szCSDVersion: [0; 128],
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = (rtl_get_version)(&mut vi as _);
|
|
||||||
|
|
||||||
if status >= 0 {
|
|
||||||
Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_win11() -> bool {
|
|
||||||
let v = get_windows_ver().unwrap_or_default();
|
|
||||||
v.2 >= 22000
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_version() {
|
|
||||||
dbg!(get_windows_ver().unwrap_or_default());
|
|
||||||
}
|
|
@ -1,42 +1,44 @@
|
|||||||
{
|
{
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "1.3.8"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
"devPath": "http://localhost:3000/",
|
"devPath": "http://localhost:3000/proxy",
|
||||||
"beforeDevCommand": "yarn run web:dev",
|
"beforeDevCommand": "yarn run web:dev",
|
||||||
"beforeBuildCommand": "yarn run web:build"
|
"beforeBuildCommand": "yarn run web:build"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"systemTray": {
|
"systemTray": {
|
||||||
"iconPath": "icons/tray-icon.ico",
|
"iconPath": "icons/icon.png",
|
||||||
"iconAsTemplate": true
|
"iconAsTemplate": true
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"identifier": "top.gydi.clashverge",
|
"identifier": "com.tauri.dev",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon-new.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["resources"],
|
"resources": ["resources"],
|
||||||
"externalBin": ["sidecar/clash", "sidecar/clash-meta"],
|
"externalBin": ["sidecar/clash"],
|
||||||
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
"copyright": "",
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"shortDescription": "A Clash GUI based on tauri.",
|
"shortDescription": "",
|
||||||
"longDescription": "A Clash GUI based on tauri.",
|
"longDescription": "",
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": ["openssl"]
|
"depends": [],
|
||||||
|
"useBootstrapper": false
|
||||||
},
|
},
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"frameworks": [],
|
"frameworks": [],
|
||||||
"minimumSystemVersion": "",
|
"minimumSystemVersion": "",
|
||||||
|
"useBootstrapper": false,
|
||||||
"exceptionDomain": "",
|
"exceptionDomain": "",
|
||||||
"signingIdentity": null,
|
"signingIdentity": null,
|
||||||
"entitlements": null
|
"entitlements": null
|
||||||
@ -44,38 +46,30 @@
|
|||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": "",
|
"timestampUrl": ""
|
||||||
"wix": {
|
|
||||||
"language": ["zh-CN", "en-US", "ru-RU"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": false
|
||||||
"endpoints": [
|
|
||||||
"https://ghproxy.com/https://github.com/zzzgydi/clash-verge/releases/download/updater/update-proxy.json",
|
|
||||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json"
|
|
||||||
],
|
|
||||||
"dialog": false,
|
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
|
||||||
},
|
},
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"shell": {
|
|
||||||
"all": true
|
"all": true
|
||||||
},
|
},
|
||||||
"window": {
|
"windows": [
|
||||||
"all": true
|
{
|
||||||
},
|
"title": "Clash Verge",
|
||||||
"process": {
|
"width": 800,
|
||||||
"all": true
|
"height": 600,
|
||||||
},
|
"resizable": true,
|
||||||
"globalShortcut": {
|
"fullscreen": false,
|
||||||
"all": true
|
"decorations": true,
|
||||||
|
"transparent": false,
|
||||||
|
"minWidth": 600,
|
||||||
|
"minHeight": 520
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
"windows": [],
|
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
|
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 57 KiB |
@ -4,18 +4,12 @@ body {
|
|||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-main: #5b5c9d;
|
--primary-main: #5b5c9d;
|
||||||
--text-primary: #637381;
|
--text-primary: #637381;
|
||||||
--selection-color: #f5f5f5;
|
--selection-color: #f5f5f5;
|
||||||
--scroller-color: #90939980;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
@ -30,21 +24,7 @@ body {
|
|||||||
}
|
}
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: var(--scroller-color);
|
background-color: rgba(#909399, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "./layout.scss";
|
@import "./layout.scss";
|
||||||
@import "./page.scss";
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
background-color: rgba(18, 18, 18, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-none {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
|